From cf8102af9261dbc1ae81b8ab785a37d034126dbf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 28 Jul 2026 07:55:36 -0700 Subject: [PATCH] Mute both channels from the audio hero, one from each slider The hero switch is the panel's on/off, so it now carries output and input together rather than output alone. It reads as on while anything is still audible, which keeps muting a single channel from flipping the master switch. Right-clicking a volume bar mutes just that channel -- output, input, or one app's stream -- matching what the `m` key already does for the focused row. Only channels that exist get a vote: a machine with no default source would otherwise report its input unmuted forever, leaving the switch able to mute but never to unmute. Co-Authored-By: Claude Opus 5 (1M context) --- shell/plugins/panels/audio/Panel.qml | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/shell/plugins/panels/audio/Panel.qml b/shell/plugins/panels/audio/Panel.qml index f93ff0b6..8dcdb86a 100644 --- a/shell/plugins/panels/audio/Panel.qml +++ b/shell/plugins/panels/audio/Panel.qml @@ -165,7 +165,13 @@ Panel { // "header" is a virtual section for the hero output mute toggle; it sits // above the output section so the speaker can be muted from the keyboard. readonly property bool headerHasCursor: cursorActive && focusSection === "header" - readonly property string toggleHint: root.outputMuted ? "Unmute output" : "Mute output" + // Only channels that actually exist get a vote. A box with no default source + // would otherwise report "input unmuted" forever, leaving the hero switch + // able to mute but never to unmute. + readonly property bool hasOutput: !!(volumeSink && volumeSink.audio) + readonly property bool hasInput: !!(source && source.audio) + readonly property bool anyAudible: (hasOutput && !outputMuted) || (hasInput && !inputMuted) + readonly property string toggleHint: anyAudible ? "Mute" : "Unmute" readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) @@ -279,7 +285,7 @@ Panel { // Enter/Space: activate whatever the cursor is on. function activateCursor() { - if (focusSection === "header") { toggleOutputMute(); return } + if (focusSection === "header") { toggleAllMuted(); return } if (focusSection === "output") { if (selectedIndex === -1) { toggleOutputMute(); return } var sink = displayAudioSinks[selectedIndex] @@ -432,6 +438,15 @@ Panel { if (source && source.audio) source.audio.muted = !source.audio.muted } + // The hero switch is the whole panel's on/off, so it carries both channels + // at once. It reads as on while anything is still audible, which keeps + // muting a single channel from the row below flipping the master switch. + function toggleAllMuted() { + var mute = anyAudible + if (hasOutput) volumeSink.audio.muted = mute + if (hasInput) source.audio.muted = mute + } + function setDefaultSink(node) { if (!node) return Pipewire.preferredDefaultAudioSink = node @@ -689,17 +704,17 @@ Panel { } // Compact on/off switch on the trailing edge of the hero, and the - // header's only cursor target. Checked means audible, so muting - // reads as switching the output off. + // header's only cursor target. Checked means something is still + // audible, so muting everything reads as switching audio off. ToggleSwitch { id: powerSwitch - checked: !root.outputMuted + checked: root.anyAudible hasCursor: root.headerHasCursor foreground: root.bar.foreground anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter onHovered: function(on) { if (on) root.setHeaderCursor() } - onToggled: root.toggleOutputMute() + onToggled: root.toggleAllMuted() PanelToolTip { visible: powerSwitch.containsMouse @@ -803,6 +818,7 @@ Panel { enabled: !!root.sink onMoved: function(v) { root.setOutputVolume(v) } + onRightClicked: root.toggleOutputMute() } HoverHandler { @@ -894,6 +910,7 @@ Panel { enabled: !!root.source onMoved: function(v) { root.setInputVolume(v) } + onRightClicked: root.toggleInputMute() } Rectangle { @@ -1181,6 +1198,10 @@ Panel { onMoved: function(v) { if (streamRow.node && streamRow.node.audio) streamRow.node.audio.volume = v } + onRightClicked: { + if (streamRow.node && streamRow.node.audio) + streamRow.node.audio.muted = !streamRow.node.audio.muted + } } }