fix: the math editor's source dropdown no longer hands a GParamSpec to a value handler

notify::selected passes a GParamSpec* ahead of the user data and
GtkSpinButton::value-changed does not, so on_math_param_changed read the
pspec as its MathDialog* and faulted on the first field it touched --
which is to say on opening the math editor and picking a source.

The probe missed it because filling the form sets the dropdown to the
value it already holds, and GObject emits no notify for that: a channel
sourcing channels().front() picks index 0 on a dropdown already at 0.
math_probe now opens on a channel sourcing the *last* acquired signal,
which does move the selection, and reads back the source the handler
wrote into the node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-08 16:56:25 +02:00
1 parent 219ae91fd8
commit cd9657663a
1 file changed
+41 -3
+41 -3
View File
@@ -4696,6 +4696,16 @@ void on_math_param_changed(GtkWidget*, gpointer user) {
gtk_widget_queue_draw(d->canvas); gtk_widget_queue_draw(d->canvas);
} }
/// The same read, for the source dropdown, and it needs its own entry point for the reason
/// `on_trigger_drop_changed()` gives: `notify::<prop>` passes a `GParamSpec*` ahead of the user
/// data and `GtkSpinButton::value-changed` does not, so wiring the two-parameter reader above to
/// `notify::selected` made it read the pspec as its `MathDialog*`. That is not a latent hazard --
/// it segfaulted on the first field it touched, on the first change of the source dropdown, which
/// is to say on opening the math editor and picking a source.
void on_math_source_changed(GObject*, GParamSpec*, gpointer user) {
on_math_param_changed(nullptr, user);
}
/// The op changed: the new op may take a different number of inputs, so the ones it cannot use are /// The op changed: the new op may take a different number of inputs, so the ones it cannot use are
/// cleared rather than left pointing at a node the graph would then evaluate from anyway. /// cleared rather than left pointing at a node the graph would then evaluate from anyway.
void on_math_op_changed(GObject* object, GParamSpec*, gpointer user) { void on_math_op_changed(GObject* object, GParamSpec*, gpointer user) {
@@ -5027,7 +5037,7 @@ void open_math_dialog(App& app, int edit) {
gtk_box_append(GTK_BOX(form), op_row); gtk_box_append(GTK_BOX(form), op_row);
d->source_drop = gtk_drop_down_new_from_strings(nullptr); d->source_drop = gtk_drop_down_new_from_strings(nullptr);
g_signal_connect(d->source_drop, "notify::selected", G_CALLBACK(on_math_param_changed), d); g_signal_connect(d->source_drop, "notify::selected", G_CALLBACK(on_math_source_changed), d);
gtk_box_append(GTK_BOX(form), math_row("channel", d->source_drop, &d->rows[0])); gtk_box_append(GTK_BOX(form), math_row("channel", d->source_drop, &d->rows[0]));
d->gain = gtk_spin_button_new_with_range(-1e6, 1e6, 0.1); d->gain = gtk_spin_button_new_with_range(-1e6, 1e6, 0.1);
@@ -8012,10 +8022,20 @@ gboolean math_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
std::string name; std::string name;
std::string late_name;
{ {
std::lock_guard<std::mutex> lock(a->model->mutex()); std::lock_guard<std::mutex> lock(a->model->mutex());
if (a->model->channels().empty()) return G_SOURCE_REMOVE; if (a->model->channels().empty()) return G_SOURCE_REMOVE;
name = a->model->channels().front().name; name = a->model->channels().front().name;
// The *last* acquired channel, and the probe needs it rather than merely liking it.
// Filling the editor's form calls `gtk_drop_down_set_selected()` on the source
// dropdown, and GObject does not emit `notify` when a property is set to the value it
// already holds -- so a channel sourcing `channels().front()` picks index 0 on a
// freshly built dropdown that is already at 0, the signal never fires, and the whole
// `notify::selected` path goes unexercised. That is exactly how a handler of the wrong
// shape on that signal survived every run of this probe while crashing the app on the
// first editor a user opened.
late_name = a->model->channels().back().name;
} }
std::vector<MathChannel> channels; std::vector<MathChannel> channels;
MathChannel gain; MathChannel gain;
@@ -8030,7 +8050,7 @@ gboolean math_probe(gpointer user) {
MathChannel lp; MathChannel lp;
lp.name = "math lp"; lp.name = "math lp";
lp.enabled = true; lp.enabled = true;
lp.nodes = {MathNode{MathOp::Source, name}, MathNode{MathOp::LowPass}}; lp.nodes = {MathNode{MathOp::Source, late_name}, MathNode{MathOp::LowPass}};
lp.nodes[1].in_a = 0; lp.nodes[1].in_a = 0;
lp.nodes[1].fc = 20.0; lp.nodes[1].fc = 20.0;
lp.output = 1; lp.output = 1;
@@ -8040,7 +8060,8 @@ gboolean math_probe(gpointer user) {
// directly left the two disagreeing, and the next pane edit then silently dropped its channels. // directly left the two disagreeing, and the next pane edit then silently dropped its channels.
a->math = std::move(channels); a->math = std::move(channels);
apply_math_channels(*a); apply_math_channels(*a);
std::fprintf(stderr, "[math-probe] 2 math channels on '%s'\n", name.c_str()); std::fprintf(stderr, "[math-probe] 2 math channels on '%s' and '%s'\n", name.c_str(),
late_name.c_str());
// And open the editor on the first one: as much a construction crash test as a state check, // And open the editor on the first one: as much a construction crash test as a state check,
// since a modal window is presented and its canvas, form and gestures are all built here. // since a modal window is presented and its canvas, form and gestures are all built here.
// The pane's own `+`, which is the path the user takes: it must source an *acquired* channel. // The pane's own `+`, which is the path the user takes: it must source an *acquired* channel.
@@ -8055,6 +8076,23 @@ gboolean math_probe(gpointer user) {
} }
open_math_dialog(*a, 0); open_math_dialog(*a, 0);
std::fprintf(stderr, "[math-probe] editor opened\n"); std::fprintf(stderr, "[math-probe] editor opened\n");
// Opened on the channel sourcing the *last* acquired signal, which is the one open that moves
// the source dropdown off index 0 and so emits `notify::selected` -- see `late_name` above for
// why nothing else here does. The reading is the source the handler wrote back into the node:
// it must be `late_name` and not the dropdown's first entry, because a handler that reads the
// wrong argument writes through a `GParamSpec*` and the Gtk-CRITICAL baseline is what catches
// it when the dereference happens not to fault.
{
if (a->math_dialog != nullptr) gtk_window_close(GTK_WINDOW(a->math_dialog));
open_math_dialog(*a, 1);
auto* ed = static_cast<MathDialog*>(
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
const char* read = "NO DIALOG";
if (ed != nullptr && !ed->nodes.empty()) read = ed->nodes[0].source.c_str();
std::fprintf(stderr, "[math-probe] source drop: node reads '%s' (want '%s')\n", read,
late_name.c_str());
if (a->math_dialog != nullptr) gtk_window_close(GTK_WINDOW(a->math_dialog));
}
// The user's own flow, through the app's handlers: the pane's `+` makes a bare source graph, the // The user's own flow, through the app's handlers: the pane's `+` makes a bare source graph, the
// editor adds a gain, the gain is set, Apply saves. The *output* must be the node that was just // editor adds a gain, the gain is set, Apply saves. The *output* must be the node that was just
// added -- it defaulted to node 0, the source, so every operation was computed and discarded. // added -- it defaulted to node 0, the source, so every operation was computed and discarded.