From 91f32f2421b1949f9945c0338915ee204e5a39b1 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Fri, 2 Feb 2024 18:38:51 -0500
Subject: [PATCH] ongoingcallpage: refactor local preview corner snapping

Uses a more declarative approach to anchor the local preview.

Change-Id: I2544428a0c2585a8629639566c808dfc2808fd14
---
 .../mainview/components/OngoingCallPage.qml   | 159 +++++++++---------
 tests/qml/src/tst_OngoingCallPage.qml         |  64 ++++++-
 2 files changed, 138 insertions(+), 85 deletions(-)

diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml
index 9da40a6a0..6eaeadefa 100644
--- a/src/app/mainview/components/OngoingCallPage.qml
+++ b/src/app/mainview/components/OngoingCallPage.qml
@@ -30,12 +30,11 @@ import "../../commoncomponents"
 Rectangle {
     id: root
 
-    property point clickPos
+    // Constraints for the preview component.
     property int previewMargin: 15
     property int previewMarginYTop: previewMargin + 42
     property int previewMarginYBottom: previewMargin + 84
-    property int previewToX: 0
-    property int previewToY: 0
+
     property alias chatViewContainer: chatViewContainer
     property string callPreviewId
 
@@ -92,42 +91,6 @@ Rectangle {
         callOverlay.closeContextMenuAndRelatedWindows();
     }
 
-    function previewMagneticSnap() {
-        // Calculate the position where the previewRenderer should attach to.
-        var previewRendererCenter = Qt.point(previewRenderer.x + previewRenderer.width / 2, previewRenderer.y + previewRenderer.height / 2);
-        var parentCenter = Qt.point(parent.x + parent.width / 2, parent.y + parent.height / 2);
-        if (previewRendererCenter.x >= parentCenter.x) {
-            if (previewRendererCenter.y >= parentCenter.y) {
-                // Bottom right.
-                previewToX = Qt.binding(function () {
-                        return callPageMainRect.width - previewRenderer.width - previewMargin;
-                    });
-                previewToY = Qt.binding(function () {
-                        return callPageMainRect.height - previewRenderer.height - previewMarginYBottom;
-                    });
-            } else {
-                // Top right.
-                previewToX = Qt.binding(function () {
-                        return callPageMainRect.width - previewRenderer.width - previewMargin;
-                    });
-                previewToY = previewMarginYTop;
-            }
-        } else {
-            if (previewRendererCenter.y >= parentCenter.y) {
-                // Bottom left.
-                previewToX = previewMargin;
-                previewToY = Qt.binding(function () {
-                        return callPageMainRect.height - previewRenderer.height - previewMarginYBottom;
-                    });
-            } else {
-                // Top left.
-                previewToX = previewMargin;
-                previewToY = previewMarginYTop;
-            }
-        }
-        previewRenderer.state = "geoChanging";
-    }
-
     onWidthChanged: {
         if (chatViewContainer.visible && root.width < JamiTheme.mainViewPaneMinWidth * 2) {
             callPageMainRect.visible = false;
@@ -209,8 +172,9 @@ Rectangle {
 
             LocalVideo {
                 id: previewRenderer
-                visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference
+                objectName: "localPreview"
 
+                visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference
                 height: width * invAspectRatio
                 width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth)
                 x: callPageMainRect.width - previewRenderer.width - previewMargin
@@ -241,54 +205,97 @@ Rectangle {
                     controlPreview.start();
                 }
 
+                anchors.topMargin: previewMarginYTop
+                anchors.leftMargin: previewMargin
+                anchors.rightMargin: previewMargin
+                anchors.bottomMargin: previewMarginYBottom
+
+                state: "anchor_top_right"
+
                 states: [
+                    // Not anchored
+                    State {
+                        name: "unanchored"
+                        AnchorChanges {
+                            target: previewRenderer
+                            anchors.top: undefined
+                            anchors.right: undefined
+                            anchors.bottom: undefined
+                            anchors.left: undefined
+                        }
+                    },
+                    State {
+                        name: "anchor_top_left"
+                        AnchorChanges {
+                            target: previewRenderer
+                            anchors.top: callPageMainRect.top
+                            anchors.left: callPageMainRect.left
+                        }
+                    },
+                    State {
+                        name: "anchor_top_right"
+                        AnchorChanges {
+                            target: previewRenderer
+                            anchors.top: callPageMainRect.top
+                            anchors.right: callPageMainRect.right
+                        }
+                    },
                     State {
-                        name: "geoChanging"
-                        PropertyChanges {
+                        name: "anchor_bottom_right"
+                        AnchorChanges {
                             target: previewRenderer
-                            x: previewToX
-                            y: previewToY
+                            anchors.bottom: callPageMainRect.bottom
+                            anchors.right: callPageMainRect.right
+                        }
+                    },
+                    State {
+                        name: "anchor_bottom_left"
+                        AnchorChanges {
+                            target: previewRenderer
+                            anchors.bottom: callPageMainRect.bottom
+                            anchors.left: callPageMainRect.left
                         }
                     }
                 ]
 
                 transitions: Transition {
-                    PropertyAnimation {
-                        properties: "x,y"
-                        easing.type: Easing.OutExpo
+                    AnchorAnimation {
                         duration: 250
-
-                        onStopped: {
-                            previewRenderer.state = "";
-                        }
+                        easing.type: Easing.OutBack
+                        easing.overshoot: 1.5
                     }
                 }
 
-                MouseArea {
-                    id: dragMouseArea
-
-                    anchors.fill: previewRenderer
-
-                    onPressed: function (mouse) {
-                        clickPos = Qt.point(mouse.x, mouse.y);
-                    }
-
-                    onReleased: {
-                        previewRenderer.state = "";
-                        previewMagneticSnap();
-                    }
-
-                    onPositionChanged: function (mouse) {
-                        // Calculate mouse position relative change.
-                        var delta = Qt.point(mouse.x - clickPos.x, mouse.y - clickPos.y);
-                        var deltaW = previewRenderer.x + delta.x + previewRenderer.width;
-                        var deltaH = previewRenderer.y + delta.y + previewRenderer.height;
-
-                        // Check if the previewRenderer exceeds the border of callPageMainRect.
-                        if (deltaW < callPageMainRect.width && previewRenderer.x + delta.x > 1)
-                            previewRenderer.x += delta.x;
-                        if (deltaH < callPageMainRect.height && previewRenderer.y + delta.y > 1)
-                            previewRenderer.y += delta.y;
+                DragHandler {
+                    readonly property var container: callPageMainRect
+                    target: parent
+                    dragThreshold: 4
+                    xAxis.maximum: container.width - parent.width - previewMargin
+                    xAxis.minimum: previewMargin
+                    yAxis.maximum: container.height - parent.height - previewMarginYBottom
+                    yAxis.minimum: previewMarginYTop
+                    onActiveChanged: {
+                        if (active) {
+                            previewRenderer.state = "unanchored";
+                        } else {
+                            const center = Qt.point(target.x + target.width / 2,
+                                                    target.y + target.height / 2);
+                            const containerCenter = Qt.point(container.x + container.width / 2,
+                                                             container.y + container.height / 2);
+                            if (center.x >= containerCenter.x) {
+                                if (center.y >= containerCenter.y) {
+                                    previewRenderer.state = "anchor_bottom_right";
+                                } else {
+                                    previewRenderer.state = "anchor_top_right";
+                                }
+                            } else {
+                                if (center.y >= containerCenter.y) {
+                                    previewRenderer.state = "anchor_bottom_left";
+                                } else {
+                                    previewRenderer.state = "anchor_top_left";
+                                }
+                            }
+                        }
                     }
                 }
 
diff --git a/tests/qml/src/tst_OngoingCallPage.qml b/tests/qml/src/tst_OngoingCallPage.qml
index 0485d61c4..8d3e06b30 100644
--- a/tests/qml/src/tst_OngoingCallPage.qml
+++ b/tests/qml/src/tst_OngoingCallPage.qml
@@ -21,6 +21,7 @@ import QtTest
 
 import net.jami.Models 1.1
 import net.jami.Constants 1.1
+import net.jami.Adapters 1.1
 
 import "../../../src/app/"
 import "../../../src/app/mainview/components"
@@ -33,15 +34,11 @@ TestWrapper {
         height: 600
 
         TestCase {
-            name: "Check basic visibility of action bar during a call"
+            name: "Check various components within the OngoingCallPage"
             when: windowShown // Mouse events can only be handled
                               // after the window has been shown.
 
-            property var mainOverlay
-
             function initTestCase() {
-                mainOverlay = findChild(uut, "mainOverlay")
-
                 // The CallActionBar on the OngoingCallPage starts out invisible and
                 // is made visible whenever the user moves their mouse.
                 // This is implemented via an event filter in the CallOverlayModel
@@ -49,12 +46,15 @@ TestWrapper {
                 // visible. In the actual Jami application, this happens when a call
                 // is started, but we need to toggle the visiblity manually here
                 // because the MainOverlay is visible at the beginning of the test.
-                mainOverlay.visible = false
-                mainOverlay.visible = true
+                const mainOverlay = findChild(uut, "mainOverlay");
+                mainOverlay.visible = false;
+                mainOverlay.visible = true;
             }
 
-            function test_checkCallActionBarVisibility() {
-                var callActionBar = findChild(mainOverlay, "callActionBar")
+            // The following test is labeled with "0" to make sure it runs first.
+            // This prevents having to wait for the CallActionBar to fade out.
+            function test_0_checkCallActionBarVisibility() {
+                var callActionBar = findChild(uut, "callActionBar")
 
                 // The primary and secondary actions in the CallActionBar are currently being added
                 // one by one (not using a loop) to CallOverlayModel in the Component.onCompleted
@@ -78,6 +78,52 @@ TestWrapper {
                 wait(waitTime)
                 compare(callActionBar.visible, true)
             }
+
+            function test_checkLocalPreviewAnchoring() {
+                const localPreview = findChild(uut, "localPreview");
+                const container = localPreview.parent;
+
+                // The preview should normally be invisible at first, but there is a bug
+                // in the current implementation that makes it visible. This will need to
+                // be adjusted once the bug is fixed.
+                compare(localPreview.visible, true);
+
+                // Start a preview of a local resource.
+                const dummyImgFile = UtilsAdapter.createDummyImage();
+                localPreview.startWithId(UtilsAdapter.urlFromLocalPath(dummyImgFile));
+
+                // First check that the preview is anchored.
+                verify(localPreview.state.indexOf("unanchored") === -1);
+
+                const containerCenter = Qt.point(container.width / 2, container.height / 2);
+                function moveAndVerifyState(dx, dy, expectedState) {
+                    const previewCenter = Qt.point(localPreview.x + localPreview.width / 2,
+                                                   localPreview.y + localPreview.height / 2);
+                    const destination = Qt.point(containerCenter.x + dx,
+                                                 containerCenter.y + dy);
+                    // Position the mouse at the center of the preview, then drag it until
+                    // we reach the destination point.
+                    mouseDrag(container, previewCenter.x, previewCenter.y,
+                              destination.x - previewCenter.x, destination.y - previewCenter.y);
+                    wait(250);
+                    compare(localPreview.state, expectedState);
+                }
+
+                const dx = 1;
+                const dy = 1;
+                moveAndVerifyState(-dx, -dy, "anchor_top_left");
+                moveAndVerifyState(dx, -dy, "anchor_top_right");
+                moveAndVerifyState(-dx, dy, "anchor_bottom_left");
+                moveAndVerifyState(dx, dy, "anchor_bottom_right");
+
+                // Verify that during a drag process, the preview is unanchored.
+                mousePress(localPreview);
+                mouseMove(localPreview, 100, 100);
+                verify(localPreview.state.indexOf("unanchored") !== -1);
+
+                // Stop the preview.
+                localPreview.startWithId("");
+            }
         }
     }
 }
-- 
GitLab