From 6607e0e9cfc7c94eea3d2ab69351e8d0fbf291b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Fri, 24 Jul 2020 15:15:47 -0400 Subject: [PATCH] videocallpage: handle conference infos Change-Id: I4cfc7c9c525c66f4e483089864bec059c388a1bd --- .gitreview | 2 +- images/icons/ic_call_end_white_24px.svg | 2 +- qml.qrc | 2 + src/calladapter.cpp | 194 +++++++++++++++++- src/calladapter.h | 11 +- src/distantrenderer.cpp | 39 +++- src/distantrenderer.h | 11 + src/lrcinstance.h | 2 +- src/mainview/components/CallOverlay.qml | 49 ++++- .../components/CallOverlayButtonGroup.qml | 14 +- src/mainview/components/CallStackView.qml | 12 +- .../components/ParticipantContextMenu.qml | 127 ++++++++++++ .../components/ParticipantOverlay.qml | 169 +++++++++++++++ src/mainview/components/VideoCallPage.qml | 15 ++ 14 files changed, 629 insertions(+), 20 deletions(-) create mode 100644 src/mainview/components/ParticipantContextMenu.qml create mode 100644 src/mainview/components/ParticipantOverlay.qml diff --git a/.gitreview b/.gitreview index 08e68f356..382e2644e 100644 --- a/.gitreview +++ b/.gitreview @@ -1,6 +1,6 @@ [gerrit] host=gerrit-ring.savoirfairelinux.com port=29420 -project=ring-client-window.git +project=jami-client-qt.git defaultremote=origin defaultbranch=master diff --git a/images/icons/ic_call_end_white_24px.svg b/images/icons/ic_call_end_white_24px.svg index 9c90cb39c..09f87248d 100644 --- a/images/icons/ic_call_end_white_24px.svg +++ b/images/icons/ic_call_end_white_24px.svg @@ -1,4 +1,4 @@ -<svg fill="#ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> +<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0h24v24H0z" fill="none"/> <path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08c-.18-.17-.29-.42-.29-.7 0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28-.79-.74-1.69-1.36-2.67-1.85-.33-.16-.56-.5-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/> </svg> diff --git a/qml.qrc b/qml.qrc index b8483b66d..19294fa8d 100644 --- a/qml.qrc +++ b/qml.qrc @@ -67,6 +67,7 @@ <file>src/mainview/components/ContactSearchBar.qml</file> <file>src/mainview/components/VideoCallPage.qml</file> <file>src/mainview/components/CallAdvancedOptions.qml</file> + <file>src/mainview/components/ParticipantOverlay.qml</file> <file>src/mainview/components/ChangeLogScrollView.qml</file> <file>src/mainview/components/ProjectCreditsScrollView.qml</file> <file>src/mainview/components/AccountComboBoxPopup.qml</file> @@ -77,6 +78,7 @@ <file>src/commoncomponents/GeneralMenuItem.qml</file> <file>src/mainview/components/ConversationSmartListContextMenu.qml</file> <file>src/mainview/components/CallViewContextMenu.qml</file> + <file>src/mainview/components/ParticipantContextMenu.qml</file> <file>src/commoncomponents/GeneralMenuSeparator.qml</file> <file>src/mainview/components/UserProfile.qml</file> <file>src/mainview/js/videodevicecontextmenuitemcreation.js</file> diff --git a/src/calladapter.cpp b/src/calladapter.cpp index 8cd968231..26ec8c09f 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -37,7 +37,7 @@ CallAdapter::~CallAdapter() {} void CallAdapter::initQmlObject() { - connectCallStatusChanged(LRCInstance::getCurrAccId()); + connectCallModel(LRCInstance::getCurrAccId()); connect(&LRCInstance::behaviorController(), &BehaviorController::showIncomingCallView, @@ -233,12 +233,91 @@ CallAdapter::shouldShowPreview(bool force) return shouldShowPreview; } +QVariantList +CallAdapter::getConferencesInfos() +{ + QVariantList map; + auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_); + if (convInfo.uid.isEmpty()) + return map; + auto callId = convInfo.confId.isEmpty()? convInfo.callId : convInfo.confId; + if (!callId.isEmpty()) { + try { + auto call = LRCInstance::getCurrentCallModel()->getCall(callId); + for (const auto& participant: call.participantsInfos) { + QJsonObject data; + data["x"] = participant["x"].toInt(); + data["y"] = participant["y"].toInt(); + data["w"] = participant["w"].toInt(); + data["h"] = participant["h"].toInt(); + data["active"] = participant["active"] == "true"; + auto bestName = participant["uri"]; + auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_); + data["isLocal"] = false; + if (bestName == accInfo.profileInfo.uri) { + bestName = tr("me"); + data["isLocal"] = true; + } else { + try { + auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]); + bestName = Utils::bestNameForContact(contact); + } catch (...) {} + } + data["bestName"] = bestName; + + map.push_back(QVariant(data)); + } + return map; + } catch (...) {} + } + return map; +} + void -CallAdapter::connectCallStatusChanged(const QString &accountId) +CallAdapter::connectCallModel(const QString &accountId) { auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId); QObject::disconnect(callStatusChangedConnection_); + QObject::disconnect(onParticipantsChangedConnection_); + + onParticipantsChangedConnection_ = QObject::connect( + accInfo.callModel.get(), + &lrc::api::NewCallModel::onParticipantsChanged, + [this, accountId](const QString &confId) { + auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId); + auto &callModel = accInfo.callModel; + auto call = callModel->getCall(confId); + auto convInfo = LRCInstance::getConversationFromCallId(confId); + if (!convInfo.uid.isEmpty()) { + // Convert to QML + QVariantList map; + for (const auto& participant: call.participantsInfos) { + QJsonObject data; + data["x"] = participant["x"].toInt(); + data["y"] = participant["y"].toInt(); + data["w"] = participant["w"].toInt(); + data["h"] = participant["h"].toInt(); + data["uri"] = participant["uri"]; + data["active"] = participant["active"] == "true"; + auto bestName = participant["uri"]; + data["isLocal"] = false; + auto &accInfo = LRCInstance::accountModel().getAccountInfo(accountId_); + if (bestName == accInfo.profileInfo.uri) { + bestName = tr("me"); + data["isLocal"] = true; + } else { + try { + auto &contact = LRCInstance::getCurrentAccountInfo().contactModel->getContact(participant["uri"]); + bestName = Utils::bestNameForContact(contact); + } catch (...) {} + } + data["bestName"] = bestName; + map.push_back(QVariant(data)); + } + emit updateParticipantsInfos(map, accountId, confId); + } + }); callStatusChangedConnection_ = QObject::connect( accInfo.callModel.get(), @@ -345,7 +424,7 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info &convInfo) bool isAudioOnly = call->isAudioOnly && !isPaused; bool isAudioMuted = call->audioMuted && (call->status != lrc::api::call::Status::PAUSED); bool isVideoMuted = call->videoMuted && !isPaused && !call->isAudioOnly; - bool isRecording = accInfo.callModel->isRecording(convInfo.callId); + bool isRecording = isRecordingThisCall(); emit updateOverlay(isPaused, isAudioOnly, @@ -359,16 +438,16 @@ CallAdapter::updateCallOverlay(const lrc::api::conversation::Info &convInfo) } void -CallAdapter::hangUpThisCall() +CallAdapter::hangupCall(const QString& uri) { - auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_); + auto convInfo = LRCInstance::getConversationFromPeerUri(uri, accountId_); if (!convInfo.uid.isEmpty()) { auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); if (callModel->hasCall(convInfo.callId)) { /* - * Store the last remaining participant of the conference, - * so we can switch the smartlist index after termination. - */ + * Store the last remaining participant of the conference, + * so we can switch the smartlist index after termination. + */ if (!convInfo.confId.isEmpty()) { auto callList = LRCInstance::getAPI().getConferenceSubcalls(convInfo.confId); if (callList.size() == 2) { @@ -379,6 +458,73 @@ CallAdapter::hangUpThisCall() } } } + + callModel->hangUp(convInfo.callId); + } + } +} + +void +CallAdapter::maximizeParticipant(const QString& uri, bool isActive) +{ + auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); + auto confId = LRCInstance::getCurrentConversation().confId; + QString callId; + if (LRCInstance::getCurrentAccountInfo().profileInfo.uri != uri) { + auto convInfo = LRCInstance::getConversationFromPeerUri(uri, accountId_); + if (!convInfo.uid.isEmpty()) { + callId = convInfo.callId; + } + } + try { + auto call = callModel->getCall(confId); + switch (call.layout) { + case lrc::api::call::Layout::GRID: + callModel->setActiveParticipant(confId, callId); + callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL); + break; + case lrc::api::call::Layout::ONE_WITH_SMALL: + callModel->setActiveParticipant(confId, callId); + callModel->setConferenceLayout(confId, + isActive? lrc::api::call::Layout::ONE : lrc::api::call::Layout::ONE_WITH_SMALL); + break; + case lrc::api::call::Layout::ONE: + callModel->setActiveParticipant(confId, callId); + callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); + break; + }; + } catch (...) {} +} + +void +CallAdapter::minimizeParticipant() +{ + auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); + auto confId = LRCInstance::getCurrentConversation().confId; + try { + auto call = callModel->getCall(confId); + switch (call.layout) { + case lrc::api::call::Layout::GRID: + break; + case lrc::api::call::Layout::ONE_WITH_SMALL: + callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); + break; + case lrc::api::call::Layout::ONE: + callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL); + break; + }; + } catch (...) {} +} + +void +CallAdapter::hangUpThisCall() +{ + auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_); + if (!convInfo.uid.isEmpty()) { + auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); + if (!convInfo.confId.isEmpty() && callModel->hasCall(convInfo.confId)) { + callModel->hangUp(convInfo.confId); + } else if (callModel->hasCall(convInfo.callId)) { callModel->hangUp(convInfo.callId); } } @@ -393,6 +539,38 @@ CallAdapter::isRecordingThisCall() || accInfo.callModel->isRecording(convInfo.callId); } +bool +CallAdapter::isCurrentMaster() const +{ + auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_); + if (!convInfo.uid.isEmpty()) { + auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); + try { + if (!convInfo.confId.isEmpty() && callModel->hasCall(convInfo.confId)) { + return true; + } else { + auto call = callModel->getCall(convInfo.callId); + return call.participantsInfos.size() == 0; + } + } catch (...) {} + } + return true; +} + +int +CallAdapter::getCurrentLayoutType() const +{ + auto convInfo = LRCInstance::getConversationFromConvUid(convUid_, accountId_); + if (!convInfo.uid.isEmpty()) { + auto callModel = LRCInstance::getAccountInfo(accountId_).callModel.get(); + try { + auto call = callModel->getCall(convInfo.confId); + return Utils::toUnderlyingValue(call.layout); + } catch (...) {} + } + return -1; +} + void CallAdapter::holdThisCallToggle() { diff --git a/src/calladapter.h b/src/calladapter.h index b68e08a6e..ce439731b 100644 --- a/src/calladapter.h +++ b/src/calladapter.h @@ -24,6 +24,7 @@ #include <QObject> #include <QString> +#include <QVariant> class CallAdapter : public QmlAdapterBase { @@ -44,17 +45,23 @@ public: Q_INVOKABLE void refuseACall(const QString &accountId, const QString &convUid); Q_INVOKABLE void acceptACall(const QString &accountId, const QString &convUid); - Q_INVOKABLE void connectCallStatusChanged(const QString &accountId); + Q_INVOKABLE void connectCallModel(const QString &accountId); /* * For Call Overlay */ + Q_INVOKABLE void hangupCall(const QString& uri); + Q_INVOKABLE void maximizeParticipant(const QString& uri, bool isActive); + Q_INVOKABLE void minimizeParticipant(); Q_INVOKABLE void hangUpThisCall(); + Q_INVOKABLE bool isCurrentMaster() const; + Q_INVOKABLE int getCurrentLayoutType() const; Q_INVOKABLE void holdThisCallToggle(); Q_INVOKABLE void muteThisCallToggle(); Q_INVOKABLE void recordThisCallToggle(); Q_INVOKABLE void videoPauseThisCallToggle(); Q_INVOKABLE bool isRecordingThisCall(); + Q_INVOKABLE QVariantList getConferencesInfos(); signals: void showOutgoingCallPage(const QString &accountId, const QString &convUid); @@ -66,6 +73,7 @@ signals: void closePotentialIncomingCallPageWindow(const QString &accountId, const QString &convUid); void callStatusChanged(const QString &status, const QString &accountId, const QString &convUid); void updateConversationSmartList(); + void updateParticipantsInfos(const QVariantList& infos, const QString &accountId, const QString &callId); void incomingCallNeedToSetupMainView(const QString &accountId, const QString &convUid); void previewVisibilityNeedToChange(bool visible); @@ -103,6 +111,7 @@ private: QString convUid_; QMetaObject::Connection callStatusChangedConnection_; + QMetaObject::Connection onParticipantsChangedConnection_; QMetaObject::Connection closeIncomingCallPageConnection_; /* diff --git a/src/distantrenderer.cpp b/src/distantrenderer.cpp index 023442b34..37b3d4413 100644 --- a/src/distantrenderer.cpp +++ b/src/distantrenderer.cpp @@ -51,15 +51,48 @@ DistantRenderer::setRendererId(const QString &id) update(QRect(0, 0, width(), height())); } +int +DistantRenderer::getXOffset() const +{ + return xOffset_; +} + +int +DistantRenderer::getYOffset() const +{ + return yOffset_; +} + +double +DistantRenderer::getScaledWidth() const +{ + return scaledWidth_; +} + +double +DistantRenderer::getScaledHeight() const +{ + return scaledHeight_; +} + void DistantRenderer::paint(QPainter *painter) { auto distantImage = LRCInstance::renderer()->getFrame(distantRenderId_); if (distantImage) { auto scaledDistant = distantImage->scaled(size().toSize(), Qt::KeepAspectRatio); - auto xDiff = (width() - scaledDistant.width()) / 2; - auto yDiff = (height() - scaledDistant.height()) / 2; - painter->drawImage(QRect(xDiff, yDiff, scaledDistant.width(), scaledDistant.height()), + auto tempScaledWidth = static_cast<int>(scaledWidth_*1000); + auto tempScaledHeight = static_cast<int>(scaledHeight_*1000); + auto tempXOffset = xOffset_; + auto tempYOffset = yOffset_; + scaledWidth_ = static_cast<double>(scaledDistant.width())/static_cast<double>(distantImage->width()); + scaledHeight_ = static_cast<double>(scaledDistant.height())/static_cast<double>(distantImage->height()); + xOffset_ = (width() - scaledDistant.width()) / 2; + yOffset_ = (height() - scaledDistant.height()) / 2; + if (tempXOffset != xOffset_ or tempYOffset != yOffset_ or static_cast<int>(scaledWidth_*1000) != tempScaledWidth or static_cast<int>(scaledHeight_*1000) != tempScaledHeight) { + emit offsetChanged(); + } + painter->drawImage(QRect(xOffset_, yOffset_, scaledDistant.width(), scaledDistant.height()), scaledDistant); } } \ No newline at end of file diff --git a/src/distantrenderer.h b/src/distantrenderer.h index d26f2c2c7..81281e5ef 100644 --- a/src/distantrenderer.h +++ b/src/distantrenderer.h @@ -34,6 +34,13 @@ public: ~DistantRenderer(); Q_INVOKABLE void setRendererId(const QString &id); + Q_INVOKABLE int getXOffset() const; + Q_INVOKABLE int getYOffset() const; + Q_INVOKABLE double getScaledWidth() const; + Q_INVOKABLE double getScaledHeight() const; + +signals: + void offsetChanged(); private: void paint(QPainter *painter); @@ -42,4 +49,8 @@ private: * Unique DistantRenderId for each call. */ QString distantRenderId_; + int xOffset_ {0}; + int yOffset_ {0}; + double scaledWidth_ {0}; + double scaledHeight_ {0}; }; \ No newline at end of file diff --git a/src/lrcinstance.h b/src/lrcinstance.h index 190b3839e..262bb1401 100644 --- a/src/lrcinstance.h +++ b/src/lrcinstance.h @@ -229,7 +229,7 @@ public: { return getConversation( !accountId.isEmpty() ? accountId : getCurrAccId(), - [&](const conversation::Info &conv) -> bool { return callId == conv.callId; }, + [&](const conversation::Info &conv) -> bool { return callId == conv.callId or callId == conv.confId; }, filtered); } static const conversation::Info & diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml index 7a6b3f1e1..2f4eac56a 100644 --- a/src/mainview/components/CallOverlay.qml +++ b/src/mainview/components/CallOverlay.qml @@ -35,6 +35,9 @@ Rectangle { signal overlayChatButtonClicked + property var participantOverlays: [] + property var participantComponent: Qt.createComponent("ParticipantOverlay.qml") + function setRecording(isRecording) { callViewContextMenu.isRecording = isRecording recordingRect.visible = isRecording @@ -52,6 +55,10 @@ Rectangle { isConferenceCall) } + function updateMaster() { + callOverlayButtonGroup.updateMaster() + } + function showOnHoldImage(visible) { onHoldImage.visible = visible } @@ -60,6 +67,41 @@ Rectangle { ContactPickerCreation.closeContactPicker() } + function handleParticipantsInfo(infos) { + videoCallOverlay.updateMaster() + var isMaster = CallAdapter.isCurrentMaster() + for (var p in participantOverlays) { + if (participantOverlays[p]) + participantOverlays[p].destroy() + } + participantOverlays = [] + if (infos.length == 0) { + previewRenderer.visible = true + } else { + previewRenderer.visible = false + for (var infoVariant in infos) { + var hover = participantComponent.createObject(callOverlayRectMouseArea, { + x: distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth(), + y: distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight(), + width: infos[infoVariant].w * distantRenderer.getScaledWidth(), + height: infos[infoVariant].h * distantRenderer.getScaledHeight(), + visible: infos[infoVariant].w != 0 && infos[infoVariant].h != 0 + }) + if (!hover) { + console.log("Error when creating the hover") + return + } + hover.setParticipantName(infos[infoVariant].bestName) + hover.active = infos[infoVariant].active; + hover.isLocal = infos[infoVariant].isLocal; + hover.setMenuVisible(isMaster) + hover.uri = infos[infoVariant].uri + hover.injectedContextMenu = participantContextMenu + participantOverlays.push(hover) + } + } + } + anchors.fill: parent @@ -313,10 +355,9 @@ Rectangle { id: callOverlayRectMouseArea anchors.top: callOverlayRect.top - anchors.topMargin: 50 width: callOverlayRect.width - height: callOverlayRect.height - callOverlayButtonGroup.height - 50 + height: callOverlayRect.height hoverEnabled: true propagateComposedEvents: true @@ -370,4 +411,8 @@ Rectangle { ContactPickerCreation.openContactPicker() } } + + ParticipantContextMenu { + id: participantContextMenu + } } diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml index cddc0ea85..49511f32c 100644 --- a/src/mainview/components/CallOverlayButtonGroup.qml +++ b/src/mainview/components/CallOverlayButtonGroup.qml @@ -34,13 +34,24 @@ Rectangle { * since no other methods can make buttons at the layout center. */ property int buttonPreferredSize: 24 + property var isMaster: true + property var isSip: false signal chatButtonClicked signal addToConferenceButtonClicked + function updateMaster() { + root.isMaster = CallAdapter.isCurrentMaster() + addToConferenceButton.visible = !root.isSip && root.isMaster + } + function setButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted, isRecording, isSIP, isConferenceCall) { + root.isMaster = CallAdapter.isCurrentMaster() + root.isSip = isSIP noVideoButton.visible = !isAudioOnly - addToConferenceButton.visible = !isSIP + addToConferenceButton.visible = !isSIP && isMaster + transferCallButton.visible = isSIP + sipInputPanelButton.visible = isSIP noMicButton.checked = isAudioMuted noVideoButton.checked = isVideoMuted @@ -148,6 +159,7 @@ Rectangle { Layout.preferredWidth: buttonPreferredSize * 2 Layout.preferredHeight: buttonPreferredSize * 2 + visible: !isMaster backgroundColor: Qt.rgba(0, 0, 0, 0.75) onEnterColor: Qt.rgba(0, 0, 0, 0.6) diff --git a/src/mainview/components/CallStackView.qml b/src/mainview/components/CallStackView.qml index 27a2b55e8..1f46174b3 100644 --- a/src/mainview/components/CallStackView.qml +++ b/src/mainview/components/CallStackView.qml @@ -157,11 +157,17 @@ Rectangle { } function onCallStatusChanged(status, accountId, convUid) { - if (responsibleConvUid === convUid - && responsibleAccountId === accountId) { + if (responsibleConvUid === convUid && responsibleAccountId === accountId) { outgoingCallPage.callStatusPresentation = status } } + + function onUpdateParticipantsInfos(infos, accountId, callId) { + var responsibleCallId = ClientWrapper.utilsAdaptor.getCallId(responsibleAccountId, responsibleConvUid) + if (responsibleCallId === callId) { + videoCallPage.handleParticipantsInfo(infos) + } + } } AudioCallPage { @@ -198,6 +204,8 @@ Rectangle { videoCallPage.parent = callStackMainView VideoCallFullScreenWindowContainerCreation.closeVideoCallFullScreenWindowContainer() } + + videoCallPage.handleParticipantsInfo(CallAdapter.getConferencesInfos()) } } diff --git a/src/mainview/components/ParticipantContextMenu.qml b/src/mainview/components/ParticipantContextMenu.qml new file mode 100644 index 000000000..9397701cf --- /dev/null +++ b/src/mainview/components/ParticipantContextMenu.qml @@ -0,0 +1,127 @@ + +/* + * 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 QtQuick.Controls 2.14 +import QtGraphicalEffects 1.12 +import net.jami.Models 1.0 + +import "../../commoncomponents" + +import "../js/videodevicecontextmenuitemcreation.js" as VideoDeviceContextMenuItemCreation +import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation + +Menu { + id: root + + property int generalMenuSeparatorCount: 0 + property int commonBorderWidth: 1 + font.pointSize: JamiTheme.textFontSize + 3 + property var uri: "" + property var maximized: true + property var active: true + + function showHangup(show) { + if (show) { + hangupItem.visible = true + hangupItem.height = hangupItem.preferredHeight + } else { + hangupItem.visible = false + hangupItem.height = 0 + } + } + + function showMaximize(show) { + if (show) { + maximizeItem.visible = true + maximizeItem.height = hangupItem.preferredHeight + } else { + maximizeItem.visible = false + maximizeItem.height = 0 + } + } + + function showMinimize(show) { + if (show) { + minimizeItem.visible = true + minimizeItem.height = hangupItem.preferredHeight + } else { + minimizeItem.visible = false + minimizeItem.height = 0 + } + } + + function setHeight(visibleItems) { + root.height = hangupItem.preferredHeight * visibleItems; + } + + /* + * All GeneralMenuItems should remain the same width / height. + */ + GeneralMenuItem { + id: hangupItem + + itemName: qsTr("Hangup") + iconSource: "qrc:/images/icons/ic_call_end_white_24px.svg" + icon.color: "black" + leftBorderWidth: commonBorderWidth + rightBorderWidth: commonBorderWidth + + onClicked: { + CallAdapter.hangupCall(uri) + root.close() + } + } + GeneralMenuItem { + id: maximizeItem + + itemName: qsTr("Maximize participant") + iconSource: "qrc:/images/icons/open_in_full-24px.svg" + leftBorderWidth: commonBorderWidth + rightBorderWidth: commonBorderWidth + visible: !maximized + + onClicked: { + CallAdapter.maximizeParticipant(uri, active) + root.close() + } + } + GeneralMenuItem { + id: minimizeItem + + itemName: qsTr("Minimize participant") + iconSource: "qrc:/images/icons/close_fullscreen-24px.svg" + leftBorderWidth: commonBorderWidth + rightBorderWidth: commonBorderWidth + visible: maximized + + onClicked: { + CallAdapter.minimizeParticipant() + root.close() + } + } + + background: Rectangle { + implicitWidth: hangupItem.preferredWidth + implicitHeight: hangupItem.preferredHeight * 3 + + border.width: commonBorderWidth + border.color: JamiTheme.tabbarBorderColor + } +} + diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml new file mode 100644 index 000000000..fb49a4687 --- /dev/null +++ b/src/mainview/components/ParticipantOverlay.qml @@ -0,0 +1,169 @@ +/* + * 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 QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls.Universal 2.12 +import QtGraphicalEffects 1.14 +import net.jami.Models 1.0 + +import "../../commoncomponents" + +Rectangle { + id: root + + property int buttonPreferredSize: 12 + property var uri: "" + property var active: true + property var isLocal: true + property var injectedContextMenu: null + + function setParticipantName(name) { + participantName.text = name + } + + function setMenuVisible(isVisible) { + optionsButton.visible = isVisible + } + + border.width: 1 + opacity: 0 + color: "transparent" + z: 1 + + MouseArea { + id: mouseAreaHover + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.LeftButton + + RowLayout { + id: bottomLabel + + height: 24 + width: parent.width + anchors.bottom: parent.bottom + + Rectangle { + color: "black" + opacity: 0.8 + height: parent.height + width: parent.width + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + Text { + id: participantName + anchors.fill: parent + leftPadding: 8.0 + + TextMetrics { + id: participantMetrics + elide: Text.ElideRight + elideWidth: bottomLabel.width - 8 + } + + text: participantMetrics.elidedText + + color: "white" + font.pointSize: JamiTheme.textFontSize + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + + Button { + id: optionsButton + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + background: Rectangle { + color: "transparent" + } + + + icon.color: "white" + icon.height: buttonPreferredSize + icon.width: buttonPreferredSize + icon.source: "qrc:/images/icons/more_vert-24px.svg" + + onClicked: { + if (!injectedContextMenu) { + console.log("Participant's overlay don't have any injected context menu") + return + } + var mousePos = mapToItem(videoCallPageRect, parent.x, parent.y) + var layout = CallAdapter.getCurrentLayoutType() + var showMaximized = layout !== 2 + var showMinimized = !(layout === 0 || (layout === 1 && !active)) + injectedContextMenu.showHangup(!root.isLocal) + injectedContextMenu.showMaximize(showMaximized) + injectedContextMenu.showMinimize(showMinimized) + injectedContextMenu.setHeight( + (root.isLocal ? 0 : 1) + + (showMaximized ? 1 : 0) + + (showMinimized ? 1 : 0)) + injectedContextMenu.uri = uri + injectedContextMenu.active = active + injectedContextMenu.x = mousePos.x + injectedContextMenu.y = mousePos.y - injectedContextMenu.height + injectedContextMenu.open() + } + } + } + } + + onClicked: { + CallAdapter.maximizeParticipant(uri, active) + } + + onEntered: { + root.state = "entered" + } + + onExited: { + root.state = "exited" + } + } + + states: [ + State { + name: "entered" + PropertyChanges { + target: root + opacity: 1 + } + }, + State { + name: "exited" + PropertyChanges { + target: root + opacity: 0 + } + } + ] + + transitions: Transition { + PropertyAnimation { + target: root + property: "opacity" + duration: 500 + } + } +} \ No newline at end of file diff --git a/src/mainview/components/VideoCallPage.qml b/src/mainview/components/VideoCallPage.qml index 4657a7ed3..047e332d6 100644 --- a/src/mainview/components/VideoCallPage.qml +++ b/src/mainview/components/VideoCallPage.qml @@ -42,6 +42,8 @@ Rectangle { signal needToShowInFullScreen function updateUI(accountId, convUid) { + videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos()) + bestName = ClientWrapper.utilsAdaptor.getBestName(accountId, convUid) var id = ClientWrapper.utilsAdaptor.getBestId(accountId, convUid) @@ -74,6 +76,15 @@ Rectangle { videoCallOverlay.closePotentialContactPicker() } + function handleParticipantsInfo(infos) { + if (infos.length === 0) { + bestName = ClientWrapper.utilsAdaptor.getBestName(accountId, convUid) + } else { + bestName = "" + } + videoCallOverlay.handleParticipantsInfo(infos) + } + function previewMagneticSnap() { @@ -225,6 +236,10 @@ Rectangle { width: videoCallPageMainRect.width height: videoCallPageMainRect.height + + onOffsetChanged: { + videoCallOverlay.handleParticipantsInfo(CallAdapter.getConferencesInfos()) + } } VideoCallPreviewRenderer { -- GitLab