linux.x86.linphone/Linphone/view/Page/Layout/Main/MainLayout.qml
Julien Wadel a6561ccb19 Fix reentrency issues with magic search :
- store search parameters into Core.
- add search limitation to avoid 300 useless items.
- retrieve old parameters on proxy when changing list.
- store parent proxy to avoid MOC warnings.

Fix contacts search views:
- add a loading state for buzy indicators.
- limit results on suggestions.
- avoid to create MagicSearchProxy if not needed.
- add a status to know if friend is stored or not.
- propagate invalidateFilter.
- delay search while typing.

Fix margins and participants selection.
Do not search contacts when contact panel is not shown.

Avoid search on empty magicbar.
Avoid repeating section on object that disappeared from cache.
Focus on new contact after creation.

Avoid changing maxresult if not needed.

Redirect only if friend is not ldap

Fix empty display name on making favorite a ldap contact.

Fix focus and positions on favorites.
2024-11-18 15:53:58 +01:00

703 lines
24 KiB
QML

/**
* Qml template used for welcome and login/register pages
**/
import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic as Control
import QtQuick.Effects
import Linphone
import UtilsCpp
import SettingsCpp
Item {
id: mainItem
property var callObj
property var contextualMenuOpenedComponent: undefined
signal addAccountRequest()
signal openNewCallRequest()
signal callCreated()
signal openCallHistory()
signal openNumPadRequest()
signal displayContactRequested(string contactAddress)
signal createContactRequested(string name, string address)
signal accountRemoved()
function goToNewCall() {
tabbar.currentIndex = 0
mainItem.openNewCallRequest()
}
function goToCallHistory() {
tabbar.currentIndex = 0
mainItem.openCallHistory()
}
function displayContactPage(contactAddress) {
tabbar.currentIndex = 1
mainItem.displayContactRequested(contactAddress)
}
function createContact(name, address) {
tabbar.currentIndex = 1
mainItem.createContactRequested(name, address)
}
function openContextualMenuComponent(component) {
if (mainItem.contextualMenuOpenedComponent && mainItem.contextualMenuOpenedComponent != component) {
mainStackView.pop()
mainItem.contextualMenuOpenedComponent = undefined
}
if (!mainItem.contextualMenuOpenedComponent) {
mainStackView.push(component)
mainItem.contextualMenuOpenedComponent = component
}
settingsMenuButton.popup.close()
}
function closeContextualMenuComponent() {
mainStackView.pop()
mainItem.contextualMenuOpenedComponent = undefined
}
function openAccountSettings(account: AccountGui) {
var page = accountSettingsPageComponent.createObject(parent, {"account": account});
openContextualMenuComponent(page)
}
AccountProxy {
id: accountProxy
sourceModel: AppCpp.accounts
onDefaultAccountChanged: if (tabbar.currentIndex === 0 && defaultAccount) defaultAccount.core?.lResetMissedCalls()
}
CallProxy {
id: callsModel
sourceModel: AppCpp.calls
}
Item{
Popup {
id: currentCallNotif
background: Item{}
closePolicy: Control.Popup.NoAutoClose
visible: currentCall
&& currentCall.core.state != LinphoneEnums.CallState.Idle
&& currentCall.core.state != LinphoneEnums.CallState.IncomingReceived
&& currentCall.core.state != LinphoneEnums.CallState.PushIncomingReceived
x: mainItem.width/2 - width/2
y: contentItem.height/2
property var currentCall: callsModel.currentCall ? callsModel.currentCall : null
property string remoteName: currentCall ? currentCall.core.remoteName : ""
contentItem: Button {
text: currentCallNotif.currentCall
? currentCallNotif.currentCall.core.conference
? ("Réunion en cours : ") + currentCallNotif.currentCall.core.conference.core.subject
: (("Appel en cours : ") + currentCallNotif.remoteName) : "appel en cours"
color: DefaultStyle.success_500main
onClicked: {
var callsWindow = UtilsCpp.getCallsWindow(currentCallNotif.currentCall)
UtilsCpp.smartShowWindow(callsWindow)
}
}
}
anchors.fill: parent
RowLayout {
anchors.fill: parent
spacing: 0
anchors.topMargin: 25 * DefaultStyle.dp
VerticalTabBar {
id: tabbar
Layout.fillHeight: true
Layout.preferredWidth: 82 * DefaultStyle.dp
defaultAccount: accountProxy.defaultAccount
currentIndex: SettingsCpp.getLastActiveTabIndex()
Binding on currentIndex {
when: mainItem.contextualMenuOpenedComponent != undefined
value: -1
}
model: [
{icon: AppIcons.phone, selectedIcon: AppIcons.phoneSelected, label: qsTr("Appels")},
{icon: AppIcons.adressBook, selectedIcon: AppIcons.adressBookSelected, label: qsTr("Contacts")},
{icon: AppIcons.chatTeardropText, selectedIcon: AppIcons.chatTeardropTextSelected, label: qsTr("Conversations"), visible: !SettingsCpp.disableChatFeature},
{icon: AppIcons.videoconference, selectedIcon: AppIcons.videoconferenceSelected, label: qsTr("Réunions"), visible: !SettingsCpp.disableMeetingsFeature}
]
onCurrentIndexChanged: {
if (currentIndex == -1) return
SettingsCpp.setLastActiveTabIndex(currentIndex)
if (currentIndex === 0 && accountProxy.defaultAccount) accountProxy.defaultAccount.core?.lResetMissedCalls()
if (mainItem.contextualMenuOpenedComponent) {
closeContextualMenuComponent()
}
}
Keys.onPressed: (event)=>{
if(event.key == Qt.Key_Right){
mainStackView.currentItem.forceActiveFocus()
}
}
}
ColumnLayout {
spacing:0
RowLayout {
id: topRow
Layout.preferredHeight: 50 * DefaultStyle.dp
Layout.leftMargin: 45 * DefaultStyle.dp
Layout.rightMargin: 41 * DefaultStyle.dp
spacing: 25 * DefaultStyle.dp
SearchBar {
id: magicSearchBar
Layout.fillWidth: true
placeholderText: SettingsCpp.disableChatFeature ? qsTr("Rechercher un contact, appeler...") : qsTr("Rechercher un contact, appeler ou envoyer un message...")
focusedBorderColor: DefaultStyle.main1_500_main
numericPadButton.visible: text.length === 0
numericPadButton.checkable: false
onOpenNumericPadRequested:mainItem.goToNewCall()
Connections {
target: mainItem
function onCallCreated() {
magicSearchBar.focus = false
magicSearchBar.clearText()
}
}
onTextChanged: {
if (text.length != 0) listPopup.open()
else listPopup.close()
}
KeyNavigation.down: contactLoader.item?.count > 0 || !contactLoader.item?.footerItem? contactLoader.item : contactLoader.item?.footerItem
KeyNavigation.up: contactLoader.item?.footerItem ? contactLoader.item?.footerItem : contactLoader.item
component MagicSearchButton: Button {
id: button
width: 45 * DefaultStyle.dp
height: 45 * DefaultStyle.dp
topPadding: 16 * DefaultStyle.dp
bottomPadding: 16 * DefaultStyle.dp
leftPadding: 16 * DefaultStyle.dp
rightPadding: 16 * DefaultStyle.dp
contentImageColor: DefaultStyle.main2_500main
icon.width: 24 * DefaultStyle.dp
icon.height: 24 * DefaultStyle.dp
background: Rectangle {
anchors.fill: parent
radius: 40 * DefaultStyle.dp
color: DefaultStyle.main2_200
}
}
Popup {
id: listPopup
width: magicSearchBar.width
property int maxHeight: 400 * DefaultStyle.dp
property bool displayScrollbar: contactLoader.item?.contentHeight + topPadding + bottomPadding> maxHeight
height: Math.min(contactLoader.item?.contentHeight + topPadding + bottomPadding, maxHeight)
y: magicSearchBar.height
// closePolicy: Popup.NoAutoClose
topPadding: 20 * DefaultStyle.dp
bottomPadding: 20 * DefaultStyle.dp
rightPadding: 10 * DefaultStyle.dp
leftPadding: 20 * DefaultStyle.dp
background: Item {
anchors.fill: parent
Rectangle {
id: popupBg
radius: 16 * DefaultStyle.dp
color: DefaultStyle.grey_0
anchors.fill: parent
border.color: DefaultStyle.main1_500_main
border.width: contactLoader.item?.activeFocus ? 2 : 0
}
MultiEffect {
source: popupBg
anchors.fill: popupBg
shadowEnabled: true
shadowBlur: 0.1
shadowColor: DefaultStyle.grey_1000
shadowOpacity: 0.1
}
ScrollBar {
id: scrollbar
Component.onCompleted: x = -10 * DefaultStyle.dp
policy: Control.ScrollBar.AsNeeded// Don't work as expected
visible: listPopup.displayScrollbar
interactive: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 10 * DefaultStyle.dp
}
}
contentItem: Loader{
// This is a hack for an incomprehensible behavior on sections title where they doesn't match with their delegate and can be unordered after resetting models.
id: contactLoader
Layout.fillWidth: true
Layout.fillHeight: true
property bool deactivate: false
active: !deactivate && magicSearchBar.text != ''
property string t: magicSearchBar.text
onTChanged: {
contactLoader.deactivate = true
Qt.callLater(function(){contactLoader.deactivate=false})
}
//-------------------------------------------------------------
sourceComponent: ContactListView {
id: contactList
visible: magicSearchBar.text.length != 0
Layout.preferredHeight: item?.contentHeight
Layout.fillWidth: true
itemsRightMargin: 5 * DefaultStyle.dp //(Actions have already 10 of margin)
showInitials: false
showContactMenu: false
showActions: true
showFavorites: false
selectionEnabled: false
showDefaultAddress: true
hideSuggestions: true
sectionsPixelSize: 13 * DefaultStyle.dp
sectionsWeight: 700 * DefaultStyle.dp
sectionsSpacing: 5 * DefaultStyle.dp
Control.ScrollBar.vertical: scrollbar
searchBarText: magicSearchBar.text
Keys.onPressed: (event) => {
if(event.key == Qt.Key_Down){
if(contactList.currentIndex == contactList.count -1) {
contactList.currentIndex = -1
contactList.footerItem.forceActiveFocus()
event.accepted = true
}
} else if(event.key == Qt.Key_Up){
if(contactList.currentIndex <= 0) {
contactList.currentIndex = -1
contactList.footerItem.forceActiveFocus()
event.accepted = true
}
}
}
footer: FocusScope{
id: suggestionFocusScope
width: contactList.width
height: visible ? content.implicitHeight : 0
onActiveFocusChanged: if(activeFocus) contactList.positionViewAtEnd()
visible: !contactList.haveAddress(suggestionText.text)
Rectangle{
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: suggestionRow.implicitHeight
color: suggestionFocusScope.activeFocus ? DefaultStyle.numericPadPressedButtonColor : 'transparent'
}
ColumnLayout {
id: content
anchors.fill: parent
anchors.leftMargin: 5 * DefaultStyle.dp
anchors.rightMargin: 15 * DefaultStyle.dp
spacing: 10 * DefaultStyle.dp
Text {
text: qsTr("Suggestion")
color: DefaultStyle.main2_500main
font {
pixelSize: 13 * DefaultStyle.dp
weight: 700 * DefaultStyle.dp
}
}
Keys.onPressed: (event) => {
if(contactList.count <= 0) return;
if(event.key == Qt.Key_Down){
contactList.currentIndex = 0
event.accepted = true
} else if(event.key == Qt.Key_Up){
contactList.currentIndex = contactList.count - 1
event.accepted = true
}
}
RowLayout {
id: suggestionRow
spacing: 10 * DefaultStyle.dp
Avatar {
Layout.preferredWidth: 45 * DefaultStyle.dp
Layout.preferredHeight: 45 * DefaultStyle.dp
_address: magicSearchBar.text
}
Text {
id: suggestionText
property var urlObj: UtilsCpp.interpretUrl(magicSearchBar.text)
text: SettingsCpp.onlyDisplaySipUriUsername ? UtilsCpp.getUsername(urlObj?.value) : urlObj?.value
font {
pixelSize: 12 * DefaultStyle.dp
weight: 300 * DefaultStyle.dp
}
}
Item {
Layout.fillWidth: true
}
MagicSearchButton {
id: callButton
Layout.preferredWidth: 45 * DefaultStyle.dp
Layout.preferredHeight: 45 * DefaultStyle.dp
icon.source: AppIcons.phone
focus: true
onClicked: {
UtilsCpp.createCall(magicSearchBar.text)
magicSearchBar.clearText()
}
KeyNavigation.right: chatButton
KeyNavigation.left: chatButton
}
MagicSearchButton {
id: chatButton
visible: !SettingsCpp.disableChatFeature
Layout.preferredWidth: 45 * DefaultStyle.dp
Layout.preferredHeight: 45 * DefaultStyle.dp
icon.source: AppIcons.chatTeardropText
KeyNavigation.right: callButton
KeyNavigation.left: callButton
}
}
}
}
}
}
}
}
RowLayout {
spacing: 10 * DefaultStyle.dp
PopupButton {
id: deactivateDndButton
Layout.preferredWidth: 32 * DefaultStyle.dp
Layout.preferredHeight: 32 * DefaultStyle.dp
popup.padding: 14 * DefaultStyle.dp
visible: SettingsCpp.dnd
contentItem: EffectImage {
imageSource: AppIcons.bellDnd
width: 32 * DefaultStyle.dp
height: 32 * DefaultStyle.dp
Layout.preferredWidth: 32 * DefaultStyle.dp
Layout.preferredHeight: 32 * DefaultStyle.dp
fillMode: Image.PreserveAspectFit
colorizationColor: DefaultStyle.main1_500_main
}
popup.contentItem: ColumnLayout {
IconLabelButton {
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
focus: visible
iconSize: 32 * DefaultStyle.dp
text: qsTr("Désactiver ne pas déranger")
iconSource: AppIcons.bellDnd
onClicked: {
deactivateDndButton.popup.close()
SettingsCpp.dnd = false
}
}
}
}
Voicemail {
id: voicemail
Layout.preferredWidth: 27 * DefaultStyle.dp
Layout.preferredHeight: 28 * DefaultStyle.dp
Repeater {
model: accountProxy
delegate: Item {
Connections {
target: modelData.core
onShowMwiChanged: voicemail.updateCumulatedMwi()
}
}
}
function updateCumulatedMwi() {
var count = 0
var show = false
for (var i=0 ; i < accountProxy.count ; i++ ) {
count += accountProxy.getAt(i).core.voicemailCount
show |= accountProxy.getAt(i).core.showMwi
}
voicemail.visible = show
voicemail.voicemailCount = count
}
Component.onCompleted: {
updateCumulatedMwi()
}
onClicked: {
if (accountProxy.count > 1) {
avatarButton.popup.open()
} else {
if (accountProxy.defaultAccount.core.voicemailAddress.length > 0)
UtilsCpp.createCall(accountProxy.defaultAccount.core.voicemailAddress)
else
UtilsCpp.showInformationPopup(qsTr("Erreur"), qsTr("L'URI de messagerie vocale n'est pas définie."), false)
}
}
}
PopupButton {
id: avatarButton
Layout.preferredWidth: 54 * DefaultStyle.dp
Layout.preferredHeight: width
popup.padding: 14 * DefaultStyle.dp
contentItem: Avatar {
id: avatar
height: avatarButton.height
width: avatarButton.width
account: accountProxy.defaultAccount
}
popup.contentItem: ColumnLayout {
AccountListView {
id: accounts
onAddAccountRequest: mainItem.addAccountRequest()
onEditAccount: function(account) {
avatarButton.popup.close()
openAccountSettings(account)
}
}
}
}
PopupButton {
id: settingsMenuButton
Layout.preferredWidth: 24 * DefaultStyle.dp
Layout.preferredHeight: 24 * DefaultStyle.dp
popup.width: 271 * DefaultStyle.dp
popup.padding: 14 * DefaultStyle.dp
popup.contentItem: FocusScope {
id: popupFocus
implicitHeight: settingsButtons.implicitHeight
Keys.onPressed: (event)=> {
if (event.key == Qt.Key_Left || event.key == Qt.Key_Escape) {
settingsMenuButton.popup.close()
event.accepted = true;
}
}
ColumnLayout {
id: settingsButtons
anchors.fill: parent
spacing: 16 * DefaultStyle.dp
function getPreviousItem(index){
if(visibleChildren.length == 0) return null
--index
while(index >= 0){
if( index!= 4 && children[index].visible) return children[index]
--index
}
return getPreviousItem(children.length)
}
function getNextItem(index){
++index
while(index < children.length){
if( index!= 4 && children[index].visible) return children[index]
++index
}
return getNextItem(-1)
}
IconLabelButton {
id: accountButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
visible: !SettingsCpp.hideAccountSettings
focus: visible
iconSize: 32 * DefaultStyle.dp
text: qsTr("Mon compte")
iconSource: AppIcons.manageProfile
onClicked: openAccountSettings(accountProxy.defaultAccount ? accountProxy.defaultAccount : accountProxy.firstAccount())
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(0) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(0) : null
}
IconLabelButton {
id: dndButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
iconSize: 32 * DefaultStyle.dp
text: SettingsCpp.dnd ? qsTr("Désactiver ne pas déranger") : qsTr("Activer ne pas déranger")
iconSource: AppIcons.bellDnd
onClicked: {
settingsMenuButton.popup.close()
SettingsCpp.dnd = !SettingsCpp.dnd
}
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(0) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(0) : null
}
IconLabelButton {
id: settingsButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
visible: !SettingsCpp.hideSettings
focus: !accountButton.visible && visible
iconSize: 32 * DefaultStyle.dp
text: qsTr("Paramètres")
iconSource: AppIcons.settings
onClicked: openContextualMenuComponent(settingsPageComponent)
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(1) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(1) : null
}
IconLabelButton {
id: recordsButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
visible: !SettingsCpp.disableCallRecordings
focus: !accountButton.visible && !settingsButton.visible && visible
iconSize: 32 * DefaultStyle.dp
text: qsTr("Enregistrements")
iconSource: AppIcons.micro
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(2) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(2) : null
}
IconLabelButton {
id: helpButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
iconSize: 32 * DefaultStyle.dp
focus: !accountButton.visible && !settingsButton.visible && !recordsButton.visible
text: qsTr("Aide")
iconSource: AppIcons.question
onClicked: openContextualMenuComponent(helpPageComponent)
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(3) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(3) : null
}
IconLabelButton {
id: quitButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
focus: !accountButton.visible && !settingsButton.visible && visible
iconSize: 32 * DefaultStyle.dp
text: qsTr("Quitter Linphone")
iconSource: AppIcons.power
onClicked: {
settingsMenuButton.popup.close()
UtilsCpp.getMainWindow().showConfirmationLambdaPopup("",
qsTr("Quitter Linphone ?"),
"",
function (confirmed) {
if (confirmed) {
console.info("Exiting App from Top Menu");
Qt.quit()
}
}
)
}
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(4) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(4) : null
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1 * DefaultStyle.dp
visible: addAccountButton.visible
color: DefaultStyle.main2_400
}
IconLabelButton {
id: addAccountButton
Layout.preferredHeight: 32 * DefaultStyle.dp
Layout.fillWidth: true
visible: SettingsCpp.maxAccount == 0 || SettingsCpp.maxAccount > accountProxy.count
iconSize: 32 * DefaultStyle.dp
text: qsTr("Ajouter un compte")
iconSource: AppIcons.plusCircle
onClicked: mainItem.addAccountRequest()
KeyNavigation.up: visibleChildren.length != 0 ? settingsButtons.getPreviousItem(5) : null
KeyNavigation.down: visibleChildren.length != 0 ? settingsButtons.getNextItem(5) : null
}
}
}
}
}
}
Component {
id: mainStackLayoutComponent
StackLayout {
id: mainStackLayout
objectName: "mainStackLayout"
currentIndex: tabbar.currentIndex
onActiveFocusChanged: if(activeFocus && currentIndex >= 0) children[currentIndex].forceActiveFocus()
CallPage {
id: callPage
Connections {
target: mainItem
function onOpenNewCallRequest(){ callPage.goToNewCall()}
function onCallCreated(){ callPage.goToCallHistory()}
function onOpenCallHistory(){ callPage.goToCallHistory()}
function onOpenNumPadRequest(){ callPage.openNumPadRequest()}
}
onCreateContactRequested: (name, address) => {
mainItem.createContact(name, address)
}
Component.onCompleted: {
magicSearchBar.numericPadPopup = callPage.numericPadPopup
}
}
ContactPage{
id: contactPage
Connections {
target: mainItem
function onCreateContactRequested(name, address) {
contactPage.createContact(name, address)
}
function onDisplayContactRequested(contactAddress) {
contactPage.initialFriendToDisplay = contactAddress
}
}
}
Item{}
//ConversationPage{}
MeetingPage{}
}
}
Component {
id: accountSettingsPageComponent
AccountSettingsPage {
onGoBack: closeContextualMenuComponent()
onAccountRemoved: {
closeContextualMenuComponent()
mainItem.accountRemoved()
}
}
}
Component {
id: settingsPageComponent
SettingsPage {
onGoBack: closeContextualMenuComponent()
}
}
Component {
id: helpPageComponent
HelpPage {
onGoBack: closeContextualMenuComponent()
}
}
Control.StackView {
id: mainStackView
property Transition noTransition: Transition {
PropertyAnimation { property: "opacity"; from: 1; to: 1; duration: 0 }
}
pushEnter: noTransition
pushExit: noTransition
popEnter: noTransition
popExit: noTransition
Layout.topMargin: 24 * DefaultStyle.dp
Layout.fillWidth: true
Layout.fillHeight: true
initialItem: mainStackLayoutComponent
}
}
}
}
}