From 786b8f3fa19602d0b1319d9f116389ef2b6f8e9d Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 25 Jul 2025 11:24:27 -0400 Subject: [PATCH] audio: create and connect volume popup to IPCs --- Modules/ControlCenter/Audio/VolumeControl.qml | 22 +- Modules/VolumePopup.qml | 209 ++++++++++++++++++ README.md | 64 +++++- Services/AudioService.qml | 16 +- shell.qml | 4 + 5 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 Modules/VolumePopup.qml diff --git a/Modules/ControlCenter/Audio/VolumeControl.qml b/Modules/ControlCenter/Audio/VolumeControl.qml index 41937548..2ef29eca 100644 --- a/Modules/ControlCenter/Audio/VolumeControl.qml +++ b/Modules/ControlCenter/Audio/VolumeControl.qml @@ -23,7 +23,6 @@ Column { id: volumeSlider width: parent.width - value: Math.round(root.volumeLevel) minimum: 0 maximum: 100 leftIcon: root.volumeMuted ? "volume_off" : "volume_down" @@ -32,16 +31,18 @@ Column { showValue: true unit: "%" - onSliderValueChanged: (newValue) => { - if (AudioService.sink && AudioService.sink.audio) { - AudioService.sink.audio.muted = false; - AudioService.sink.audio.volume = newValue / 100; + Connections { + target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null + function onVolumeChanged() { + volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100); } } - // Add click handler for mute icon Component.onCompleted: { - // Find the left icon and add mouse area + if (AudioService.sink && AudioService.sink.audio) { + value = Math.round(AudioService.sink.audio.volume * 100); + } + let leftIconItem = volumeSlider.children[0].children[0]; if (leftIconItem) { let mouseArea = Qt.createQmlObject( @@ -51,5 +52,12 @@ Column { ); } } + + onSliderValueChanged: (newValue) => { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = false; + AudioService.sink.audio.volume = newValue / 100; + } + } } } \ No newline at end of file diff --git a/Modules/VolumePopup.qml b/Modules/VolumePopup.qml new file mode 100644 index 00000000..7d9d1be3 --- /dev/null +++ b/Modules/VolumePopup.qml @@ -0,0 +1,209 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + property bool volumePopupVisible: false + + visible: volumePopupVisible + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + + anchors { + top: true + left: true + right: true + bottom: true + } + + Timer { + id: hideTimer + interval: 3000 + repeat: false + onTriggered: { + if (!volumePopup.containsMouse) { + root.volumePopupVisible = false + } else { + hideTimer.restart() + } + } + } + + function show() { + root.volumePopupVisible = true; + hideTimer.restart(); + } + + function resetHideTimer() { + if (root.volumePopupVisible) { + hideTimer.restart(); + } + } + + Connections { + target: AudioService + function onVolumeChanged() { + root.show(); + } + function onSinkChanged() { + if (root.volumePopupVisible) { + root.show(); + } + } + } + + + Rectangle { + id: volumePopup + + property bool containsMouse: popupMouseArea.containsMouse + + width: Math.min(260, Screen.width - Theme.spacingM * 2) + height: volumeContent.height + Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacingM + + color: Theme.popupBackground() + radius: Theme.cornerRadiusLarge + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + opacity: root.volumePopupVisible ? 1 : 0 + scale: root.volumePopupVisible ? 1 : 0.9 + + Column { + id: volumeContent + + anchors.centerIn: parent + width: parent.width - Theme.spacingS * 2 + spacing: Theme.spacingXS + + Item { + width: parent.width + height: 40 + + property int gap: Theme.spacingS + + Rectangle { + width: Theme.iconSize + height: Theme.iconSize + radius: Theme.iconSize / 2 + color: "transparent" + x: parent.gap + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + anchors.centerIn: parent + name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? + "volume_off" : "volume_up" + size: Theme.iconSize + color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText + } + + MouseArea { + id: muteButton + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + AudioService.toggleMute(); + root.resetHideTimer(); + } + } + } + + DankSlider { + id: volumeSlider + width: parent.width - Theme.iconSize - parent.gap * 3 + height: 40 + x: parent.gap * 2 + Theme.iconSize + anchors.verticalCenter: parent.verticalCenter + minimum: 0 + maximum: 100 + enabled: AudioService.sink && AudioService.sink.audio + showValue: true + unit: "%" + + Connections { + target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null + function onVolumeChanged() { + volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100); + } + } + + Component.onCompleted: { + if (AudioService.sink && AudioService.sink.audio) { + value = Math.round(AudioService.sink.audio.volume * 100); + } + } + + onSliderValueChanged: function(newValue) { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.volume = newValue / 100; + root.resetHideTimer(); + } + } + } + } + + } + + MouseArea { + id: popupMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + z: -1 + } + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowHorizontalOffset: 0 + shadowVerticalOffset: 4 + shadowBlur: 0.8 + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowOpacity: 0.3 + } + + transform: Translate { + y: root.volumePopupVisible ? 0 : 20 + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on transform { + PropertyAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + } + + mask: Region { + item: volumePopup + } +} \ No newline at end of file diff --git a/README.md b/README.md index 84a86287..8a7f9f50 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,60 @@ IPC Events are events that can be triggered with `qs` cli. qs -c DankMaterialShell ipc call ``` -| Target | Function | Description | -|--------|----------|-------------| -| spotlight | toggle | Toggle spotlight (app launcher) | -| clipboard | toggle | Toggle clipboard history view | -| processlist | toggle | Toggle process list (task manager) | -| wallpaper | set \ | Set wallpaper to image path and refresh theme (if auto theme enabled) | -| wallpaper | get | Get current wallpaper path | -| lock | lock | Triggers lockscreen | -| lock | demo | Triggers lockscreen demo mode | -| lock | isLocked | Whether we're locked or not | +## System Controls + +| Target | Function | Parameters | Description | +|--------|----------|------------|-------------| +| audio | setvolume | percentage (string) | Set audio volume to specific percentage (0-100) | +| audio | increment | step (string, default: "5") | Increase volume by step percentage | +| audio | decrement | step (string, default: "5") | Decrease volume by step percentage | +| audio | mute | none | Toggle audio mute | +| audio | setmic | percentage (string) | Set microphone volume to specific percentage | +| audio | micmute | none | Toggle microphone mute | +| audio | status | none | Get current audio status (output/input levels and mute states) | + +## Application Controls + +| Target | Function | Parameters | Description | +|--------|----------|------------|-------------| +| spotlight | open | none | Open spotlight (app launcher) | +| spotlight | close | none | Close spotlight (app launcher) | +| spotlight | toggle | none | Toggle spotlight (app launcher) | +| clipboard | open | none | Open clipboard history view | +| clipboard | close | none | Close clipboard history view | +| clipboard | toggle | none | Toggle clipboard history view | +| processlist | open | none | Open process list (task manager) | +| processlist | close | none | Close process list (task manager) | +| processlist | toggle | none | Toggle process list (task manager) | +| lock | lock | none | Activate lockscreen | +| lock | demo | none | Show lockscreen in demo mode | +| lock | isLocked | none | Returns whether screen is currently locked | +| picker | open | none | Open area picker/screenshot tool | +| picker | openFreeze | none | Open area picker in freeze mode | + +## Media Controls + +| Target | Function | Parameters | Description | +|--------|----------|------------|-------------| +| mpris | list | none | Get list of available media players | +| mpris | play | none | Start media playback on active player | +| mpris | pause | none | Pause media playback on active player | +| mpris | playPause | none | Toggle play/pause state on active player | +| mpris | previous | none | Skip to previous track on active player | +| mpris | next | none | Skip to next track on active player | +| mpris | stop | none | Stop media playback on active player | +| mpris | getActive | prop (string) | Get specified property from active player | + +## System Services + +| Target | Function | Parameters | Description | +|--------|----------|------------|-------------| +| wallpaper | get | none | Get current wallpaper path | +| wallpaper | set | path (string) | Set wallpaper to image path and refresh theme | +| wallpaper | clear | none | Clear current wallpaper | +| notifs | clear | none | Clear all notifications | +| drawers | toggle | drawer (string) | Toggle visibility of specified drawer/panel | +| drawers | list | none | Get list of available drawers | ## (Optional) Setup Calendar events (Google, Microsoft, other Caldev, etc.) diff --git a/Services/AudioService.qml b/Services/AudioService.qml index 7c3e8a92..89a93626 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -128,19 +128,27 @@ Singleton { target: "audio" function setvolume(percentage: string): string { - return root.setVolume(parseInt(percentage)); + const result = root.setVolume(parseInt(percentage)); + root.volumeChanged(); + return result; } function increment(step: string): string { - return root.incrementVolume(parseInt(step || "5")); + const result = root.incrementVolume(parseInt(step || "5")); + root.volumeChanged(); + return result; } function decrement(step: string): string { - return root.decrementVolume(parseInt(step || "5")); + const result = root.decrementVolume(parseInt(step || "5")); + root.volumeChanged(); + return result; } function mute(): string { - return root.toggleMute(); + const result = root.toggleMute(); + root.volumeChanged(); + return result; } function setmic(percentage: string): string { diff --git a/shell.qml b/shell.qml index 01ba438d..c1cd521f 100644 --- a/shell.qml +++ b/shell.qml @@ -145,4 +145,8 @@ ShellRoot { Toast { id: toastWidget } + + VolumePopup { + id: volumePopup + } }