diff --git a/src/math_channel.cpp b/src/math_channel.cpp index f96d586..ab751ff 100644 --- a/src/math_channel.cpp +++ b/src/math_channel.cpp @@ -106,6 +106,13 @@ MathOp math_op_at(int index) { } } +int math_op_index(MathOp op) { + for (int i = 0; i < math_op_count(); ++i) { + if (math_op_at(i) == op) return i; + } + return 0; +} + int math_op_arity(MathOp op) { switch (op) { case MathOp::Source: return 0; diff --git a/src/math_channel.hpp b/src/math_channel.hpp index 70ad52d..2f97e3e 100644 --- a/src/math_channel.hpp +++ b/src/math_channel.hpp @@ -30,6 +30,10 @@ enum class MathOp { const char* math_op_name(MathOp op); int math_op_count(); MathOp math_op_at(int index); +/// An op's position in `math_op_at()`'s order. That order is the *dialog's* -- what a dropdown or a +/// menu lists, and what a session file stores -- and it is deliberately not promised to be the +/// enum's declaration order, so a caller that needs a position must ask rather than cast. +int math_op_index(MathOp op); /// Inputs the op takes: 0 for `Source`, 1 for the unary ones, 2 for the arithmetic. int math_op_arity(MathOp op); /// Which of the per-node parameters an op actually reads, so a dialog can show only those rows -- diff --git a/src/ui.cpp b/src/ui.cpp index 0adec1d..14fa795 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(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(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(k % 2u); n.y = 20.0 + 58.0 * static_cast(k / 2u); const int arity = math_op_arity(n.op); if (arity >= 1 && k > 0) n.in_a = static_cast(k) - 1; if (arity >= 2 && k > 0) n.in_b = static_cast(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 acquired = math_source_names(*d->app); + if (!acquired.empty()) n.source = acquired.front(); + } d->nodes.push_back(n); d->selected = static_cast(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(user), MathOp::Gain); +} + void on_math_node_deleted(GtkButton*, gpointer user) { MathDialog* d = static_cast(user); if (d->selected < 0 || d->selected >= static_cast(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(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(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(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( + 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; }