From b0fd803245e5da0f34dd54f2de9b2cdead57226a Mon Sep 17 00:00:00 2001 From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> Date: Mon, 10 Jan 2022 19:36:54 -0500 Subject: [PATCH] mainapplication: isolate window mode logic in a top-level component Logic for fullscreen/windowed mode switching is scattered within the main ApplicationWindow and other components. Ideally, components that need to transition to fullscreen, can make a dumb request to module in charge of making the necessary checks and carrying out the mode change. This patch introduces the LayoutManager used to isolate this logic. Change-Id: I0e5b932617d2b88eda1533f25a5d55fc1c66c438 --- qml.qrc | 1 + src/LayoutManager.qml | 184 ++++++++++++++++++ src/MainApplicationWindow.qml | 21 +- .../DataTransferMessageDelegate.qml | 40 +--- src/constant/JamiQmlUtils.qml | 12 -- src/mainview/MainView.qml | 25 +-- src/mainview/components/CallStackView.qml | 43 ++-- .../components/CallViewContextMenu.qml | 7 +- 8 files changed, 224 insertions(+), 109 deletions(-) create mode 100644 src/LayoutManager.qml diff --git a/qml.qrc b/qml.qrc index fa1d2ebe3..50e329b37 100644 --- a/qml.qrc +++ b/qml.qrc @@ -177,5 +177,6 @@ <file>src/mainview/components/KeyboardShortcutTable.qml</file> <file>src/mainview/components/KeyboardShortcutKeyDelegate.qml</file> <file>src/mainview/components/KeyboardShortcutTabButton.qml</file> + <file>src/LayoutManager.qml</file> </qresource> </RCC> diff --git a/src/LayoutManager.qml b/src/LayoutManager.qml new file mode 100644 index 000000000..d971edb61 --- /dev/null +++ b/src/LayoutManager.qml @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls + +import net.jami.Adapters 1.1 + +import "mainview/components" + +QtObject { + id: root + + // A window-sized container for reparenting components. + required property Item appContainer + + // True if the main window is fullscreen. + readonly property bool isFullScreen: visibility === Window.FullScreen + + // Both the Hidden and Minimized states combined for convenience. + readonly property bool isHidden: visibility === Window.Hidden || + visibility === Window.Minimized + + // Used to store if a OngoingCallPage component is fullscreened. + property bool isCallFullscreen: false + + // Restore a visible windowed mode. + function restoreApp() { + if (isHidden) { + if (priv.windowedVisibility === Window.Hidden + || priv.windowedVisibility === Window.Minimized) { + showNormal() + return + } + visibility = priv.windowedVisibility + } + } + + // Adds an item to the fullscreen item stack. Automatically puts + // the main window in fullscreen mode if needed. Callbacks should be used + // to perform component-specific tasks upon successful transitions. + function pushFullScreenItem(item, originalParent, pushedCb, removedCb) { + if (item === null || item === undefined + || priv.fullScreenItems.length >= 3) { + return + } + + // Make sure our window is in fullscreen mode. + priv.requestWindowModeChange(true) + + // Add the item to our list and reparent it to appContainer. + priv.fullScreenItems.push({ + "item": item, + "originalParent": originalParent, + "removedCb": removedCb + }) + item.parent = appContainer + if (pushedCb) { + pushedCb() + } + + // Reevaluate isCallFullscreen. + priv.fullScreenItemsChanged() + } + + // Remove an item if specified, or by default, the top item. Automatically + // resets the main window to windowed mode if no items remain in the stack. + function popFullScreenItem(obj=null) { + // Remove the item and reparent it to its original parent. + if (obj === null) { + obj = priv.fullScreenItems.pop() + } else { + const index = priv.fullScreenItems.indexOf(obj); + if (index > -1) { + priv.fullScreenItems.splice(index, 1); + } + } + if (obj !== undefined) { + if (obj.item !== appWindow) { + obj.item.parent = obj.originalParent + if (obj.removedCb) { + obj.removedCb() + } + } + + // Reevaluate isCallFullscreen. + priv.fullScreenItemsChanged() + } + + // Only leave fullscreen mode if our window isn't in fullscreen + // mode already. + if (priv.fullScreenItems.length === 0) { + // Simply recall the last visibility state. + visibility = priv.windowedVisibility + } + } + + // Used to filter removal for a specific item. + function removeFullScreenItem(item) { + priv.fullScreenItems.forEach(o => { + if (o.item === item) { + popFullScreenItem(o) + return + } + }); + } + + // Toggle the application window in fullscreen mode. + function toggleWindowFullScreen() { + priv.requestWindowModeChange(!isFullScreen) + + // If we succeeded, place a dummy item onto the stack as + // a state indicator to prevent returning to windowed mode + // when popping an item on top. The corresponding pop will + // be made within requestWindowModeChange. + if (isFullScreen) { + priv.fullScreenItems.push({ "item": appWindow }) + } + } + + property var data: QtObject { + id: priv + + // Used to store the last windowed mode visibility. + property int windowedVisibility + + // An stack of items that are fullscreened. + property variant fullScreenItems: [] + + // When fullScreenItems is changed, we can recompute isCallFullscreen. + onFullScreenItemsChanged: { + isCallFullscreen = fullScreenItems + .filter(o => o.item instanceof OngoingCallPage) + .length + } + + // Listen for a hangup combined with a fullscreen call state and + // remove the OngoingCallPage component. + property var data: Connections { + target: CallAdapter + function onHasCallChanged() { + if (!CallAdapter.hasCall && isCallFullscreen) { + priv.fullScreenItems.forEach(o => { + if (o.item instanceof OngoingCallPage) { + popFullScreenItem(o) + return + } + }); + } + } + } + + // Used internally to switch modes. + function requestWindowModeChange(fullScreen) { + if (fullScreen) { + if (!isFullScreen) { + // Save the previous visibility state. + windowedVisibility = visibility + showFullScreen() + } + } else { + // Clear the stack. + while (fullScreenItems.length) { + popFullScreenItem() + } + } + } + } +} diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml index acaf6bbce..17e3a9d09 100644 --- a/src/MainApplicationWindow.qml +++ b/src/MainApplicationWindow.qml @@ -33,6 +33,7 @@ import net.jami.Helpers 1.1 import net.jami.Constants 1.1 import "mainview" +import "mainview/components" import "wizardview" import "commoncomponents" @@ -46,15 +47,9 @@ ApplicationWindow { None } - property ApplicationWindow appWindow : root - property bool isFullscreen: visibility === Window.FullScreen - - function toggleFullScreen() { - if (isFullscreen) { - showNormal() - } else { - showFullScreen() - } + property ApplicationWindow appWindow: root + property LayoutManager layoutManager: LayoutManager { + appContainer: appContainer } function checkLoadedSource() { @@ -165,17 +160,13 @@ ApplicationWindow { function onRestoreAppRequested() { requestActivate() - if (visibility === Window.Hidden || visibility === Window.Minimized) { - showNormal() - } + layoutManager.restoreApp() } function onNotificationClicked() { requestActivate() raise() - if (visibility === Window.Hidden || visibility === Window.Minimized) { - showNormal() - } + layoutManager.restoreApp() } } diff --git a/src/commoncomponents/DataTransferMessageDelegate.qml b/src/commoncomponents/DataTransferMessageDelegate.qml index daea1bf4f..707ab5630 100644 --- a/src/commoncomponents/DataTransferMessageDelegate.qml +++ b/src/commoncomponents/DataTransferMessageDelegate.qml @@ -35,7 +35,6 @@ Loader { property bool showTime: false property int seq: MsgSeq.single property string author: Author - property bool changeWindowVisibility: false width: ListView.view ? ListView.view.width : 0 @@ -279,7 +278,7 @@ Loader { settings.fullScreenSupportEnabled: mediaInfo.isVideo settings.javascriptCanOpenWindows: false Component.onCompleted: loadHtml(mediaInfo.html, 'file://') - layer.enabled: parent !== appContainer && !appWindow.isFullscreen + layer.enabled: !isFullScreen layer.effect: OpacityMask { maskSource: MessageBubble { out: isOutgoing @@ -289,43 +288,20 @@ Loader { radius: msgRadius } } - - function leaveFullScreen() { - parent = localMediaCompLoader - if (root.changeWindowVisibility) { - root.changeWindowVisibility = false - appWindow.showNormal() - } - } - onFullScreenRequested: function(request) { - if (JamiQmlUtils.callIsFullscreen) - return if (request.toggleOn) { - parent = appContainer - if (!appWindow.isFullscreen) { - root.changeWindowVisibility = true - appWindow.showFullScreen() - } - } else { - leaveFullScreen() + layoutManager.pushFullScreenItem( + this, + localMediaCompLoader, + null, + function() { wev.fullScreenCancelled() }) + } else if (!request.toggleOn) { + layoutManager.removeFullScreenItem(this) } request.accept() } - - Connections { - target: appWindow - - function onVisibilityChanged() { - if (wev.isFullScreen && !appWindow.isFullScreen) { - wev.fullScreenCancelled() - leaveFullScreen() - } - } - } } } - Component { id: animatedImageComp AnimatedImage { diff --git a/src/constant/JamiQmlUtils.qml b/src/constant/JamiQmlUtils.qml index 5028853e7..4863b8a8d 100644 --- a/src/constant/JamiQmlUtils.qml +++ b/src/constant/JamiQmlUtils.qml @@ -33,9 +33,6 @@ Item { property var mainApplicationScreen: "" - property bool callIsFullscreen: false - signal fullScreenCallEnded - property var accountCreationInputParaObject: ({}) function setUpAccountCreationInputPara(inputPara) { @@ -71,15 +68,6 @@ Item { } } - Connections { - target: CallAdapter - - function onHasCallChanged() { - if (!CallAdapter.hasCall && callIsFullscreen) - fullScreenCallEnded() - } - } - Text { id: globalTextMetrics } diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml index 5792f2882..16a978268 100644 --- a/src/mainview/MainView.qml +++ b/src/mainview/MainView.qml @@ -528,13 +528,13 @@ Rectangle { Shortcut { sequence: "F11" context: Qt.ApplicationShortcut - onActivated: { - // Don't toggle fullscreen mode when we're already - // in a fullscreen call. - if (JamiQmlUtils.callIsFullscreen) - return - appWindow.toggleFullScreen() - } + onActivated: layoutManager.toggleWindowFullScreen() + } + + Shortcut { + sequence: "Escape" + context: Qt.ApplicationShortcut + onActivated: layoutManager.popFullScreenItem() } Shortcut { @@ -556,17 +556,6 @@ Rectangle { onActivated: startWizard() } - Shortcut { - sequence: "Escape" - context: Qt.ApplicationShortcut - onActivated: { - if (JamiQmlUtils.callIsFullscreen) - callStackView.toggleFullScreen() - else if (appWindow.visibility === Window.FullScreen) - appWindow.toggleFullScreen() - } - } - Shortcut { sequence: StandardKey.Quit context: Qt.ApplicationShortcut diff --git a/src/mainview/components/CallStackView.qml b/src/mainview/components/CallStackView.qml index 1f1d927fb..fd65c5a97 100644 --- a/src/mainview/components/CallStackView.qml +++ b/src/mainview/components/CallStackView.qml @@ -29,7 +29,6 @@ Rectangle { id: root property bool isAudioOnly: false - property bool changeWindowVisibility: false property var sipKeys: [ "1", "2", "3", "A", "4", "5", "6", "B", @@ -111,36 +110,22 @@ Rectangle { } function toggleFullScreen() { - var callPage = callStackMainView.currentItem - if (!callPage) - return - - // manual toggle here because of our fake fullscreen mode (F11) - // TODO: handle and save window states, not just a boolean isFullScreen - if (!appWindow.isFullscreen && !JamiQmlUtils.callIsFullscreen) { - root.changeWindowVisibility = true - appWindow.showFullScreen() - } else if (JamiQmlUtils.callIsFullscreen && root.changeWindowVisibility) { - root.changeWindowVisibility = false - appWindow.showNormal() - } - - JamiQmlUtils.callIsFullscreen = !JamiQmlUtils.callIsFullscreen - callPage.parent = JamiQmlUtils.callIsFullscreen ? - appContainer : - callStackMainView - if (!root.isAudioOnly) { - ongoingCallPage.handleParticipantsInfo(CallAdapter.getConferencesInfos()) + const transitionCb = function() { + if (!root.isAudioOnly) { + ongoingCallPage.handleParticipantsInfo( + CallAdapter.getConferencesInfos()) + } } - } - - Connections { - target: JamiQmlUtils - function onFullScreenCallEnded() { - if (appWindow.isFullscreen) { - toggleFullScreen() - } + if (!layoutManager.isCallFullscreen) { + layoutManager.pushFullScreenItem( + callStackMainView.currentItem, + callStackMainView, + transitionCb, + transitionCb) + } else { + layoutManager.removeFullScreenItem( + callStackMainView.currentItem) } } diff --git a/src/mainview/components/CallViewContextMenu.qml b/src/mainview/components/CallViewContextMenu.qml index 80ec92f32..d83ab7793 100644 --- a/src/mainview/components/CallViewContextMenu.qml +++ b/src/mainview/components/CallViewContextMenu.qml @@ -88,9 +88,10 @@ ContextMenuAutoLoader { GeneralMenuItem { id: fullScreen - itemName: JamiQmlUtils.callIsFullscreen ? - JamiStrings.exitFullScreen : JamiStrings.fullScreen - iconSource: JamiQmlUtils.callIsFullscreen ? + itemName: layoutManager.callIsFullscreen ? + JamiStrings.exitFullScreen : + JamiStrings.fullScreen + iconSource: layoutManager.callIsFullscreen ? JamiResources.close_fullscreen_24dp_svg : JamiResources.open_in_full_24dp_svg onClicked: { -- GitLab