diff --git a/qml.qrc b/qml.qrc
index e7ab3be11..8e731998c 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -136,6 +136,8 @@
src/app/mainview/components/SmartListItemDelegate.qml
src/app/mainview/components/BadgeNotifier.qml
src/app/mainview/components/ParticipantsLayer.qml
+ src/app/mainview/components/ParticipantsLayoutVertical.qml
+ src/app/mainview/components/ParticipantsLayoutHorizontal.qml
src/app/mainview/components/MainOverlay.qml
src/app/mainview/components/CallButtonDelegate.qml
src/app/mainview/components/CallActionBar.qml
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 @@
+
+
+
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 @@
+
+
+
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
- }
+ onLayoutCountChanged: root.count = layoutCount
+ }
- 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
- }
- }
- }
+ 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
+ * Aline Gondim Santos
+ *
+ * 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 .
+ */
+
+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
+ * Aline Gondim Santos
+ *
+ * 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 .
+ */
+
+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
+ }
+ }
+ }
+}