feat: the op is picked when a node is added, not changed afterwards

\`add node\` made a gain and nothing else, so every other op was two steps. It
is a menu of the twelve now, each carrying its index through
\`math_op_index()\`'s space rather than a cast of the enum -- which is also
what \`math_dialog_sync()\` now uses to drive the op dropdown, since that
order is the dialog's and not the declaration's.
This commit is contained in:
Martino Ferrari committed 2026-10-07 02:39:26 +02:00
1 parent bafa391ad8
commit 5a69b2147b
3 files changed
+85 -9

No files matched your search

+74 -9
View File
@@ -3988,28 +3988,39 @@ gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifie
return FALSE;
}
void on_math_node_added(GtkButton*, gpointer user) {
MathDialog* d = static_cast<MathDialog*>(user);
/// Adds a node of `op` and selects it. The grid placement is the same one the auto-place uses, so a
/// node added here lands clear of the others -- 160 is narrower than `kNodeW` plus a wire's tread,
/// which is why the tidy layout uses a wider pitch; this is only a first guess at a free cell.
void math_add_node(MathDialog* d, MathOp op) {
// A node added to a graph that was *linear* -- its output was the last node -- becomes the output,
// which is what "add a gain and it applies" means. A graph whose output is an earlier node is a
// deliberate choice and is left alone.
const bool was_linear =
d->output == static_cast<int>(d->nodes.size()) - 1 || d->nodes.empty();
MathNode n;
n.op = MathOp::Gain;
n.op = op;
const std::size_t k = d->nodes.size();
// The same grid as the auto-place, so a node added here lands clear of the others.
n.x = 24.0 + 160.0 * static_cast<double>(k % 2u);
n.y = 20.0 + 58.0 * static_cast<double>(k / 2u);
const int arity = math_op_arity(n.op);
if (arity >= 1 && k > 0) n.in_a = static_cast<int>(k) - 1;
if (arity >= 2 && k > 0) n.in_b = static_cast<int>(k) - 1;
if (op == MathOp::Source) {
// Only an *acquired* channel: `signals().front()` is the stream's timestamp signal, which has
// no samples, so a graph sourced from it validates and then reads NaN for ever.
const std::vector<std::string> acquired = math_source_names(*d->app);
if (!acquired.empty()) n.source = acquired.front();
}
d->nodes.push_back(n);
d->selected = static_cast<int>(d->nodes.size()) - 1;
if (was_linear) d->output = d->selected;
math_dialog_sync(d);
}
void on_math_node_added(GtkButton*, gpointer user) {
math_add_node(static_cast<MathDialog*>(user), MathOp::Gain);
}
void on_math_node_deleted(GtkButton*, gpointer user) {
MathDialog* d = static_cast<MathDialog*>(user);
if (d->selected < 0 || d->selected >= static_cast<int>(d->nodes.size())) return;
@@ -4148,9 +4159,12 @@ void math_dialog_sync(MathDialog* d) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(d->source_drop), pick);
g_object_unref(list);
}
// The op dropdown follows the selection without firing its own handler back into the node.
if (d->op_drop != nullptr) gtk_drop_down_set_selected(GTK_DROP_DOWN(d->op_drop),
static_cast<guint>(op));
// Through `math_op_index()`, not a cast: the dropdown is built from `math_op_at()`, whose order
// is the dialog's rather than the enum's, and the cast worked only because they happen to agree.
if (d->op_drop != nullptr) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(d->op_drop),
static_cast<guint>(math_op_index(op)));
}
std::string inputs;
const auto describe = [&](int index) -> std::string {
if (index < 0) return "—";
@@ -4440,10 +4454,37 @@ void open_math_dialog(App& app, int edit) {
gtk_widget_set_visible(d->problem, FALSE);
gtk_box_append(GTK_BOX(form), d->problem);
GtkWidget* add = gtk_button_new_with_label("add node");
// A menu of the twelve ops rather than one button that always makes a gain: building anything
// else was add-then-change-the-dropdown, which is two steps for the common case.
GtkWidget* add = gtk_menu_button_new();
gtk_menu_button_set_label(GTK_MENU_BUTTON(add), "add node");
GtkWidget* add_menu = gtk_popover_new();
GtkWidget* add_list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
for (int i = 0; i < math_op_count(); ++i) {
const MathOp op = math_op_at(i);
GtkWidget* item = gtk_button_new_with_label(math_op_name(op));
gtk_widget_add_css_class(item, "flat");
gtk_widget_set_halign(gtk_button_get_child(GTK_BUTTON(item)), GTK_ALIGN_START);
// The op travels in the widget's own data, as an `int` through `math_op_index()`'s space: a
// pointer-sized enum smuggled through `gpointer` is the kind of cast that survives a
// reordering of the enum and then means something else.
g_object_set_data(G_OBJECT(item), "udpscope-math-op", GINT_TO_POINTER(i));
g_object_set_data(G_OBJECT(item), "udpscope-math-popover", add_menu);
g_signal_connect(item, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
MathDialog* dd = static_cast<MathDialog*>(user);
const int index =
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-math-op"));
auto* pop = GTK_POPOVER(
g_object_get_data(G_OBJECT(b), "udpscope-math-popover"));
if (pop != nullptr) gtk_popover_popdown(pop);
math_add_node(dd, math_op_at(index));
}), d);
gtk_box_append(GTK_BOX(add_list), item);
}
gtk_popover_set_child(GTK_POPOVER(add_menu), add_list);
gtk_menu_button_set_popover(GTK_MENU_BUTTON(add), add_menu);
GtkWidget* del = gtk_button_new_with_label("delete node");
GtkWidget* out = gtk_button_new_with_label("set as output");
g_signal_connect(add, "clicked", G_CALLBACK(on_math_node_added), d);
g_signal_connect(del, "clicked", G_CALLBACK(on_math_node_deleted), d);
g_signal_connect(out, "clicked", G_CALLBACK(on_math_output_set), d);
GtkWidget* fit = gtk_button_new_with_label("fit");
@@ -7573,6 +7614,30 @@ gboolean math_probe(gpointer user) {
v2->selected = was_sel;
}
}
if (a->math_dialog != nullptr) {
auto* o = static_cast<MathDialog*>(
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
if (o != nullptr) {
const std::size_t before = o->nodes.size();
math_add_node(o, MathOp::BandPass);
const int op_added = o->nodes.empty() ? -1 : math_op_index(o->nodes.back().op);
// The op picker must make the node the user asked for, and the `rows` the form shows must
// follow it: a band pass is the one op that shows the Q row, so it is the case worth the
// check.
const int q_row = gtk_widget_get_visible(o->rows[3]) ? 1 : 0;
std::fprintf(stderr,
"[math-probe] op pick: nodes %zu->%zu op_index=%d q_row=%d "
"(want %zu %d 1)\n",
before, o->nodes.size(), op_added, q_row, before + 1,
math_op_index(MathOp::BandPass));
// And the identity the whole picker rests on: a round trip through both accessors.
int round = 1;
for (int i = 0; i < math_op_count(); ++i) {
if (math_op_index(math_op_at(i)) != i) round = 0;
}
std::fprintf(stderr, "[math-probe] op pick: index round trip=%d (want 1)\n", round);
}
}
std::fflush(stderr);
return G_SOURCE_REMOVE;
}