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:
1 parent
bafa391ad8
commit
5a69b2147b
3 files changed
+85
-9
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 --
|
||||
|
||||
+74
-9
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user