From 824ba581c8dedc33d07e8835acc25f404c650d5d Mon Sep 17 00:00:00 2001 From: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> Date: Mon, 1 Aug 2022 12:32:32 -0300 Subject: [PATCH] callbuttons: create alternate layouts GitLab: #729 Change-Id: Ice67d8649c1ad2a92eba7c02cebc446eac5ac90e --- qml.qrc | 2 + resources/icons/ontheside_black_24dp.svg | 8 + resources/icons/onthetop_black_24dp.svg | 8 + src/app/appsettingsmanager.h | 1 + src/app/calladapter.cpp | 1 - src/app/constant/JamiStrings.qml | 2 + src/app/mainview/components/CallActionBar.qml | 16 + src/app/mainview/components/CallOverlay.qml | 2 + .../mainview/components/OngoingCallPage.qml | 1 + .../mainview/components/ParticipantsLayer.qml | 272 +-------------- .../ParticipantsLayoutHorizontal.qml | 321 ++++++++++++++++++ .../components/ParticipantsLayoutVertical.qml | 293 ++++++++++++++++ 12 files changed, 668 insertions(+), 259 deletions(-) create mode 100644 resources/icons/ontheside_black_24dp.svg create mode 100644 resources/icons/onthetop_black_24dp.svg create mode 100644 src/app/mainview/components/ParticipantsLayoutHorizontal.qml create mode 100644 src/app/mainview/components/ParticipantsLayoutVertical.qml diff --git a/qml.qrc b/qml.qrc index e7ab3be11..8e731998c 100644 --- a/qml.qrc +++ b/qml.qrc @@ -136,6 +136,8 @@ <file>src/app/mainview/components/SmartListItemDelegate.qml</file> <file>src/app/mainview/components/BadgeNotifier.qml</file> <file>src/app/mainview/components/ParticipantsLayer.qml</file> + <file>src/app/mainview/components/ParticipantsLayoutVertical.qml</file> + <file>src/app/mainview/components/ParticipantsLayoutHorizontal.qml</file> <file>src/app/mainview/components/MainOverlay.qml</file> <file>src/app/mainview/components/CallButtonDelegate.qml</file> <file>src/app/mainview/components/CallActionBar.qml</file> diff --git a/resources/icons/ontheside_black_24dp.svg b/resources/icons/ontheside_black_24dp.svg new file mode 100644 index 000000000..872f97e45 --- /dev/null +++ b/resources/icons/ontheside_black_24dp.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> +<path d="M18.3,2H5.8C3.7,2,2,3.7,2,5.8v12.4C2,20.3,3.7,22,5.8,22h12.4c2.1,0,3.8-1.7,3.8-3.7V5.8C22,3.7,20.3,2,18.3,2z M20.6,5.8 + v2.7h-3.7v-5h1.4C19.6,3.5,20.6,4.5,20.6,5.8z M16.9,10h3.7v4.3h-3.7V10z M3.5,18.2V5.7c0-1.2,1-2.2,2.3-2.2h9.6v17H5.8 + C4.6,20.5,3.5,19.5,3.5,18.2z M18.3,20.5h-1.4v-4.8h3.7v2.5C20.6,19.5,19.6,20.5,18.3,20.5z"/> +</svg> diff --git a/resources/icons/onthetop_black_24dp.svg b/resources/icons/onthetop_black_24dp.svg new file mode 100644 index 000000000..664e31e0a --- /dev/null +++ b/resources/icons/onthetop_black_24dp.svg @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> +<path d="M18.3,2H5.8C3.7,2,2,3.7,2,5.8v12.4C2,20.3,3.7,22,5.8,22h12.4c2.1,0,3.8-1.7,3.8-3.7V5.8C22,3.7,20.3,2,18.3,2z M20.6,5.8 + V7h-4.8V3.5h2.5C19.6,3.5,20.6,4.5,20.6,5.8z M10.1,3.5h4.3V7h-4.3V3.5z M5.8,3.5h2.8V7H3.5V5.7C3.5,4.5,4.5,3.5,5.8,3.5z + M18.3,20.5H5.8c-1.2,0-2.3-1-2.3-2.3V8.5h17.1v9.7C20.6,19.5,19.6,20.5,18.3,20.5z"/> +</svg> diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 3ecc4a88d..0f13b2a33 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -46,6 +46,7 @@ extern const QString defaultDownloadPath; X(EnableExperimentalSwarm, false) \ X(EnableDarkTheme, false) \ X(BaseZoom, 1.0) \ + X(ParticipantsSide, false) \ X(AutoUpdate, true) \ X(StartMinimized, false) \ X(ShowChatviewHorizontally, true) \ diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp index 08d8ce0dc..d7ce8a7c8 100644 --- a/src/app/calladapter.cpp +++ b/src/app/calladapter.cpp @@ -679,7 +679,6 @@ CallAdapter::sipInputPanelPlayDTMF(const QString& key) void CallAdapter::updateCallOverlay(const lrc::api::conversation::Info& convInfo) { - qWarning() << "CallAdapter::updateCallOverlay"; auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId_); auto* callModel = accInfo.callModel.get(); diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index 930c43798..8724cabbf 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -250,6 +250,8 @@ Item { property string bothMuted: qsTr("Local and Moderator muted") property string moderatorMuted: qsTr("Moderator muted") property string notMuted: qsTr("Not muted") + property string participantsSide: qsTr("On the side") + property string participantsTop: qsTr("On the top") // LineEditContextMenu property string copy: qsTr("Copy") diff --git a/src/app/mainview/components/CallActionBar.qml b/src/app/mainview/components/CallActionBar.qml index b1fe1b218..e58221ad5 100644 --- a/src/app/mainview/components/CallActionBar.qml +++ b/src/app/mainview/components/CallActionBar.qml @@ -23,6 +23,7 @@ import QtQuick.Layouts import net.jami.Models 1.1 import net.jami.Adapters 1.1 import net.jami.Constants 1.1 +import net.jami.Enums 1.1 import "../../commoncomponents" @@ -163,6 +164,16 @@ Control { if (!isGrid) CallAdapter.showGridConferenceLayout() break + case JamiStrings.participantsSide: + var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide) + UtilsAdapter.setAppValue(Settings.ParticipantsSide, !onTheSide) + participantsSide = !onTheSide + break + case JamiStrings.participantsTop: + var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide) + UtilsAdapter.setAppValue(Settings.ParticipantsSide, !onTheSide) + participantsSide = !onTheSide + break } } onTriggered: { @@ -172,6 +183,11 @@ Control { "ActiveSetting": layoutManager.isCallFullscreen}) if (isConference) { layoutModel.append({}) + var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide) + layoutModel.append({"Name": onTheSide ? JamiStrings.participantsSide : JamiStrings.participantsTop, + "IconSource": onTheSide ? JamiResources.ontheside_black_24dp_svg : JamiResources.onthetop_black_24dp_svg, + "ActiveSetting": true}) + layoutModel.append({}) layoutModel.append({"Name": JamiStrings.mosaic, "IconSource": JamiResources.mosaic_black_24dp_svg, "ActiveSetting": isGrid}) diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml index 2a9beb8f2..bb78bbe32 100644 --- a/src/app/mainview/components/CallOverlay.qml +++ b/src/app/mainview/components/CallOverlay.qml @@ -23,6 +23,7 @@ import QtQuick import net.jami.Models 1.1 import net.jami.Adapters 1.1 import net.jami.Constants 1.1 +import net.jami.Enums 1.1 import "../js/contactpickercreation.js" as ContactPickerCreation import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation @@ -44,6 +45,7 @@ Item { property bool isModerator property bool isConference property bool isGrid + property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide) property bool localHandRaised property bool sharingActive: AvAdapter.isSharing() property string callId: "" diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml index 79e55c502..3674ddc6f 100644 --- a/src/app/mainview/components/OngoingCallPage.qml +++ b/src/app/mainview/components/OngoingCallPage.qml @@ -177,6 +177,7 @@ Rectangle { anchors.centerIn: parent anchors.margins: 3 visible: participantsLayer.count !== 0 + participantsSide: callOverlay.participantsSide onCountChanged: { callOverlay.isConference = participantsLayer.count > 0 diff --git a/src/app/mainview/components/ParticipantsLayer.qml b/src/app/mainview/components/ParticipantsLayer.qml index 5881c7f64..ceac7cfc2 100644 --- a/src/app/mainview/components/ParticipantsLayer.qml +++ b/src/app/mainview/components/ParticipantsLayer.qml @@ -25,12 +25,15 @@ import QtQuick.Controls 2.15 import net.jami.Adapters 1.1 import net.jami.Models 1.1 import net.jami.Constants 1.1 +import net.jami.Enums 1.1 +import "../../commoncomponents" Item { id: root - property int count: commonParticipants.count + activeParticipants.count + property int count: 0 property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL + property bool participantsSide Component { id: callVideoMedia @@ -70,265 +73,18 @@ Item { } } - SplitView { + ParticipantsLayoutVertical { anchors.fill: parent + participantComponent: callVideoMedia + visible: !participantsSide - 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 - - TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton } - - SplitView.preferredHeight: (parent.height / 4) - SplitView.minimumHeight: parent.height / 6 - SplitView.maximumHeight: inLine? parent.height / 2 : 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: { - if (!inLine) - return commonParticipants.count - if (commonParticipantsFlow.componentWidth === 0) - return 1 - var placeableElements = Math.floor((width * 0.9)/commonParticipantsFlow.componentWidth) - if (commonParticipants.count - placeableElements < currentPos) - currentPos = Math.max(commonParticipants.count - placeableElements, 0) - return Math.max(1, placeableElements) - } - - RowLayout { - anchors.fill: parent - - RoundButton { - Layout.alignment: Qt.AlignVCenter - width : 30 - height : 30 - radius: 10 - text: "<" - visible: genericParticipantsRect.currentPos > 0 - && activeParticipantsFlow.visible - onClicked: { - if (genericParticipantsRect.currentPos > 0) - genericParticipantsRect.currentPos-- - } - background: Rectangle { - anchors.fill: parent - color: JamiTheme.lightGrey_ - radius: JamiTheme.primaryRadius - } - } - - Item { - id: centerItem - Layout.fillHeight: true - Layout.fillWidth: true - Layout.margins: 4 - - // GENERIC - Flow { - id: commonParticipantsFlow - anchors.fill: parent - - spacing: 4 - property int columns: { - if (inLine) - return commonParticipants.count - var ratio = Math.floor(root.width / root.height) - // If ratio is 2 we can have 2 times more elements on each columns - var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio)) - var cols = Math.min(commonParticipants.count, wantedCol) - // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1) - var rows = Math.max(1, Math.ceil(commonParticipants.count/cols)) - return Math.min(Math.ceil(commonParticipants.count / rows), cols) - } - property int rows: Math.max(1, Math.ceil(commonParticipants.count/columns)) - property int componentWidth: { - var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns - var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns) - if (inLine) { - w = Math.max(w, height) - w = Math.min(w, height * 4 / 3) // Avoid too wide elements - } - return w - } - - Item { - height: parent.height - width: { - if (!inLine) - return 0 - var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.columns) - return Math.max(0, Math.ceil((centerItem.width - commonParticipantsFlow.componentWidth * showed) / 2)) - } - } - - Repeater { - id: commonParticipants - - model: GenericParticipantsFilterModel - delegate: Loader { - sourceComponent: callVideoMedia - active: root.visible - asynchronous: true - visible: { - if (status !== Loader.Ready) - return false - if (inLine) - return index >= genericParticipantsRect.currentPos - && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable - return true - } - width: commonParticipantsFlow.componentWidth + leftMargin_ - height: { - if (inLine || commonParticipantsFlow.rows === 1) - return genericParticipantsRect.height - var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows - return Math.floor((genericParticipantsRect.height - totalSpacing)/ commonParticipantsFlow.rows) - } - - property int leftMargin_: { - if (inLine || commonParticipantsFlow.rows === 1) - return 0 - var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns) - if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) { - var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing - var lastLineW = lastParticipants * compW - return Math.floor((commonParticipantsFlow.width - lastLineW) / 2) - } - return 0 - } - - property string uri_: Uri - property string deviceId_: Device - 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 - } - } - } - } - - RoundButton { - Layout.alignment: Qt.AlignVCenter - width : 30 - height : 30 - radius: 10 - text: ">" - visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos - && activeParticipantsFlow.visible - onClicked: { - if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos) - genericParticipantsRect.currentPos++ - } - background: Rectangle { - anchors.fill: parent - color: JamiTheme.lightGrey_ - radius: JamiTheme.primaryRadius - } - } - } - } - - // ACTIVE - Flow { - id: activeParticipantsFlow - - TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton } - - 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 { - active: root.visible - asynchronous: true - sourceComponent: callVideoMedia - visible: status == Loader.Ready - - width: Math.ceil(activeParticipantsFlow.width / activeParticipantsFlow.columns) - activeParticipantsFlow.columnsSpacing - height: Math.ceil(activeParticipantsFlow.height / activeParticipantsFlow.rows) - activeParticipantsFlow.rowsSpacing + onLayoutCountChanged: root.count = layoutCount + } - property string uri_: Uri - property string bestName_: BestName - property string avatar_: Avatar ? Avatar : "" - property string sinkId_: SinkId ? SinkId : "" - property string deviceId_: Device - property int leftMargin_: 0 - 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 - } - } - } + ParticipantsLayoutHorizontal { + anchors.fill: parent + participantComponent: callVideoMedia + visible: participantsSide + onLayoutCountChanged: root.count = layoutCount } } diff --git a/src/app/mainview/components/ParticipantsLayoutHorizontal.qml b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml new file mode 100644 index 000000000..e0fc08a0d --- /dev/null +++ b/src/app/mainview/components/ParticipantsLayoutHorizontal.qml @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2020-2022 Savoir-faire Linux Inc. + * 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 + * 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 + +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 +import net.jami.Enums 1.1 + +SplitView { + id: root + + property int layoutCount: commonParticipants.count + activeParticipants.count + property var participantComponent + + orientation: Qt.Horizontal + handle: Rectangle { + implicitHeight: root.height + implicitWidth: 11 + color: "transparent" + Rectangle { + anchors.centerIn: parent + width: 1 + height: parent.implicitHeight - 40 + color: JamiTheme.darkGreyColor + } + + Rectangle { + height: 45 + anchors.centerIn: parent + width: 1 + color: "black" + } + + RowLayout { + anchors.centerIn: parent + height: 45 + width: 11 + Rectangle { + Layout.fillHeight: true + Layout.topMargin: 10 + Layout.bottomMargin: 10 + width: 2 + color: JamiTheme.darkGreyColor + } + Rectangle { + Layout.fillHeight: true + Layout.topMargin: 10 + Layout.bottomMargin: 10 + width: 2 + color: JamiTheme.darkGreyColor + } + } + } + + // ACTIVE + Flow { + id: activeParticipantsFlow + + TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton } + + SplitView.minimumWidth: parent.width / 4 + SplitView.maximumWidth: parent.width + SplitView.fillWidth: 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 { + active: root.visible + asynchronous: true + sourceComponent: callVideoMedia + visible: status == Loader.Ready + + 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 string deviceId_: Device + property int leftMargin_: 0 + 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 + } + } + } + + Rectangle { + id: genericParticipantsRect + + TapHandler { acceptedButtons: Qt.TopButton | Qt.BottomButton } + + SplitView.preferredWidth: (parent.width / 4) + SplitView.minimumWidth: parent.width / 6 + SplitView.maximumWidth: inLine? parent.width / 2 : parent.width + + visible: inLine || CallParticipantsModel.conferenceLayout === CallParticipantsModel.GRID + color: "transparent" + + property int lowLimit: 0 + property int topLimit: commonParticipants.count + property int currentPos: 0 + property int showable: { + if (!inLine) + return commonParticipants.count + if (commonParticipantsFlow.componentHeight === 0) + return 1 + var placeableElements = Math.floor((height * 0.9)/commonParticipantsFlow.componentHeight) + if (commonParticipants.count - placeableElements < currentPos) + currentPos = Math.max(commonParticipants.count - placeableElements, 0) + return Math.max(1, placeableElements) + } + + ColumnLayout { + anchors.fill: parent + width: parent.width + + RowLayout { + Layout.alignment: Qt.AlignHCenter + width: parent.width + height: 30 + Layout.bottomMargin: 16 + Layout.topMargin: 16 + spacing: 8 + visible: (genericParticipantsRect.currentPos > 0 && activeParticipantsFlow.visible) || + (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos && activeParticipantsFlow.visible) + + RoundButton { + width : 30 + height : 30 + radius: 10 + text: "^" + visible: genericParticipantsRect.currentPos > 0 + && activeParticipantsFlow.visible + onClicked: { + if (genericParticipantsRect.currentPos > 0) + genericParticipantsRect.currentPos-- + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + + RoundButton { + width : 30 + height : 30 + radius: 10 + text: "v" + visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos + && activeParticipantsFlow.visible + onClicked: { + if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos) + genericParticipantsRect.currentPos++ + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + } + + Item { + id: centerItem + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 4 + + // GENERIC + Flow { + id: commonParticipantsFlow + anchors.fill: parent + + spacing: 4 + property int columns: { + if (inLine) + return 1 + var ratio = Math.floor(root.width / root.height) + // If ratio is 2 we can have 2 times more elements on each columns + var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio)) + var cols = Math.min(commonParticipants.count, wantedCol) + // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1) + var rows = Math.max(1, Math.ceil(commonParticipants.count/cols)) + return Math.min(Math.ceil(commonParticipants.count / rows), cols) + } + property int rows: { + if (inLine) + return commonParticipants.count + Math.max(1, Math.ceil(commonParticipants.count/columns)) + } + property int componentHeight: { + var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows + var h = Math.floor((commonParticipantsFlow.height - totalSpacing)/ commonParticipantsFlow.rows) + if (inLine) { + h = Math.max(width, h) + h = Math.min(width, h * 4 / 3) // Avoid too high elements + } + return h + } + property int componentWidth: { + var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns + var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns) + if (inLine) { + w = commonParticipantsFlow.width + } + return w + } + + Item { + width: parent.width + height: { + if (!inLine) + return 0 + var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.rows) + return Math.max(0, Math.ceil((centerItem.height - commonParticipantsFlow.componentHeight * showed) / 2)) + } + } + + Repeater { + id: commonParticipants + + model: GenericParticipantsFilterModel + delegate: Loader { + sourceComponent: callVideoMedia + active: root.visible + asynchronous: true + visible: { + if (status !== Loader.Ready) + return false + if (inLine) + return index >= genericParticipantsRect.currentPos + && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable + return true + } + width: commonParticipantsFlow.componentWidth + leftMargin_ + height: { + if (inLine || commonParticipantsFlow.columns === 1) + return commonParticipantsFlow.componentHeight + var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows + return Math.floor((genericParticipantsRect.height - totalSpacing) / commonParticipantsFlow.rows) + } + + property int leftMargin_: { + if (inLine || commonParticipantsFlow.rows === 1) + return 0 + var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns) + if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) { + var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing + var lastLineW = lastParticipants * compW + return Math.floor((commonParticipantsFlow.width - lastLineW) / 2) + } + return 0 + } + + property string uri_: Uri + property string deviceId_: Device + 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 + } + } + } + } + + Item { + Layout.alignment: Qt.AlignHCenter + width: parent.width + height : 30 + } + } + } +} diff --git a/src/app/mainview/components/ParticipantsLayoutVertical.qml b/src/app/mainview/components/ParticipantsLayoutVertical.qml new file mode 100644 index 000000000..6989a879b --- /dev/null +++ b/src/app/mainview/components/ParticipantsLayoutVertical.qml @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2020-2022 Savoir-faire Linux Inc. + * 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 + * 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 + +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 +import net.jami.Enums 1.1 + +SplitView { + id: root + + property int layoutCount: commonParticipants.count + activeParticipants.count + property var participantComponent + + 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 + + TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton } + + SplitView.preferredHeight: (parent.height / 4) + SplitView.minimumHeight: parent.height / 6 + SplitView.maximumHeight: inLine? parent.height / 2 : 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: { + if (!inLine) + return commonParticipants.count + if (commonParticipantsFlow.componentWidth === 0) + return 1 + var placeableElements = Math.floor((width * 0.9)/commonParticipantsFlow.componentWidth) + if (commonParticipants.count - placeableElements < currentPos) + currentPos = Math.max(commonParticipants.count - placeableElements, 0) + return Math.max(1, placeableElements) + } + + RowLayout { + anchors.fill: parent + + RoundButton { + Layout.alignment: Qt.AlignVCenter + width : 30 + height : 30 + radius: 10 + text: "<" + visible: genericParticipantsRect.currentPos > 0 + && activeParticipantsFlow.visible + onClicked: { + if (genericParticipantsRect.currentPos > 0) + genericParticipantsRect.currentPos-- + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + + Item { + id: centerItem + Layout.fillHeight: true + Layout.fillWidth: true + Layout.margins: 4 + + // GENERIC + Flow { + id: commonParticipantsFlow + anchors.fill: parent + + spacing: 4 + property int columns: { + if (inLine) + return commonParticipants.count + var ratio = Math.floor(root.width / root.height) + // If ratio is 2 we can have 2 times more elements on each columns + var wantedCol = Math.max(1, Math.round(Math.sqrt(commonParticipants.count) * ratio)) + var cols = Math.min(commonParticipants.count, wantedCol) + // Optimize with the rows (eg 7 with ratio 2 should have 4 and 3 items, not 6 and 1) + var rows = Math.max(1, Math.ceil(commonParticipants.count/cols)) + return Math.min(Math.ceil(commonParticipants.count / rows), cols) + } + property int rows: Math.max(1, Math.ceil(commonParticipants.count/columns)) + property int componentWidth: { + var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.columns + var w = Math.floor((commonParticipantsFlow.width - totalSpacing)/ commonParticipantsFlow.columns) + if (inLine) { + w = Math.max(w, height) + w = Math.min(w, height * 4 / 3) // Avoid too wide elements + } + return w + } + + Item { + height: parent.height + width: { + if (!inLine) + return 0 + var showed = Math.min(genericParticipantsRect.showable, commonParticipantsFlow.columns) + return Math.max(0, Math.ceil((centerItem.width - commonParticipantsFlow.componentWidth * showed) / 2)) + } + } + + Repeater { + id: commonParticipants + + model: GenericParticipantsFilterModel + delegate: Loader { + sourceComponent: callVideoMedia + active: root.visible + asynchronous: true + visible: { + if (status !== Loader.Ready) + return false + if (inLine) + return index >= genericParticipantsRect.currentPos + && index < genericParticipantsRect.currentPos + genericParticipantsRect.showable + return true + } + width: commonParticipantsFlow.componentWidth + leftMargin_ + height: { + if (inLine || commonParticipantsFlow.rows === 1) + return genericParticipantsRect.height + var totalSpacing = commonParticipantsFlow.spacing * commonParticipantsFlow.rows + return Math.floor((genericParticipantsRect.height - totalSpacing)/ commonParticipantsFlow.rows) + } + + property int leftMargin_: { + if (inLine || commonParticipantsFlow.rows === 1) + return 0 + var lastParticipants = (commonParticipants.count % commonParticipantsFlow.columns) + if (lastParticipants !== 0 && index === commonParticipants.count - lastParticipants) { + var compW = commonParticipantsFlow.componentWidth + commonParticipantsFlow.spacing + var lastLineW = lastParticipants * compW + return Math.floor((commonParticipantsFlow.width - lastLineW) / 2) + } + return 0 + } + + property string uri_: Uri + property string deviceId_: Device + 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 + } + } + } + } + + RoundButton { + Layout.alignment: Qt.AlignVCenter + width : 30 + height : 30 + radius: 10 + text: ">" + visible: genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos + && activeParticipantsFlow.visible + onClicked: { + if (genericParticipantsRect.topLimit - genericParticipantsRect.showable > genericParticipantsRect.currentPos) + genericParticipantsRect.currentPos++ + } + background: Rectangle { + anchors.fill: parent + color: JamiTheme.lightGrey_ + radius: JamiTheme.primaryRadius + } + } + } + } + + // ACTIVE + Flow { + id: activeParticipantsFlow + + TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton } + + 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 { + active: root.visible + asynchronous: true + sourceComponent: callVideoMedia + visible: status == Loader.Ready + + 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 string deviceId_: Device + property int leftMargin_: 0 + 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 + } + } + } +} -- GitLab