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

+11
View File
@@ -298,3 +298,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
than no mnemonic, because the underscore is there in the source for a reader to trust. `Conn_ect` freed than no mnemonic, because the underscore is there in the source for a reader to trust. `Conn_ect` freed
it. Rows on different pages of a preferences dialog may share a letter: an unshown page cannot be it. Rows on different pages of a preferences dialog may share a letter: an unshown page cannot be
activated. activated.
- **A `GtkExpander`'s child is not in the widget-parent chain.** `gtk_widget_get_parent()` on the widget an
expander holds answers NULL, before *and* after the window is presented, while `gtk_expander_get_child()`
returns it -- measured all three ways (the `.ui`'s `child` property, a code `gtk_expander_set_child()`, a
code-made expander). So a probe that walks parents to prove a widget is inside a panel reads 0 for
everything behind an expander, on a panel that is entirely correct; the walk has to start at
`gtk_expander_get_child()`. That mistake cost a false regression, a "the property does not attach it"
conclusion drawn from it, and a pointless second root in the file before the throwaway program settled it.
Related: `gtk_expander_set_child()` asserts that its child has no parent yet, so a block the `.ui` has
already put in the panel cannot then be handed to an expander -- the file's `child` property is the right
way, and it works.
+18
View File
@@ -1161,3 +1161,21 @@ mnemonic. It is FALSE on `AdwPreferencesRow` by default, which is why these titl
literal underscore; anything below the full count is that bug back. Read by property name rather than literal underscore; anything below the full count is that bug back. Read by property name rather than
through one row type, for the reason in `invariants-gtk-and-chrome.md`. The form's line appears only when through one row type, for the reason in `invariants-gtk-and-chrome.md`. The form's line appears only when
`UDPSCOPE_PROBE_NEWHOST` drives the form. `UDPSCOPE_PROBE_NEWHOST` drives the form.
## `trigger panel:`
```
[udpscope] trigger panel: boxes=13 controls_inside=13/13 search_widgets=4/4 search_inside=4/4
```
The trigger panel's frame, read from the window: `boxes` is the row boxes the `.ui` holds, `controls_inside`
counts the thirteen controls the code built and then appended into those rows, `search_widgets` is the four
search buttons having resolved by id, and `search_inside` is those four being inside the block the expander
opens onto. A control built and never placed -- an id that drifted between the file and the code -- reads as
`controls_inside` below 13 while every other line still looks right, which is the panel that has a field
missing.
The search walk starts at `gtk_expander_get_child()`, not at the panel: an expander's child is not in the
widget-parent chain at all, so walking up from a search button reaches no ancestor of the panel (see
`invariants-gtk-and-chrome.md`). `[defaults] trigger:` prints the same panel's seven spins in tree order, so
the two lines together read what the file gave and what the code put in it.
+58
View File
@@ -5585,6 +5585,64 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
"toggle off=%d\n", "toggle off=%d\n",
app.dbg_trigger_revealed_default, app.dbg_trigger_items, app.dbg_trigger_revealed_default, app.dbg_trigger_items,
app.dbg_trigger_selected, app.dbg_side_scroll_after_hide); 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; plot::Plot& p = *rows.row(0).plot;
const plot::PlotLayout& lay = p.layout(); const plot::PlotLayout& lay = p.layout();
+276
View File
@@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The trigger panel's frame: the heading, one labelled row per setting, the two buttons that run it, and
the search block.
Every row here is a label and a place for a control, and the control itself is built in code. A
dropdown's model and a spin button's range are the *application's* (the condition list, the level
range that follows the channel's scale), while a label is text a translator owns, so the split runs
between them: the row is the file's and the control is appended into it. That is the arrangement
`labeled_box()` used to make in code, with the label moved where all the other labels are.
Between the controls, the labels and the search block are what "read the panel" means, so they are
here: which of these rows `sync_trigger_rows()` shows depends on the condition (a pulse width has a
width, a window has two levels, a dropout has a timeout), and that is the code's decision, not this
file's — this file just has to have a row for each of them to show or hide.
-->
<interface>
<object class="GtkBox" id="trigger_panel">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<child>
<object class="GtkLabel" id="trigger_heading">
<property name="label" translatable="yes">Trigger</property>
<property name="halign">start</property>
</object>
</child>
<child>
<object class="GtkBox" id="signal_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">signal</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="condition_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">condition</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="edge_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">edge</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="cmp_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">width is</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="width_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">width s</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="width2_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">and s</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="level2_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">to level</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="timeout_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">timeout s</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="level_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">level</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="pre_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">pre-trigger %</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="holdoff_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">hold-off s</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="mode_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">mode</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="record_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="record_label">
<property name="label" translatable="yes">record events</property>
<property name="halign">start</property>
<property name="hexpand">1</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="trigger_arm">
<property name="label" translatable="yes">Arm</property>
</object>
</child>
<child>
<object class="GtkButton" id="trigger_live">
<property name="label" translatable="yes">Go live (trigger off)</property>
<property name="tooltip-text" translatable="yes">disable trigger mode and stream continuously</property>
</object>
</child>
<child>
<object class="GtkExpander" id="search_exp">
<property name="label" translatable="yes">Search the buffer</property>
<property name="tooltip-text" translatable="yes">find every place in the retained buffer where the condition above holds, and step through them</property>
<property name="child">
<object class="GtkBox" id="search_sbox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<property name="margin-top">4</property>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="search_run">
<property name="label" translatable="yes">Search</property>
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkButton" id="search_cancel">
<property name="label" translatable="yes">Stop</property>
<property name="sensitive">0</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="search_status">
<property name="label" translatable="yes">not searched</property>
<property name="halign">start</property>
<property name="wrap">1</property>
<property name="css-classes">dim-label</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="search_prev">
<property name="icon-name">go-previous-symbolic</property>
<property name="tooltip-text" translatable="yes">the previous hit</property>
<property name="sensitive">0</property>
</object>
</child>
<child>
<object class="GtkLabel" id="search_pos">
<property name="label">--</property>
<property name="hexpand">1</property>
</object>
</child>
<child>
<object class="GtkButton" id="search_next">
<property name="icon-name">go-next-symbolic</property>
<property name="tooltip-text" translatable="yes">the next hit</property>
<property name="sensitive">0</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="search_marks">
<property name="label"> </property>
<property name="halign">start</property>
<property name="css-classes">dim-label</property>
<property name="ellipsize">3</property>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</interface>
+1
View File
@@ -17,5 +17,6 @@
<file preprocess="xml-stripblanks">main-window.ui</file> <file preprocess="xml-stripblanks">main-window.ui</file>
<file preprocess="xml-stripblanks">cursors-panel.ui</file> <file preprocess="xml-stripblanks">cursors-panel.ui</file>
<file preprocess="xml-stripblanks">measures-panel.ui</file> <file preprocess="xml-stripblanks">measures-panel.ui</file>
<file preprocess="xml-stripblanks">trigger-panel.ui</file>
</gresource> </gresource>
</gresources> </gresources>
+118 -142
View File
@@ -99,12 +99,17 @@ void sync_trigger_rows(App& app) {
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop))); width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
const bool pulse = type == TriggerType::PulseWidth; const bool pulse = type == TriggerType::PulseWidth;
const bool window = type == TriggerType::Window; const bool window = type == TriggerType::Window;
gtk_widget_set_visible(app.trigger_cmp_row, pulse); // The rows come from the panel's `.ui`, so a missing one is a mistake there rather than an impossibility
gtk_widget_set_visible(app.trigger_width_row, pulse); // here: `show()` names that by doing nothing, and `[udpscope] trigger panel:` is what proves the row
gtk_widget_set_visible(app.trigger_width2_row, // exists.
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside)); const auto show = [](GtkWidget* row, bool visible) {
gtk_widget_set_visible(app.trigger_level2_row, window); if (row != nullptr) gtk_widget_set_visible(row, visible);
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout); };
show(app.trigger_cmp_row, pulse);
show(app.trigger_width_row, pulse);
show(app.trigger_width2_row, pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
show(app.trigger_level2_row, window);
show(app.trigger_timeout_row, type == TriggerType::Dropout);
// A band of zero width reduces the condition to a float-equality test on a real-valued // A band of zero width reduces the condition to a float-equality test on a real-valued
// signal, which is unusable: it fires on the exact value and nothing else, which in practice // signal, which is unusable: it fires on the exact value and nothing else, which in practice
// is never. The panel is where that is refused: the second level is pushed at least one // is never. The panel is where that is refused: the second level is pushed at least one
@@ -197,47 +202,57 @@ void on_go_live(GtkButton*, gpointer user) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->trigger_toggle), FALSE); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->trigger_toggle), FALSE);
} }
GtkWidget* build_trigger_panel(App& app) { /// The row the `.ui` declares for a control, with the control appended to it.
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); ///
gtk_widget_set_margin_top(box, 8); /// The label is the file's, because it is text a translator owns; the control is code's, because a
gtk_widget_set_margin_bottom(box, 8); /// dropdown's model and a spin button's range are the application's. `labeled_box()` did both in code and
gtk_widget_set_margin_start(box, 8); /// this is what replaces it, row by row. `nullptr` comes back when the file has no such row: the control is
gtk_widget_set_margin_end(box, 8); /// then built and never placed, which is what `[udpscope] trigger panel:` reads.
GtkWidget* row_for(GtkBuilder* ui, const char* id, GtkWidget* child) {
GtkWidget* row = GTK_WIDGET(gtk_builder_get_object(ui, id));
if (row == nullptr) return nullptr;
gtk_widget_set_hexpand(child, true);
gtk_box_append(GTK_BOX(row), child);
return row;
}
GtkWidget* heading = gtk_label_new("Trigger"); GtkWidget* build_trigger_panel(App& app) {
gtk_widget_set_halign(heading, GTK_ALIGN_START); // The panel's frame -- the heading, one labelled row per setting, the two buttons and the search block --
gtk_box_append(GTK_BOX(box), heading); // is a UI file. What is not is every control that carries a value: a dropdown's model and a spin button's
// range are the application's, so they are built here and appended into the file's rows. `make test-ui`
// checks the file's types against the GIR; the ids below are the only thing that keeps the code and the
// file agreeing on the names.
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/trigger-panel.ui");
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_panel"));
if (box == nullptr) return gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr); app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr);
g_signal_connect(app.trigger_signal_drop, "notify::selected", g_signal_connect(app.trigger_signal_drop, "notify::selected",
G_CALLBACK(on_trigger_signal_changed), &app); G_CALLBACK(on_trigger_signal_changed), &app);
gtk_box_append(GTK_BOX(box), labeled_box("signal", app.trigger_signal_drop)); row_for(ui, "signal_row", app.trigger_signal_drop);
const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr}; const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
app.trigger_type_drop = gtk_drop_down_new_from_strings(types); app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
gtk_widget_set_tooltip_text(app.trigger_type_drop, gtk_widget_set_tooltip_text(app.trigger_type_drop,
"what counts as an event; the fields below change to suit it"); "what counts as an event; the fields below change to suit it");
gtk_box_append(GTK_BOX(box), labeled_box("condition", app.trigger_type_drop)); row_for(ui, "condition_row", app.trigger_type_drop);
const char* edges[] = {"rising", "falling", "both", nullptr}; const char* edges[] = {"rising", "falling", "both", nullptr};
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges); app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
gtk_box_append(GTK_BOX(box), labeled_box("edge", app.trigger_edge_drop)); row_for(ui, "edge_row", app.trigger_edge_drop);
const char* cmps[] = {"<", ">", "within", "outside", nullptr}; const char* cmps[] = {"<", ">", "within", "outside", nullptr};
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps); app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
gtk_widget_set_tooltip_text(app.trigger_cmp_drop, gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
"how the measured pulse width is compared: '<' is the glitch " "how the measured pulse width is compared: '<' is the glitch "
"trigger -- a pulse shorter than it should be"); "trigger -- a pulse shorter than it should be");
app.trigger_cmp_row = labeled_box("width is", app.trigger_cmp_drop); app.trigger_cmp_row = row_for(ui, "cmp_row", app.trigger_cmp_drop);
gtk_box_append(GTK_BOX(box), app.trigger_cmp_row);
app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6, 0.001); app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
app.trigger_width_row = labeled_box("width s", app.trigger_width_spin); app.trigger_width_row = row_for(ui, "width_row", app.trigger_width_spin);
gtk_box_append(GTK_BOX(box), app.trigger_width_row);
app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6, 0.002); app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6, 0.002);
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin); app.trigger_width2_row = row_for(ui, "width2_row", app.trigger_width2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
// Zero, not the range's minimum: the level this row reads is unset until a channel is chosen, // Zero, not the range's minimum: the level this row reads is unset until a channel is chosen,
// and the panel's sync writes the declared range's middle then -- but a dialog that opens at // and the panel's sync writes the declared range's middle then -- but a dialog that opens at
@@ -249,30 +264,28 @@ GtkWidget* build_trigger_panel(App& app) {
"condition becomes an exact float-equality test against the level, " "condition becomes an exact float-equality test against the level, "
"which an ordinary signal never satisfies and a signal sitting at " "which an ordinary signal never satisfies and a signal sitting at "
"the level satisfies on every transition"); "the level satisfies on every transition");
app.trigger_level2_row = labeled_box("to level", app.trigger_level2_spin); app.trigger_level2_row = row_for(ui, "level2_row", app.trigger_level2_spin);
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6, 0.001); app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
app.trigger_timeout_row = labeled_box("timeout s", app.trigger_timeout_spin); app.trigger_timeout_row = row_for(ui, "timeout_row", app.trigger_timeout_spin);
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0); app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0);
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) { g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
static_cast<App*>(u)->trigger_level_touched = true; static_cast<App*>(u)->trigger_level_touched = true;
}), }),
&app); &app);
gtk_box_append(GTK_BOX(box), labeled_box("level", app.trigger_level_spin)); row_for(ui, "level_row", app.trigger_level_spin);
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0); app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
gtk_box_append(GTK_BOX(box), labeled_box("pre-trigger %", app.trigger_pre_spin)); row_for(ui, "pre_row", app.trigger_pre_spin);
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4, app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
app.model->config().trigger_holdoff_s); app.model->config().trigger_holdoff_s);
gtk_box_append(GTK_BOX(box), labeled_box("hold-off s", app.trigger_holdoff_spin)); row_for(ui, "holdoff_row", app.trigger_holdoff_spin);
const char* modes[] = {"normal", "single", nullptr}; const char* modes[] = {"normal", "single", nullptr};
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes); app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
gtk_box_append(GTK_BOX(box), labeled_box("mode", app.trigger_mode_drop)); row_for(ui, "mode_row", app.trigger_mode_drop);
app.events_record_switch = gtk_switch_new(); app.events_record_switch = gtk_switch_new();
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER); gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
@@ -282,29 +295,22 @@ GtkWidget* build_trigger_panel(App& app) {
"picture of each; the history button goes with it"); "picture of each; the history button goes with it");
g_signal_connect(app.events_record_switch, "notify::active", g_signal_connect(app.events_record_switch, "notify::active",
G_CALLBACK(on_events_record_toggled), &app); G_CALLBACK(on_events_record_toggled), &app);
// Not `labeled_box()`: that expands its child, which is what a spin button wants and what a switch // Not `row_for()`: that expands its child, which is what a spin button wants and what a switch must not
// must not have -- a switch's natural width *is* its size, so an expanded one stretches into a wide // have -- a switch's natural width *is* its size, so an expanded one stretches into a wide pill that
// pill that reads as a slider. The label takes the slack instead. // reads as a slider. The label takes the slack instead, which is why the file's label carries the
{ // `hexpand` on this one row and no other.
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); if (GtkWidget* record_row = GTK_WIDGET(gtk_builder_get_object(ui, "record_row"))) {
GtkWidget* text = gtk_label_new("record events");
gtk_widget_set_halign(text, GTK_ALIGN_START);
gtk_widget_set_hexpand(text, TRUE);
gtk_box_append(GTK_BOX(row), text);
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END); gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER); gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), app.events_record_switch); gtk_box_append(GTK_BOX(record_row), app.events_record_switch);
gtk_box_append(GTK_BOX(box), row);
} }
app.arm_button = gtk_button_new_with_label("Arm"); app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app); if (app.arm_button != nullptr) {
gtk_box_append(GTK_BOX(box), app.arm_button); g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
}
GtkWidget* live = gtk_button_new_with_label("Go live (trigger off)"); GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_live"));
gtk_widget_set_tooltip_text(live, "disable trigger mode and stream continuously"); if (live != nullptr) g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
gtk_box_append(GTK_BOX(box), live);
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin, for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
app.trigger_width_spin, app.trigger_width2_spin, app.trigger_width_spin, app.trigger_width2_spin,
@@ -319,98 +325,68 @@ GtkWidget* build_trigger_panel(App& app) {
// Searching *is* the trigger's condition applied to history, so it belongs in this panel // Searching *is* the trigger's condition applied to history, so it belongs in this panel
// rather than in one of its own: a second place to spell the same condition would be a // rather than in one of its own: a second place to spell the same condition would be a
// second place to get it wrong. Collapsed by default -- it is not part of running a trigger. // second place to get it wrong. Collapsed by default -- it is not part of running a trigger.
GtkWidget* search_exp = gtk_expander_new("Search the buffer"); // Its widgets come from the same file, and only their signals are wired here.
gtk_widget_set_tooltip_text(search_exp, app.search_run_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_run"));
"find every place in the retained buffer where the condition " app.search_cancel_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_cancel"));
"above holds, and step through them"); app.search_status_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_status"));
GtkWidget* sbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); app.search_prev_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_prev"));
gtk_widget_set_margin_top(sbox, 4); app.search_pos_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_pos"));
app.search_next_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_next"));
GtkWidget* srun = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); app.search_marks_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_marks"));
app.search_run_button = gtk_button_new_with_label("Search"); if (app.search_run_button != nullptr) {
gtk_widget_set_hexpand(app.search_run_button, true); g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { App* a = static_cast<App*>(user);
App* a = static_cast<App*>(user); // Published, not read back off the widgets: the producer's copy is the
// Published, not read back off the widgets: the producer's copy is the // one the live trigger uses, so the search asks exactly its question.
// one the live trigger uses, so the search asks exactly its question. publish_trigger_fields(a);
publish_trigger_fields(a); EventCondition c;
EventCondition c; int channel = -1;
int channel = -1; {
{ std::lock_guard<std::mutex> lock(a->model->mutex());
std::lock_guard<std::mutex> lock(a->model->mutex()); c = a->model->trigger().condition;
c = a->model->trigger().condition; channel = a->model->trigger().channel;
channel = a->model->trigger().channel; }
} a->search.start(c, channel);
a->search.start(c, channel);
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(srun), app.search_run_button);
app.search_cancel_button = gtk_button_new_with_label("Stop");
gtk_widget_set_sensitive(app.search_cancel_button, false);
g_signal_connect(app.search_cancel_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
a->search.cancel();
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(srun), app.search_cancel_button);
gtk_box_append(GTK_BOX(sbox), srun);
app.search_status_label = gtk_label_new("not searched");
gtk_widget_set_halign(app.search_status_label, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(app.search_status_label), true);
gtk_widget_add_css_class(app.search_status_label, "dim-label");
gtk_box_append(GTK_BOX(sbox), app.search_status_label);
GtkWidget* snav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
app.search_prev_button = gtk_button_new_from_icon_name("go-previous-symbolic");
gtk_widget_set_tooltip_text(app.search_prev_button, "the previous hit");
gtk_widget_set_sensitive(app.search_prev_button, false);
g_signal_connect(app.search_prev_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
if (a->search.prev()) search_jump(*a, a->search.hits()[static_cast<std::size_t>(
a->search.cursor())]);
update_search_panel(*a);
}),
&app);
gtk_box_append(GTK_BOX(snav), app.search_prev_button);
app.search_pos_label = gtk_label_new("--");
gtk_widget_set_hexpand(app.search_pos_label, true);
gtk_box_append(GTK_BOX(snav), app.search_pos_label);
app.search_next_button = gtk_button_new_from_icon_name("go-next-symbolic");
gtk_widget_set_tooltip_text(app.search_next_button, "the next hit");
gtk_widget_set_sensitive(app.search_next_button, false);
g_signal_connect(app.search_next_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
// The first press with no cursor goes to the first hit, not the second.
if (a->search.cursor() < 0) {
a->search.select(0);
} else if (!a->search.next()) {
update_search_panel(*a); update_search_panel(*a);
return; }),
} &app);
search_jump(*a, a->search.hits()[static_cast<std::size_t>( }
a->search.cursor())]); if (app.search_cancel_button != nullptr) {
update_search_panel(*a); g_signal_connect(app.search_cancel_button, "clicked",
}), G_CALLBACK(+[](GtkButton*, gpointer user) {
&app); App* a = static_cast<App*>(user);
gtk_box_append(GTK_BOX(snav), app.search_next_button); a->search.cancel();
gtk_box_append(GTK_BOX(sbox), snav); update_search_panel(*a);
}),
app.search_marks_label = gtk_label_new(" "); &app);
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START); }
gtk_widget_add_css_class(app.search_marks_label, "dim-label"); if (app.search_prev_button != nullptr) {
// Ellipsize so the label's minimum width is bounded: without this, the longest text g_signal_connect(app.search_prev_button, "clicked",
// (~58 chars) would widen the side pane just as `search_status_label` would without its own G_CALLBACK(+[](GtkButton*, gpointer user) {
// wrap — the same fix the statistics label carries at line 2780. App* a = static_cast<App*>(user);
gtk_label_set_ellipsize(GTK_LABEL(app.search_marks_label), PANGO_ELLIPSIZE_END); if (a->search.prev()) {
gtk_box_append(GTK_BOX(sbox), app.search_marks_label); search_jump(*a, a->search.hits()[static_cast<std::size_t>(a->search.cursor())]);
}
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox); update_search_panel(*a);
gtk_box_append(GTK_BOX(box), search_exp); }),
&app);
}
if (app.search_next_button != nullptr) {
g_signal_connect(app.search_next_button, "clicked",
G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user);
// The first press with no cursor goes to the first hit, not the second.
if (a->search.cursor() < 0) {
a->search.select(0);
} else if (!a->search.next()) {
update_search_panel(*a);
return;
}
search_jump(*a, a->search.hits()[static_cast<std::size_t>(a->search.cursor())]);
update_search_panel(*a);
}),
&app);
}
g_signal_connect(app.trigger_type_drop, "notify::selected", g_signal_connect(app.trigger_type_drop, "notify::selected",
G_CALLBACK(on_trigger_shape_changed), &app); G_CALLBACK(on_trigger_shape_changed), &app);