diff --git a/qml.qrc b/qml.qrc index b80096d9a076c7f550e0b672610bfc36459bf66d..5b974ba7e5de68a4f0d5f8855f3f95fc2fc2e0b4 100644 --- a/qml.qrc +++ b/qml.qrc @@ -136,5 +136,6 @@ <file>src/mainview/components/ConversationListView.qml</file> <file>src/mainview/components/SmartListItemDelegate.qml</file> <file>src/mainview/components/BadgeNotifier.qml</file> + <file>src/mainview/components/ParticipantsLayer.qml</file> </qresource> </RCC> diff --git a/src/calloverlaymodel.cpp b/src/calloverlaymodel.cpp index aae7c75207d74dedef9c47fbe57a7871b9355872..fb130de346dd4016665c1a2a1fd8b029e237f95b 100644 --- a/src/calloverlaymodel.cpp +++ b/src/calloverlaymodel.cpp @@ -18,6 +18,10 @@ #include "calloverlaymodel.h" +#include <QEvent> +#include <QMouseEvent> +#include <QQuickWindow> + CallControlListModel::CallControlListModel(QObject* parent) : QAbstractListModel(parent) {} @@ -183,6 +187,42 @@ CallOverlayModel::overflowHiddenModel() return QVariant::fromValue(overflowHiddenModel_); } +void +CallOverlayModel::registerFilter(QQuickWindow* object, QQuickItem* item) +{ + if (!object || !item || watchedItems_.contains(item)) + return; + watchedItems_.push_back(item); + if (watchedItems_.size() == 1) + object->installEventFilter(this); +} + +void +CallOverlayModel::unregisterFilter(QQuickWindow* object, QQuickItem* item) +{ + if (!object || !item || !watchedItems_.contains(item)) + return; + watchedItems_.removeOne(item); + if (watchedItems_.size() == 0) + object->removeEventFilter(this); +} + +bool +CallOverlayModel::eventFilter(QObject* object, QEvent* event) +{ + if (event->type() == QEvent::MouseMove) { + auto mouseEvent = static_cast<QMouseEvent*>(event); + QPoint eventPos(mouseEvent->x(), mouseEvent->y()); + auto windowItem = static_cast<QQuickWindow*>(object)->contentItem(); + Q_FOREACH (const auto& item, watchedItems_) { + if (item->contains(windowItem->mapToItem(item, eventPos))) { + Q_EMIT mouseMoved(item); + } + } + } + return QObject::eventFilter(object, event); +} + void CallOverlayModel::setControlRanges() { diff --git a/src/calloverlaymodel.h b/src/calloverlaymodel.h index f3f70fb1f77c4eb3e3fab22a62c25c3ec5a012cf..856a71a3dd91d78aaf0d97b668aff37ca199f36c 100644 --- a/src/calloverlaymodel.h +++ b/src/calloverlaymodel.h @@ -25,7 +25,7 @@ #include <QObject> #include <QQmlEngine> #include <QSortFilterProxyModel> -#include <QDebug> +#include <QQuickItem> #define CC_ROLES \ X(QObject*, ItemAction) \ @@ -103,6 +103,13 @@ public: Q_INVOKABLE QVariant overflowVisibleModel(); Q_INVOKABLE QVariant overflowHiddenModel(); + Q_INVOKABLE void registerFilter(QQuickWindow* object, QQuickItem* item); + Q_INVOKABLE void unregisterFilter(QQuickWindow* object, QQuickItem* item); + bool eventFilter(QObject* object, QEvent* event) override; + +Q_SIGNALS: + void mouseMoved(QQuickItem* item); + private Q_SLOTS: void setControlRanges(); @@ -114,4 +121,6 @@ private: IndexRangeFilterProxyModel* overflowModel_; IndexRangeFilterProxyModel* overflowVisibleModel_; IndexRangeFilterProxyModel* overflowHiddenModel_; + + QList<QQuickItem*> watchedItems_; }; diff --git a/src/commoncomponents/ModalPopup.qml b/src/commoncomponents/ModalPopup.qml index 83e7d8201ca0f2819fdd0f2c45af963c2a905bd9..53d4753b3d70e380c4dc46700acb0cef2d83ef98 100644 --- a/src/commoncomponents/ModalPopup.qml +++ b/src/commoncomponents/ModalPopup.qml @@ -25,8 +25,6 @@ import net.jami.Constants 1.0 Popup { id: root - property int fadeDuration: 100 - // convient access to closePolicy property bool autoClose: true @@ -76,13 +74,13 @@ Popup { enter: Transition { NumberAnimation { properties: "opacity"; from: 0.0; to: 1.0 - duration: fadeDuration + duration: JamiTheme.shortFadeDuration } } exit: Transition { NumberAnimation { properties: "opacity"; from: 1.0; to: 0.0 - duration: fadeDuration + duration: JamiTheme.shortFadeDuration } } } diff --git a/src/commoncomponents/PushButton.qml b/src/commoncomponents/PushButton.qml index 314d5df1b4c53b89e56f60e0827bff6e80564a00..255fb853a20acc99f1f12a2fb107f76b31a9ba33 100644 --- a/src/commoncomponents/PushButton.qml +++ b/src/commoncomponents/PushButton.qml @@ -58,7 +58,7 @@ AbstractButton { property string checkedColor: pressedColor // State transition duration - property int duration: JamiTheme.fadeDuration + property int duration: JamiTheme.shortFadeDuration // Image properties property alias source: image.source diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml index 55f256c9220b0d9eaeecde5dd7e8e2cd8c4ee604..f70caac3eea636a2c25b84493a514f7695c30916 100644 --- a/src/constant/JamiTheme.qml +++ b/src/constant/JamiTheme.qml @@ -163,7 +163,10 @@ Item { property color bgSideBarDarkMode_: rgba256(24, 24, 24, 100) property color bgDarkMode_: rgba256(32, 32, 32, 100) - property int fadeDuration: 150 + property int shortFadeDuration: 150 + property int overlayFadeDelay: 2000 + property int overlayFadeDuration: 500 + property int smartListTransitionDuration: 120 // Sizes property real splitViewHandlePreferredWidth: 4 @@ -185,7 +188,6 @@ Item { property real accountListAvatarSize: 40 property real smartListItemHeight: 64 property real smartListAvatarSize: 52 - property real smartListTransitionDuration: 120 property real avatarSizeInCall: 130 property real callButtonPreferredSize: 50 diff --git a/src/mainview/components/AccountComboBox.qml b/src/mainview/components/AccountComboBox.qml index 28152bb7a4da09f1af5f910ca44d9f2aad433066..36a4eb2a11fca77f2e0b3b5f9901592f237d651f 100644 --- a/src/mainview/components/AccountComboBox.qml +++ b/src/mainview/components/AccountComboBox.qml @@ -76,7 +76,7 @@ Label { Qt.lighter(JamiTheme.hoverColor, 1.05) : JamiTheme.backgroundColor Behavior on color { - ColorAnimation { duration: JamiTheme.fadeDuration } + ColorAnimation { duration: JamiTheme.shortFadeDuration } } // TODO: this can be removed when frameless window is implemented diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml index 1c375c737137c5ac6fc33ff1cc8e0bc720f02de3..ea5b150fc8b492052de4e77ec5e28268e945294a 100644 --- a/src/mainview/components/CallOverlay.qml +++ b/src/mainview/components/CallOverlay.qml @@ -33,22 +33,25 @@ import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation import "../../commoncomponents" -Rectangle { +Item { id: root + property alias participantsLayer: __participantsLayer property string timeText: "00:00" property string remoteRecordingLabel: "" property bool isVideoMuted: true property bool isAudioOnly: false property string bestName: "" - property var participantOverlays: [] - property var participantComponent: Qt.createComponent("ParticipantOverlay.qml") - signal overlayChatButtonClicked onVisibleChanged: if (!visible) callViewContextMenu.close() + ParticipantsLayer { + id: __participantsLayer + anchors.fill: parent + } + function setRecording(localIsRecording) { callViewContextMenu.localIsRecording = localIsRecording recordingRect.visible = localIsRecording @@ -57,6 +60,7 @@ Rectangle { function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, isRecording, isSIP, isConferenceCall) { + root.isVideoMuted = isVideoMuted callViewContextMenu.isSIP = isSIP callViewContextMenu.isPaused = isPaused callViewContextMenu.isAudioOnly = isAudioOnly @@ -83,112 +87,6 @@ Rectangle { PluginHandlerPickerCreation.closePluginHandlerPicker() } - // returns true if participant is not fully maximized - function showMaximize(pX, pY, pW, pH) { - // Hack: -1 offset added to avoid problems with odd sizes - return (pX - distantRenderer.getXOffset() !== 0 - || pY - distantRenderer.getYOffset() !== 0 - || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1) - || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1)) - } - - function handleParticipantsInfo(infos) { - if (root.isAudioOnly) - return; - // TODO: in the future the conference layout should be entirely managed by the client - // Hack: truncate and ceil participant's overlay position and size to correct - // when they are not exacts - callOverlay.updateMenu() - var showMax = false - var showMin = false - - var deletedUris = [] - var currentUris = [] - for (var p in participantOverlays) { - if (participantOverlays[p]) { - var participant = infos.find(e => e.uri === participantOverlays[p].uri); - if (participant) { - // Update participant's information - var newX = Math.trunc(distantRenderer.getXOffset() - + participant.x * distantRenderer.getScaledWidth()) - var newY = Math.trunc(distantRenderer.getYOffset() - + participant.y * distantRenderer.getScaledHeight()) - - var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth()) - var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight()) - - var newVisible = participant.w !== 0 && participant.h !== 0 - if (participantOverlays[p].x !== newX) - participantOverlays[p].x = newX - if (participantOverlays[p].y !== newY) - participantOverlays[p].y = newY - if (participantOverlays[p].width !== newWidth) - participantOverlays[p].width = newWidth - if (participantOverlays[p].height !== newHeight) - participantOverlays[p].height = newHeight - if (participantOverlays[p].visible !== newVisible) - participantOverlays[p].visible = newVisible - - showMax = showMaximize(participantOverlays[p].x, - participantOverlays[p].y, - participantOverlays[p].width, - participantOverlays[p].height) - - participantOverlays[p].setMenu(participant.uri, participant.bestName, - participant.isLocal, participant.active, showMax) - if (participant.videoMuted) - participantOverlays[p].setAvatar(true, participant.avatar, participant.uri, participant.isLocal, participant.isContact) - else - participantOverlays[p].setAvatar(false) - currentUris.push(participantOverlays[p].uri) - } else { - // Participant is no longer in conference - deletedUris.push(participantOverlays[p].uri) - participantOverlays[p].destroy() - } - } - } - participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri)) - - if (infos.length === 0) { // Return to normal call - previewRenderer.visible = true - for (var part in participantOverlays) { - if (participantOverlays[part]) { - participantOverlays[part].destroy() - } - } - participantOverlays = [] - } else { - previewRenderer.visible = false - for (var infoVariant in infos) { - // Only create overlay for new participants - if (!currentUris.includes(infos[infoVariant].uri)) { - var hover = participantComponent.createObject(callOverlayRectMouseArea, { - x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()), - y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()), - width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()), - height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()), - visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0 - }) - if (!hover) { - console.log("Error when creating the hover") - return - } - - showMax = showMaximize(hover.x, hover.y, hover.width, hover.height) - - hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName, - infos[infoVariant].isLocal, infos[infoVariant].active, showMax) - if (infos[infoVariant].videoMuted) - hover.setAvatar(true, infos[infoVariant].avatar, infos[infoVariant].uri, infos[infoVariant].isLocal, infos[infoVariant].isContact) - else - hover.setAvatar(false) - participantOverlays.push(hover) - } - } - } - } - // x, y position does not need to be translated // since they all fill the call page function openCallViewContextMenuInPos(x, y) { @@ -224,8 +122,6 @@ Rectangle { recordingRect.visible = callViewContextMenu.localIsRecording } - anchors.fill: parent - SipInputPanel { id: sipInputPanel @@ -233,158 +129,6 @@ Rectangle { y: root.height / 2 - sipInputPanel.height / 2 } - // Timer to decide when overlay fade out. - Timer { - id: callOverlayTimer - interval: 5000 - onTriggered: { - if (overlayUpperPartRect.state !== 'freezed') { - overlayUpperPartRect.state = 'freezed' - resetLabelsTimer.restart() - } - if (callOverlayButtonGroup.state !== 'freezed') { - callOverlayButtonGroup.state = 'freezed' - resetLabelsTimer.restart() - } - } - } - - // Timer to reset recording label and call duration time - Timer { - id: resetLabelsTimer - - interval: 1000 - running: root.visible - repeat: true - onTriggered: { - timeText = CallAdapter.getCallDurationTime(LRCInstance.currentAccountId, - LRCInstance.selectedConvUid) - if (callOverlayButtonGroup.state === 'freezed' - && !callViewContextMenu.peerIsRecording) - remoteRecordingLabel = "" - } - } - - Rectangle { - id: overlayUpperPartRect - - anchors.top: root.top - - width: root.width - height: 50 - opacity: 0 - - RowLayout { - id: overlayUpperPartRectRowLayout - - anchors.fill: parent - - Text { - id: jamiBestNameText - - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.preferredWidth: overlayUpperPartRect.width / 3 - Layout.preferredHeight: 50 - leftPadding: 16 - - font.pointSize: JamiTheme.textFontSize - - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - - text: textMetricsjamiBestNameText.elidedText - color: "white" - - TextMetrics { - id: textMetricsjamiBestNameText - font: jamiBestNameText.font - text: { - if (!root.isAudioOnly) { - if (remoteRecordingLabel === "") { - return root.bestName - } else { - return remoteRecordingLabel - } - } - return "" - } - elideWidth: overlayUpperPartRect.width / 3 - elide: Qt.ElideRight - } - } - - Text { - id: callTimerText - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.preferredWidth: 64 - Layout.minimumWidth: 64 - Layout.preferredHeight: 48 - Layout.rightMargin: recordingRect.visible? - 0 : JamiTheme.preferredMarginSize - font.pointSize: JamiTheme.textFontSize - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - text: textMetricscallTimerText.elidedText - color: "white" - TextMetrics { - id: textMetricscallTimerText - font: callTimerText.font - text: timeText - elideWidth: overlayUpperPartRect.width / 4 - elide: Qt.ElideRight - } - } - - Rectangle { - id: recordingRect - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.rightMargin: JamiTheme.preferredMarginSize - height: 16 - width: 16 - radius: height / 2 - color: "red" - - SequentialAnimation on color { - loops: Animation.Infinite - running: true - ColorAnimation { from: "red"; to: "transparent"; duration: 500 } - ColorAnimation { from: "transparent"; to: "red"; duration: 500 } - } - } - } - - color: "transparent" - - - // Rect states: "entered" state should make overlay fade in, - // "freezed" state should make overlay fade out. - // Combine with PropertyAnimation of opacity. - states: [ - State { - name: "entered" - PropertyChanges { - target: overlayUpperPartRect - opacity: 1 - } - }, - State { - name: "freezed" - PropertyChanges { - target: overlayUpperPartRect - opacity: 0 - } - } - ] - - transitions: Transition { - PropertyAnimation { - target: overlayUpperPartRect - property: "opacity" - duration: 1000 - } - } - } - ResponsiveImage { id: onHoldImage @@ -399,136 +143,167 @@ Rectangle { source: "qrc:/images/icons/ic_pause_white_100px.svg" } - CallOverlayButtonGroup { - id: callOverlayButtonGroup - - anchors.bottom: root.bottom - anchors.bottomMargin: 10 - anchors.horizontalCenter: root.horizontalCenter + Item { + id: mainOverlay - height: 56 - width: root.width + anchors.fill: parent opacity: 0 - onChatButtonClicked: { - root.overlayChatButtonClicked() - } + // (un)subscribe to an app-wide mouse move event trap filtered + // for the overlay's geometry + onVisibleChanged: visible ? + CallOverlayModel.registerFilter(appWindow, this) : + CallOverlayModel.unregisterFilter(appWindow, this) - onAddToConferenceButtonClicked: { - // Create contact picker - conference. - ContactPickerCreation.createContactPickerObjects( - ContactList.CONFERENCE, - root) - ContactPickerCreation.openContactPicker() - } + Connections { + target: CallOverlayModel - states: [ - State { - name: "entered" - PropertyChanges { - target: callOverlayButtonGroup - opacity: 1 - } - }, - State { - name: "freezed" - PropertyChanges { - target: callOverlayButtonGroup - opacity: 0 + function onMouseMoved(item) { + if (item === mainOverlay) { + mainOverlay.opacity = 1 + fadeOutTimer.restart() } } - ] + } - transitions: Transition { - PropertyAnimation { - target: callOverlayButtonGroup - property: "opacity" - duration: 1000 + // control overlay fade out. + Timer { + id: fadeOutTimer + interval: JamiTheme.overlayFadeDelay + onTriggered: { + if (callOverlayButtonGroup.hovered) + return + mainOverlay.opacity = 0 + resetLabelsTimer.restart() } } - } - // MouseAreas to make sure that overlay states are correctly set. - MouseArea { - id: callOverlayButtonGroupLeftSideMouseArea + // Timer to reset recording label and call duration time + Timer { + id: resetLabelsTimer + interval: 1000 + running: root.visible + repeat: true + onTriggered: { + timeText = CallAdapter.getCallDurationTime(LRCInstance.currentAccountId, + LRCInstance.selectedConvUid) + if (mainOverlay.opacity === 0 && !callViewContextMenu.peerIsRecording) + remoteRecordingLabel = "" + } + } - anchors.bottom: root.bottom - anchors.left: root.left + Item { + id: overlayUpperPartRect - width: root.width / 6 - height: 60 + anchors.top: parent.top - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.NoButton + width: parent.width + height: 50 - onEntered: { - callOverlayRectMouseArea.entered() - } + RowLayout { + anchors.fill: parent - onMouseXChanged: { - callOverlayRectMouseArea.entered() - } - } + Text { + id: jamiBestNameText - MouseArea { - id: callOverlayButtonGroupRightSideMouseArea + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.preferredWidth: overlayUpperPartRect.width / 3 + Layout.preferredHeight: 50 + leftPadding: 16 - anchors.bottom: root.bottom - anchors.right: root.right + font.pointSize: JamiTheme.textFontSize - width: root.width / 6 - height: 60 + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.NoButton + text: textMetricsjamiBestNameText.elidedText + color: "white" - onEntered: { - callOverlayRectMouseArea.entered() - } + TextMetrics { + id: textMetricsjamiBestNameText + font: jamiBestNameText.font + text: { + if (!root.isAudioOnly) { + if (remoteRecordingLabel === "") { + return root.bestName + } else { + return remoteRecordingLabel + } + } + return "" + } + elideWidth: overlayUpperPartRect.width / 3 + elide: Qt.ElideRight + } + } - onMouseXChanged: { - callOverlayRectMouseArea.entered() - } - } + Text { + id: callTimerText + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.preferredWidth: 64 + Layout.minimumWidth: 64 + Layout.preferredHeight: 48 + Layout.rightMargin: recordingRect.visible? + 0 : JamiTheme.preferredMarginSize + font.pointSize: JamiTheme.textFontSize + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: textMetricscallTimerText.elidedText + color: "white" + TextMetrics { + id: textMetricscallTimerText + font: callTimerText.font + text: timeText + elideWidth: overlayUpperPartRect.width / 4 + elide: Qt.ElideRight + } + } - MouseArea { - id: callOverlayRectMouseArea + Rectangle { + id: recordingRect + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.rightMargin: JamiTheme.preferredMarginSize + height: 16 + width: 16 + radius: height / 2 + color: "red" + + SequentialAnimation on color { + loops: Animation.Infinite + running: true + ColorAnimation { from: "red"; to: "transparent"; duration: 500 } + ColorAnimation { from: "transparent"; to: "red"; duration: 500 } + } + } + } + } - anchors.top: root.top + CallOverlayButtonGroup { + id: callOverlayButtonGroup - width: root.width - height: root.height + anchors.bottom: parent.bottom + anchors.bottomMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.LeftButton + height: 56 + width: root.width - function resetStates() { - if (overlayUpperPartRect.state !== 'entered') { - overlayUpperPartRect.state = 'entered' + onChatButtonClicked: { + root.overlayChatButtonClicked() } - if (callOverlayButtonGroup.state !== 'entered') { - callOverlayButtonGroup.state = 'entered' - } - callOverlayTimer.restart() - } - onReleased: { - resetStates() - } - onEntered: { - resetStates() + onAddToConferenceButtonClicked: { + // Create contact picker - conference. + ContactPickerCreation.createContactPickerObjects( + ContactList.CONFERENCE, + root) + ContactPickerCreation.openContactPicker() + } } - onMouseXChanged: { - resetStates() - } + Behavior on opacity { NumberAnimation { duration: JamiTheme.overlayFadeDuration }} } - color: "transparent" - CallViewContextMenu { id: callViewContextMenu diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml index 1e6eee8e3612f36f3fd2581f6f63eef64a029736..4783fb1d40aab295478cdb769fd87f8c68838204 100644 --- a/src/mainview/components/CallOverlayButtonGroup.qml +++ b/src/mainview/components/CallOverlayButtonGroup.qml @@ -29,7 +29,7 @@ import net.jami.Constants 1.0 import "../../commoncomponents" -Rectangle { +Control { id: root // ButtonCounts here is to make sure that flow layout margin is calculated correctly, @@ -56,7 +56,6 @@ Rectangle { noVideoButton.checked = isVideoMuted } - color: "transparent" z: 2 RowLayout { diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml index a549db802ee5ab11e9f05963bc1811e0d48299c0..45bfbc0ef617e834e9bf9243c6315d75880e16ac 100644 --- a/src/mainview/components/OngoingCallPage.qml +++ b/src/mainview/components/OngoingCallPage.qml @@ -30,7 +30,7 @@ import net.jami.Constants 1.0 import "../../commoncomponents" -Rectangle { +Rectangle { id: root property var accountPeerPair: ["", ""] @@ -45,11 +45,13 @@ Rectangle { property alias callId: distantRenderer.rendererId property var linkedWebview: null + color: "black" + onAccountPeerPairChanged: { if (accountPeerPair[0] === "" || accountPeerPair[1] === "") return; contactImage.updateImage(accountPeerPair[1]) - callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos()) + callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos()) bestName = UtilsAdapter.getBestName(accountPeerPair[0], accountPeerPair[1]) var id = UtilsAdapter.getBestId(accountPeerPair[0], accountPeerPair[1]) @@ -95,7 +97,7 @@ Rectangle { } else { bestName = "" } - callOverlay.handleParticipantsInfo(infos) + callOverlay.participantsLayer.update(infos) } function previewMagneticSnap() { @@ -178,49 +180,6 @@ Rectangle { callOverlay.openCallViewContextMenuInPos(mouse.x, mouse.y) } - CallOverlay { - id: callOverlay - - anchors.fill: parent - - Connections { - target: CallAdapter - - function onUpdateOverlay(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, - isRecording, isSIP, isConferenceCall, bestName) { - callOverlay.showOnHoldImage(isPaused) - audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly - callOverlay.updateButtonStatus(isPaused, - isAudioOnly, - isAudioMuted, - isVideoMuted, - isRecording, isSIP, - isConferenceCall) - root.bestName = bestName - callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos()) - } - - function onShowOnHoldLabel(isPaused) { - callOverlay.showOnHoldImage(isPaused) - audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly - } - - function onRemoteRecordingChanged(label, state) { - callOverlay.showRemoteRecording(label, state) - } - - function onEraseRemoteRecording() { - callOverlay.resetRemoteRecording() - } - } - - onOverlayChatButtonClicked: { - inCallMessageWebViewStack.visible ? - closeInCallConversation() : - openInCallConversation() - } - } - DistantRenderer { id: distantRenderer @@ -232,7 +191,7 @@ Rectangle { visible: !root.isAudioOnly onOffsetChanged: { - callOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos()) + callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos()) } } @@ -334,6 +293,49 @@ Rectangle { } } + CallOverlay { + id: callOverlay + + anchors.fill: parent + + Connections { + target: CallAdapter + + function onUpdateOverlay(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, + isRecording, isSIP, isConferenceCall, bestName) { + callOverlay.showOnHoldImage(isPaused) + audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly + callOverlay.updateButtonStatus(isPaused, + isAudioOnly, + isAudioMuted, + isVideoMuted, + isRecording, isSIP, + isConferenceCall) + root.bestName = bestName + callOverlay.participantsLayer.update(CallAdapter.getConferencesInfos()) + } + + function onShowOnHoldLabel(isPaused) { + callOverlay.showOnHoldImage(isPaused) + audioCallPageRectCentralRect.visible = !isPaused && root.isAudioOnly + } + + function onRemoteRecordingChanged(label, state) { + callOverlay.showRemoteRecording(label, state) + } + + function onEraseRemoteRecording() { + callOverlay.resetRemoteRecording() + } + } + + onOverlayChatButtonClicked: { + inCallMessageWebViewStack.visible ? + closeInCallConversation() : + openInCallConversation() + } + } + ColumnLayout { id: audioCallPageRectCentralRect anchors.centerIn: parent @@ -400,6 +402,4 @@ Rectangle { clip: true } } - - color: "black" } diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml index cee174d776d837819fbf64f9601b9806339a80d8..7d166ae171db9b35174b36077a2253763bf26123 100644 --- a/src/mainview/components/ParticipantOverlay.qml +++ b/src/mainview/components/ParticipantOverlay.qml @@ -30,7 +30,7 @@ import net.jami.Constants 1.0 import "../../commoncomponents" -Rectangle { +Item { id: root // svg path for the participant indicators background shape @@ -38,8 +38,10 @@ Rectangle { property int shapeHeight: 16 property int shapeRadius: 6 property string pathShape: "M0,0 h%1 q%2,0 %2,%2 v%3 h-%4 z" - .arg(shapeWidth-shapeRadius).arg(shapeRadius).arg(shapeHeight-shapeRadius). - arg(shapeWidth) + .arg(shapeWidth - shapeRadius) + .arg(shapeRadius) + .arg(shapeHeight - shapeRadius) + .arg(shapeWidth) property string uri: overlayMenu.uri property bool participantIsActive: false @@ -49,6 +51,8 @@ Rectangle { property bool participantIsModeratorMuted: false property bool participantMenuActive: false + z: -1 + function setAvatar(show, avatar, uri, local, isContact) { if (!show) contactImage.visible = false @@ -98,9 +102,6 @@ Rectangle { overlayMenu.showHangup = isModerator && !isLocal && !participantIsHost } - color: "transparent" - z: 1 - // Participant header with host, moderator and mute indicators Rectangle { id: participantIndicators @@ -213,90 +214,40 @@ Rectangle { layer.smooth: true } - // Participant background, mousearea, hover and buttons for moderation - Rectangle { + // Participant background and buttons for moderation + MouseArea { id: participantRect anchors.fill: parent opacity: 0 - color: "transparent" z: 1 - MouseArea { - id: mouseAreaHover - - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.LeftButton - - ParticipantOverlayMenu { - id: overlayMenu - visible: participantRect.opacity !== 0 - - onMouseAreaExited: { - root.z = 1 - participantRect.state = "exited" - } - onMouseChanged: { - participantRect.state = "entered" - fadeOutTimer.restart() - participantMenuActive = true - } - } - - onEntered: { - root.z = 2 - participantRect.state = "entered" - } - - onExited: { - root.z = 1 - participantRect.state = "exited" - } - - onMouseXChanged: { - // Hack: avoid listening mouseXChanged emitted when - // ParticipantOverlayMenu is exited - if (participantMenuActive) { - participantMenuActive = false - } else { - participantRect.state = "entered" - fadeOutTimer.restart() - } - } + propagateComposedEvents: true + hoverEnabled: true + onPositionChanged: { + participantRect.opacity = 1 + fadeOutTimer.restart() + // Here we could call: root.parent.positionChanged(mouse) + // to relay the event to a main overlay mouse area, either + // as a parent object or some property passed in. But, this + // will still fail when hovering over menus, etc. } - - states: [ - State { - name: "entered" - PropertyChanges { - target: participantRect - opacity: 1 - } - }, - State { - name: "exited" - PropertyChanges { - target: participantRect - opacity: 0 - } - } - ] - - transitions: Transition { - PropertyAnimation { - target: participantRect - property: "opacity" - duration: 50 + onExited: participantRect.opacity = 0 + onEntered: participantRect.opacity = 1 + + // Timer to decide when ParticipantOverlay fade out + Timer { + id: fadeOutTimer + interval: JamiTheme.overlayFadeDelay + onTriggered: { + if (overlayMenu.hovered) + return + participantRect.opacity = 0 } } - } - // Timer to decide when ParticipantOverlay fade out - Timer { - id: fadeOutTimer - interval: 5000 - onTriggered: participantRect.state = "exited" + ParticipantOverlayMenu { id: overlayMenu } + + Behavior on opacity { NumberAnimation { duration: JamiTheme.shortFadeDuration }} } } diff --git a/src/mainview/components/ParticipantOverlayMenu.qml b/src/mainview/components/ParticipantOverlayMenu.qml index 5f8169414b811c0de870a87fbd5b87e1aea2a820..2535724a9c56ff6137e6e9808a5ced3e8475ce12 100644 --- a/src/mainview/components/ParticipantOverlayMenu.qml +++ b/src/mainview/components/ParticipantOverlayMenu.qml @@ -29,7 +29,7 @@ import net.jami.Constants 1.0 import "../../commoncomponents" // Overlay menu for conference moderation -Rectangle { +Control { id: root property string uri: "" @@ -71,9 +71,6 @@ Rectangle { property int isSmall: !isBarLayout && (height < 100 || width < 160) - signal mouseAreaExited - signal mouseChanged - width: isBarLayout? bestNameLabel.contentWidth + buttonsSize + 32 : (isOverlayRect? buttonsSize + 32 : parent.width) height: isBarLayout? shapeHeight : (isOverlayRect? 80 : parent.height) @@ -82,20 +79,13 @@ Rectangle { anchors.left: isBarLayout? parent.left : undefined anchors.centerIn: isBarLayout? undefined : parent - color: isBarLayout? "transparent" : JamiTheme.darkGreyColorOpacity - - radius: (isBarLayout || !isOverlayRect)? 0 : 10 - - MouseArea { - id: mouseAreaHover + background: Rectangle { + color: isBarLayout? "transparent" : JamiTheme.darkGreyColorOpacity + radius: (isBarLayout || !isOverlayRect)? 0 : 10 + } + Item { anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - acceptedButtons: Qt.LeftButton - - onExited: mouseAreaExited() - onMouseXChanged: mouseChanged() Shape { id: myShape diff --git a/src/mainview/components/ParticipantsLayer.qml b/src/mainview/components/ParticipantsLayer.qml new file mode 100644 index 0000000000000000000000000000000000000000..5a15616e702865eb7129f1ae575a81701d28ba48 --- /dev/null +++ b/src/mainview/components/ParticipantsLayer.qml @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Sébastien Blin <sebastien.blin@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 2.14 +import QtQml 2.14 + +Item { + id: root + + property var participantOverlays: [] + property var participantComponent: Qt.createComponent("ParticipantOverlay.qml") + + // returns true if participant is not fully maximized + function showMaximize(pX, pY, pW, pH) { + // Hack: -1 offset added to avoid problems with odd sizes + return (pX - distantRenderer.getXOffset() !== 0 + || pY - distantRenderer.getYOffset() !== 0 + || pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1) + || pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1)) + } + + function update(infos) { + if (isAudioOnly) + return; + // TODO: in the future the conference layout should be entirely managed by the client + // Hack: truncate and ceil participant's overlay position and size to correct + // when they are not exacts + callOverlay.updateMenu() + var showMax = false + var showMin = false + + var deletedUris = [] + var currentUris = [] + for (var p in participantOverlays) { + if (participantOverlays[p]) { + var participant = infos.find(e => e.uri === participantOverlays[p].uri); + if (participant) { + // Update participant's information + var newX = Math.trunc(distantRenderer.getXOffset() + + participant.x * distantRenderer.getScaledWidth()) + var newY = Math.trunc(distantRenderer.getYOffset() + + participant.y * distantRenderer.getScaledHeight()) + + var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth()) + var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight()) + + var newVisible = participant.w !== 0 && participant.h !== 0 + if (participantOverlays[p].x !== newX) + participantOverlays[p].x = newX + if (participantOverlays[p].y !== newY) + participantOverlays[p].y = newY + if (participantOverlays[p].width !== newWidth) + participantOverlays[p].width = newWidth + if (participantOverlays[p].height !== newHeight) + participantOverlays[p].height = newHeight + if (participantOverlays[p].visible !== newVisible) + participantOverlays[p].visible = newVisible + + showMax = showMaximize(participantOverlays[p].x, + participantOverlays[p].y, + participantOverlays[p].width, + participantOverlays[p].height) + + participantOverlays[p].setMenu(participant.uri, participant.bestName, + participant.isLocal, participant.active, showMax) + if (participant.videoMuted) + participantOverlays[p].setAvatar(true, participant.avatar, participant.uri, participant.isLocal, participant.isContact) + else + participantOverlays[p].setAvatar(false) + currentUris.push(participantOverlays[p].uri) + } else { + // Participant is no longer in conference + deletedUris.push(participantOverlays[p].uri) + participantOverlays[p].destroy() + } + } + } + participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri)) + + if (infos.length === 0) { // Return to normal call + previewRenderer.visible = !isVideoMuted + for (var part in participantOverlays) { + if (participantOverlays[part]) { + participantOverlays[part].destroy() + } + } + participantOverlays = [] + } else { + previewRenderer.visible = false + for (var infoVariant in infos) { + // Only create overlay for new participants + if (!currentUris.includes(infos[infoVariant].uri)) { + var hover = participantComponent.createObject(root, { + x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()), + y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()), + width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()), + height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()), + visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0 + }) + if (!hover) { + console.log("Error when creating the hover") + return + } + + showMax = showMaximize(hover.x, hover.y, hover.width, hover.height) + + hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName, + infos[infoVariant].isLocal, infos[infoVariant].active, showMax) + if (infos[infoVariant].videoMuted) + hover.setAvatar(true, infos[infoVariant].avatar, infos[infoVariant].uri, infos[infoVariant].isLocal, infos[infoVariant].isContact) + else + hover.setAvatar(false) + participantOverlays.push(hover) + } + } + } + } +}