fix: hold the check-button reference across the signal-panel rebuild

`gtk_check_button_set_active` emits both `notify::active` and `toggled`
in sequence.  GLib takes a per-emission reference, so it protects the
instance only through the end of `notify::active` — but our
`on_signal_switch` handler calls `rebuild_signal_panels`, which removes
every row from `signal_list`.  The row that contained the emitting check
button is destroyed, dropping its reference.  After `notify::active`
finishes, GLib unrefs the instance: with no external holder left that
is refcount 0, the instance is finalised.  The subsequent `toggled`
emission then fires into freed memory and produces two
`GLib-GObject-CRITICAL` diagnostics.

The fix: take a `g_object_ref` at the top of the handler and release it
from a `g_idle_add_full` destroy function.  The idle fires after
`set_active` has returned and all its emissions are complete, so the
instance lives until the emission chain is fully done.  The rebuild
itself stays synchronous — it has run before the next frame reads the
list — and the behaviour the handler delivers is unchanged.

This is a latent bug predating Task 1.  Previously the window's focus
reference happened to land on the row's `GtkListBoxRow` and kept the
row alive as a side effect; the new `AdwViewSwitcher` takes initial
focus instead, so nothing holds that reference any more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-14 11:44:40 +02:00
1 parent 44d2fd35dc
commit f224f56c4c
1 file changed
+14
+14
View File
@@ -24,6 +24,15 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
const std::vector<SignalInfo>& signals = app->model->signals(); const std::vector<SignalInfo>& signals = app->model->signals();
if (index < signals.size()) name = signals[index].name; if (index < signals.size()) name = signals[index].name;
} }
// Hold a reference on the emitting widget across the rebuild. `rebuild_signal_panels` destroys
// the list that owns `obj` (this check button's row), dropping the row's reference and leaving
// only GLib's own per-emission ref. `gtk_check_button_set_active` emits both `notify::active`
// and `toggled` in sequence: GLib's ref covers only one emission at a time, so after
// `notify::active` finishes GLib unrefs the instance and, without our extra hold, that is the
// last reference -- and the subsequent `toggled` emission is then into freed memory, which is
// the two `GLib-GObject-CRITICAL` diagnostics. Taking our own ref here and releasing it from
// an idle keeps the instance alive until after `set_active` has emitted everything and returned.
g_object_ref(obj);
app->model->set_signal_enabled(index, on); app->model->set_signal_enabled(index, on);
if (!on) { if (!on) {
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take. // A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
@@ -48,6 +57,11 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
} }
// The enable state is part of the session (a signal can be on only as a math channel's source). // The enable state is part of the session (a signal can be on only as a math channel's source).
schedule_layout_save(*app); schedule_layout_save(*app);
// Release the extra reference from an idle so that `set_active`'s full emission chain --
// `notify::active` and `toggled` -- has completed before the instance can be finalized.
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
+[](gpointer) -> gboolean { return G_SOURCE_REMOVE; },
obj, g_object_unref);
} }
/// The palette entry a signal owns: its position among the *plottable* signals, the /// The palette entry a signal owns: its position among the *plottable* signals, the