From 9d1a81c93c1a56298c337f24ddd2e3d87e61d09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Sun, 14 Jun 2026 04:51:03 +0700 Subject: [PATCH] feat(media-control): support scroll and right-click on audio output devices in media popout (#2615) * feat(media-control): support scroll and right-click on audio output devices in media popout * feat(media-control): make device list volume scrolling optional --- quickshell/Common/SettingsData.qml | 1 + quickshell/Common/settings/SettingsSpec.js | 1 + .../Modules/DankDash/DankDashPopout.qml | 3 -- .../Modules/DankDash/MediaDropdownOverlay.qml | 22 ++++++++++- .../Modules/DankDash/MediaPlayerTab.qml | 22 ++++++++++- .../Modules/Settings/MediaPlayerTab.qml | 7 ++++ quickshell/Services/AudioService.qml | 37 ++++++++++++++++++- 7 files changed, 86 insertions(+), 7 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 59b3facf..7174ec6c 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -397,6 +397,7 @@ Singleton { property bool audioVisualizerEnabled: true property string audioScrollMode: "volume" property int audioWheelScrollAmount: 5 + property bool audioDeviceScrollVolumeEnabled: false property bool clockCompactMode: false property int focusedWindowSize: 1 property bool focusedWindowCompactMode: false diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 693154fa..d9e88a8d 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -156,6 +156,7 @@ var SPEC = { audioVisualizerEnabled: { def: true }, audioScrollMode: { def: "volume" }, audioWheelScrollAmount: { def: 5 }, + audioDeviceScrollVolumeEnabled: { def: false }, clockCompactMode: { def: false }, focusedWindowCompactMode: { def: false }, focusedWindowSize: { def: 1 }, diff --git a/quickshell/Modules/DankDash/DankDashPopout.qml b/quickshell/Modules/DankDash/DankDashPopout.qml index b0ee9257..fca2bef7 100644 --- a/quickshell/Modules/DankDash/DankDashPopout.qml +++ b/quickshell/Modules/DankDash/DankDashPopout.qml @@ -108,9 +108,6 @@ DankPopout { MprisController.setActivePlayer(player); root.__hideDropdowns(); } - onDeviceSelected: device => { - root.__hideDropdowns(); - } } } diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 797ba8b0..43edbf22 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -383,7 +383,27 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: mouse => { + if (mouse.button === Qt.RightButton) { + mouse.accepted = true; + } + } + onWheel: wheelEvent => { + if (SettingsData.audioDeviceScrollVolumeEnabled && wheelEvent.x >= deviceMouseArea.width / 2) { + AudioService.handleNodeVolumeWheel(modelData, wheelEvent); + } else { + wheelEvent.accepted = false; + } + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (modelData && modelData.audio) { + SessionData.suppressOSDTemporarily(); + modelData.audio.muted = !modelData.audio.muted; + } + return; + } if (modelData && modelData.name) { AudioService.setDefaultSinkByName(modelData.name); root.deviceSelected(modelData); diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 7214d09f..abe0849f 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -866,7 +866,27 @@ Item { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onPressed: mouse => { + if (mouse.button === Qt.RightButton) { + mouse.accepted = true; + } + } + onWheel: wheelEvent => { + const delta = wheelEvent.angleDelta.y; + if (delta !== 0) { + AudioService.cycleAudioOutputDirection(delta < 0); + wheelEvent.accepted = true; + } + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (AudioService.sink?.audio) { + SessionData.suppressOSDTemporarily(); + AudioService.sink.audio.muted = !AudioService.sink.audio.muted; + } + return; + } if (devicesExpanded) { const sinks = AudioService.getAvailableSinks(); if (sinks && sinks.length > 1) { diff --git a/quickshell/Modules/Settings/MediaPlayerTab.qml b/quickshell/Modules/Settings/MediaPlayerTab.qml index a844a86f..c6b13adb 100644 --- a/quickshell/Modules/Settings/MediaPlayerTab.qml +++ b/quickshell/Modules/Settings/MediaPlayerTab.qml @@ -113,6 +113,13 @@ Item { } } } + + SettingsToggleRow { + text: I18n.tr("Device list scroll volume") + description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list") + checked: SettingsData.audioDeviceScrollVolumeEnabled + onToggled: checked => SettingsData.set("audioDeviceScrollVolumeEnabled", checked) + } } } } diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index bf608daa..065f6535 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -58,6 +58,8 @@ Singleton { return SessionData.deviceMaxVolumes[name] ?? 100; } + readonly property int wheelVolumeStep: SettingsData.audioWheelScrollAmount + signal micMuteChanged signal audioOutputCycled(string deviceName, string deviceIcon) signal deviceAliasChanged(string nodeName, string newAlias) @@ -156,14 +158,19 @@ Singleton { return false; } - function cycleAudioOutput() { + function cycleAudioOutputDirection(forward) { const sinks = getAvailableSinks(); if (sinks.length < 2) return null; const currentName = root.sink?.name ?? ""; const currentIndex = sinks.findIndex(s => s.name === currentName); - const nextIndex = (currentIndex + 1) % sinks.length; + let nextIndex; + if (forward) { + nextIndex = (currentIndex + 1) % sinks.length; + } else { + nextIndex = (currentIndex - 1 + sinks.length) % sinks.length; + } const nextSink = sinks[nextIndex]; setDefaultSinkByName(nextSink.name); const name = displayName(nextSink); @@ -171,6 +178,10 @@ Singleton { return name; } + function cycleAudioOutput() { + return cycleAudioOutputDirection(true); + } + function getDeviceAlias(nodeName) { if (!nodeName) return null; @@ -833,6 +844,28 @@ EOFCONFIG return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"; } + function handleNodeVolumeWheel(node, wheelEvent) { + if (!node?.audio) + return; + + SessionData.suppressOSDTemporarily(); + const delta = wheelEvent.angleDelta.y; + if (delta === 0) + return; + + const current = Math.round(node.audio.volume * 100); + const maxVol = getMaxVolumePercent(node); + const newVolume = delta > 0 ? Math.min(maxVol, current + root.wheelVolumeStep) : Math.max(0, current - root.wheelVolumeStep); + + node.audio.muted = false; + node.audio.volume = newVolume / 100; + + if (node === sink) { + playVolumeChangeSoundIfEnabled(); + } + wheelEvent.accepted = true; + } + function setMicVolume(percentage) { if (!root.source?.audio) { return "No audio source available";