From 230537699d01166e7c3101eb9a277bc0ff0a5f95 Mon Sep 17 00:00:00 2001 From: Hannes Date: Mon, 24 Aug 2026 20:31:44 +0200 Subject: [PATCH] feat: add volume change OSD modules for audio sink and source and added Default Sink and Source to the Bar --- config/Config.qml | 6 +- modules/VolumeChangeOSD/SinkOSD.qml | 89 +++++++++++++++++++ modules/VolumeChangeOSD/SourceOSD.qml | 81 +++++++++++++++++ modules/bar/Bar.qml | 6 +- modules/bar/states/Audio/DefaultSinkState.qml | 36 ++++++++ .../bar/states/Audio/DefaultSourceState.qml | 36 ++++++++ modules/bar/states/Audio/qmldir | 2 + modules/bar/widgets/AudioSink.qml | 63 +++++++++++++ modules/bar/widgets/AudioSource.qml | 32 +++++++ shell.qml | 5 ++ 10 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 modules/VolumeChangeOSD/SinkOSD.qml create mode 100644 modules/VolumeChangeOSD/SourceOSD.qml create mode 100644 modules/bar/states/Audio/DefaultSinkState.qml create mode 100644 modules/bar/states/Audio/DefaultSourceState.qml create mode 100644 modules/bar/states/Audio/qmldir create mode 100644 modules/bar/widgets/AudioSink.qml create mode 100644 modules/bar/widgets/AudioSource.qml diff --git a/config/Config.qml b/config/Config.qml index eca7844..38f061a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -1,8 +1,12 @@ pragma Singleton -import QtQuick 2.15 +import QtQuick +import Quickshell.Hyprland import "./path" QtObject { + // Not really usable since it is not updating when changed + property var focusedMonitor: Hyprland.focusedMonitor + readonly property string configDir: ConfigPath.path readonly property string wallpaperDir: WallpaperPath.wallpaperDir diff --git a/modules/VolumeChangeOSD/SinkOSD.qml b/modules/VolumeChangeOSD/SinkOSD.qml new file mode 100644 index 0000000..dfd9a21 --- /dev/null +++ b/modules/VolumeChangeOSD/SinkOSD.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import '../bar/states/Audio' +import "../../config" + +LazyLoader { + active: Sink.volumeChanged + + + PanelWindow { + // Since the panel's screen is unset, it will be picked by the compositor + // when the window is created. Most compositors pick the current active monitor. + + anchors.bottom: true + margins.bottom: screen.height / 5 + exclusiveZone: 0 + + implicitWidth: Config.focusedMonitor.width/5 + implicitHeight: Config.focusedMonitor.height/40 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: "transparent" + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Text { + Layout.alignment: Qt.AlignLeft + rightPadding: (text == "󰕿" || text == "󰸈" ? font.pixelSize/4 : text == "󰖀" ? font.pixelSize/6 : 0) + leftPadding: (text == "󰕿" || text == "󰸈" ? -font.pixelSize/15 : 0) + + text: ( + Sink.mute ? "󰝟" : + Sink.volume > 60 ? "󰕾" : + Sink.volume > 30 ? "󰖀" : + Sink.volume > 0 ? "󰕿" : + "󰸈") + font.pixelSize: 30 + + color: Colors.primary + } + + Rectangle { + // Stretches to fill all left-over space + Layout.fillWidth: true + + implicitHeight: 10 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + implicitWidth: parent.width * (Sink.volume/100/1.5 ?? 0) + radius: parent.radius + + color: Sink.volume/100 > 1 ? Colors.tertiary : Colors.primary + } + } + + Text { + Layout.alignment: Qt.AlignRight + + text: Sink.volume + "%" + font.pixelSize: 30 + + color: Colors.primary + } + } + } + } +} \ No newline at end of file diff --git a/modules/VolumeChangeOSD/SourceOSD.qml b/modules/VolumeChangeOSD/SourceOSD.qml new file mode 100644 index 0000000..06b04f4 --- /dev/null +++ b/modules/VolumeChangeOSD/SourceOSD.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import '../bar/states/Audio' +import "../../config" + +LazyLoader { + active: Source.volumeChanged + + + PanelWindow { + // Since the panel's screen is unset, it will be picked by the compositor + // when the window is created. Most compositors pick the current active monitor. + + anchors.bottom: true + margins.bottom: screen.height / 5 - implicitHeight + exclusiveZone: 0 + + implicitWidth: Config.focusedMonitor.width/5 + implicitHeight: Config.focusedMonitor.height/40 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: "transparent" + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Text { + Layout.alignment: Qt.AlignVCenter + + text: Source.mute ? "󰍭" : "󰍬" + font.pixelSize: 30 + + color: Colors.primary + } + + Rectangle { + // Stretches to fill all left-over space + Layout.fillWidth: true + + implicitHeight: 10 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + implicitWidth: parent.width * (Source.volume/100 ?? 0) + radius: parent.radius + + color: Colors.primary + } + } + + Text { + Layout.alignment: Qt.AlignRight + + text: Source.volume + "%" + font.pixelSize: 30 + + color: Colors.primary + } + } + } + } +} \ No newline at end of file diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 686659a..93fd216 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -68,10 +68,12 @@ PanelWindow { // TODO: Add bluetooth, network, audio, power, maybe HW monitor Tray{} + // AudioSource{} + AudioSink{} Battery{} - Network {} + Network{} IdleButton { panelWindow: panel } - PowerButton {} + PowerButton{} } } diff --git a/modules/bar/states/Audio/DefaultSinkState.qml b/modules/bar/states/Audio/DefaultSinkState.qml new file mode 100644 index 0000000..da57d3f --- /dev/null +++ b/modules/bar/states/Audio/DefaultSinkState.qml @@ -0,0 +1,36 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink ] + } + + property var audioSink: Pipewire.defaultAudioSink + + property bool exists: audioSink != null + property bool mute: audioSink?.audio?.muted ?? false + property real volume: Math.round((audioSink?.audio?.volume ?? 0) * 1000) / 10 + + property bool volumeChanged: false + + Connections { + target: audioSink?.audio + + function onVolumeChanged() { + root.volumeChanged = true; + hideTimer.restart(); + } + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.volumeChanged = false + } +} \ No newline at end of file diff --git a/modules/bar/states/Audio/DefaultSourceState.qml b/modules/bar/states/Audio/DefaultSourceState.qml new file mode 100644 index 0000000..f12b78b --- /dev/null +++ b/modules/bar/states/Audio/DefaultSourceState.qml @@ -0,0 +1,36 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire + +Item { + id: root + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSource ] + } + + property var audioSource: Pipewire.defaultAudioSource + + property bool exists: audioSource != null + property bool mute: audioSource?.audio?.muted ?? false + property real volume: Math.round((audioSource?.audio?.volume ?? 0) * 1000) / 10 + + property bool volumeChanged: false + + Connections { + target: audioSource?.audio + + function onVolumeChanged() { + root.volumeChanged = true; + hideTimer.restart(); + } + } + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.volumeChanged = false + } +} \ No newline at end of file diff --git a/modules/bar/states/Audio/qmldir b/modules/bar/states/Audio/qmldir new file mode 100644 index 0000000..2635137 --- /dev/null +++ b/modules/bar/states/Audio/qmldir @@ -0,0 +1,2 @@ +singleton Sink DefaultSinkState.qml +singleton Source DefaultSourceState.qml \ No newline at end of file diff --git a/modules/bar/widgets/AudioSink.qml b/modules/bar/widgets/AudioSink.qml new file mode 100644 index 0000000..6809cf2 --- /dev/null +++ b/modules/bar/widgets/AudioSink.qml @@ -0,0 +1,63 @@ +import QtQuick +import "../../../config" +import "../states/Audio" +import Quickshell.Hyprland + +MouseArea { + id: audioButton + + width: Math.max(audioRow.width + Config.spacing_fun(height)*3.5, height) + height: bar.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: Sink.exists + + Rectangle { + anchors.fill: parent + color: audioButton.containsMouse ? Colors.tertiary : Qt.alpha(Colors.tertiaryContainer, 0.25) + radius: Config.radius_fun(audioButton.height) + border.width: Config.border_width + border.color: audioButton.containsMouse ? Colors.tertiaryContainer_fg + : Colors.secondaryContainer_fg + + Row { + id: audioRow + width: audioSourceText.width + audioSinkText.width + audioPercentageText.width + Config.spacing_fun(height)*2 + height: parent.height + spacing: Config.spacing_fun(height)/2 + anchors.centerIn: parent + + Text { + id: audioSourceText + text: (Source.mute ? "󰍭" : + "󰍬") + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Config.iconSize(bar.height) + font.family: Config.font + color: audioButton.containsMouse ? Colors.tertiary_fg : Colors.primary + rightPadding: Config.spacing_fun(height)*1.5 + } + + Text { + id: audioSinkText + text: (Sink.mute ? "󰝟" : + Sink.volume > 60 ? "󰕾" : + Sink.volume > 30 ? "󰖀" : + "󰕿") + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Config.iconSize(bar.height) + font.family: Config.font + color: audioButton.containsMouse ? Colors.tertiary_fg : Colors.primary + } + + Text { + id: audioPercentageText + text: Sink.volume + "%" + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Config.iconSize(bar.height) + font.family: Config.font + color: audioButton.containsMouse ? Colors.tertiary_fg : Colors.primary + } + } + } +} diff --git a/modules/bar/widgets/AudioSource.qml b/modules/bar/widgets/AudioSource.qml new file mode 100644 index 0000000..4e5ef17 --- /dev/null +++ b/modules/bar/widgets/AudioSource.qml @@ -0,0 +1,32 @@ +import QtQuick +import "../../../config" +import "../states/Audio" + +MouseArea { + id: audioButton + + width: Math.max(audioText.width + Config.spacing_fun(height)*2, height) + height: bar.height + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: Sink.exists + + Rectangle { + anchors.fill: parent + color: audioButton.containsMouse ? Colors.tertiary : Qt.alpha(Colors.tertiaryContainer, 0.25) + radius: Config.radius_fun(audioButton.height) + border.width: Config.border_width + border.color: audioButton.containsMouse ? Colors.tertiaryContainer_fg + : Colors.secondaryContainer_fg + + Text { + id: audioText + text: (Source.mute ? "󰍭" : + "󰍬") + anchors.centerIn: parent + font.pixelSize: Config.iconSize(bar.height) + font.family: Config.font + color: audioButton.containsMouse ? Colors.tertiary_fg : Colors.primary + } + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index d8fa682..0ec54c9 100644 --- a/shell.qml +++ b/shell.qml @@ -8,6 +8,7 @@ import "./modules/wp" import "./modules/wlogout" import "./modules/jl" import "./modules/background" +import "./modules/VolumeChangeOSD" ShellRoot { id: root @@ -26,6 +27,10 @@ ShellRoot { id : wlLoader } + //VolumeChangeOSD + SourceOSD {} + SinkOSD {} + // Background {} function openWlogout() {