fix: an add or a delete outlives the list it rebuilds under itself

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.
This commit is contained in:
Martino Ferrari committed 2026-10-14 12:59:21 +02:00
1 parent 6492bb910f
commit f9cea4885b
1 file changed
+14 -1
+14 -1
View File
@@ -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<App*>(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);