-
Sébastien Blin authored
Change-Id: I8d5f968fbedbc884c91416246049a0ef4cd652eb
Sébastien Blin authoredChange-Id: I8d5f968fbedbc884c91416246049a0ef4cd652eb
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CallActionBar.qml 26.10 KiB
/*
* Copyright (C) 2021-2024 Savoir-faire Linux Inc.
* Author: Andreas Traczyk <andreas.traczyk@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 <http://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import SortFilterProxyModel 0.2
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"
Control {
id: root
enum ActionPopupMode {
MediaDevice,
ListElement,
LayoutOption
}
property alias overflowOpen: overflowButton.popup.visible
property bool subMenuOpen: false
property real parentHeight
property bool barHovered: false
property real itemSpacing: 2
signal chatClicked
signal addToConferenceClicked
signal transferClicked
signal resumePauseCallClicked
signal showInputPanelClicked
signal shareScreenClicked
signal shareWindowClicked
signal stopSharingClicked
signal shareScreenAreaClicked
signal shareFileClicked
signal pluginsClicked
signal recordCallClicked
signal fullScreenClicked
signal swarmDetailsClicked
Component {
id: buttonDelegate
CallButtonDelegate {
width: root.height
height: width
barWidth: root.width
onSubMenuVisibleChanged: subMenuOpen = subMenuVisible
onHoveredChanged: root.barHovered = hovered
}
}
Connections {
target: AvAdapter
function onAudioDeviceListChanged(inputs, outputs) {
audioInputDeviceListModel.reset();
audioInputMenuAction.enabled = inputs;
audioOutputDeviceListModel.reset();
audioOutputMenuAction.enabled = outputs;
}
}
property list<Action> menuActions: [
Action {
id: audioInputMenuAction
text: JamiStrings.selectAudioInputDevice
Component.onCompleted: enabled = audioInputDeviceListModel.rowCount()
property var listModel: AudioDeviceModel {
id: audioInputDeviceListModel
lrcInstance: LRCInstance
type: AudioDeviceModel.Type.Record
}
function accept(index) {
AvAdapter.stopAudioMeter();
AVModel.setInputDevice(index);
AvAdapter.startAudioMeter();
}
},
Action {
id: audioOutputMenuAction
text: JamiStrings.selectAudioOutputDevice
Component.onCompleted: enabled = audioOutputDeviceListModel.rowCount()
property var listModel: AudioDeviceModel {
id: audioOutputDeviceListModel
lrcInstance: LRCInstance
type: AudioDeviceModel.Type.Playback
}
function accept(index) {
AvAdapter.stopAudioMeter();
AVModel.setOutputDevice(index);
AvAdapter.startAudioMeter();
}
},
Action {
id: shareMenuAction
text: JamiStrings.selectShareMethod
property int popupMode: CallActionBar.ActionPopupMode.ListElement
property var listModel: ListModel {
id: shareModel
}
onTriggered: {
shareModel.clear();
shareModel.append({
"Name": JamiStrings.shareScreen,
"IconSource": JamiResources.laptop_black_24dp_svg
});
if (Qt.platform.os.toString() !== "osx" && !UtilsAdapter.isWayland()) {
shareModel.append({
"Name": JamiStrings.shareWindow,
"IconSource": JamiResources.window_black_24dp_svg
});
}
if (Qt.platform.os.toString() !== "windows" && !UtilsAdapter.isWayland()) {
// temporarily disable for windows
shareModel.append({
"Name": JamiStrings.shareScreenArea,
"IconSource": JamiResources.share_area_black_24dp_svg
});
}
shareModel.append({
"Name": JamiStrings.shareFile,
"IconSource": JamiResources.file_black_24dp_svg
});
}
function accept(index) {
switch (shareModel.get(index).Name) {
case JamiStrings.shareScreen:
shareScreenClicked();
break;
case JamiStrings.shareWindow:
shareWindowClicked();
break;
case JamiStrings.shareScreenArea:
shareScreenAreaClicked();
break;
case JamiStrings.shareFile:
shareFileClicked();
break;
}
}
},
Action {
id: layoutMenuAction
text: JamiStrings.layoutSettings
property int popupMode: CallActionBar.ActionPopupMode.LayoutOption
property var listModel: ListModel {
id: layoutModel
}
function accept(index) {
switch (layoutModel.get(index).Name) {
case JamiStrings.viewFullScreen:
root.fullScreenClicked();
layoutModel.get(index).ActiveSetting = layoutManager.isCallFullscreen;
break;
case JamiStrings.mosaic:
if (!CurrentCall.isGrid)
CallAdapter.showGridConferenceLayout();
break;
case JamiStrings.participantsSide:
if (!UtilsAdapter.getAppValue(Settings.ParticipantsSide)) {
UtilsAdapter.setAppValue(Settings.ParticipantsSide, true);
participantsSide = true;
}
break;
case JamiStrings.participantsTop:
if (UtilsAdapter.getAppValue(Settings.ParticipantsSide)) {
UtilsAdapter.setAppValue(Settings.ParticipantsSide, false);
participantsSide = false;
}
break;
case JamiStrings.hideSelf:
UtilsAdapter.setAppValue(Settings.HideSelf, !layoutModel.get(index).ActiveSetting);
CurrentCall.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf);
break;
case JamiStrings.hideSpectators:
UtilsAdapter.setAppValue(Settings.HideSpectators, !layoutModel.get(index).ActiveSetting);
CurrentCall.hideSpectators = UtilsAdapter.getAppValue(Settings.HideSpectators);
break;
}
}
onTriggered: {
layoutModel.clear();
if (CurrentCall.isConference) {
layoutModel.append({
"Name": JamiStrings.mosaic,
"IconSource": JamiResources.mosaic_black_24dp_svg,
"ActiveSetting": CurrentCall.isGrid,
"TopMargin": true,
"BottomMargin": true,
"SectionEnd": true
});
var onTheSide = UtilsAdapter.getAppValue(Settings.ParticipantsSide);
layoutModel.append({
"Name": JamiStrings.participantsTop,
"IconSource": JamiResources.onthetop_black_24dp_svg,
"ActiveSetting": !onTheSide,
"TopMargin": true,
"BottomMargin": false,
"SectionEnd": false
});
layoutModel.append({
"Name": JamiStrings.participantsSide,
"IconSource": JamiResources.ontheside_black_24dp_svg,
"ActiveSetting": onTheSide,
"TopMargin": false,
"BottomMargin": true,
"SectionEnd": true
});
layoutModel.append({
"Name": JamiStrings.hideSelf,
"IconSource": JamiResources.hidemyself_black_24dp_svg,
"ActiveSetting": UtilsAdapter.getAppValue(Settings.HideSelf),
"TopMargin": true,
"BottomMargin": false,
"SectionEnd": false
});
}
layoutModel.append({
"Name": JamiStrings.viewFullScreen,
"IconSource": JamiResources.open_in_full_24dp_svg,
"ActiveSetting": layoutManager.isCallFullscreen,
"TopMargin": true,
"BottomMargin": true,
"SectionEnd": CurrentCall.isConference
});
if (CurrentCall.isConference) {
layoutModel.append({
"Name": JamiStrings.hideSpectators,
"IconSource": JamiResources.videocam_off_24dp_svg,
"ActiveSetting": UtilsAdapter.getAppValue(Settings.HideSpectators),
"TopMargin": true,
"BottomMargin": true
});
}
}
},
Action {
id: videoInputMenuAction
enabled: VideoDevices.listSize !== 0
text: JamiStrings.selectVideoDevice
property var listModel: VideoDevices.deviceSourceModel
function accept(index) {
VideoDevices.setDefaultDevice(index);
}
}
]
property list<Action> primaryActions: [
Action {
id: muteAudioAction
onTriggered: {
var muteState = CallAdapter.getMuteState(CurrentAccount.uri);
var modMuted = muteState === CallAdapter.MODERATOR_MUTED || muteState === CallAdapter.BOTH_MUTED;
if (muteAudioAction.checked && modMuted) {
muteAlertActive = true;
muteAlertMessage = JamiStrings.participantModIsStillMuted;
}
CallAdapter.muteAudioToggle();
}
checkable: true
icon.source: checked ? JamiResources.micro_off_black_24dp_svg : JamiResources.micro_black_24dp_svg
icon.color: checked ? "red" : "white"
text: !checked ? JamiStrings.mute : JamiStrings.unmute
checked: CurrentCall.isAudioMuted
property var menuAction: audioInputMenuAction
},
Action {
id: hangupAction
onTriggered: CallAdapter.hangUpThisCall()
icon.source: JamiResources.ic_call_end_white_24dp_svg
icon.color: "white"
text: JamiStrings.endCall
property bool hasBg: true
},
Action {
id: muteVideoAction
onTriggered: CallAdapter.muteCameraToggle()
checkable: true
icon.source: checked ? JamiResources.videocam_off_24dp_svg : JamiResources.videocam_24dp_svg
icon.color: checked ? "red" : "white"
text: !checked ? JamiStrings.muteCamera : JamiStrings.unmuteCamera
checked: !CurrentCall.isCapturing
property var menuAction: videoInputMenuAction
enabled: CurrentAccount.videoEnabled_Video
onEnabledChanged: CallOverlayModel.setEnabled(this, muteVideoAction.enabled)
}
]
property list<Action> secondaryActions: [
Action {
id: audioOutputAction
// temp hack for missing back-end, just open device selection
property bool openPopupWhenClicked: true
checkable: !openPopupWhenClicked
icon.source: JamiResources.spk_black_24dp_svg
icon.color: "white"
text: JamiStrings.selectAudioOutputDevice
property var menuAction: audioOutputMenuAction
},
Action {
id: addPersonAction
onTriggered: root.addToConferenceClicked()
icon.source: JamiResources.add_people_black_24dp_svg
icon.color: "white"
text: JamiStrings.addParticipants
enabled: CurrentCall.isModerator && !CurrentCall.isSIP
onEnabledChanged: CallOverlayModel.setEnabled(this, addPersonAction.enabled)
},
Action {
id: chatAction
onTriggered: root.chatClicked()
icon.source: JamiResources.chat_black_24dp_svg
icon.color: "white"
text: JamiStrings.chat
},
Action {
id: resumePauseCallAction
onTriggered: root.resumePauseCallClicked()
icon.source: CurrentCall.isPaused ? JamiResources.play_circle_outline_24dp_svg : JamiResources.pause_circle_outline_24dp_svg
icon.color: "white"
text: CurrentCall.isPaused ? JamiStrings.resumeCall : JamiStrings.pauseCall
enabled: CurrentCall.isSIP
onEnabledChanged: CallOverlayModel.setEnabled(this, resumePauseCallAction.enabled)
},
Action {
id: inputPanelSIPAction
onTriggered: root.showInputPanelClicked()
icon.source: JamiResources.ic_keypad_svg
icon.color: "white"
text: JamiStrings.sipInputPanel
enabled: CurrentCall.isSIP
onEnabledChanged: CallOverlayModel.setEnabled(this, inputPanelSIPAction.enabled)
},
Action {
id: callTransferAction
onTriggered: root.transferClicked()
icon.source: JamiResources.phone_forwarded_24dp_svg
icon.color: "white"
text: JamiStrings.transferCall
enabled: CurrentCall.isSIP
onEnabledChanged: CallOverlayModel.setEnabled(this, callTransferAction.enabled)
},
Action {
id: shareAction
onTriggered: {
if (CurrentCall.isSharing)
root.stopSharingClicked();
else
root.shareScreenClicked();
}
icon.source: CurrentCall.isSharing ? JamiResources.share_stop_black_24dp_svg : JamiResources.share_screen_black_24dp_svg
icon.color: CurrentCall.isSharing ? "red" : "white"
text: CurrentCall.isSharing ? JamiStrings.stopSharing : JamiStrings.shareScreen
property real size: 34
property var menuAction: shareMenuAction
enabled: CurrentAccount.videoEnabled_Video && !CurrentCall.isSIP
onEnabledChanged: CallOverlayModel.setEnabled(this, shareAction.enabled)
},
Action {
id: raiseHandAction
onTriggered: CallAdapter.raiseHand("", "", !CallAdapter.isHandRaised())
checkable: true
icon.source: JamiResources.hand_black_24dp_svg
icon.color: checked ? JamiTheme.raiseHandColor : "white"
text: checked ? JamiStrings.lowerHand : JamiStrings.raiseHand
checked: CurrentCall.isHandRaised
property real size: 34
enabled: CurrentCall.isConference
onEnabledChanged: CallOverlayModel.setEnabled(this, raiseHandAction.enabled)
},
Action {
id: layoutAction
onTriggered: {
if (!CurrentCall.isGrid)
CallAdapter.showGridConferenceLayout();
}
checkable: true
icon.source: JamiResources.mosaic_black_24dp_svg
icon.color: "white"
text: JamiStrings.layoutSettings
property real size: 28
property var menuAction: layoutMenuAction
},
Action {
id: recordAction
onTriggered: root.recordCallClicked()
checkable: true
icon.source: JamiResources.record_black_24dp_svg
icon.color: checked ? "red" : "white"
text: !checked ? JamiStrings.startRec : JamiStrings.stopRec
property bool blinksWhenChecked: true
property real size: 28
checked: CurrentCall.isRecordingLocally
onCheckedChanged: function (checked) {
CallOverlayModel.setUrgentCount(recordAction, checked ? -1 : 0);
}
},
Action {
id: pluginsAction
onTriggered: root.pluginsClicked()
icon.source: JamiResources.plugins_24dp_svg
icon.color: "white"
text: JamiStrings.viewPlugin
enabled: PluginAdapter.callMediaHandlersListCount
onEnabledChanged: CallOverlayModel.setEnabled(this, pluginsAction.enabled)
},
Action {
id: swarmDetailsAction
onTriggered: root.swarmDetailsClicked()
icon.source: JamiResources.swarm_details_panel_svg
icon.color: "white"
text: JamiStrings.details
enabled: {
if (CurrentCall.isSIP)
return true;
if (!CurrentConversation.isTemporary && !CurrentConversation.isSwarm)
return false;
if (CurrentConversation.isRequest || CurrentConversation.needsSyncing)
return false;
return true;
}
onEnabledChanged: CallOverlayModel.setEnabled(this, swarmDetailsAction.enabled)
}
]
property var overflowItemCount
Component.onCompleted: {
CallOverlayModel.clearControls();
// centered controls
CallOverlayModel.addPrimaryControl(muteAudioAction, muteAudioAction.enabled);
CallOverlayModel.addPrimaryControl(hangupAction, hangupAction.enabled);
CallOverlayModel.addPrimaryControl(muteVideoAction, muteVideoAction.enabled);
// overflow controls
CallOverlayModel.addSecondaryControl(audioOutputAction, audioOutputAction.enabled);
CallOverlayModel.addSecondaryControl(raiseHandAction, raiseHandAction.enabled);
CallOverlayModel.addSecondaryControl(addPersonAction, addPersonAction.enabled);
CallOverlayModel.addSecondaryControl(resumePauseCallAction, resumePauseCallAction.enabled);
CallOverlayModel.addSecondaryControl(inputPanelSIPAction, inputPanelSIPAction.enabled);
CallOverlayModel.addSecondaryControl(callTransferAction, callTransferAction.enabled);
CallOverlayModel.addSecondaryControl(chatAction, chatAction.enabled);
CallOverlayModel.addSecondaryControl(shareAction, shareAction.enabled);
CallOverlayModel.addSecondaryControl(layoutAction, layoutAction.enabled);
CallOverlayModel.addSecondaryControl(recordAction, recordAction.enabled);
CallOverlayModel.addSecondaryControl(pluginsAction, pluginsAction.enabled);
CallOverlayModel.addSecondaryControl(swarmDetailsAction, swarmDetailsAction.enabled);
overflowItemCount = CallOverlayModel.secondaryModel().rowCount();
}
Item {
id: centralControls
anchors.centerIn: parent
width: childrenRect.width
height: root.height
RowLayout {
spacing: 0
ListView {
id: itemListView
property bool centeredGroup: true
orientation: ListView.Horizontal
implicitWidth: contentWidth
height: root.height
interactive: false
model: SortFilterProxyModel {
sourceModel: root.visible ? CallOverlayModel.primaryModel() : null
filters: ValueFilter {
roleName: "Enabled"
value: true
}
}
delegate: buttonDelegate
}
}
}
Item {
id: overflowRect
property real remainingSpace: (root.width - centralControls.width) / 2
anchors.right: parent.right
width: childrenRect.width
height: root.height
//put in top
z: 1
RowLayout {
spacing: itemSpacing
ListView {
id: overflowItemListView
orientation: ListView.Horizontal
implicitWidth: contentWidth
implicitHeight: overflowRect.height
interactive: false
spacing: itemSpacing
property int overflowIndex: {
var maxItems = Math.floor((overflowRect.remainingSpace) / (root.height + itemSpacing)) - 2;
var idx = Math.min(overflowItemCount, maxItems);
idx = Math.max(0, idx);
if (CallOverlayModel.overflowModel().rowCount() > 0 || CallOverlayModel.overflowHiddenModel().rowCount() > 0) {
var visibleIdx = CallOverlayModel.overflowModel().mapToSource(CallOverlayModel.overflowModel().index(idx, 0)).row;
var hiddenIdx = CallOverlayModel.overflowHiddenModel().mapToSource(CallOverlayModel.overflowHiddenModel().index(idx - CallOverlayModel.overflowModel().rowCount(), 0)).row;
if (visibleIdx >= 0 || hiddenIdx >= 0)
idx = Math.max(visibleIdx, hiddenIdx);
}
return idx;
}
property int nOverflowItems: overflowItemCount - overflowIndex
onNOverflowItemsChanged: {
var diff = overflowItemListView.count - nOverflowItems;
var effectiveOverflowIndex = overflowIndex;
if (effectiveOverflowIndex === overflowItemCount - 2)
effectiveOverflowIndex += diff;
CallOverlayModel.overflowIndex = effectiveOverflowIndex;
}
model: root.visible ? CallOverlayModel.overflowModel() : null
delegate: buttonDelegate
}
ComboBox {
id: overflowButton
visible: CallOverlayModel.overflowIndex < overflowItemCount - 2
width: root.height
height: width
model: root.visible ? CallOverlayModel.overflowHiddenModel() : null
delegate: buttonDelegate
indicator: null
contentItem: ResponsiveImage {
color: "white"
source: JamiResources.more_vert_24dp_svg
anchors.fill: parent
anchors.margins: 17
}
background: HalfPill {
implicitWidth: root.height
implicitHeight: implicitWidth
radius: type === HalfPill.None ? 0 : 5
color: overflowButton.down ? "#c4777777" : overflowButton.hovered ? "#c4444444" : "#c4272727"
type: {
if (overflowItemListView.count || urgentOverflowListView.count || (overflowHiddenListView.count && overflowButton.popup.visible)) {
return HalfPill.None;
} else {
return HalfPill.Left;
}
}
Behavior on color {
ColorAnimation {
duration: JamiTheme.shortFadeDuration
}
}
}
Item {
implicitHeight: (overflowButton.width + itemSpacing) * urgentOverflowListView.count
width: overflowButton.width
anchors.bottom: parent.top
anchors.bottomMargin: itemSpacing
visible: !overflowButton.popup.visible
JamiListView {
id: urgentOverflowListView
spacing: itemSpacing
anchors.fill: parent
model: !overflowButton.popup.visible ? CallOverlayModel.overflowVisibleModel() : null
delegate: buttonDelegate
add: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1.0
duration: 80
}
NumberAnimation {
property: "scale"
from: 0
to: 1.0
duration: 80
}
}
}
}
popup: Popup {
y: overflowButton.height + itemSpacing
width: overflowButton.width
implicitHeight: Math.min(root.parentHeight - itemSpacing, (overflowButton.width + itemSpacing) * overflowHiddenListView.count)
padding: 0
contentItem: JamiListView {
id: overflowHiddenListView
spacing: itemSpacing
implicitHeight: Math.min(contentHeight, parent.height)
interactive: true
model: overflowButton.popup.visible ? overflowButton.delegateModel : null
}
background: Rectangle {
color: "transparent"
}
}
}
}
}
}