ui: the trigger panel's frame is a .ui file

The panel's shape -- the heading, one labelled row per setting, the two buttons that
run it, and the search block -- now lives in src/ui/trigger-panel.ui. Every control
that carries a value stays in code and is appended into the file's row: a dropdown's
model and a spin's range are the application's, a label is text a translator owns, so
that is where the line runs. `row_for()` is `labeled_box()`'s arrangement with the
labels moved into the file; the thirteen old calls to `labeled_box()` here are gone and
its eleven uses elsewhere are untouched.

`sync_trigger_rows()` now guards the rows it shows and hides, because a row that comes
from the file can be missing rather than merely misnamed.

The frame is read back by a new dump line:

  trigger panel: boxes=13 controls_inside=13/13 search_widgets=4/4 search_inside=4/4

where `controls_inside` is what a drifted id breaks -- a control built and never placed
leaves the panel with a field missing and nothing else complaining -- and
`[defaults] trigger: 0.001 0.002 0 0.001 0 50 0.01` still reads the seven spins in the
same tree order, which is the check that the restructure kept the panel's contents.

Two days of grief came out of one GTK fact, now in the invariants: an expander's child
is not in the widget-parent chain (`gtk_widget_get_parent()` is NULL for it even after
the window is presented, while `gtk_expander_get_child()` returns it), so the probe
walk had to start there instead of at the panel. The throwaway program that settled it
is what stopped the workaround -- a second root in the file plus a code attach -- from
being the answer.
This commit is contained in:
Martino Ferrari committed 2026-10-12 15:47:50 +02:00
1 parent 07fcf61a70
commit 1ccc70c5ac
6 files changed
+482 -142

No files matched your search

+58
View File
@@ -5585,6 +5585,64 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
"toggle off=%d\n",
app.dbg_trigger_revealed_default, app.dbg_trigger_items,
app.dbg_trigger_selected, app.dbg_side_scroll_after_hide);
{
// The panel's *frame* read back: the row boxes the `.ui` holds, and that every control the code built
// ended up inside the panel rather than in a row that is not there. A control is created in code and
// placed by an id, so an id that drifted leaves it built and nowhere -- which looks like a panel
// missing a field, and is what `controls_inside` counts. `[defaults]` prints the same panel's spins
// in tree order, so the two together read what the file gave and what the code put in it.
const auto inside = [](GtkWidget* w, GtkWidget* root) {
if (w == nullptr) return 0;
for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) {
if (p == root) return 1;
}
return 0;
};
int boxes = 0;
for (GtkWidget* c = gtk_widget_get_first_child(app.trigger_panel); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
if (GTK_IS_BOX(c)) ++boxes;
}
// `search_widgets` separated from `search_inside` on purpose: the first says the ids resolved, the
// second says they are in the panel. A `.ui` that lost one and a `.ui` that lost its *place* are two
// different mistakes, and only one of them shows as a missing button.
// The expander's child is **not** in the widget-parent chain: `gtk_widget_get_parent()` on it stays
// NULL even after the window is presented, while `gtk_expander_get_child()` returns it (measured with
// a throwaway program, all three ways of setting it). So the walk the search widgets are read with has
// to start at the expander's child and not at the panel -- walking to the panel reads 0/4 on a panel
// that is entirely correct, which is what this line did first.
GtkWidget* search_root = app.trigger_panel;
for (GtkWidget* c = gtk_widget_get_first_child(app.trigger_panel); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
if (GTK_IS_EXPANDER(c)) {
if (GtkWidget* inner = gtk_expander_get_child(GTK_EXPANDER(c))) search_root = inner;
break;
}
}
const int search_present =
(app.search_run_button != nullptr) + (app.search_cancel_button != nullptr) +
(app.search_prev_button != nullptr) + (app.search_next_button != nullptr);
std::printf(
"[udpscope] trigger panel: boxes=%d controls_inside=%d/13 search_widgets=%d/4 "
"search_inside=%d/4\n",
boxes,
inside(app.trigger_signal_drop, app.trigger_panel) +
inside(app.trigger_type_drop, app.trigger_panel) +
inside(app.trigger_edge_drop, app.trigger_panel) +
inside(app.trigger_cmp_drop, app.trigger_panel) +
inside(app.trigger_mode_drop, app.trigger_panel) +
inside(app.trigger_width_spin, app.trigger_panel) +
inside(app.trigger_width2_spin, app.trigger_panel) +
inside(app.trigger_level2_spin, app.trigger_panel) +
inside(app.trigger_timeout_spin, app.trigger_panel) +
inside(app.trigger_level_spin, app.trigger_panel) +
inside(app.trigger_pre_spin, app.trigger_panel) +
inside(app.trigger_holdoff_spin, app.trigger_panel) +
inside(app.events_record_switch, app.trigger_panel),
search_present,
inside(app.search_run_button, search_root) + inside(app.search_cancel_button, search_root) +
inside(app.search_prev_button, search_root) + inside(app.search_next_button, search_root));
}
{
plot::Plot& p = *rows.row(0).plot;
const plot::PlotLayout& lay = p.layout();