From 1a029466569435aa0c1e625c68316f843fd34900 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 12 Oct 2026 15:57:43 +0200 Subject: [PATCH] ui: the math editor's window, identity row and canvas are a .ui file The second half of this plan row, and the half of that dialog which is template. src/ui/math-editor.ui holds the window (its size and modality), the box around it, the identity row -- the name and the unit -- and the body with the canvas in it. What stays in code is the drawing: the canvas is a GtkDrawingArea the file *declares* and the code draws, with the gestures that make it a graph editor, and the parameter form follows which node is selected. The palette and the buttons go next. The builder is held on `App` and cleared on the *next* opening rather than with the window: the window belongs to the builder's object list, so freeing the builder under a live window frees the window with it. That is the source form's pattern, and the probe's close-then-reopen path is what exercises it -- `reopened: dialog=ok, nodes=2`. The identity row is read back too, since a `.ui` can hold the widget but not the value: `dialog build: ... name='math3' unit='' ...`, the entries having been filled from the channel being edited. check_ui is at 12 ok, 0 failed. --- docs/claude/invariants-math-channels.md | 5 +- .../2026-10-11-ui-files-and-gtk4-practice.md | 5 ++ src/ui.cpp | 66 ++++++++++--------- src/ui/math-editor.ui | 65 ++++++++++++++++++ src/ui/udpscope-ui.gresource.xml | 1 + src/ui_app.hpp | 4 ++ src/ui_probe.cpp | 13 +++- 7 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 src/ui/math-editor.ui diff --git a/docs/claude/invariants-math-channels.md b/docs/claude/invariants-math-channels.md index 5acc470..24d61c3 100644 --- a/docs/claude/invariants-math-channels.md +++ b/docs/claude/invariants-math-channels.md @@ -39,7 +39,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear `math cost: S slots, N ns/slot (T timed), X ms total est` is the per-slot cost; see the math-cost invariant below for why it is sampled, why it is an upper bound, and why a figure anywhere in 78--101 ns/slot is one reading of the same thing. - The probe also asserts the new editor mechanics. Each line carries `(want ...)` and the reading + The probe also asserts the new editor mechanics. `dialog build:` reads the identity row back as well -- +`name='math3' unit=''` -- which is the pair the `.ui` holds the widgets for and the code fills from the +channel being edited, so a row that stopped being the one the code writes shows there. Each line carries +`(want ...)` and the reading taken from a real run is the reference: * `[math-probe] hit: inside node=0 which=-1, past-edge node=0 which=-1 (want 0 -1 0 -1)` — the output square straddles the box edge, so a hit test that recovers the node by a box test misses diff --git a/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md b/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md index 3cf828e..bf4b5e9 100644 --- a/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md +++ b/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md @@ -74,6 +74,11 @@ needs a probe that proves it *loads* and that its contents match whatever drives | `src/ui/ui_layout.cpp` (~20 kB) | the layout editor's overlay buttons, which are positioned by code | only the buttons are template | | `build_toolbar` (`src/ui.cpp`, ~212 lines) | the header bar's layout, with every button still wired in code | delicate: the `[udpscope] header:` probe reads its geometry | +**Done since, one row per commit:** the source chooser's window (`0a64e9e`), the measurements panel +(`d344361`, `07fcf61`), the trigger panel (`1ccc70c`), the signals panel (`c37fbdd`), the math panel +(`90a17eb`) and the math editor's window, identity row and canvas. What is left of that row is the palette +and the buttons around the canvas, which stay in code for the reason the canvas's *drawing* does. + **Done since:** the sidebar is an `AdwOverlaySplitView` (`src/ui/main-window.ui`) rather than a paned's end child, so its width is its own 320 px and it overlays the plot instead of collapsing to zero — see `[sidebar-probe]`. diff --git a/src/ui.cpp b/src/ui.cpp index eb525da..6684c9e 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -1943,11 +1943,29 @@ void open_math_dialog(App& app, int edit) { } d->selected = d->output; - GtkWidget* window = gtk_window_new(); - d->window = window; + // The window and its identity row are a `.ui` file (`src/ui/math-editor.ui`). The canvas, the palette and + // the parameter form below them stay in code: the canvas is drawn rather than declared, and the form + // follows which node is selected. + g_clear_object(&app.math_dialog_ui); + app.math_dialog_ui = gtk_builder_new_from_resource("/org/udpscope/ui/math-editor.ui"); + GtkBuilder* ui = app.math_dialog_ui; + d->window = GTK_WIDGET(gtk_builder_get_object(ui, "math_window")); + GtkWidget* outer = GTK_WIDGET(gtk_builder_get_object(ui, "math_outer")); + GtkWidget* head = GTK_WIDGET(gtk_builder_get_object(ui, "math_head")); + d->name = GTK_WIDGET(gtk_builder_get_object(ui, "math_name")); + d->unit = GTK_WIDGET(gtk_builder_get_object(ui, "math_unit")); + if (d->window == nullptr || outer == nullptr || head == nullptr || d->name == nullptr || + d->unit == nullptr) { + // A missing id is a mistake in the `.ui`. There is no editor to open, so this says so and gives up + // rather than building the rest of a window around a hole; `[math-probe] editor` reads the difference. + std::fprintf(stderr, "[math] editor: math-editor.ui is missing an id this code reads\n"); + delete d; + return; + } + GtkWidget* window = d->window; gtk_window_set_title(GTK_WINDOW(window), edit >= 0 ? "Edit math channel" : "New math channel"); - gtk_window_set_default_size(GTK_WINDOW(window), 720, 420); - gtk_window_set_modal(GTK_WINDOW(window), true); + // The dialog's own state, freed with the window: a plain local here would leak the graph and every drag + // it is halfway through on each opening. g_object_set_data_full(G_OBJECT(window), "udpscope-math-dialog", d, +[](gpointer p) { delete static_cast(p); }); g_signal_connect(window, "close-request", G_CALLBACK(+[](GtkWindow* w, gpointer) { @@ -1959,35 +1977,22 @@ void open_math_dialog(App& app, int edit) { }), nullptr); - GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); - gtk_widget_set_margin_top(outer, 10); - gtk_widget_set_margin_bottom(outer, 10); - gtk_widget_set_margin_start(outer, 10); - gtk_widget_set_margin_end(outer, 10); - gtk_window_set_child(GTK_WINDOW(window), outer); - - // The channel's own identity: the name is also how another graph names it as a source. - GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - d->name = gtk_entry_new(); - gtk_entry_set_placeholder_text(GTK_ENTRY(d->name), "name"); + // The two values the file cannot hold: what this channel is called and the unit its readouts carry. The + // name is also how another graph refers to this channel as a source, which is why it takes the slack. if (edit >= 0 && edit < static_cast(app.math.size())) { gtk_editable_set_text(GTK_EDITABLE(d->name), app.math[static_cast(edit)].name.c_str()); - } - d->unit = gtk_entry_new(); - gtk_entry_set_placeholder_text(GTK_ENTRY(d->unit), "unit (optional)"); - if (edit >= 0 && edit < static_cast(app.math.size())) { gtk_editable_set_text(GTK_EDITABLE(d->unit), app.math[static_cast(edit)].unit.c_str()); } - gtk_widget_set_hexpand(d->name, true); - gtk_box_append(GTK_BOX(head), d->name); - gtk_box_append(GTK_BOX(head), d->unit); - gtk_box_append(GTK_BOX(outer), head); - GtkWidget* body = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10); - d->canvas = gtk_drawing_area_new(); - gtk_widget_set_size_request(d->canvas, 420, 300); - gtk_widget_set_hexpand(d->canvas, true); - gtk_widget_set_vexpand(d->canvas, true); + // The body and the canvas are the file's as well; what is code is the *drawing* -- the function below and + // the gestures under it, which are what make the area a graph editor rather than a picture. + GtkWidget* body = GTK_WIDGET(gtk_builder_get_object(ui, "math_body")); + d->canvas = GTK_WIDGET(gtk_builder_get_object(ui, "math_canvas")); + if (body == nullptr || d->canvas == nullptr) { + std::fprintf(stderr, "[math] editor: math-editor.ui is missing its body or canvas\n"); + delete d; + return; + } gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d->canvas), math_canvas_draw, d, nullptr); GtkGesture* click = gtk_gesture_click_new(); g_signal_connect(click, "pressed", G_CALLBACK(+[](GtkGestureClick* g, int, double x, double y, @@ -2028,8 +2033,6 @@ void open_math_dialog(App& app, int edit) { g_signal_connect(keys, "key-pressed", G_CALLBACK(math_canvas_key), d); gtk_widget_add_controller(d->canvas, keys); - gtk_box_append(GTK_BOX(body), d->canvas); - // The form: every row exists from the start and is shown or hidden per op, so no rebuild can // invalidate a callback's handle. GtkWidget* form = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); @@ -2181,8 +2184,7 @@ void open_math_dialog(App& app, int edit) { gtk_box_append(GTK_BOX(buttons), fit); gtk_box_append(GTK_BOX(buttons), tidy); gtk_box_append(GTK_BOX(form), buttons); - gtk_box_append(GTK_BOX(body), form); - gtk_box_append(GTK_BOX(outer), body); + gtk_box_append(GTK_BOX(body), form); // the form is the body's second child; the canvas is the file's GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); GtkWidget* cancel = gtk_button_new_with_label("Cancel"); diff --git a/src/ui/math-editor.ui b/src/ui/math-editor.ui new file mode 100644 index 0000000..32984f3 --- /dev/null +++ b/src/ui/math-editor.ui @@ -0,0 +1,65 @@ + + + + + + + 720 + 420 + 1 + + + vertical + 8 + 10 + 10 + 10 + 10 + + + horizontal + 8 + + + name + 1 + + + + + unit (optional) + + + + + + + + + horizontal + 10 + + + 420 + 300 + 1 + 1 + + + + + + diff --git a/src/ui/udpscope-ui.gresource.xml b/src/ui/udpscope-ui.gresource.xml index b55de40..2f4c05c 100644 --- a/src/ui/udpscope-ui.gresource.xml +++ b/src/ui/udpscope-ui.gresource.xml @@ -18,6 +18,7 @@ cursors-panel.ui measures-panel.ui math-panel.ui + math-editor.ui trigger-panel.ui signals-panel.ui diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 75f97c2..09368eb 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -246,6 +246,10 @@ struct App { GtkWidget* math_toggle = nullptr; GtkWidget* math_revealer = nullptr; GtkWidget* math_list = nullptr; + /// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is + /// held for as long as the window is up and cleared on the *next* opening rather than with the window -- + /// freeing it under a live window frees the window with it. That is the source form's pattern. + GtkBuilder* math_dialog_ui = nullptr; GtkWidget* math_dialog = nullptr; /// One value label per measure, in `measures` order; rebuilt with the list. std::vector measure_labels; diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 095053e..4a52180 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -389,6 +389,12 @@ gboolean math_probe(gpointer user) { const std::size_t built = ed->nodes.size(); const double gain_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().gain; const double offset_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().offset; + // The identity row, which the file cannot hold values for: the entries were filled from the channel + // being edited, so reading them back is what shows the row is the same one the code writes. + const std::string name_read = + ed->name != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->name)) : "-"; + const std::string unit_read = + ed->unit != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->unit)) : "-"; on_math_apply(nullptr, ed); // frees the dialog: nothing below may touch `ed` const std::size_t saved = a->math.empty() ? 0u @@ -397,9 +403,10 @@ gboolean math_probe(gpointer user) { a->math.empty() ? -1 : a->math[static_cast(index)].output; std::fprintf(stderr, "[math-probe] dialog build: nodes=%zu, output after add=%d (want %zu); " - "reading back gain=%.2f offset=%.2f; saved nodes=%zu output=%d\n", - built, output_after_add, built - 1, gain_read, offset_read, saved, - output_saved); + "reading back gain=%.2f offset=%.2f; name='%s' unit='%s'; " + "saved nodes=%zu output=%d\n", + built, output_after_add, built - 1, gain_read, offset_read, name_read.c_str(), + unit_read.c_str(), saved, output_saved); } } // Closed and reopened: `gtk_window_destroy()` does not emit `close-request`, so a dialog closed