diff --git a/src/app/commoncomponents/LocalVideo.qml b/src/app/commoncomponents/LocalVideo.qml index 2a8ccd04e9d4ec924ffc8a579c24304389808d09..88e28125a80ca9eae7c4a28b8e4ca8ebf852454b 100644 --- a/src/app/commoncomponents/LocalVideo.qml +++ b/src/app/commoncomponents/LocalVideo.qml @@ -23,19 +23,26 @@ import net.jami.Adapters 1.1 VideoView { id: root + property bool visibilityCondition: true + crop: true + visible: isRendering && visibilityCondition function startWithId(id, force = false) { if (id !== undefined && id.length === 0) { + stop(); + return; + } + const forceRestart = rendererId === id; + if (!forceRestart) { + // Stop previous device VideoDevices.stopDevice(rendererId); - rendererId = id; - } else { - const forceRestart = rendererId === id; - if (!forceRestart) { - // Stop previous device - VideoDevices.stopDevice(rendererId); - } - rendererId = VideoDevices.startDevice(id, forceRestart); } + rendererId = VideoDevices.startDevice(id, forceRestart); + } + + function stop() { + VideoDevices.stopDevice(rendererId); + rendererId = ""; } } diff --git a/src/app/commoncomponents/VideoView.qml b/src/app/commoncomponents/VideoView.qml index 67d92bad3e28f802eeeb06ef4e64770a1939a6b3..9f345fa377c0cb91bce851273186885ef1e9cd11 100644 --- a/src/app/commoncomponents/VideoView.qml +++ b/src/app/commoncomponents/VideoView.qml @@ -29,6 +29,10 @@ Item { property real invAspectRatio: (videoOutput.sourceRect.height / videoOutput.sourceRect.width) || 0.5625 // 16:9 default property bool crop: false property bool flip: false + property real blurRadius: 0 + + // We need to know if the frames are being rendered to the screen or not. + readonly property bool isRendering: videoProvider.activeRenderers[rendererId] !== undefined // This rect describes the actual rendered content rectangle // as the VideoOutput component may use PreserveAspectFit @@ -55,7 +59,7 @@ Item { antialiasing: true anchors.fill: parent - opacity: videoProvider.activeRenderers[rendererId] === true + opacity: isRendering visible: opacity fillMode: crop ? VideoOutput.PreserveAspectCrop : VideoOutput.PreserveAspectFit @@ -70,7 +74,7 @@ Item { layer.effect: FastBlur { source: videoOutput anchors.fill: root - radius: (1. - opacity) * 100 + radius: blurRadius ? blurRadius : (1. - opacity) * 100 } transform: Scale { diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml index b50e54193b4a94de7716ae3e57149a5e526e263c..26e47c1027041deb86b250f54216d002d84dd678 100644 --- a/src/app/mainview/components/CallOverlay.qml +++ b/src/app/mainview/components/CallOverlay.qml @@ -32,6 +32,7 @@ Item { id: root property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide) + property alias mainOverlayOpacity: mainOverlay.opacity signal chatButtonClicked signal fullScreenClicked diff --git a/src/app/mainview/components/InitialCallPage.qml b/src/app/mainview/components/InitialCallPage.qml index 1ce28c3c0d76a37054451e5c8ec4b467445572c9..4c9448e99fe305fe7e5f9b646c34e45a67176daa 100644 --- a/src/app/mainview/components/InitialCallPage.qml +++ b/src/app/mainview/components/InitialCallPage.qml @@ -36,34 +36,28 @@ Rectangle { color: "black" LocalVideo { - id: previewRenderer + id: localPreview anchors.centerIn: parent anchors.fill: parent - visible: !CurrentCall.isAudioOnly && CurrentAccount.videoEnabled_Video && VideoDevices.listSize !== 0 && ((CurrentCall.status >= Call.Status.INCOMING_RINGING && CurrentCall.status <= Call.Status.SEARCHING) || CurrentCall.status === Call.Status.CONNECTED) - opacity: 0.5 - // HACK: this is a workaround to the preview video starting - // and stopping a few times. The root cause should be investigated ASAP. - Timer { - id: controlPreview - property bool startVideo - interval: 1000 - onTriggered: { - var rendId = visible && startVideo ? VideoDevices.getDefaultDevice() : ""; - previewRenderer.startWithId(rendId); + readonly property bool start: { + if (CurrentCall.isAudioOnly || !CurrentAccount.videoEnabled_Video) { + return false; } - } - onVisibleChanged: { - controlPreview.stop(); - if (visible) { - controlPreview.startVideo = true; - controlPreview.interval = 1000; - } else { - controlPreview.startVideo = false; - controlPreview.interval = 0; + if (!VideoDevices.listSize) { + return false; } - controlPreview.start(); + const isCallStatusEligible = + (CurrentCall.status >= Call.Status.INCOMING_RINGING && + CurrentCall.status <= Call.Status.SEARCHING) || + CurrentCall.status === Call.Status.CONNECTED; + if (!isCallStatusEligible) { + return false; + } + return true; } + onStartChanged: localPreview.startWithId(start ? VideoDevices.getDefaultDevice() : "") + opacity: 0.5 } ListModel { diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml index 6eaeadefa74659154a82799c4f85e0ae4fa3d64e..b233c95fa398362ebccd07a5fbba8149a2d00256 100644 --- a/src/app/mainview/components/OngoingCallPage.qml +++ b/src/app/mainview/components/OngoingCallPage.qml @@ -41,10 +41,6 @@ Rectangle { // A link to the first child will provide access to the chat view. property var chatView: chatViewContainer.children[0] - onCallPreviewIdChanged: { - controlPreview.start(); - } - color: "black" Connections { @@ -131,8 +127,8 @@ Rectangle { onTapped: function (eventPoint, button) { if (button === Qt.RightButton) { - var isOnLocal = eventPoint.position.x >= previewRenderer.x && eventPoint.position.x <= previewRenderer.x + previewRenderer.width; - isOnLocal &= eventPoint.position.y >= previewRenderer.y && eventPoint.position.y <= previewRenderer.y + previewRenderer.height; + var isOnLocal = eventPoint.position.x >= localPreview.x && eventPoint.position.x <= localPreview.x + localPreview.width; + isOnLocal &= eventPoint.position.y >= localPreview.y && eventPoint.position.y <= localPreview.y + localPreview.height; isOnLocal |= participantsLayer.hoveredOverlaySinkId.indexOf("camera://") === 0; callOverlay.openCallViewContextMenuInPos(eventPoint.position.x, eventPoint.position.y, participantsLayer.hoveredOverlayUri, participantsLayer.hoveredOverlaySinkId, participantsLayer.hoveredOverVideoMuted, isOnLocal); } @@ -171,53 +167,90 @@ Rectangle { } LocalVideo { - id: previewRenderer + id: localPreview objectName: "localPreview" - visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference + readonly property var container: parent + readonly property string callPreviewId: root.callPreviewId + + visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && + !CurrentCall.isConference height: width * invAspectRatio - width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth) - x: callPageMainRect.width - previewRenderer.width - previewMargin - y: previewMarginYTop + width: Math.max(container.width / 5, JamiTheme.minimumPreviewWidth) flip: CurrentCall.flipSelf && !CurrentCall.isSharing + blurRadius: hidden ? 25 : 0 + onCallPreviewIdChanged: startWithId(callPreviewId) + onVisibleChanged: if (!visible) stop() - // HACK: this is a workaround to the preview video starting - // and stopping a few times. The root cause should be investigated ASAP. - Timer { - id: controlPreview - property bool startVideo - interval: 1000 - onTriggered: { - var rendId = visible && startVideo ? root.callPreviewId : ""; - previewRenderer.startWithId(rendId); - } - } + anchors.topMargin: previewMarginYTop + anchors.leftMargin: sideMargin + anchors.rightMargin: sideMargin + anchors.bottomMargin: previewMarginYBottom - onVisibleChanged: { - controlPreview.stop(); - if (visible) { - controlPreview.startVideo = true; - controlPreview.interval = 1000; - } else { - controlPreview.startVideo = false; - controlPreview.interval = 0; + opacity: hidden ? callOverlay.mainOverlayOpacity : 1 + + // Allow hiding the preview (available when anchored) + readonly property bool anchored: state !== "unanchored" + property bool hidden: false + readonly property real hiddenHandleSize: 32 + // Compute the margin as a function of the preview width in order to + // apply a negative margin and expose a constant width handle. + // If not hidden, return the previewMargin. + property real sideMargin: !hidden ? previewMargin : -(width - hiddenHandleSize) + // Animate the hiddenSize with a Behavior. + Behavior on sideMargin { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }} + readonly property bool onLeft: state.indexOf("left") !== -1 + PushButton { + id: hidePreviewButton + objectName: "hidePreviewButton" + + width: localPreview.hiddenHandleSize + state: localPreview.onLeft ? + (localPreview.hidden ? "right" : "left") : + (localPreview.hidden ? "left" : "right") + states: [ + State { + name: "left" + AnchorChanges { + target: hidePreviewButton + anchors.left: parent.left + } + }, + State { + name: "right" + AnchorChanges { + target: hidePreviewButton + anchors.right: parent.right + } + } + ] + anchors.top: parent.top + anchors.bottom: parent.bottom + opacity: (localPreview.anchored && hoverHandler.hovered) || localPreview.hidden + Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }} + visible: opacity > 0 + background: Rectangle { + readonly property color normalColor: JamiTheme.mediumGrey + color: JamiTheme.mediumGrey + opacity: hidePreviewButton.hovered ? 0.7 : 0.5 + Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }} } - controlPreview.start(); + normalImageSource: hidePreviewButton.state === "left" ? + JamiResources.chevron_left_black_24dp_svg : + JamiResources.chevron_right_black_24dp_svg + imageColor: JamiTheme.darkGreyColor + onClicked: localPreview.hidden = !localPreview.hidden + toolTipText: localPreview.hidden ? + JamiStrings.showLocalVideo : + JamiStrings.hideLocalVideo } - 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 + target: localPreview anchors.top: undefined anchors.right: undefined anchors.bottom: undefined @@ -227,33 +260,33 @@ Rectangle { State { name: "anchor_top_left" AnchorChanges { - target: previewRenderer - anchors.top: callPageMainRect.top - anchors.left: callPageMainRect.left + target: localPreview + anchors.top: localPreview.container.top + anchors.left: localPreview.container.left } }, State { name: "anchor_top_right" AnchorChanges { - target: previewRenderer - anchors.top: callPageMainRect.top - anchors.right: callPageMainRect.right + target: localPreview + anchors.top: localPreview.container.top + anchors.right: localPreview.container.right } }, State { name: "anchor_bottom_right" AnchorChanges { - target: previewRenderer - anchors.bottom: callPageMainRect.bottom - anchors.right: callPageMainRect.right + target: localPreview + anchors.bottom: localPreview.container.bottom + anchors.right: localPreview.container.right } }, State { name: "anchor_bottom_left" AnchorChanges { - target: previewRenderer - anchors.bottom: callPageMainRect.bottom - anchors.left: callPageMainRect.left + target: localPreview + anchors.bottom: localPreview.container.bottom + anchors.left: localPreview.container.left } } ] @@ -266,17 +299,23 @@ Rectangle { } } + HoverHandler { + id: hoverHandler + } + DragHandler { - readonly property var container: callPageMainRect + id: dragHandler + readonly property var container: localPreview.container target: parent dragThreshold: 4 + enabled: !localPreview.hidden 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"; + localPreview.state = "unanchored"; } else { const center = Qt.point(target.x + target.width / 2, target.y + target.height / 2); @@ -284,15 +323,15 @@ Rectangle { container.y + container.height / 2); if (center.x >= containerCenter.x) { if (center.y >= containerCenter.y) { - previewRenderer.state = "anchor_bottom_right"; + localPreview.state = "anchor_bottom_right"; } else { - previewRenderer.state = "anchor_top_right"; + localPreview.state = "anchor_top_right"; } } else { if (center.y >= containerCenter.y) { - previewRenderer.state = "anchor_bottom_left"; + localPreview.state = "anchor_bottom_left"; } else { - previewRenderer.state = "anchor_top_left"; + localPreview.state = "anchor_top_left"; } } } @@ -302,8 +341,8 @@ Rectangle { layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { - width: previewRenderer.width - height: previewRenderer.height + width: localPreview.width + height: localPreview.height radius: JamiTheme.primaryRadius } } diff --git a/src/app/net/jami/Constants/JamiStrings.qml b/src/app/net/jami/Constants/JamiStrings.qml index 611564a2275cdd8d6be0e7bbaad3e0710b2c5b1f..3316ed0797091a94e4e105e1d7f72892cdf176bf 100644 --- a/src/app/net/jami/Constants/JamiStrings.qml +++ b/src/app/net/jami/Constants/JamiStrings.qml @@ -785,6 +785,8 @@ Item { property string becomeHostOneCall: qsTr("Host only this call") property string hostThisCall: qsTr("Host this call") property string becomeDefaultHost: qsTr("Make me the default host for future calls") + property string showLocalVideo: qsTr("Show local video") + property string hideLocalVideo: qsTr("Hide local video") // Invitation View property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.") diff --git a/src/app/videodevices.cpp b/src/app/videodevices.cpp index a986c4f4b058e9269eb843f10fcc40a8db58ae6f..86eb9eed3f4359784befd2391b50a077bffa9e69 100644 --- a/src/app/videodevices.cpp +++ b/src/app/videodevices.cpp @@ -257,8 +257,12 @@ void VideoDevices::stopDevice(const QString& id) { if (!id.isEmpty()) { - lrcInstance_->avModel().stopPreview(id); - deviceOpen_ = false; + qInfo() << "Stopping device" << id; + if (lrcInstance_->avModel().stopPreview(id)) { + deviceOpen_ = false; + } else { + qWarning() << "Failed to stop device" << id; + } } } diff --git a/src/libclient/api/avmodel.h b/src/libclient/api/avmodel.h index fc4e277d16d4775d6d01f30bd5395fad04c1a46d..bb22c5c1b5d24891df226a8d42b95abee84047cc 100644 --- a/src/libclient/api/avmodel.h +++ b/src/libclient/api/avmodel.h @@ -243,7 +243,7 @@ public: * Stop preview renderer and the camera. * @param resource */ - Q_INVOKABLE void stopPreview(const QString& resource); + Q_INVOKABLE bool stopPreview(const QString& resource); /** * Get the list of available windows ids * X11: a id is of the form 0x0000000 diff --git a/src/libclient/avmodel.cpp b/src/libclient/avmodel.cpp index 214a514e234eaea1e0f96dfc89aeaa43a778be1a..3712bfcb1660e42a3316093711398b4869144d50 100644 --- a/src/libclient/avmodel.cpp +++ b/src/libclient/avmodel.cpp @@ -560,10 +560,10 @@ AVModel::startPreview(const QString& resource) return VideoManager::instance().openVideoInput(resource); } -void +bool AVModel::stopPreview(const QString& resource) { - VideoManager::instance().closeVideoInput(resource); + return VideoManager::instance().closeVideoInput(resource); } #ifdef WIN32 @@ -795,42 +795,50 @@ AVModel::useDirectRenderer() const #endif } -QString AVModel::createMediaPlayer(const QString& resource) +QString +AVModel::createMediaPlayer(const QString& resource) { return VideoManager::instance().createMediaPlayer(resource); } -void AVModel::closeMediaPlayer(const QString& resource) +void +AVModel::closeMediaPlayer(const QString& resource) { VideoManager::instance().closeMediaPlayer(resource); } -bool AVModel::pausePlayer(const QString& id, bool pause) +bool +AVModel::pausePlayer(const QString& id, bool pause) { return VideoManager::instance().pausePlayer(id, pause); } -bool AVModel::mutePlayerAudio(const QString& id, bool mute) +bool +AVModel::mutePlayerAudio(const QString& id, bool mute) { return VideoManager::instance().mutePlayerAudio(id, mute); } -bool AVModel::playerSeekToTime(const QString& id, int time) +bool +AVModel::playerSeekToTime(const QString& id, int time) { return VideoManager::instance().playerSeekToTime(id, time); } -qint64 AVModel::getPlayerPosition(const QString& id) +qint64 +AVModel::getPlayerPosition(const QString& id) { return VideoManager::instance().getPlayerPosition(id); } -qint64 AVModel::getPlayerDuration(const QString& id) +qint64 +AVModel::getPlayerDuration(const QString& id) { return VideoManager::instance().getPlayerDuration(id); } -void AVModel::setAutoRestart(const QString& id, bool restart) +void +AVModel::setAutoRestart(const QString& id, bool restart) { VideoManager::instance().setAutoRestart(id, restart); } diff --git a/src/libclient/qtwrapper/videomanager_wrap.h b/src/libclient/qtwrapper/videomanager_wrap.h index 9625c2b91a7cb029ed47fe9db2add9ef4f69c60d..88601c0bf66002183afb732e259280a506e5da59 100644 --- a/src/libclient/qtwrapper/videomanager_wrap.h +++ b/src/libclient/qtwrapper/videomanager_wrap.h @@ -130,10 +130,10 @@ public Q_SLOTS: // METHODS #endif } - void closeVideoInput(const QString& resource) + bool closeVideoInput(const QString& resource) { #ifdef ENABLE_VIDEO - libjami::closeVideoInput(resource.toLatin1().toStdString()); + return libjami::closeVideoInput(resource.toLatin1().toStdString()); #endif } diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp index d652a63edb8a400efd96b0bd58067a72bdff161e..ef1617ddac6f20cf9b93e46cc1594dcc4097b392 100644 --- a/tests/qml/main.cpp +++ b/tests/qml/main.cpp @@ -88,11 +88,9 @@ public Q_SLOTS: auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath); lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/"; - // Create 2 Account - QSignalSpy accountStatusChangedSpy(&lrcInstance_->accountModel(), - &AccountModel::accountStatusChanged); - + // Create 2 Accounts QSignalSpy accountAddedSpy(&lrcInstance_->accountModel(), &AccountModel::accountAdded); + aliceId = lrcInstance_->accountModel().createNewAccount(profile::Type::JAMI, "Alice"); accountAddedSpy.wait(15000); QCOMPARE(accountAddedSpy.count(), 1); diff --git a/tests/qml/src/TestWrapper.qml b/tests/qml/src/TestWrapper.qml index 0f50f3f8af329f70d12fdabb3509cab034cdec63..80dffc31d63be01df2f0f7be162d082673984451 100644 --- a/tests/qml/src/TestWrapper.qml +++ b/tests/qml/src/TestWrapper.qml @@ -32,6 +32,20 @@ Item { width: childrenRect.width height: childrenRect.height + // This is a helper function to wait for a signal to be emitted and check a condition. + function waitForSignalAndCheck(signalObject, signalName, action, checkExpression) { + // Create the SignalSpy component dynamically with the provided signal object and name. + const spy = Qt.createQmlObject('import QtTest 1.0; SignalSpy {}', this); + spy.target = signalObject; + spy.signalName = signalName; + // Perform the action that should emit the signal. + action(); + // Wait a maximum of 1 second for the signal to be emitted. + spy.wait(1000); + // Check the signal count and the provided expression. + return spy.count > 0 && checkExpression(); + } + // A binding to the windowShown property Binding { tw.appWindow: uut.Window.window diff --git a/tests/qml/src/tst_OngoingCallPage.qml b/tests/qml/src/tst_OngoingCallPage.qml index 8d3e06b3068399bbe35289d5a8e5230bc1d2c88b..6ff04036cd0c5cfbf12fcd83fe0633856e87d0db 100644 --- a/tests/qml/src/tst_OngoingCallPage.qml +++ b/tests/qml/src/tst_OngoingCallPage.qml @@ -38,7 +38,12 @@ TestWrapper { when: windowShown // Mouse events can only be handled // after the window has been shown. + property string dummyImgUrl + function initTestCase() { + // Create a dummy image file to use for the local preview. + dummyImgUrl = UtilsAdapter.urlFromLocalPath(UtilsAdapter.createDummyImage()); + // 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 @@ -79,50 +84,88 @@ TestWrapper { compare(callActionBar.visible, true) } - function test_checkLocalPreviewAnchoring() { + // Define a generic local preview test wrapper function that starts and stops + // the local preview, and calls the provided test function in between. + function localPreviewTestWrapper(testFunction) { 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); + + // The preview should be invisible at first. + compare(localPreview.visible, false); + + // Start a preview of a local resource and wait for the preview to become visible. + verify(waitForSignalAndCheck(localPreview, + "visibleChanged", + () => localPreview.startWithId(dummyImgUrl), + () => localPreview.visible)); + + // Call the provided test function. + testFunction(localPreview); // Stop the preview. - localPreview.startWithId(""); + verify(waitForSignalAndCheck(localPreview, + "visibleChanged", + () => localPreview.stop(), + () => !localPreview.visible)); + } + + function test_localPreviewAnchoring() { + localPreviewTestWrapper(function(localPreview) { + const container = localPreview.parent; + + // 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); + mouseRelease(localPreview); + }); + } + + function test_localPreviewHiding() { + localPreviewTestWrapper(function(localPreview) { + // Make sure the preview is anchored. + verify(localPreview.state.indexOf("unanchored") === -1); + + // It should also not be hidden. + compare(localPreview.hidden, false); + + // We presume that the preview is anchored and that once we hover over the + // local preview, that the hide button will become visible. + // Note: There is currently an issue where the CallOverlay is detecting hover + // events, but child controls are not. This should be addressed as it seems + // to affect MouseArea, HoverHandler, Buttons, and other controls that rely + // on hover events. For now, we'll manually trigger the onClicked event. + const hidePreviewButton = findChild(localPreview, "hidePreviewButton"); + hidePreviewButton.onClicked(); + compare(localPreview.hidden, true); + + // "Click" the hide button again to unhide the preview. + hidePreviewButton.onClicked(); + compare(localPreview.hidden, false); + }); } } }