-
Sébastien Blin authored
Support single tap for showing call overlay and long press to show context menu for the smartlist Change-Id: I31a77169827860c07ec9d60e076c0c5c4e875408
Sébastien Blin authoredSupport single tap for showing call overlay and long press to show context menu for the smartlist Change-Id: I31a77169827860c07ec9d60e076c0c5c4e875408
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CallOverlay.qml 17.98 KiB
/*
* Copyright (C) 2020 by Savoir-faire Linux
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
* Author: 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 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick.Controls.Universal 2.14
import QtQml 2.14
import net.jami.Models 1.0
import net.jami.Adapters 1.0
import net.jami.Constants 1.0
import "../js/contactpickercreation.js" as ContactPickerCreation
import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
import "../../commoncomponents"
Rectangle {
id: callOverlayRect
property string timeText: "00:00"
property string remoteRecordingLabel: ""
property var participantOverlays: []
property var participantComponent: Qt.createComponent("ParticipantOverlay.qml")
signal overlayChatButtonClicked
function setRecording(localIsRecording) {
callViewContextMenu.localIsRecording = localIsRecording
recordingRect.visible = localIsRecording
|| callViewContextMenu.peerIsRecording
}
function updateButtonStatus(isPaused, isAudioOnly, isAudioMuted, isVideoMuted,
isRecording, isSIP, isConferenceCall) {
callViewContextMenu.isSIP = isSIP
callViewContextMenu.isPaused = isPaused
callViewContextMenu.isAudioOnly = isAudioOnly
callViewContextMenu.localIsRecording = isRecording
recordingRect.visible = isRecording
callOverlayButtonGroup.setButtonStatus(isPaused, isAudioOnly,
isAudioMuted, isVideoMuted,
isSIP, isConferenceCall)
}
function updateMenu() {
callOverlayButtonGroup.updateMenu()
}
function showOnHoldImage(visible) {
onHoldImage.visible = visible
}
function closePotentialContactPicker() {
ContactPickerCreation.closeContactPicker()
}
function closePotentialPluginHandlerPicker() {
PluginHandlerPickerCreation.closePluginHandlerPicker()
}
// returns true if participant is not fully maximized
function showMaximize(pX, pY, pW, pH) {
// Hack: -1 offset added to avoid problems with odd sizes
return (pX - distantRenderer.getXOffset() !== 0
|| pY - distantRenderer.getYOffset() !== 0
|| pW < (distantRenderer.width - distantRenderer.getXOffset() * 2 - 1)
|| pH < (distantRenderer.height - distantRenderer.getYOffset() * 2 - 1))
}
function handleParticipantsInfo(infos) {
// TODO: in the future the conference layout should be entirely managed by the client
// Hack: truncate and ceil participant's overlay position and size to correct
// when they are not exacts
videoCallOverlay.updateMenu()
var showMax = false
var showMin = false
var deletedUris = []
var currentUris = []
for (var p in participantOverlays) {
if (participantOverlays[p]) {
var participant = infos.find(e => e.uri === participantOverlays[p].uri);
if (participant) {
// Update participant's information
var newX = Math.trunc(distantRenderer.getXOffset()
+ participant.x * distantRenderer.getScaledWidth())
var newY = Math.trunc(distantRenderer.getYOffset()
+ participant.y * distantRenderer.getScaledHeight())
var newWidth = Math.ceil(participant.w * distantRenderer.getScaledWidth())
var newHeight = Math.ceil(participant.h * distantRenderer.getScaledHeight())
var newVisible = participant.w !== 0 && participant.h !== 0
if (participantOverlays[p].x !== newX)
participantOverlays[p].x = newX
if (participantOverlays[p].y !== newY)
participantOverlays[p].y = newY
if (participantOverlays[p].width !== newWidth)
participantOverlays[p].width = newWidth
if (participantOverlays[p].height !== newHeight)
participantOverlays[p].height = newHeight
if (participantOverlays[p].visible !== newVisible)
participantOverlays[p].visible = newVisible
showMax = showMaximize(participantOverlays[p].x,
participantOverlays[p].y,
participantOverlays[p].width,
participantOverlays[p].height)
participantOverlays[p].setMenu(participant.uri, participant.bestName,
participant.isLocal, participant.active, showMax)
if (participant.videoMuted)
participantOverlays[p].setAvatar(participant.avatar)
else
participantOverlays[p].setAvatar("")
currentUris.push(participantOverlays[p].uri)
} else {
// Participant is no longer in conference
deletedUris.push(participantOverlays[p].uri)
participantOverlays[p].destroy()
}
}
}
participantOverlays = participantOverlays.filter(part => !deletedUris.includes(part.uri))
if (infos.length === 0) { // Return to normal call
previewRenderer.visible = true
for (var part in participantOverlays) {
if (participantOverlays[part]) {
participantOverlays[part].destroy()
}
}
participantOverlays = []
} else {
previewRenderer.visible = false
for (var infoVariant in infos) {
// Only create overlay for new participants
if (!currentUris.includes(infos[infoVariant].uri)) {
var hover = participantComponent.createObject(callOverlayRectMouseArea, {
x: Math.trunc(distantRenderer.getXOffset() + infos[infoVariant].x * distantRenderer.getScaledWidth()),
y: Math.trunc(distantRenderer.getYOffset() + infos[infoVariant].y * distantRenderer.getScaledHeight()),
width: Math.ceil(infos[infoVariant].w * distantRenderer.getScaledWidth()),
height: Math.ceil(infos[infoVariant].h * distantRenderer.getScaledHeight()),
visible: infos[infoVariant].w !== 0 && infos[infoVariant].h !== 0
})
if (!hover) {
console.log("Error when creating the hover")
return
}
showMax = showMaximize(hover.x, hover.y, hover.width, hover.height)
hover.setMenu(infos[infoVariant].uri, infos[infoVariant].bestName,
infos[infoVariant].isLocal, infos[infoVariant].active, showMax)
if (infos[infoVariant].videoMuted)
hover.setAvatar(infos[infoVariant].avatar)
else
hover.setAvatar("")
participantOverlays.push(hover)
}
}
}
}
// x, y position does not need to be translated
// since they all fill the call page
function openCallViewContextMenuInPos(x, y) {
callViewContextMenu.x = x
callViewContextMenu.y = y
callViewContextMenu.openMenu()
}
function showRemoteRecording(peers, state) {
var label = ""
var i = 0
if (state) {
for (var p in peers) {
label += peers[p]
if (i !== (peers.length - 1))
label += ", "
i += 1
}
label += " " + ((peers.length > 1)? JamiStrings.areRecording
: JamiStrings.isRecording)
}
remoteRecordingLabel = state? label : JamiStrings.peerStoppedRecording
callViewContextMenu.peerIsRecording = state
recordingRect.visible = callViewContextMenu.localIsRecording
|| callViewContextMenu.peerIsRecording
callOverlayRectMouseArea.entered()
}
anchors.fill: parent
SipInputPanel {
id: sipInputPanel
x: callOverlayRect.width / 2 - sipInputPanel.width / 2
y: callOverlayRect.height / 2 - sipInputPanel.height / 2
}
// Timer to decide when overlay fade out.
Timer {
id: callOverlayTimer
interval: 5000
onTriggered: {
if (overlayUpperPartRect.state !== 'freezed') {
overlayUpperPartRect.state = 'freezed'
resetRecordingLabelTimer.restart()
}
if (callOverlayButtonGroup.state !== 'freezed') {
callOverlayButtonGroup.state = 'freezed'
resetRecordingLabelTimer.restart()
}
}
}
// Timer to reset recording label text
Timer {
id: resetRecordingLabelTimer
interval: 1000
onTriggered: {
if (callOverlayButtonGroup.state === 'freezed'
&& !callViewContextMenu.peerIsRecording)
remoteRecordingLabel = ""
}
}
Rectangle {
id: overlayUpperPartRect
anchors.top: callOverlayRect.top
width: callOverlayRect.width
height: 50
opacity: 0
RowLayout {
id: overlayUpperPartRectRowLayout
anchors.fill: parent
Text {
id: jamiBestNameText
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.preferredWidth: overlayUpperPartRect.width / 3
Layout.preferredHeight: 50
leftPadding: 16
font.pointSize: JamiTheme.textFontSize
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
text: textMetricsjamiBestNameText.elidedText
color: "white"
TextMetrics {
id: textMetricsjamiBestNameText
font: jamiBestNameText.font
text: {
if (videoCallPageRect) {
if (remoteRecordingLabel === "") {
return videoCallPageRect.bestName
} else {
return remoteRecordingLabel
}
}
return ""
}
elideWidth: overlayUpperPartRect.width / 3
elide: Qt.ElideRight
}
}
Text {
id: callTimerText
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: 64
Layout.minimumWidth: 64
Layout.preferredHeight: 48
Layout.rightMargin: recordingRect.visible?
0 : JamiTheme.preferredMarginSize
font.pointSize: JamiTheme.textFontSize
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: textMetricscallTimerText.elidedText
color: "white"
TextMetrics {
id: textMetricscallTimerText
font: callTimerText.font
text: timeText
elideWidth: overlayUpperPartRect.width / 4
elide: Qt.ElideRight
}
}
Rectangle {
id: recordingRect
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.rightMargin: JamiTheme.preferredMarginSize
height: 16
width: 16
radius: height / 2
color: "red"
SequentialAnimation on color {
loops: Animation.Infinite
running: true
ColorAnimation { from: "red"; to: "transparent"; duration: 500 }
ColorAnimation { from: "transparent"; to: "red"; duration: 500 }
}
}
}
color: "transparent"
// Rect states: "entered" state should make overlay fade in,
// "freezed" state should make overlay fade out.
// Combine with PropertyAnimation of opacity.
states: [
State {
name: "entered"
PropertyChanges {
target: overlayUpperPartRect
opacity: 1
}
},
State {
name: "freezed"
PropertyChanges {
target: overlayUpperPartRect
opacity: 0
}
}
]
transitions: Transition {
PropertyAnimation {
target: overlayUpperPartRect
property: "opacity"
duration: 1000
}
}
}
ResponsiveImage {
id: onHoldImage
anchors.verticalCenter: callOverlayRect.verticalCenter
anchors.horizontalCenter: callOverlayRect.horizontalCenter
width: 200
height: 200
visible: false
source: "qrc:/images/icons/ic_pause_white_100px.svg"
}
CallOverlayButtonGroup {
id: callOverlayButtonGroup
anchors.bottom: callOverlayRect.bottom
anchors.bottomMargin: 10
anchors.horizontalCenter: callOverlayRect.horizontalCenter
height: 56
width: callOverlayRect.width
opacity: 0
onChatButtonClicked: {
callOverlayRect.overlayChatButtonClicked()
}
onAddToConferenceButtonClicked: {
// Create contact picker - conference.
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.JAMICONFERENCE,
callOverlayRect)
ContactPickerCreation.openContactPicker()
}
states: [
State {
name: "entered"
PropertyChanges {
target: callOverlayButtonGroup
opacity: 1
}
},
State {
name: "freezed"
PropertyChanges {
target: callOverlayButtonGroup
opacity: 0
}
}
]
transitions: Transition {
PropertyAnimation {
target: callOverlayButtonGroup
property: "opacity"
duration: 1000
}
}
}
// MouseAreas to make sure that overlay states are correctly set.
MouseArea {
id: callOverlayButtonGroupLeftSideMouseArea
anchors.bottom: callOverlayRect.bottom
anchors.left: callOverlayRect.left
width: callOverlayRect.width / 6
height: 60
hoverEnabled: true
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
onEntered: {
callOverlayRectMouseArea.entered()
}
onMouseXChanged: {
callOverlayRectMouseArea.entered()
}
}
MouseArea {
id: callOverlayButtonGroupRightSideMouseArea
anchors.bottom: callOverlayRect.bottom
anchors.right: callOverlayRect.right
width: callOverlayRect.width / 6
height: 60
hoverEnabled: true
propagateComposedEvents: true
acceptedButtons: Qt.NoButton
onEntered: {
callOverlayRectMouseArea.entered()
}
onMouseXChanged: {
callOverlayRectMouseArea.entered()
}
}
MouseArea {
id: callOverlayRectMouseArea
anchors.top: callOverlayRect.top
width: callOverlayRect.width
height: callOverlayRect.height
hoverEnabled: true
propagateComposedEvents: true
acceptedButtons: Qt.LeftButton
function resetStates() {
if (overlayUpperPartRect.state !== 'entered') {
overlayUpperPartRect.state = 'entered'
}
if (callOverlayButtonGroup.state !== 'entered') {
callOverlayButtonGroup.state = 'entered'
}
callOverlayTimer.restart()
}
onReleased: {
resetStates()
}
onEntered: {
resetStates()
}
onMouseXChanged: {
resetStates()
}
}
color: "transparent"
CallViewContextMenu {
id: callViewContextMenu
onTransferCallButtonClicked: {
// Create contact picker - sip transfer.
ContactPickerCreation.createContactPickerObjects(
ContactPicker.ContactPickerType.SIPTRANSFER,
callOverlayRect)
ContactPickerCreation.openContactPicker()
}
onPluginItemClicked: {
// Create plugin handler picker - PLUGINS
PluginHandlerPickerCreation.createPluginHandlerPickerObjects(callOverlayRect, true)
PluginHandlerPickerCreation.openPluginHandlerPicker()
}
}
}