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