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:
1 parent
07fcf61a70
commit
1ccc70c5ac
6 files changed
+482
-142
No files matched your search
+118
-142
@@ -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)));
|
||||
const bool pulse = type == TriggerType::PulseWidth;
|
||||
const bool window = type == TriggerType::Window;
|
||||
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width2_row,
|
||||
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
||||
gtk_widget_set_visible(app.trigger_level2_row, window);
|
||||
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout);
|
||||
// The rows come from the panel's `.ui`, so a missing one is a mistake there rather than an impossibility
|
||||
// here: `show()` names that by doing nothing, and `[udpscope] trigger panel:` is what proves the row
|
||||
// exists.
|
||||
const auto show = [](GtkWidget* row, bool visible) {
|
||||
if (row != nullptr) gtk_widget_set_visible(row, visible);
|
||||
};
|
||||
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
|
||||
// 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
|
||||
@@ -197,47 +202,57 @@ void on_go_live(GtkButton*, gpointer user) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->trigger_toggle), FALSE);
|
||||
}
|
||||
|
||||
GtkWidget* build_trigger_panel(App& app) {
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_margin_top(box, 8);
|
||||
gtk_widget_set_margin_bottom(box, 8);
|
||||
gtk_widget_set_margin_start(box, 8);
|
||||
gtk_widget_set_margin_end(box, 8);
|
||||
/// The row the `.ui` declares for a control, with the control appended to it.
|
||||
///
|
||||
/// The label is the file's, because it is text a translator owns; the control is code's, because a
|
||||
/// dropdown's model and a spin button's range are the application's. `labeled_box()` did both in code and
|
||||
/// this is what replaces it, row by row. `nullptr` comes back when the file has no such row: the control is
|
||||
/// 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");
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(box), heading);
|
||||
GtkWidget* build_trigger_panel(App& app) {
|
||||
// The panel's frame -- the heading, one labelled row per setting, the two buttons and the search block --
|
||||
// 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);
|
||||
g_signal_connect(app.trigger_signal_drop, "notify::selected",
|
||||
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};
|
||||
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
||||
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
||||
"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};
|
||||
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};
|
||||
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
|
||||
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
|
||||
"how the measured pulse width is compared: '<' is the glitch "
|
||||
"trigger -- a pulse shorter than it should be");
|
||||
app.trigger_cmp_row = labeled_box("width is", app.trigger_cmp_drop);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_cmp_row);
|
||||
app.trigger_cmp_row = row_for(ui, "cmp_row", app.trigger_cmp_drop);
|
||||
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width_row);
|
||||
app.trigger_width_row = row_for(ui, "width_row", app.trigger_width_spin);
|
||||
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
|
||||
app.trigger_width2_row = row_for(ui, "width2_row", app.trigger_width2_spin);
|
||||
|
||||
// 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
|
||||
@@ -249,30 +264,28 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
"condition becomes an exact float-equality test against the level, "
|
||||
"which an ordinary signal never satisfies and a signal sitting at "
|
||||
"the level satisfies on every transition");
|
||||
app.trigger_level2_row = labeled_box("to level", app.trigger_level2_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
|
||||
app.trigger_level2_row = row_for(ui, "level2_row", app.trigger_level2_spin);
|
||||
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
|
||||
app.trigger_timeout_row = row_for(ui, "timeout_row", app.trigger_timeout_spin);
|
||||
|
||||
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) {
|
||||
static_cast<App*>(u)->trigger_level_touched = true;
|
||||
}),
|
||||
&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);
|
||||
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.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};
|
||||
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();
|
||||
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");
|
||||
g_signal_connect(app.events_record_switch, "notify::active",
|
||||
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
|
||||
// must not have -- a switch's natural width *is* its size, so an expanded one stretches into a wide
|
||||
// pill that reads as a slider. The label takes the slack instead.
|
||||
{
|
||||
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
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);
|
||||
// Not `row_for()`: that expands its child, which is what a spin button wants and what a switch must not
|
||||
// have -- a switch's natural width *is* its size, so an expanded one stretches into a wide pill that
|
||||
// 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.
|
||||
if (GtkWidget* record_row = GTK_WIDGET(gtk_builder_get_object(ui, "record_row"))) {
|
||||
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
|
||||
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(box), row);
|
||||
gtk_box_append(GTK_BOX(record_row), app.events_record_switch);
|
||||
}
|
||||
|
||||
app.arm_button = gtk_button_new_with_label("Arm");
|
||||
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(box), app.arm_button);
|
||||
|
||||
GtkWidget* live = gtk_button_new_with_label("Go live (trigger off)");
|
||||
gtk_widget_set_tooltip_text(live, "disable trigger mode and stream continuously");
|
||||
g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
|
||||
gtk_box_append(GTK_BOX(box), live);
|
||||
app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
|
||||
if (app.arm_button != nullptr) {
|
||||
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
|
||||
}
|
||||
GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_live"));
|
||||
if (live != nullptr) g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
|
||||
|
||||
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_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
|
||||
// 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.
|
||||
GtkWidget* search_exp = gtk_expander_new("Search the buffer");
|
||||
gtk_widget_set_tooltip_text(search_exp,
|
||||
"find every place in the retained buffer where the condition "
|
||||
"above holds, and step through them");
|
||||
GtkWidget* sbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_margin_top(sbox, 4);
|
||||
|
||||
GtkWidget* srun = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.search_run_button = gtk_button_new_with_label("Search");
|
||||
gtk_widget_set_hexpand(app.search_run_button, true);
|
||||
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// 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.
|
||||
publish_trigger_fields(a);
|
||||
EventCondition c;
|
||||
int channel = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
c = a->model->trigger().condition;
|
||||
channel = a->model->trigger().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()) {
|
||||
// Its widgets come from the same file, and only their signals are wired here.
|
||||
app.search_run_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_run"));
|
||||
app.search_cancel_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_cancel"));
|
||||
app.search_status_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_status"));
|
||||
app.search_prev_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_prev"));
|
||||
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"));
|
||||
app.search_marks_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_marks"));
|
||||
if (app.search_run_button != nullptr) {
|
||||
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// 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.
|
||||
publish_trigger_fields(a);
|
||||
EventCondition c;
|
||||
int channel = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
c = a->model->trigger().condition;
|
||||
channel = a->model->trigger().channel;
|
||||
}
|
||||
a->search.start(c, channel);
|
||||
update_search_panel(*a);
|
||||
return;
|
||||
}
|
||||
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_next_button);
|
||||
gtk_box_append(GTK_BOX(sbox), snav);
|
||||
|
||||
app.search_marks_label = gtk_label_new(" ");
|
||||
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(app.search_marks_label, "dim-label");
|
||||
// Ellipsize so the label's minimum width is bounded: without this, the longest text
|
||||
// (~58 chars) would widen the side pane just as `search_status_label` would without its own
|
||||
// wrap — the same fix the statistics label carries at line 2780.
|
||||
gtk_label_set_ellipsize(GTK_LABEL(app.search_marks_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(sbox), app.search_marks_label);
|
||||
|
||||
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
|
||||
gtk_box_append(GTK_BOX(box), search_exp);
|
||||
}),
|
||||
&app);
|
||||
}
|
||||
if (app.search_cancel_button != nullptr) {
|
||||
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);
|
||||
}
|
||||
if (app.search_prev_button != nullptr) {
|
||||
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);
|
||||
}
|
||||
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_CALLBACK(on_trigger_shape_changed), &app);
|
||||
|
||||
Reference in new issue
Block a user