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) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-28 07:55:36 -07:00
co-authored by Claude Opus 5
parent 4a8091cd41
commit cf8102af92
+27 -6
View File
@@ -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
}
}
}