linphone-desktop/Linphone/view/Page/Layout/Main/MainLayout.qml
Gaelle Braud 5da7a9fd6b fix has file content chat message
only show address for suggestions

do not refresh devices if current account is null

fix crash

add error message on account parameters saved and apply changes on text changed instead of edited (fix #LINQT-1935)
fix disable meeting feature setting in wrong thread
destroy parameter page when closed (to avoid multiplied connections)

fix show/add contact in conversation info
2025-09-15 17:50:19 +02:00

703 lines
37 KiB
QML

import QtCore
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic as Control
import QtQuick.Effects
import Linphone
import UtilsCpp
import SettingsCpp
import "qrc:/qt/qml/Linphone/view/Control/Tool/Helper/utils.js" as Utils
import "qrc:/qt/qml/Linphone/view/Style/buttonStyle.js" as ButtonStyle
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 displayChatRequested(string contactAddress)
signal openChatRequested(ChatGui chat)
signal createContactRequested(string name, string address)
signal scheduleMeetingRequested(string subject, list<string> addresses)
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 displayChatPage(contactAddress) {
tabbar.currentIndex = 2
mainItem.displayChatRequested(contactAddress)
}
function openChat(chat) {
tabbar.currentIndex = 2
mainItem.openChatRequested(chat)
}
function createContact(name, address) {
tabbar.currentIndex = 1
mainItem.createContactRequested(name, address)
}
function scheduleMeeting(subject, addresses) {
tabbar.currentIndex = 3
mainItem.scheduleMeetingRequested(subject, addresses)
}
function openContextualMenuComponent(component) {
if (mainItem.contextualMenuOpenedComponent
&& mainItem.contextualMenuOpenedComponent != component) {
mainStackView.pop()
if (mainItem.contextualMenuOpenedComponent) {
mainItem.contextualMenuOpenedComponent.destroy()
}
mainItem.contextualMenuOpenedComponent = undefined
}
if (!mainItem.contextualMenuOpenedComponent) {
mainStackView.push(component)
mainItem.contextualMenuOpenedComponent = component
}
settingsMenuButton.popup.close()
}
function closeContextualMenuComponent() {
mainStackView.pop()
if (mainItem.contextualMenuOpenedComponent)
mainItem.contextualMenuOpenedComponent.destroy()
mainItem.contextualMenuOpenedComponent = undefined
}
function openAccountSettings(account) {
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 {
anchors.fill: parent
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: MediumButton {
style: ButtonStyle.toast
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"
onClicked: {
var callsWindow = UtilsCpp.getCallsWindow(
currentCallNotif.currentCall)
UtilsCpp.smartShowWindow(callsWindow)
}
}
}
RowLayout {
anchors.fill: parent
spacing: 0
anchors.topMargin: Math.round(25 * DefaultStyle.dp)
VerticalTabBar {
id: tabbar
Layout.fillHeight: true
Layout.preferredWidth: Math.round(82 * DefaultStyle.dp)
defaultAccount: accountProxy.defaultAccount
currentIndex: 0
Binding on currentIndex {
when: mainItem.contextualMenuOpenedComponent != undefined
value: -1
}
model: [{
"icon": AppIcons.phone,
"selectedIcon": AppIcons.phoneSelected,
//: "Appels"
"label": qsTr("bottom_navigation_calls_label")
}, {
"icon": AppIcons.adressBook,
"selectedIcon": AppIcons.adressBookSelected,
//: "Contacts"
"label": qsTr("bottom_navigation_contacts_label")
}, {
"icon": AppIcons.chatTeardropText,
"selectedIcon": AppIcons.chatTeardropTextSelected,
//: "Conversations"
"label": qsTr("bottom_navigation_conversations_label"),
"visible": !SettingsCpp.disableChatFeature
}, {
"icon": AppIcons.videoconference,
"selectedIcon": AppIcons.videoconferenceSelected,
//: "Réunions"
"label": qsTr("bottom_navigation_meetings_label"),
"visible": !SettingsCpp.disableMeetingsFeature
}]
onCurrentIndexChanged: {
if (currentIndex === -1)
return
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()
}
}
Component.onCompleted: {
if (SettingsCpp.shortcutCount > 0) {
var shortcuts = SettingsCpp.shortcuts
shortcuts.forEach(shortcut => {
model.push({
"icon": shortcut.icon,
"selectedIcon": shortcut.icon,
"label": shortcut.name,
"colored": true,
"link": shortcut.link
})
})
}
initButtons()
currentIndex = SettingsCpp.getLastActiveTabIndex()
if (currentIndex === -1) currentIndex = 0
}
}
ColumnLayout {
spacing: 0
RowLayout {
id: topRow
Layout.preferredHeight: Math.round(50 * DefaultStyle.dp)
Layout.leftMargin: Math.round(45 * DefaultStyle.dp)
Layout.rightMargin: Math.round(41 * DefaultStyle.dp)
spacing: Math.round(25 * DefaultStyle.dp)
SearchBar {
id: magicSearchBar
Layout.fillWidth: true
//: "Rechercher un contact, appeler %1"
placeholderText: qsTr("searchbar_placeholder_text").arg(SettingsCpp.disableChatFeature
? "…"
//: "ou envoyer un message …"
: qsTr("searchbar_placeholder_text_chat_feature_enabled"))
focusedBorderColor: DefaultStyle.main1_500_main
numericPadButton.visible: text.length === 0
numericPadButton.checkable: false
handleNumericPadPopupButtonsPressed: 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: contactList //contactLoader.item?.count > 0 || !contactLoader.item?.footerItem? contactLoader.item : contactLoader.item?.footerItem
KeyNavigation.up: contactList //contactLoader.item?.footerItem ? contactLoader.item?.footerItem : contactLoader.item
Popup {
id: listPopup
width: magicSearchBar.width
property real maxHeight: Math.round(400 * DefaultStyle.dp)
property bool displayScrollbar: contactList.height > maxHeight
height: Math.min(
contactList.contentHeight,
maxHeight) + topPadding + bottomPadding
y: magicSearchBar.height
// closePolicy: Popup.CloseOnEscape
topPadding: Math.round(20 * DefaultStyle.dp)
bottomPadding: Math.round((contactList.haveContacts ? 20 : 10) * DefaultStyle.dp)
rightPadding: Math.round(8 * DefaultStyle.dp)
leftPadding: Math.round(20 * DefaultStyle.dp)
visible: magicSearchBar.text.length != 0
background: Item {
anchors.fill: parent
Rectangle {
id: popupBg
radius: Math.round(16 * DefaultStyle.dp)
color: DefaultStyle.grey_0
anchors.fill: parent
border.color: DefaultStyle.main1_500_main
border.width: contactList.activeFocus ? 2 : 0
}
MultiEffect {
source: popupBg
anchors.fill: popupBg
shadowEnabled: true
shadowBlur: 0.1
shadowColor: DefaultStyle.grey_1000
shadowOpacity: 0.1
}
}
contentItem: AllContactListView {
id: contactList
width: listPopup.width - listPopup.leftPadding
- listPopup.rightPadding
itemsRightMargin: Math.round(5 * DefaultStyle.dp) //(Actions have already 10 of margin)
showInitials: false
showContactMenu: false
showActions: true
showFavorites: false
selectionEnabled: false
searchOnEmpty: false
sectionsPixelSize: Typography.p2.pixelSize
sectionsWeight: Typography.p2.weight
sectionsSpacing: Math.round(5 * DefaultStyle.dp)
searchBarText: magicSearchBar.text
}
}
}
RowLayout {
spacing: Math.round(10 * DefaultStyle.dp)
PopupButton {
id: deactivateDndButton
Layout.preferredWidth: Math.round(32 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(32 * DefaultStyle.dp)
popup.padding: Math.round(14 * DefaultStyle.dp)
visible: SettingsCpp.dnd
contentItem: EffectImage {
imageSource: AppIcons.bellDnd
width: Math.round(32 * DefaultStyle.dp)
height: Math.round(32 * DefaultStyle.dp)
Layout.preferredWidth: Math.round(32 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(32 * DefaultStyle.dp)
fillMode: Image.PreserveAspectFit
colorizationColor: DefaultStyle.main1_500_main
}
popup.contentItem: ColumnLayout {
IconLabelButton {
Layout.fillWidth: true
focus: visible
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Désactiver ne pas déranger"
text: qsTr("contact_presence_status_disable_do_not_disturb")
icon.source: AppIcons.bellDnd
onClicked: {
deactivateDndButton.popup.close()
SettingsCpp.dnd = false
}
}
}
}
Voicemail {
id: voicemail
Layout.preferredWidth: Math.round(42 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(36 * DefaultStyle.dp)
Repeater {
model: accountProxy
delegate: Item {
Connections {
target: modelData.core
function onShowMwiChanged() {
voicemail.updateCumulatedMwi()
}
function onVoicemailAddressChanged() {
voicemail.updateCumulatedMwi()
}
}
}
}
function updateCumulatedMwi() {
var count = 0
var showMwi = false
var supportsVoiceMail = false
for (var i = 0; i < accountProxy.count; i++) {
var core = accountProxy.getAt(i).core
count += core.voicemailCount
showMwi |= core.showMwi
supportsVoiceMail |= core.voicemailAddress.length > 0
}
voicemail.showMwi = showMwi
voicemail.voicemailCount = count
voicemail.visible = showMwi || supportsVoiceMail
}
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("information_popup_error_title"),
//: "L'URI de messagerie vocale n'est pas définie."
qsTr("no_voicemail_uri_error_message"), false)
}
}
}
PopupButton {
id: avatarButton
Layout.preferredWidth: Math.round(54 * DefaultStyle.dp)
Layout.preferredHeight: width
popup.topPadding: Math.round(23 * DefaultStyle.dp)
popup.bottomPadding: Math.round(23 * DefaultStyle.dp)
popup.leftPadding: Math.round(24 * DefaultStyle.dp)
popup.rightPadding: Math.round(24 * 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: Math.round(24 * DefaultStyle.dp)
Layout.preferredHeight: Math.round(24 * DefaultStyle.dp)
popup.width: Math.round(271 * DefaultStyle.dp)
popup.padding: Math.round(14 * DefaultStyle.dp)
popup.contentItem: FocusScope {
id: popupFocus
implicitHeight: settingsButtons.implicitHeight
implicitWidth: settingsButtons.implicitWidth
Keys.onPressed: event => {
if (event.key == Qt.Key_Left
|| event.key == Qt.Key_Escape) {
settingsMenuButton.popup.close()
event.accepted = true
}
}
ColumnLayout {
id: settingsButtons
spacing: Math.round(16 * DefaultStyle.dp)
anchors.fill: parent
IconLabelButton {
id: accountButton
Layout.fillWidth: true
visible: !SettingsCpp.hideAccountSettings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: Mon compte
text: qsTr("drawer_menu_manage_account")
icon.source: AppIcons.manageProfile
onClicked: openAccountSettings(accountProxy.defaultAccount
? accountProxy.defaultAccount
: accountProxy.firstAccount())
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
0) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
0) : null
}
IconLabelButton {
id: dndButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
text: SettingsCpp.dnd ? qsTr("contact_presence_status_disable_do_not_disturb")
//: "Activer ne pas déranger"
: qsTr("contact_presence_status_enable_do_not_disturb")
icon.source: AppIcons.bellDnd
onClicked: {
settingsMenuButton.popup.close()
SettingsCpp.dnd = !SettingsCpp.dnd
}
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
1) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
1) : null
}
IconLabelButton {
id: settingsButton
Layout.fillWidth: true
visible: !SettingsCpp.hideSettings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
text: qsTr("settings_title")
icon.source: AppIcons.settings
onClicked: openContextualMenuComponent(settingsPageComponent)
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
2) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
2) : null
}
IconLabelButton {
id: recordsButton
Layout.fillWidth: true
visible: !SettingsCpp.disableCallRecordings
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Enregistrements"
text: qsTr("recordings_title")
icon.source: AppIcons.micro
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
3) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
3) : null
}
IconLabelButton {
id: helpButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Aide"
text: qsTr("help_title")
icon.source: AppIcons.question
onClicked: openContextualMenuComponent(
helpPageComponent)
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
4) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
4) : null
}
IconLabelButton {
id: quitButton
Layout.fillWidth: true
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Quitter l'application"
text: qsTr("help_quit_title")
icon.source: AppIcons.power
onClicked: {
settingsMenuButton.popup.close()
//: "Quitter %1 ?"
UtilsCpp.getMainWindow().showConfirmationLambdaPopup("", qsTr("quit_app_question").arg(applicationName),"",
function (confirmed) {
if (confirmed) {
console.info("Exiting App from Top Menu")
Qt.quit()
}
})
}
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
5) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
5) : null
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(Math.round(1 * DefaultStyle.dp), 1)
visible: addAccountButton.visible
color: DefaultStyle.main2_400
}
IconLabelButton {
id: addAccountButton
Layout.fillWidth: true
visible: SettingsCpp.maxAccount == 0
|| SettingsCpp.maxAccount > accountProxy.count
icon.width: Math.round(32 * DefaultStyle.dp)
icon.height: Math.round(32 * DefaultStyle.dp)
//: "Ajouter un compte"
text: qsTr("drawer_menu_add_account")
icon.source: AppIcons.plusCircle
onClicked: mainItem.addAccountRequest()
KeyNavigation.up: visibleChildren.length
!= 0 ? settingsMenuButton.getPreviousItem(
7) : null
KeyNavigation.down: visibleChildren.length
!= 0 ? settingsMenuButton.getNextItem(
7) : null
}
}
}
}
}
}
Component {
id: mainStackLayoutComponent
StackLayout {
id: mainStackLayout
objectName: "mainStackLayout"
property int _currentIndex: tabbar.currentIndex
currentIndex: -1
onActiveFocusChanged: if (activeFocus
&& currentIndex >= 0)
children[currentIndex].forceActiveFocus()
on_CurrentIndexChanged: {
if (count > 0) {
if (_currentIndex >= count && tabbar.model[_currentIndex].link) {
Qt.openUrlExternally(tabbar.model[_currentIndex].link)
} else if (_currentIndex >= 0) {
currentIndex = _currentIndex
SettingsCpp.setLastActiveTabIndex(currentIndex)
}
}
}
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
}
onGoToCallForwardSettings: {
var page = settingsPageComponent.createObject(parent, {
defaultIndex: 1
});
openContextualMenuComponent(page)
}
}
ContactPage {
id: contactPage
Connections {
target: mainItem
function onCreateContactRequested(name, address) {
contactPage.createContact(name, address)
}
function onDisplayContactRequested(contactAddress) {
contactPage.initialFriendToDisplay = contactAddress
}
}
}
ChatPage {
id: chatPage
Connections {
target: mainItem
function onDisplayChatRequested(contactAddress) {
console.log("display chat requested, open with address", contactAddress)
chatPage.remoteAddress = ""
chatPage.remoteAddress = contactAddress
}
function onOpenChatRequested(chat) {
console.log("open chat requested, open", chat.core.title)
chatPage.selectedChatGui = chat
}
}
}
MeetingPage {
id: meetingPage
Connections {
target: mainItem
function onScheduleMeetingRequested(subject, addresses) {
meetingPage.createPreFilledMeeting(subject, addresses)
}
}
}
}
}
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: Math.round(24 * DefaultStyle.dp)
Layout.fillWidth: true
Layout.fillHeight: true
initialItem: mainStackLayoutComponent
}
}
}
}
}