diff --git a/src/calladapter.cpp b/src/calladapter.cpp index 1b3955ccfb0576b128258cb17386be32f4323070..58f33f78080d1162cb2c8de70b4b9ef80f71f49f 100644 --- a/src/calladapter.cpp +++ b/src/calladapter.cpp @@ -40,6 +40,16 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject* { participantsModel_.reset(new CallParticipantsModel(lrcInstance_, this)); QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, participantsModel_.get(), "CallParticipantsModel"); + participantsModelFiltered_.reset( + new GenericParticipantsFilterModel(lrcInstance_, participantsModel_.get())); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, + participantsModelFiltered_.get(), + "GenericParticipantsFilterModel"); + activeParticipantsModel_.reset( + new ActiveParticipantsFilterModel(lrcInstance_, participantsModel_.get())); + QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, + activeParticipantsModel_.get(), + "ActiveParticipantsFilterModel"); overlayModel_.reset(new CallOverlayModel(lrcInstance_, this)); QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, overlayModel_.get(), "CallOverlayModel"); @@ -147,6 +157,8 @@ CallAdapter::onParticipantAdded(const QString& callId, int index) auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); auto& callModel = accInfo.callModel; try { + if (lrcInstance_->get_selectedConvUid().isEmpty()) + return; const auto& currentConvInfo = accInfo.conversationModel.get()->getConversationForUid( lrcInstance_->get_selectedConvUid()); if (callId != currentConvInfo->get().callId && callId != currentConvInfo->get().confId) { @@ -154,8 +166,8 @@ CallAdapter::onParticipantAdded(const QString& callId, int index) return; } auto infos = getConferencesInfos(); - participantsModel_->addParticipant(index, infos[index]); - Q_EMIT updateParticipantsLayout(); + if (index < infos.size()) + participantsModel_->addParticipant(index, infos[index]); } catch (...) { } } @@ -166,6 +178,8 @@ CallAdapter::onParticipantRemoved(const QString& callId, int index) auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); auto& callModel = accInfo.callModel; try { + if (lrcInstance_->get_selectedConvUid().isEmpty()) + return; const auto& currentConvInfo = accInfo.conversationModel.get()->getConversationForUid( lrcInstance_->get_selectedConvUid()); if (callId != currentConvInfo->get().callId && callId != currentConvInfo->get().confId) { @@ -173,7 +187,6 @@ CallAdapter::onParticipantRemoved(const QString& callId, int index) return; } participantsModel_->removeParticipant(index); - Q_EMIT updateParticipantsLayout(); } catch (...) { } } @@ -184,6 +197,8 @@ CallAdapter::onParticipantUpdated(const QString& callId, int index) auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); auto& callModel = accInfo.callModel; try { + if (lrcInstance_->get_selectedConvUid().isEmpty()) + return; const auto& currentConvInfo = accInfo.conversationModel.get()->getConversationForUid( lrcInstance_->get_selectedConvUid()); if (callId != currentConvInfo->get().callId && callId != currentConvInfo->get().confId) { @@ -298,6 +313,7 @@ CallAdapter::onCallInfosChanged(const QString& accountId, const QString& callId) const auto& convInfo = lrcInstance_->getConversationFromCallId(callId); if (!convInfo.uid.isEmpty()) { Q_EMIT callInfosChanged(call.isAudioOnly, accountId, convInfo.uid); + participantsModel_->setConferenceLayout(static_cast<int>(call.layout), callId); updateCallOverlay(convInfo); } } catch (...) { @@ -494,6 +510,7 @@ CallAdapter::updateCall(const QString& convUid, const QString& accountId, bool f updateCallOverlay(convInfo); updateRecordingPeers(true); participantsModel_->setParticipants(call->id, getConferencesInfos()); + participantsModel_->setConferenceLayout(static_cast<int>(call->layout), call->id); } void @@ -730,9 +747,6 @@ CallAdapter::maximizeParticipant(const QString& uri) if (participant["active"].toBool()) { callModel->setActiveParticipant(confId, uri); callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE_WITH_SMALL); - } else if (participant["y"].toInt() != 0) { - callModel->setActiveParticipant(confId, uri); - callModel->setConferenceLayout(confId, lrc::api::call::Layout::ONE); } else { callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); } @@ -760,12 +774,7 @@ CallAdapter::minimizeParticipant(const QString& uri) if (participant["uri"].toString() == uri) { if (participant["active"].toBool()) { participant["active"] = !participant["active"].toBool(); - if (participant["y"].toInt() == 0) { - callModel->setConferenceLayout(confId, - lrc::api::call::Layout::ONE_WITH_SMALL); - } else { - callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); - } + callModel->setConferenceLayout(confId, lrc::api::call::Layout::GRID); } return; } @@ -902,34 +911,6 @@ CallAdapter::setHandRaised(const QString& uri, bool state) } } -bool -CallAdapter::isCurrentModerator() const -{ - const auto& convInfo = lrcInstance_->getConversationFromConvUid( - lrcInstance_->get_selectedConvUid()); - if (!convInfo.uid.isEmpty()) { - auto* callModel = lrcInstance_->getAccountInfo(accountId_).callModel.get(); - try { - auto confId = convInfo.confId.isEmpty() ? convInfo.callId : convInfo.confId; - auto participants = getConferencesInfos(); - if (participants.size() == 0) { - return true; - } else { - auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); - for (const auto& part : participants) { - auto participant = part.toJsonObject(); - if (participant["uri"].toString() == accInfo.profileInfo.uri) { - return participant["isModerator"].toBool(); - } - } - } - return false; - } catch (...) { - } - } - return true; -} - void CallAdapter::setModerator(const QString& uri, const bool state) { diff --git a/src/calladapter.h b/src/calladapter.h index 03f1915ab855fb46a477ab3da4723f7e466fef70..89dabc0df660a5c199e17ac6f3fa524afddc5f01 100644 --- a/src/calladapter.h +++ b/src/calladapter.h @@ -70,7 +70,6 @@ public: Q_INVOKABLE bool isModerator(const QString& uri = {}) const; Q_INVOKABLE bool isHandRaised(const QString& uri = {}) const; Q_INVOKABLE void setHandRaised(const QString& uri, bool state); - Q_INVOKABLE bool isCurrentModerator() const; Q_INVOKABLE void holdThisCallToggle(); Q_INVOKABLE void muteThisCallToggle(bool mute); Q_INVOKABLE void recordThisCallToggle(); @@ -101,7 +100,6 @@ Q_SIGNALS: const QString& previewId); void remoteRecordingChanged(const QStringList& peers, bool state); void eraseRemoteRecording(); - void updateParticipantsLayout(); public Q_SLOTS: void onShowIncomingCallView(const QString& accountId, const QString& convUid); @@ -130,5 +128,7 @@ private: SystemTray* systemTray_; QScopedPointer<CallOverlayModel> overlayModel_; QScopedPointer<CallParticipantsModel> participantsModel_; + QScopedPointer<GenericParticipantsFilterModel> participantsModelFiltered_; + QScopedPointer<ActiveParticipantsFilterModel> activeParticipantsModel_; VectorString currentConfSubcalls_; }; diff --git a/src/callparticipantsmodel.cpp b/src/callparticipantsmodel.cpp index c35f3ac1fb9a681dc9b10ba77a920e045bec1284..d45589cb12d0fc8d513d762fb6540f840fd1d057 100644 --- a/src/callparticipantsmodel.cpp +++ b/src/callparticipantsmodel.cpp @@ -92,10 +92,13 @@ CallParticipantsModel::roleNames() const void CallParticipantsModel::addParticipant(int index, const QVariant& infos) { + if (index > participants_.size()) + return; + beginInsertRows(QModelIndex(), index, index); + auto it = participants_.begin() + index; participants_.insert(it, CallParticipant::Item {infos.toJsonObject()}); - beginInsertRows(QModelIndex(), index, index); endInsertRows(); callId_ = participants_[index].item["callId"].toString(); @@ -110,32 +113,31 @@ CallParticipantsModel::updateParticipant(int index, const QVariant& infos) (*it) = CallParticipant::Item {infos.toJsonObject()}; callId_ = participants_[index].item["callId"].toString(); - Q_EMIT updateParticipant(it->item.toVariantMap()); + Q_EMIT dataChanged(createIndex(index, 0), createIndex(index, 0)); } void CallParticipantsModel::removeParticipant(int index) { + if (participants_.size() <= index) + return; callId_ = participants_[index].item["callId"].toString(); + beginRemoveRows(QModelIndex(), index, index); + auto it = participants_.begin() + index; participants_.erase(it); - beginRemoveRows(QModelIndex(), index, index); endRemoveRows(); } void CallParticipantsModel::setParticipants(const QString& callId, const QVariantList& participants) { - if (callId_ == callId) - return; - callId_ = callId; participants_.clear(); - beginResetModel(); - endResetModel(); + reset(); if (!participants.isEmpty()) { int idx = 0; diff --git a/src/callparticipantsmodel.h b/src/callparticipantsmodel.h index 62342d8c7f66eee7640fa9099113e2e056662540..0273bfb19786cc5c8fdfa16128f543476cefa321 100644 --- a/src/callparticipantsmodel.h +++ b/src/callparticipantsmodel.h @@ -68,12 +68,87 @@ struct Item }; } // namespace CallParticipant +/* + * The CurrentAccountFilterModel class + * is for the sole purpose of filtering out current account. + */ +class GenericParticipantsFilterModel final : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit GenericParticipantsFilterModel(LRCInstance* lrcInstance, + QAbstractListModel* parent = nullptr) + : QSortFilterProxyModel(parent) + , lrcInstance_(lrcInstance) + { + setSourceModel(parent); + setFilterRole(CallParticipant::Role::Active); + } + + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override + { + // Accept all participants in participants list filtered with active status. + auto index = sourceModel()->index(sourceRow, 0, sourceParent); + return !sourceModel()->data(index, CallParticipant::Role::Active).toBool(); + } + + Q_INVOKABLE void reset() + { + beginResetModel(); + endResetModel(); + } + +protected: + LRCInstance* lrcInstance_ {nullptr}; +}; + +/* + * The ActiveParticipantsFilterModel class + * is for the sole purpose of filtering out current account. + */ +class ActiveParticipantsFilterModel final : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit ActiveParticipantsFilterModel(LRCInstance* lrcInstance, + QAbstractListModel* parent = nullptr) + : QSortFilterProxyModel(parent) + , lrcInstance_(lrcInstance) + { + setSourceModel(parent); + setFilterRole(CallParticipant::Role::Active); + } + + virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override + { + // Accept all participants in participants list filtered with active status. + auto index = sourceModel()->index(sourceRow, 0, sourceParent); + return sourceModel()->data(index, CallParticipant::Role::Active).toBool(); + } + + Q_INVOKABLE void reset() + { + beginResetModel(); + endResetModel(); + } + +protected: + LRCInstance* lrcInstance_ {nullptr}; +}; + class CallParticipantsModel : public QAbstractListModel { Q_OBJECT + + Q_PROPERTY(LayoutType conferenceLayout READ conferenceLayout NOTIFY layoutChanged) public: CallParticipantsModel(LRCInstance* instance, QObject* parent = nullptr); + typedef enum { GRID = 0, ONE_WITH_SMALL, ONE } LayoutType; + Q_ENUM(LayoutType); + int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash<int, QByteArray> roleNames() const override; @@ -83,13 +158,27 @@ public: void removeParticipant(int index); void setParticipants(const QString& callId, const QVariantList& participants); Q_INVOKABLE void reset(); + void setConferenceLayout(int layout, const QString& callId) + { + auto newLayout = static_cast<LayoutType>(layout); + if (callId == callId_ && newLayout != layout_) { + layout_ = newLayout; + Q_EMIT layoutChanged(); + } + } + const LayoutType conferenceLayout() + { + return layout_; + } Q_SIGNALS: void updateParticipant(QVariant participantInfos); + void layoutChanged(); private: LRCInstance* lrcInstance_ {nullptr}; QList<CallParticipant::Item> participants_ {}; QString callId_; + LayoutType layout_; }; diff --git a/src/commoncomponents/VideoView.qml b/src/commoncomponents/VideoView.qml index ecfb5c3d8adfb864e3b4c024882118392a1d8868..7be9099f5df2f9f9e0faf3d6199590ee33b6f689 100644 --- a/src/commoncomponents/VideoView.qml +++ b/src/commoncomponents/VideoView.qml @@ -26,6 +26,7 @@ Item { property string rendererId property alias videoSink: videoOutput.videoSink property alias underlayItems: rootUnderlayItem.children + property alias overlayItems: rootOverlayItem.children property real invAspectRatio: (videoOutput.sourceRect.height / videoOutput.sourceRect.width) || 0.5625 // 16:9 default @@ -79,4 +80,9 @@ Item { radius: (1. - opacity) * 100 } } + + Item { + id: rootOverlayItem + anchors.fill: parent + } } diff --git a/src/mainview/components/CallOverlay.qml b/src/mainview/components/CallOverlay.qml index 4fd22f4264071a674b3b83bd1f61112657d9bfe1..4940a099ead195c15a3990e6b1bee7e96fe5b462 100644 --- a/src/mainview/components/CallOverlay.qml +++ b/src/mainview/components/CallOverlay.qml @@ -71,7 +71,7 @@ Item { root.localHandRaised = CallAdapter.isHandRaised() } root.isRecording = CallAdapter.isRecordingThisCall() - root.isModerator = CallAdapter.isCurrentModerator() + root.isModerator = CallAdapter.isModerator() } function showOnHoldImage(visible) { diff --git a/src/mainview/components/OngoingCallPage.qml b/src/mainview/components/OngoingCallPage.qml index 6e91a5a892b94e939d7b8f30fe4a4f2d818c1e5c..76d6d9c9a894429a0e38ecc651ff37f2b827fa21 100644 --- a/src/mainview/components/OngoingCallPage.qml +++ b/src/mainview/components/OngoingCallPage.qml @@ -183,6 +183,10 @@ Rectangle { anchors.centerIn: parent anchors.margins: 3 visible: !root.isAudioOnly && participantsLayer.count !== 0 + + onCountChanged: { + callOverlay.isConference = participantsLayer.count > 0 + } } LocalVideo { @@ -266,7 +270,7 @@ Rectangle { id: callOverlay anchors.fill: parent - isConference: participantsLayer.count >= 0 + isConference: participantsLayer.count > 0 function toggleConversation() { if (inCallMessageWebViewStack.visible) diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml index 3b6faedd3cf2c46bbfd78597f03d325aa9c459ff..5ecc4188897203ed76fd33456ddcedd6cbeff53f 100644 --- a/src/mainview/components/ParticipantOverlay.qml +++ b/src/mainview/components/ParticipantOverlay.qml @@ -1,7 +1,8 @@ /* - * Copyright (C) 2020-2022 Savoir-faire Linux Inc. - * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> - * Author: Albert Babà <albert.babi@savoirfairelinux.com> + * Copyright (C) 2020-2022 Savoir-faire Linux + * Authors: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Albert Babà <albert.babi@savoirfairelinux.com> + * Aline Gondim Santos <aline.gondimsantos@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 @@ -34,24 +35,27 @@ Item { // svg path for the participant indicators background shape property int shapeWidth: participantFootInfo.width + 8 property int shapeHeight: 30 - property int shapeRadius: 6 + property int shapeRadius: 5 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) - property string uri: overlayMenu.uri + property string uri: "" property string bestName: "" - property alias sinkId: mediaDistRender.rendererId + property string sinkId: "" property bool participantIsActive: false - property bool participantIsHost: false + property bool participantIsHost: CallAdapter.participantIsHost(uri) property bool participantIsModerator: false - property bool participantIsMuted: false + property bool participantIsMuted: isLocalMuted || participantIsModeratorMuted property bool participantIsModeratorMuted: false property bool participantHandIsRaised: false + property bool videoMuted: true + property bool isLocalMuted: true - property bool meModerator: false + property bool meHost: CallAdapter.isCurrentHost() + property bool meModerator: CallAdapter.isModerator() property bool isMe: false property string muteAlertMessage: "" @@ -63,92 +67,12 @@ Item { } } - Connections { - target: CallParticipantsModel - - function onUpdateParticipant(participantInfos) { - if (participantInfos.uri === overlayMenu.uri) { - root.sinkId = participantInfos.videoMuted ? "" : participantInfos.sinkId - setMenu(participantInfos.uri, participantInfos.bestName, participantInfos.isLocal, participantInfos.active) - setAvatar(participantInfos.videoMuted, participantInfos.uri, participantInfos.isLocal) - } - } - } - - function setAvatar(show, uri, isLocal) { - if (!show) - avatar.active = false - else { - avatar.mode_ = isLocal ? Avatar.Mode.Account : Avatar.Mode.Contact - avatar.imageId_ = isLocal ? LRCInstance.currentAccountId : uri - avatar.active = true - } - } - - function setMenu(newUri, bestName, isLocal, isActive) { - - overlayMenu.uri = newUri - root.bestName = bestName - isMe = overlayMenu.uri === CurrentAccount.uri - - var isHost = CallAdapter.isCurrentHost() - meModerator = CallAdapter.isCurrentModerator() - participantIsHost = CallAdapter.participantIsHost(overlayMenu.uri) - participantIsModerator = CallAdapter.isModerator(overlayMenu.uri) - participantIsActive = isActive - participantHandIsRaised = CallAdapter.isHandRaised(overlayMenu.uri) - overlayMenu.showSetModerator = isHost && !isLocal && !participantIsModerator - overlayMenu.showUnsetModerator = isHost && !isLocal && participantIsModerator - - var muteState = CallAdapter.getMuteState(overlayMenu.uri) - overlayMenu.isLocalMuted = muteState === CallAdapter.LOCAL_MUTED - || muteState === CallAdapter.BOTH_MUTED - participantIsModeratorMuted = muteState === CallAdapter.MODERATOR_MUTED - || muteState === CallAdapter.BOTH_MUTED - - participantIsMuted = overlayMenu.isLocalMuted || participantIsModeratorMuted - - overlayMenu.showModeratorMute = meModerator && !participantIsModeratorMuted - overlayMenu.showModeratorUnmute = (meModerator || isMe) && participantIsModeratorMuted - overlayMenu.showMaximize = meModerator - overlayMenu.showMinimize = meModerator && participantIsActive - overlayMenu.showHangup = meModerator && !isLocal && !participantIsHost - } - TextMetrics { id: nameTextMetrics text: bestName font.pointSize: JamiTheme.participantFontSize } - Loader { - id: avatar - - anchors.centerIn: parent - - active: false - - property real size_: Math.min(parent.width / 2, parent.height / 2) - height: size_ - width: size_ - z: 0 - - property int mode_ - property string imageId_ - - sourceComponent: Component { - Avatar { - // round the avatar source size up to some nearest multiple - readonly property real step: 96 - property real size: Math.floor((size_ + step - 1) / step) * step - sourceSize: Qt.size(size, size) - mode: mode_ - imageId: size_ ? imageId_ : "" - showPresenceIndicator: false - } - } - } - // Timer to decide when ParticipantOverlay fade out Timer { id: fadeOutTimer @@ -164,251 +88,273 @@ Item { VideoView { id: mediaDistRender - width: parent.width - height: width * invAspectRatio - anchors.centerIn: parent + anchors.fill: parent rendererId: root.sinkId - visible: !root.videoMuted - // Update overlays if the internal or visual geometry changes. - property real area: width * height + underlayItems: Avatar { + property real componentSize: Math.min(mediaDistRender.contentRect.width / 2, mediaDistRender.contentRect.height / 2) + height: componentSize + width: componentSize + anchors.centerIn: parent + // round the avatar source size up to some nearest multiple + readonly property real step: 96 + property real size: Math.floor((componentSize + step - 1) / step) * step + sourceSize: Qt.size(size, size) + mode: root.isMe ? Avatar.Mode.Account : Avatar.Mode.Contact + imageId: root.isMe ? LRCInstance.currentAccountId : root.uri + showPresenceIndicator: false + visible: root.videoMuted + } - Item { - anchors.fill: parent + overlayItems: Rectangle { + id: overlayRect - HoverHandler { - onPointChanged: { - participantRect.opacity = 1 - fadeOutTimer.restart() - } + width: mediaDistRender.contentRect.width + height: mediaDistRender.contentRect.height + anchors.centerIn: parent + color: "transparent" - onHoveredChanged: { - if (overlayMenu.hovered) { + Item { + anchors.fill: parent + + HoverHandler { + onPointChanged: { participantRect.opacity = 1 fadeOutTimer.restart() - return } - participantRect.opacity = hovered ? 1 : 0 - } - } - } - layer.enabled: !root.videoMuted - layer.effect: OpacityMask { - maskSource: Item { - width: mediaDistRender.width - height: mediaDistRender.height - Rectangle { - anchors.centerIn: parent - width: participantRect.width - height: participantRect.height - radius: 10 + onHoveredChanged: { + if (overlayMenu.hovered) { + participantRect.opacity = 1 + fadeOutTimer.restart() + return + } + participantRect.opacity = hovered ? 1 : 0 + } } } - } - } - // Participant background - Rectangle { - id: participantRect - - width: mediaDistRender.width - height: mediaDistRender.height - anchors.centerIn: parent - color: "transparent" - opacity: 0 - - // Participant background and buttons for moderation - ParticipantOverlayMenu { - id: overlayMenu - visible: isMe || meModerator - anchors.fill: parent - - onHoveredChanged: { - if (hovered) { - participantRect.opacity = 1 - fadeOutTimer.restart() - } else { - participantRect.opacity = 0 - } - } - } + Rectangle { + id: participantRect + anchors.fill: parent + color: "transparent" + opacity: 0 + + // Participant buttons for moderation + ParticipantOverlayMenu { + id: overlayMenu + visible: isMe || meModerator + anchors.fill: parent + + onHoveredChanged: { + if (hovered) { + participantRect.opacity = 1 + fadeOutTimer.restart() + } else { + participantRect.opacity = 0 + } + } - // Participant footer with host, moderator and mute indicators - // Mute indicator is as follow: - // - In another participant, if i am not moderator, the mute state is isLocalMuted || participantIsModeratorMuted - // - In another participant, if i am moderator, the mute state is isLocalMuted - // - In my video, the mute state is isLocalMuted - Rectangle { - id: participantIndicators - width: participantRect.width - height: shapeHeight - color: "transparent" - anchors.bottom: parent.bottom - - Shape { - id: backgroundShape - ShapePath { - id: backgroundShapePath - strokeColor: "transparent" - fillColor: JamiTheme.darkGreyColorOpacity - capStyle: ShapePath.RoundCap - PathSvg { path: pathShape } + showSetModerator: root.meHost && !root.isMe && !root.participantIsModerator + showUnsetModerator: root.meHost && !root.isMe && root.participantIsModerator + showModeratorMute: root.meModerator && !root.participantIsModeratorMuted + showModeratorUnmute: (root.meModerator || root.isMe) && root.participantIsModeratorMuted + showMaximize: root.meModerator && !root.participantIsActive + showMinimize: root.meModerator && root.participantIsActive + showHangup: root.meModerator && !root.isMe && !root.participantIsHost } - } - - RowLayout { - id: participantFootInfo - height: parent.height - anchors.verticalCenter: parent.verticalCenter - Text { - id: bestNameLabel - Layout.leftMargin: 8 - Layout.preferredWidth: Math.min(nameTextMetrics.boundingRect.width + 8, - participantIndicators.width - indicatorsRowLayout.width - 16) - Layout.preferredHeight: shapeHeight - - text: bestName - elide: Text.ElideRight - color: JamiTheme.whiteColor - font.pointSize: JamiTheme.participantFontSize - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - HoverHandler { id: hoverName } - MaterialToolTip { - visible: hoverName.hovered && (text.length > 0) - text: bestNameLabel.truncated ? bestName : "" + // Participant footer with host, moderator and mute indicators + // Mute indicator is as follow: + // - In another participant, if i am not moderator, the mute state is isLocalMuted || participantIsModeratorMuted + // - In another participant, if i am moderator, the mute state is isLocalMuted + // - In my video, the mute state is isLocalMuted + Rectangle { + id: participantIndicators + width: participantRect.width + height: shapeHeight + color: "transparent" + anchors.bottom: parent.bottom + + Shape { + id: backgroundShape + ShapePath { + id: backgroundShapePath + strokeColor: "transparent" + fillColor: JamiTheme.darkGreyColorOpacity + capStyle: ShapePath.RoundCap + PathSvg { path: pathShape } + } } - } - - RowLayout { - id: indicatorsRowLayout - height: parent.height - Layout.alignment: Qt.AlignVCenter - - ResponsiveImage { - id: isHostIndicator - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: 6 - - containerHeight: 12 - containerWidth: 12 - - visible: participantIsHost + RowLayout { + id: participantFootInfo + height: parent.height + anchors.verticalCenter: parent.verticalCenter + Text { + id: bestNameLabel + + Layout.leftMargin: 8 + Layout.preferredWidth: Math.min(nameTextMetrics.boundingRect.width + 8, + participantIndicators.width - indicatorsRowLayout.width - 16) + Layout.preferredHeight: shapeHeight + + text: bestName + elide: Text.ElideRight + color: JamiTheme.whiteColor + font.pointSize: JamiTheme.participantFontSize + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + HoverHandler { id: hoverName } + MaterialToolTip { + visible: hoverName.hovered && (text.length > 0) + text: bestNameLabel.truncated ? bestName : "" + } + } - source: JamiResources.star_outline_24dp_svg - color: JamiTheme.whiteColor + RowLayout { + id: indicatorsRowLayout + height: parent.height + Layout.alignment: Qt.AlignVCenter - HoverHandler { id: hoverHost } - MaterialToolTip { - visible: hoverHost.hovered - text: JamiStrings.host - } - } + ResponsiveImage { + id: isHostIndicator - ResponsiveImage { - id: isModeratorIndicator + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 6 - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: 6 + containerHeight: 12 + containerWidth: 12 - containerHeight: 12 - containerWidth: 12 + visible: root.participantIsHost - visible: !participantIsHost && participantIsModerator + source: JamiResources.star_outline_24dp_svg + color: JamiTheme.whiteColor - source: JamiResources.moderator_svg - color: JamiTheme.whiteColor + HoverHandler { id: hoverHost } + MaterialToolTip { + visible: hoverHost.hovered + text: JamiStrings.host + } + } - HoverHandler { id: hoverModerator } - MaterialToolTip { - visible: hoverModerator.hovered - text: JamiStrings.moderator - } - } + ResponsiveImage { + id: isModeratorIndicator - ResponsiveImage { - id: isMutedIndicator + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 6 - Layout.alignment: Qt.AlignVCenter - Layout.leftMargin: 6 + containerHeight: 12 + containerWidth: 12 - containerHeight: 12 - containerWidth: 12 + visible: !root.participantIsHost && root.participantIsModerator - visible: (!isMe && !meModerator) ? participantIsMuted : overlayMenu.isLocalMuted + source: JamiResources.moderator_svg + color: JamiTheme.whiteColor - source: JamiResources.micro_off_black_24dp_svg - color: "red" + HoverHandler { id: hoverModerator } + MaterialToolTip { + visible: hoverModerator.hovered + text: JamiStrings.moderator + } + } - HoverHandler { id: hoverMicrophone } - MaterialToolTip { - visible: hoverMicrophone.hovered - text: { - if (!isMe && !meModerator && participantIsModeratorMuted && overlayMenu.isLocalMuted) - return JamiStrings.bothMuted - if (overlayMenu.isLocalMuted) - return JamiStrings.localMuted - if (!isMe && !meModerator && participantIsModeratorMuted) - return JamiStrings.moderatorMuted - return JamiStrings.notMuted + ResponsiveImage { + id: isMutedIndicator + + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: 6 + + containerHeight: 12 + containerWidth: 12 + + visible: (!root.isMe && !root.meModerator) ? root.participantIsMuted : root.isLocalMuted + + source: JamiResources.micro_off_black_24dp_svg + color: "red" + + HoverHandler { id: hoverMicrophone } + MaterialToolTip { + visible: hoverMicrophone.hovered + text: { + if (!root.isMe && !root.meModerator && root.participantIsModeratorMuted && root.isLocalMuted) + return JamiStrings.bothMuted + if (root.isLocalMuted) + return JamiStrings.localMuted + if (!root.isMe && !root.meModerator && root.participantIsModeratorMuted) + return JamiStrings.moderatorMuted + return JamiStrings.notMuted + } + } } } } } + + Behavior on opacity { NumberAnimation { duration: JamiTheme.shortFadeDuration }} } - } - Behavior on opacity { NumberAnimation { duration: JamiTheme.shortFadeDuration }} - } + PushButton { + id: isRaiseHandIndicator + source: JamiResources.hand_black_24dp_svg + imageColor: JamiTheme.whiteColor + preferredSize: shapeHeight + visible: root.participantHandIsRaised + anchors.right: participantRect.right + anchors.top: participantRect.top + checkable: root.meModerator + pressedColor: JamiTheme.raiseHandColor + hoveredColor: JamiTheme.raiseHandColor + normalColor: JamiTheme.raiseHandColor + z: participantRect.z + 1 + toolTipText: root.meModerator ? JamiStrings.lowerHand : "" + onClicked: CallAdapter.setHandRaised(uri, false) + radius: 5 + } - PushButton { - id: isRaiseHandIndicator - source: JamiResources.hand_black_24dp_svg - imageColor: JamiTheme.whiteColor - preferredSize: shapeHeight - visible: root.participantHandIsRaised - anchors.right: participantRect.right - anchors.top: participantRect.top - checkable: root.meModerator - pressedColor: JamiTheme.raiseHandColor - hoveredColor: JamiTheme.raiseHandColor - normalColor: JamiTheme.raiseHandColor - z: participantRect.z + 1 - toolTipText: root.meModerator ? JamiStrings.lowerHand : "" - onClicked: CallAdapter.setHandRaised(uri, false) - radius: 5 - } + Rectangle { + id: alertMessage - Rectangle { - id: alertMessage + anchors.centerIn: parent + width: alertMessageTxt.width + 16 + height: alertMessageTxt.contentHeight + 16 + radius: 5 + visible: root.muteAlertActive + color: JamiTheme.darkGreyColorOpacity - anchors.centerIn: parent - width: alertMessageTxt.width + 16 - height: alertMessageTxt.contentHeight + 16 - radius: 5 - visible: root.muteAlertActive - color: JamiTheme.darkGreyColorOpacity + Text { + id: alertMessageTxt + text: root.muteAlertMessage + anchors.centerIn: parent + width: Math.min(participantRect.width, contentWidth) + color: JamiTheme.whiteColor + font.pointSize: JamiTheme.textFontSize + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } - Text { - id: alertMessageTxt - text: root.muteAlertMessage - anchors.centerIn: parent - width: Math.min(participantRect.width, contentWidth) - color: JamiTheme.whiteColor - font.pointSize: JamiTheme.textFontSize - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + Timer { + id: alertTimer + interval: JamiTheme.overlayFadeDelay + onTriggered: { + root.muteAlertActive = false + } + } + } } - Timer { - id: alertTimer - interval: JamiTheme.overlayFadeDelay - onTriggered: { - root.muteAlertActive = false + layer.enabled: !root.videoMuted + layer.effect: OpacityMask { + maskSource: Item { + width: mediaDistRender.width + height: mediaDistRender.height + Rectangle { + anchors.centerIn: parent + width: participantRect.width + height: participantRect.height + radius: 10 + } } } } diff --git a/src/mainview/components/ParticipantOverlayMenu.qml b/src/mainview/components/ParticipantOverlayMenu.qml index 5fd6f5a7a2fc15f2aeb5d0dbfdfb607c8f4a5f83..114257c8baf9329e37e27f1dd261564a13f5cccc 100644 --- a/src/mainview/components/ParticipantOverlayMenu.qml +++ b/src/mainview/components/ParticipantOverlayMenu.qml @@ -30,8 +30,6 @@ import "../../commoncomponents" Item { id: root - property string uri: "" - property bool isLocalMuted: true property bool showSetModerator: false property bool showUnsetModerator: false property bool showModeratorMute: false @@ -41,12 +39,12 @@ Item { property bool showHangup: false property int shapeHeight: 30 - property int shapeRadius: 8 + property int shapeRadius: 10 property bool isBarLayout: root.width > 220 property int isSmall: !isBarLayout && (root.height < 100 || root.width < 160) - property int buttonPreferredSize: 24 + property int buttonPreferredSize: 20 property int iconButtonPreferredSize: 16 property alias hovered: hover.hovered diff --git a/src/mainview/components/ParticipantsLayer.qml b/src/mainview/components/ParticipantsLayer.qml index 1c24868492e702d22630678c8854ba2c1ccb37cc..f9fb361249b4b4e5d35b20238da1e7253f85c193 100644 --- a/src/mainview/components/ParticipantsLayer.qml +++ b/src/mainview/components/ParticipantsLayer.qml @@ -1,6 +1,7 @@ /* - * Copyright (C) 2020-2022 Savoir-faire Linux Inc. - * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Copyright (C) 2020-2022 by Savoir-faire Linux + * Authors: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Aline Gondim Santos <aline.gondimsantos@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 @@ -19,23 +20,17 @@ import QtQuick import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + import net.jami.Adapters 1.1 import net.jami.Models 1.1 +import net.jami.Constants 1.1 Item { id: root - property alias count: participantincall.count - - Connections { - target: CallAdapter - - function onUpdateParticipantsLayout() { - participantsFlow.columns = Math.max(1, Math.ceil(Math.sqrt(participantincall.count))) - participantsFlow.rows = Math.max(1, Math.ceil(participantincall.count/participantsFlow.columns)) - } - } - + property int count: commonParticipants.count + activeParticipants.count + property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL Component { id: callVideoMedia @@ -45,45 +40,221 @@ Item { anchors.centerIn: parent sinkId: sinkId_ - - Component.onCompleted: { - setMenu(uri_, bestName_, isLocal_, active_) - setAvatar(videoMuted_, uri_, isLocal_) - VideoDevices.startDevice(sinkId) - } + uri: uri_ + isMe: isLocal_ + participantIsModerator: isModerator_ + bestName: bestName_ + videoMuted: videoMuted_ + participantIsActive: active_ + isLocalMuted: audioLocalMuted_ + participantIsModeratorMuted: audioModeratorMuted_ + participantHandIsRaised: isHandRaised_ } } - Flow { - id: participantsFlow + SplitView { anchors.fill: parent - anchors.centerIn: parent - spacing: 8 - property int columns: Math.max(1, Math.ceil(Math.sqrt(participantincall.count))) - property int rows: Math.max(1, Math.ceil(participantincall.count/columns)) - property int columnsSpacing: 5 * (columns - 1) - property int rowsSpacing: 5 * (rows - 1) - - Repeater { - id: participantincall - anchors.fill: parent - anchors.centerIn: parent - model: CallParticipantsModel - delegate: Loader { - sourceComponent: callVideoMedia - width: Math.ceil(participantsFlow.width / participantsFlow.columns) - participantsFlow.columnsSpacing - height: Math.ceil(participantsFlow.height / participantsFlow.rows) - participantsFlow.rowsSpacing - - property string uri_: Uri - property string bestName_: BestName - property string avatar_: Avatar ? Avatar : "" - property string sinkId_: SinkId ? SinkId : "" - property bool isLocal_: IsLocal - property bool active_: Active - property bool videoMuted_: VideoMuted - property bool isContact_: IsContact - property bool isHandRaised_: HandRaised + orientation: Qt.Vertical + handle: Rectangle { + implicitWidth: root.width + implicitHeight: 11 + color: "transparent" + Rectangle { + anchors.centerIn: parent + height: 1 + width: parent.implicitWidth - 40 + color: JamiTheme.darkGreyColor + } + + Rectangle { + width: 45 + anchors.centerIn: parent + height: 1 + color: "black" + } + + ColumnLayout { + anchors.centerIn: parent + height: 11 + width: 45 + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + height: 2 + color: JamiTheme.darkGreyColor + } + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.rightMargin: 10 + height: 2 + color: JamiTheme.darkGreyColor + } + } + } + + Rectangle { + id: genericParticipantsRect + + SplitView.preferredHeight: (parent.height / 4) + SplitView.minimumHeight: parent.height / 4 + SplitView.maximumHeight: inLine? parent.height / 4 : parent.height + + visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.GRID + color: "transparent" + + property int lowLimit: 0 + property int topLimit: commonParticipants.count + property int currentPos: 0 + property int showable: { + var placeableElements = inLine ? Math.floor((width * 0.95)/commonParticipantsFlow.componentWidth) : commonParticipants.count + if (commonParticipants.count - placeableElements < currentPos) + currentPos = Math.max(commonParticipants.count - placeableElements, 0) + return placeableElements + } + + RowLayout { + anchors.fill: parent + anchors.centerIn: parent + z: 1 + + RoundButton { + Layout.alignment: Qt.AlignVCenter + width : 30 + height : 30 + radius: 10 + text: "<" + visible: genericParticipantsRect.currentPos > 0 + onClicked: { + if (genericParticipantsRect.currentPos > 0) + genericParticipantsRect.currentPos-- + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + RoundButton { + Layout.alignment: Qt.AlignVCenter + width : 30 + height : 30 + radius: 10 + text: ">" + visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos + onClicked: { + if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos) + genericParticipantsRect.currentPos++ + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + } + + Rectangle { + z:0 + anchors.centerIn: parent + property int elements: inLine ? Math.min(genericParticipantsRect.showable, commonParticipants.count) : commonParticipantsFlow.columns + width: commonParticipantsFlow.componentWidth * elements + elements - 1 + implicitHeight: parent.height + commonParticipantsFlow.rows - 1 + color: "transparent" + + // GENERIC + Flow { + id: commonParticipantsFlow + anchors.centerIn: parent + anchors.fill: parent + + spacing: 1 + property int columns: inLine ? commonParticipants.count : Math.max(1, Math.ceil(Math.sqrt(commonParticipants.count))) + property int rows: Math.max(1, Math.ceil(commonParticipants.count/columns)) + property int componentWidth: inLine ? height : Math.floor(genericParticipantsRect.width / commonParticipantsFlow.columns) - 1 + + Repeater { + id: commonParticipants + + model: GenericParticipantsFilterModel + delegate: Loader { + sourceComponent: callVideoMedia + visible: inLine ? index >= genericParticipantsRect.currentPos && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable : true + width: { + var lastLine = commonParticipants.count % commonParticipantsFlow.columns + var horComponents = ((commonParticipants.count - index) > lastLine || index < 0) ? commonParticipantsFlow.columns : lastLine + if (horComponents === lastLine) + return Math.floor(commonParticipantsFlow.width / horComponents) - 1 + else + return commonParticipantsFlow.componentWidth + } + height: inLine ? commonParticipantsFlow.componentWidth : Math.floor(genericParticipantsRect.height / commonParticipantsFlow.rows) - 1 + + property string uri_: Uri + property string bestName_: BestName + property string avatar_: Avatar ? Avatar : "" + property string sinkId_: SinkId ? SinkId : "" + property bool isLocal_: IsLocal + property bool active_: Active + property bool videoMuted_: VideoMuted + property bool isContact_: IsContact + property bool isModerator_: IsModerator + property bool audioLocalMuted_: AudioLocalMuted + property bool audioModeratorMuted_: AudioModeratorMuted + property bool isHandRaised_: HandRaised + } + } + } + } + } + + // ACTIVE + Flow { + id: activeParticipantsFlow + + SplitView.minimumHeight: parent.height / 4 + SplitView.maximumHeight: parent.height + SplitView.fillHeight: true + + spacing: 8 + property int columns: Math.max(1, Math.ceil(Math.sqrt(activeParticipants.count))) + property int rows: Math.max(1, Math.ceil(activeParticipants.count/columns)) + property int columnsSpacing: 5 * (columns - 1) + property int rowsSpacing: 5 * (rows - 1) + + visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE + + Repeater { + id: activeParticipants + anchors.fill: parent + anchors.centerIn: parent + + model: ActiveParticipantsFilterModel + delegate: Loader { + sourceComponent: callVideoMedia + width: Math.ceil(activeParticipantsFlow.width / activeParticipantsFlow.columns) - activeParticipantsFlow.columnsSpacing + height: Math.ceil(activeParticipantsFlow.height / activeParticipantsFlow.rows) - activeParticipantsFlow.rowsSpacing + + property string uri_: Uri + property string bestName_: BestName + property string avatar_: Avatar ? Avatar : "" + property string sinkId_: SinkId ? SinkId : "" + property bool isLocal_: IsLocal + property bool active_: Active + property bool videoMuted_: VideoMuted + property bool isContact_: IsContact + property bool isModerator_: IsModerator + property bool audioLocalMuted_: AudioLocalMuted + property bool audioModeratorMuted_: AudioModeratorMuted + property bool isHandRaised_: HandRaised + } } } }