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.
This commit is contained in:
Martino Ferrari committed 2026-10-12 15:57:43 +02:00
1 parent 90a17eb186
commit 1a02946656
7 files changed
+123 -36

No files matched your search

+34 -32
View File
@@ -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<MathDialog*>(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<int>(app.math.size())) {
gtk_editable_set_text(GTK_EDITABLE(d->name), app.math[static_cast<std::size_t>(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<int>(app.math.size())) {
gtk_editable_set_text(GTK_EDITABLE(d->unit), app.math[static_cast<std::size_t>(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");