Warning, /plasma/plasma-pa/applet/contents/ui/ListItemBase.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2014-2015 Harald Sitter <sitter@kde.org>
0003     SPDX-FileCopyrightText: 2019 Sefa Eyeoglu <contact@scrumplex.net>
0004     SPDX-FileCopyrightText: 2022 ivan (@ratijas) tkachenko <me@ratijas.tk>
0005 
0006     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0007 */
0008 
0009 import QtQuick 2.15
0010 import QtQuick.Layouts 1.15
0011 
0012 import org.kde.kquickcontrolsaddons 2.0
0013 import org.kde.plasma.components 3.0 as PC3
0014 import org.kde.plasma.core 2.1 as PlasmaCore
0015 import org.kde.plasma.extras 2.0 as PlasmaExtras
0016 import org.kde.plasma.private.volume 0.1
0017 
0018 import "../code/icon.js" as Icon
0019 
0020 PC3.ItemDelegate {
0021     id: item
0022 
0023     required property var model
0024     property alias label: defaultButton.text
0025     property alias draggable: dragMouseArea.enabled
0026     property alias iconSource: clientIcon.source
0027     property alias iconUsesPlasmaTheme: clientIcon.usesPlasmaTheme
0028     // TODO: convert to a proper enum?
0029     property string /* "sink" | "sink-input" | "source" | "source-output" */ type
0030     property string fullNameToShowOnHover: ""
0031 
0032     highlighted: dropArea.containsDrag || activeFocus
0033     background.visible: highlighted
0034     opacity: (plasmoid.rootItem.draggedStream && plasmoid.rootItem.draggedStream.deviceIndex === item.model.Index) ? 0.3 : 1.0
0035 
0036     ListView.delayRemove: clientIcon.Drag.active
0037 
0038     Keys.forwardTo: [slider]
0039 
0040     contentItem: RowLayout {
0041         id: controlsRow
0042         spacing: item.spacing
0043 
0044         PlasmaCore.IconItem {
0045             id: clientIcon
0046             Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
0047             implicitHeight: PlasmaCore.Units.iconSizes.medium
0048             implicitWidth: implicitHeight
0049             source: "unknown"
0050             visible: item.type === "sink-input" || item.type === "source-output"
0051 
0052             onSourceChanged: {
0053                 if (!valid && source !== "unknown") {
0054                     source = "unknown";
0055                 }
0056             }
0057 
0058             PlasmaCore.IconItem {
0059                 anchors {
0060                     right: parent.right
0061                     bottom: parent.bottom
0062                 }
0063                 implicitHeight: PlasmaCore.Units.iconSizes.small
0064                 implicitWidth: implicitHeight
0065                 source: item.type === "sink-input" || item.type === "source-output" ? "emblem-pause" : ""
0066                 visible: valid && item.model.Corked
0067 
0068                 PC3.ToolTip.visible: visible && dragMouseArea.containsMouse
0069                 PC3.ToolTip.text: item.type === "source-output"
0070                     ? i18n("Currently not recording")
0071                     : i18n("Currently not playing")
0072                 PC3.ToolTip.delay: 700
0073             }
0074 
0075             MouseArea {
0076                 id: dragMouseArea
0077                 enabled: contextMenu.status === 3 //Closed
0078                 anchors.fill: parent
0079                 cursorShape: enabled ? (pressed && pressedButtons === Qt.LeftButton ? Qt.ClosedHandCursor : Qt.OpenHandCursor) : undefined
0080                 acceptedButtons: Qt.LeftButton | Qt.MiddleButton
0081                 hoverEnabled: true
0082                 drag.target: clientIcon
0083                 onClicked: if (mouse.button === Qt.MiddleButton) {
0084                     item.model.Muted = !item.model.Muted;
0085                 }
0086                 onPressed: if (mouse.button === Qt.LeftButton) {
0087                     clientIcon.grabToImage(result => {
0088                         clientIcon.Drag.imageSource = result.url;
0089                     });
0090                 }
0091             }
0092             Drag.active: dragMouseArea.drag.active
0093             Drag.dragType: Drag.Automatic
0094             Drag.onDragStarted: {
0095                 plasmoid.rootItem.draggedStream = item.model.PulseObject;
0096                 beginMoveStream(item.type === "sink-input" ? "sink" : "source");
0097             }
0098             Drag.onDragFinished: {
0099                 plasmoid.rootItem.draggedStream = null;
0100                 endMoveStream();
0101             }
0102         }
0103 
0104         ColumnLayout {
0105             id: column
0106             spacing: 0
0107 
0108             RowLayout {
0109                 Layout.minimumHeight: contextMenuButton.implicitHeight
0110 
0111                 PC3.RadioButton {
0112                     id: defaultButton
0113                     // Maximum width of the button need to match the text. Empty area must not change the default device.
0114                     Layout.maximumWidth: controlsRow.width - Layout.leftMargin - Layout.rightMargin
0115                                             - (contextMenuButton.visible ? contextMenuButton.implicitWidth + PlasmaCore.Units.smallSpacing * 2 : 0)
0116                     Layout.leftMargin: LayoutMirroring.enabled ? 0 : Math.round((muteButton.width - defaultButton.indicator.width) / 2)
0117                     Layout.rightMargin: LayoutMirroring.enabled ? Math.round((muteButton.width - defaultButton.indicator.width) / 2) : 0
0118                     spacing: PlasmaCore.Units.smallSpacing + Math.round((muteButton.width - defaultButton.indicator.width) / 2)
0119                     checked: item.model.PulseObject.hasOwnProperty("default") ? item.model.PulseObject.default : false
0120                     visible: (item.type === "sink" || item.type === "source") && item.ListView.view.count > 1
0121                     onClicked: item.model.PulseObject.default = true;
0122                 }
0123 
0124                 RowLayout {
0125                     Layout.fillWidth: true
0126                     visible: !defaultButton.visible
0127 
0128                     // User-friendly name
0129                     PC3.Label {
0130                         Layout.fillWidth: !longDescription.visible
0131                         text: defaultButton.text
0132                         elide: Text.ElideRight
0133 
0134                         MouseArea {
0135                             id: labelHoverHandler
0136 
0137                             // Only want to handle hover for the width of
0138                             // the actual text item itself
0139                             anchors.left: parent.left
0140                             anchors.top: parent.top
0141                             width: parent.contentWidth
0142                             height: parent.contentHeight
0143 
0144                             enabled: item.fullNameToShowOnHover.length > 0
0145                             hoverEnabled: true
0146                             acceptedButtons: Qt.NoButton
0147                         }
0148                     }
0149                     // Possibly not user-friendly description; only show on hover
0150                     PC3.Label {
0151                         id: longDescription
0152 
0153                         Layout.fillWidth: true
0154                         visible: opacity > 0
0155                         opacity: labelHoverHandler.containsMouse ? 1 : 0
0156                         Behavior on opacity {
0157                             NumberAnimation {
0158                                 duration: PlasmaCore.Units.shortDuration
0159                                 easing.type: Easing.InOutQuad
0160                             }
0161                         }
0162 
0163                         // Not a word puzzle because this is not a translated string
0164                         text: "(" + item.fullNameToShowOnHover + ")"
0165                         elide: Text.ElideRight
0166                     }
0167                 }
0168 
0169                 Item {
0170                     Layout.fillWidth: true
0171                     visible: contextMenuButton.visible
0172                 }
0173 
0174                 SmallToolButton {
0175                     id: contextMenuButton
0176                     icon.name: "application-menu"
0177                     checked: contextMenu.visible && contextMenu.visualParent === this
0178                     onPressed: {
0179                         contextMenu.visualParent = this;
0180                         contextMenu.openRelative();
0181                     }
0182                     visible: contextMenu.hasContent
0183 
0184                     text: i18nc("@action:button", "Additional Options")
0185 
0186                     Accessible.description: i18n("Show additional options for %1", defaultButton.text)
0187                     Accessible.role: Accessible.ButtonMenu
0188 
0189                     PC3.ToolTip {
0190                         text: parent.Accessible.description
0191                     }
0192                 }
0193             }
0194 
0195             RowLayout {
0196                 SmallToolButton {
0197                     id: muteButton
0198                     readonly property bool isPlayback: item.type.startsWith("sink")
0199                     icon.name: Icon.name(item.model.Volume, item.model.Muted, isPlayback ? "audio-volume" : "microphone-sensitivity")
0200                     onClicked: item.model.Muted = !item.model.Muted
0201                     checked: item.model.Muted
0202 
0203                     text: item.model.Muted ? i18nc("@action:button", "Unmute") : i18nc("@action:button", "Mute")
0204 
0205                     Accessible.description: item.model.Muted ? i18n("Unmute %1", defaultButton.text) : i18n("Mute %1", defaultButton.text)
0206 
0207                     PC3.ToolTip {
0208                         text: parent.Accessible.description
0209                     }
0210                 }
0211 
0212                 VolumeSlider {
0213                     id: slider
0214 
0215                     readonly property bool forceRaiseMaxVolume: (raiseMaximumVolumeCheckbox.checked && (item.type === "sink" || item.type === "source"))
0216 
0217                     Layout.fillWidth: true
0218                     from: PulseAudio.MinimalVolume
0219                     to: forceRaiseMaxVolume || item.model.Volume >= PulseAudio.NormalVolume * 1.01 ? PulseAudio.MaximalVolume : PulseAudio.NormalVolume
0220                     stepSize: to / (to / PulseAudio.NormalVolume * 100.0)
0221                     visible: item.model.HasVolume
0222                     enabled: item.model.VolumeWritable
0223                     muted: item.model.Muted
0224                     volumeObject: item.model.PulseObject
0225                     Accessible.name: i18nc("Accessibility data on volume slider", "Adjust volume for %1", defaultButton.text)
0226 
0227                     value: to, item.model.Volume
0228                     onMoved: {
0229                         item.model.Volume = value;
0230                         item.model.Muted = value === 0;
0231                     }
0232                     onPressedChanged: {
0233                         if (!pressed) {
0234                             // Make sure to sync the volume once the button was
0235                             // released.
0236                             // Otherwise it might be that the slider is at v10
0237                             // whereas PA rejected the volume change and is
0238                             // still at v15 (e.g.).
0239                             value = Qt.binding(() => item.model.Volume);
0240                             if (type === "sink") {
0241                                 playFeedback(item.model.Index);
0242                             }
0243                         }
0244                     }
0245                     onForceRaiseMaxVolumeChanged: {
0246                         if (forceRaiseMaxVolume) {
0247                             toAnimation.from = PulseAudio.NormalVolume;
0248                             toAnimation.to = PulseAudio.MaximalVolume;
0249                         } else {
0250                             toAnimation.from = PulseAudio.MaximalVolume;
0251                             toAnimation.to = PulseAudio.NormalVolume;
0252                         }
0253                         seqAnimation.restart();
0254                     }
0255 
0256                     function updateVolume() {
0257                         if (!forceRaiseMaxVolume && item.model.Volume > PulseAudio.NormalVolume) {
0258                             item.model.Volume = PulseAudio.NormalVolume;
0259                         }
0260                     }
0261 
0262                     SequentialAnimation {
0263                         id: seqAnimation
0264                         NumberAnimation {
0265                             id: toAnimation
0266                             target: slider
0267                             property: "to"
0268                             duration: PlasmaCore.Units.shortDuration
0269                             easing.type: Easing.InOutQuad
0270                         }
0271                         ScriptAction {
0272                             script: slider.updateVolume()
0273                         }
0274                     }
0275                 }
0276                 PC3.Label {
0277                     id: percentText
0278                     readonly property real value: item.model.PulseObject.volume > slider.to ? item.model.PulseObject.volume : slider.value
0279                     readonly property real displayValue: Math.round(value / PulseAudio.NormalVolume * 100.0)
0280                     Layout.alignment: Qt.AlignHCenter
0281                     Layout.minimumWidth: percentMetrics.advanceWidth
0282                     horizontalAlignment: Qt.AlignRight
0283                     text: i18nc("volume percentage", "%1%", displayValue)
0284                     // Display a subtle visual indication that the volume
0285                     // might be dangerously high
0286                     // ------------------------------------------------
0287                     // Keep this in sync with the copies in VolumeSlider.qml
0288                     // and plasma-workspace:OSDItem.qml
0289                     color: {
0290                         if (displayValue <= 100) {
0291                             return PlasmaCore.Theme.textColor
0292                         } else if (displayValue > 100 && displayValue <= 125) {
0293                             return PlasmaCore.Theme.neutralTextColor
0294                         } else {
0295                             return PlasmaCore.Theme.negativeTextColor
0296                         }
0297                     }
0298                 }
0299 
0300                 TextMetrics {
0301                     id: percentMetrics
0302                     font: percentText.font
0303                     text: i18nc("only used for sizing, should be widest possible string", "100%")
0304                 }
0305             }
0306         }
0307     }
0308 
0309     MouseArea {
0310         z: -1
0311         parent: item
0312         anchors.fill: parent
0313         acceptedButtons: Qt.MiddleButton | Qt.RightButton
0314         onPressed: {
0315             if (mouse.button === Qt.RightButton && contextMenu.hasContent) {
0316                 contextMenu.visualParent = this;
0317                 contextMenu.open(mouse.x, mouse.y);
0318             }
0319         }
0320         onClicked: {
0321             if (mouse.button === Qt.MiddleButton) {
0322                 item.model.Muted = !item.model.Muted;
0323             }
0324         }
0325     }
0326 
0327     DropArea {
0328         id: dropArea
0329         z: -1
0330         parent: item
0331         anchors.fill: parent
0332         enabled: plasmoid.rootItem.draggedStream && plasmoid.rootItem.draggedStream.deviceIndex !== item.model.Index
0333         onDropped: {
0334             plasmoid.rootItem.draggedStream.deviceIndex = item.model.Index;
0335         }
0336     }
0337 
0338     ListItemMenu {
0339         id: contextMenu
0340         pulseObject: model.PulseObject
0341         cardModel: plasmoid.rootItem.paCardModel
0342         itemType: {
0343             switch (item.type) {
0344             case "sink":
0345                 return ListItemMenu.Sink;
0346             case "sink-input":
0347                 return ListItemMenu.SinkInput;
0348             case "source":
0349                 return ListItemMenu.Source;
0350             case "source-output":
0351                 return ListItemMenu.SourceOutput;
0352             }
0353         }
0354         sourceModel: if (item.type.startsWith("sink")) {
0355             return plasmoid.rootItem.paSinkFilterModel
0356         }  else if (item.type.startsWith("source")) {
0357             return plasmoid.rootItem.paSourceFilterModel
0358         }
0359     }
0360 
0361     function setVolumeByPercent(targetPercent) {
0362         item.model.PulseObject.volume = Math.round(PulseAudio.NormalVolume * (targetPercent/100));
0363     }
0364 
0365     Keys.onPressed: {
0366         const k = event.key;
0367 
0368         if (k === Qt.Key_M) {
0369             muteButton.clicked();
0370         } else if (k >= Qt.Key_0 && k <= Qt.Key_9) {
0371             setVolumeByPercent((k - Qt.Key_0) * 10);
0372         } else if (k === Qt.Key_Return) {
0373             if (defaultButton.visible) {
0374                 defaultButton.clicked();
0375             }
0376         } else if (k === Qt.Key_Menu && contextMenu.hasContent) {
0377             contextMenu.visualParent = contextMenuButton;
0378             contextMenu.openRelative();
0379         } else {
0380             return; // don't accept the key press
0381         }
0382         event.accepted = true;
0383     }
0384 }