From f224f56c4c92d9d5d468f022bfef83480ecfb7f0 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 11:44:40 +0200 Subject: [PATCH] fix: hold the check-button reference across the signal-panel rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- src/ui_signals.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index 01b9bc9..994b864 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -24,6 +24,15 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) { const std::vector& signals = app->model->signals(); 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); if (!on) { // 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). 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