From f9cea4885bfbd9a4bed8026c1b4d20d73c748c61 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 12:59:21 +0200 Subject: [PATCH] fix: an add or a delete outlives the list it rebuilds under itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `+` lives *inside* the signal list now, and both handlers it reaches rebuild that list: `add_math_channel()` and `apply_math_channels()` → `rebuild_signal_panels()` destroys every row, the divider's button and the row's menu popover with it. GLib holds a reference for the duration of one emission, but a button's own code keeps running *after* the handler returns -- the same last-reference hazard `on_signal_switch()` already repairs, and the same repair is used here: hold one reference on the emitting widget and drop it from an idle. Found by driving the three new controls the way a user reaches them, which the plan left to a hand check: [side-list] divider + found=1 [side-list] add: math 0 -> 1 editor_open=1 [side-list] math row tick found=1 name='math1' [side-list] tick off: App::math enabled=0 (want 0) [side-list] math row Delete found=1 [side-list] delete: math 0 (want 0) So the `+` creates a channel *and* opens the editor on it, a math row carries its own tick, that tick reaches `App::math` -- the copy the session file is written from -- and Delete removes the channel, with the popover dismissed first so nothing points at the row that is going away. The probe that measured this was a throwaway and is not in the tree. --- src/ui_signals.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index 32f9e05..86e10d5 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -189,10 +189,22 @@ GtkWidget* make_math_divider(App& app) { return row; } +/// A handler that rebuilds the signal list destroys the widget it was emitted from -- the row it +/// lives in, and that row's menu popover with it. GLib holds a reference for the duration of *one* +/// emission, and a button's own code keeps running after the handler returns, so the last reference +/// has to outlive the emission: hold one here and drop it from an idle. This is the same repair +/// `on_signal_switch()` makes, for the same reason. +static void outlive_the_rebuild(GtkWidget* widget) { + g_object_ref(widget); + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, + +[](gpointer) -> gboolean { return G_SOURCE_REMOVE; }, widget, g_object_unref); +} + /// Create a channel, then open the editor on it: a new channel is a single `Source` node with a /// default name, which is of no use until it is given a graph. -void on_math_add_clicked(GtkButton*, gpointer user) { +void on_math_add_clicked(GtkButton* button, gpointer user) { App* a = static_cast(user); + outlive_the_rebuild(GTK_WIDGET(button)); add_math_channel(*a); if (a->math.empty()) return; // `add_math_channel()` appended, so the new channel is the last one; `apply_math_channels()` @@ -233,6 +245,7 @@ void on_math_delete_clicked(GtkButton* button, gpointer user) { const int k = math_index_for_signal(*a, signal_name_at(*a, index)); if (k < 0) return; dismiss_row_menu(button); + outlive_the_rebuild(GTK_WIDGET(button)); a->math.erase(a->math.begin() + k); // Rebuilds the model's signal list, the plots and this list, and schedules the save. apply_math_channels(*a);