Merge pull request #7783 from omacom/fix/issue-6952

Keep PwNode objects out of the audio panel's Repeater models
This commit is contained in:
Omarchybot authored and GitHub committed 2026-10-04 02:33:47 +02:00
commit 5c4da02146
3 files changed
+76 -24

No files matched your search

+38 -21
View File
@@ -100,11 +100,13 @@ Panel {
return list
}
// Feed Repeaters with panel-local snapshots instead of the live PipeWire
// model. PipeWire can remove nodes while Quickshell is dispatching the
// removal signal; rebuilding a Repeater from that signal path has crashed
// in Quickshell's PipeWire service. The snapshot timer lets that mutation
// settle first, and closed panels keep their repeaters detached entirely.
// Feed Repeaters with panel-local snapshots of primitives instead of the live
// PipeWire model. PipeWire can remove nodes while Quickshell is dispatching
// the removal signal, and a row still holding the PwNode is a QObject Qt
// dereferences without a null check, which segfaults the shell. Rows carry id
// and name only; every delegate resolves its own node through nodeFor(). The
// snapshot timer lets the mutation settle first, and closed panels keep their
// repeaters detached entirely.
property var displayAudioSinks: []
property var displayAudioSources: []
property var displayAudioStreams: []
@@ -281,7 +283,7 @@ Panel {
return
}
if (focusSection === "streams" && selectedIndex >= 0 && selectedIndex < displayAudioStreams.length) {
var s = displayAudioStreams[selectedIndex]
var s = nodeFor(displayAudioStreams[selectedIndex])
if (s && s.audio) s.audio.volume = Math.max(0, Math.min(1.5, s.audio.volume + delta))
}
}
@@ -291,18 +293,18 @@ Panel {
if (focusSection === "header") { toggleAllMuted(); return }
if (focusSection === "output") {
if (selectedIndex === -1) { toggleOutputMute(); return }
var sink = displayAudioSinks[selectedIndex]
var sink = nodeFor(displayAudioSinks[selectedIndex])
if (sink) setDefaultSink(sink)
return
}
if (focusSection === "input") {
if (selectedIndex === -1) { toggleInputMute(); return }
var src = displayAudioSources[selectedIndex]
var src = nodeFor(displayAudioSources[selectedIndex])
if (src) setDefaultSource(src)
return
}
if (focusSection === "streams" && selectedIndex >= 0) {
var st = displayAudioStreams[selectedIndex]
var st = nodeFor(displayAudioStreams[selectedIndex])
if (st && st.audio) st.audio.muted = !st.audio.muted
}
}
@@ -324,15 +326,27 @@ Panel {
onAudioSourcesChanged: scheduleDisplayAudioModelRefresh()
onAudioStreamsChanged: scheduleDisplayAudioModelRefresh()
function listSnapshot(list) {
return Model.listSnapshot(list)
function rowSnapshot(list) {
return Model.rowSnapshot(list)
}
// A row that outlives its node resolves to null, or to a node PipeWire recreated
// under the same id and name; every binding below guards for null, and a row
// that never held the node cannot dangle.
function nodeFor(row) {
if (!row) return null
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i]
if (n && n.id === row.id && String(n.name || "") === row.name) return n
}
return null
}
function refreshDisplayAudioModels() {
if (!opened) return
displayAudioSinks = listSnapshot(audioSinks)
displayAudioSources = listSnapshot(audioSources)
displayAudioStreams = listSnapshot(audioStreams)
displayAudioSinks = rowSnapshot(audioSinks)
displayAudioSources = rowSnapshot(audioSources)
displayAudioStreams = rowSnapshot(audioStreams)
clampCursor()
}
@@ -559,15 +573,18 @@ Panel {
// Spotify exposes its PipeWire stream as "audio-src". For generic stream
// names, use the one MPRIS player not already represented by another audio
// stream (e.g. Chromium, or ALSA apps like cliamp).
return Model.unmatchedMprisStreamLabel(label, mprisPlayers, displayAudioStreams)
return Model.unmatchedMprisStreamLabel(label, mprisPlayers, audioStreams)
}
// These read the other streams' node properties, so they take the live list --
// displayAudioStreams carries primitives now, and resolving each row back
// would only rebuild what audioStreams already is.
function streamLabel(node) {
return Model.streamLabel(node, mprisPlayers, displayAudioStreams)
return Model.streamLabel(node, mprisPlayers, audioStreams)
}
function streamRepresentsPlayer(node, player) {
return Model.streamRepresentsPlayer(node, player, mprisPlayers, displayAudioStreams)
return Model.streamRepresentsPlayer(node, player, mprisPlayers, audioStreams)
}
implicitWidth: button.implicitWidth
@@ -675,7 +692,7 @@ Panel {
if (!root.cursorActive) return
if (root.focusSection === "streams" && root.selectedIndex >= 0
&& root.selectedIndex < root.displayAudioStreams.length) {
var s = root.displayAudioStreams[root.selectedIndex]
var s = root.nodeFor(root.displayAudioStreams[root.selectedIndex])
if (s && s.audio) s.audio.muted = !s.audio.muted
} else if (root.focusSection === "input") {
root.toggleInputMute()
@@ -857,7 +874,7 @@ Panel {
required property var modelData
required property int index
width: panelColumn.width
node: modelData
node: root.nodeFor(modelData)
rowIndex: index
}
}
@@ -965,7 +982,7 @@ Panel {
required property var modelData
required property int index
width: panelColumn.width
node: modelData
node: root.nodeFor(modelData)
rowIndex: index
}
}
@@ -995,7 +1012,7 @@ Panel {
required property var modelData
required property int index
width: panelColumn.width
node: modelData
node: root.nodeFor(modelData)
rowIndex: index
}
}