From cde8aa99aac06e8ba6168d8347479608fb4fbbf3 Mon Sep 17 00:00:00 2001 From: Damocles Date: Sun, 12 Apr 2026 18:35:40 +0200 Subject: [PATCH] volume panel: per-app stream sliders in expanded view --- modules/Volume.qml | 133 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/modules/Volume.qml b/modules/Volume.qml index 8ffab65..194a870 100644 --- a/modules/Volume.qml +++ b/modules/Volume.qml @@ -10,7 +10,7 @@ M.BarSection { tooltip: "" PwObjectTracker { - objects: [Pipewire.defaultAudioSink] + objects: [Pipewire.defaultAudioSink, ...root._streamList] } readonly property var sink: Pipewire.defaultAudioSink @@ -29,6 +29,16 @@ M.BarSection { return sinks; } + readonly property var _streamList: { + const streams = []; + if (Pipewire.nodes) { + for (const node of Pipewire.nodes.values) + if (node.isStream && node.audio) + streams.push(node); + } + return streams; + } + property bool _expanded: false property bool _panelHovered: false property bool _osdActive: false @@ -336,6 +346,127 @@ M.BarSection { } } + // Streams header (only if there are streams) + Item { + visible: root._streamList.length > 0 + width: parent.width + height: visible ? streamSep.height + streamHeader.height : 0 + + Rectangle { + id: streamSep + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + Text { + id: streamHeader + anchors.top: streamSep.bottom + width: parent.width + height: 24 + verticalAlignment: Text.AlignVCenter + leftPadding: 12 + text: "Applications" + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + } + } + + Repeater { + model: root._streamList + + delegate: Item { + id: streamEntry + required property var modelData + + width: deviceList.width + height: 32 + + readonly property string _appName: modelData.properties["application.name"] || modelData.description || modelData.name || "Unknown" + readonly property real _vol: modelData.audio?.volume ?? 0 + readonly property bool _muted: modelData.audio?.muted ?? false + + Text { + id: streamIcon + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._muted ? "\uF026" : "\uF028" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize + font.family: M.Theme.iconFontFamily + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: if (streamEntry.modelData.audio) + streamEntry.modelData.audio.muted = !streamEntry.modelData.audio.muted + } + } + + Text { + id: streamName + anchors.left: streamIcon.right + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + text: streamEntry._appName + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: 70 + } + + Item { + id: streamSlider + anchors.left: streamName.right + anchors.leftMargin: 6 + anchors.right: streamVol.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + Rectangle { + width: parent.width * Math.min(1, Math.max(0, streamEntry._vol)) + height: parent.height + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base0E + radius: 2 + } + + MouseArea { + anchors.fill: parent + anchors.margins: -6 + cursorShape: Qt.PointingHandCursor + onPressed: mouse => _set(mouse) + onPositionChanged: mouse => { if (pressed) _set(mouse); } + function _set(mouse) { + if (!streamEntry.modelData.audio) return; + streamEntry.modelData.audio.volume = Math.max(0, Math.min(1, mouse.x / streamSlider.width)); + } + } + } + + Text { + id: streamVol + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: Math.round(streamEntry._vol * 100) + "%" + color: streamEntry._muted ? M.Theme.base04 : M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 1 + font.family: M.Theme.fontFamily + width: 28 + } + } + } + // Bottom padding Item { width: 1; height: 4 } }