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 }