ui: the header bar's frame is a .ui file, closing item 10's last row

src/ui/header-bar.ui holds the bar: the time-window control's slot, the trigger, pause and
events group with the separator that marks where the view's controls end, and the five at
the far end. What stays in code is everything that acts on the model -- the overflow
menu's rows, the window control itself, and every handler.

The order is the file's, which is the part the code used to explain in three comments: a
<child type="end"> is added leftwards-from-the-right in document order, so the screen reads
the reverse of it -- cursors, measurements, math, sidebar, menu.

The first start child is a *slot* and not the control, and that is the one thing this move
could not do naively: gtk_header_bar_pack_start() appends after whatever a file has already
packed, so the control -- which is code, its menu's rows coming from the buffer's limits --
would have landed to the right of the whole group instead of at its head.

The probe is the check, and every header reading is byte-identical to before the move: 12
lines of `header:` (window_button_w=130 bar_w=756), `window control:`, `panels:`, both
width-probe frames and all seven layout-probe lines. Only `badge frames:` differs, and it
is a frame counter. check_ui: 14 ok, 0 failed.
This commit is contained in:
Martino Ferrari committed 2026-10-12 16:33:25 +02:00
1 parent c94bc4316c
commit 28d80f2e90
3 files changed
+139 -98

No files matched your search

+51 -98
View File
@@ -4186,12 +4186,21 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi
/// ///
/// [ 2 s ▾ ] [plots ▾] ............... [pause] [trigger] [panel] [menu] /// [ 2 s ▾ ] [plots ▾] ............... [pause] [trigger] [panel] [menu]
GtkWidget* build_toolbar(App& app) { GtkWidget* build_toolbar(App& app) {
GtkWidget* bar = gtk_header_bar_new(); // The bar's frame is a `.ui` file (`src/ui/header-bar.ui`): the bar, the slot the window control goes
// in, the trigger/pause/events group with its separator, and the five controls at the far end. Its
// *order* is the file's too, which is what three comments here used to explain.
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/header-bar.ui");
GtkWidget* bar = GTK_WIDGET(gtk_builder_get_object(ui, "header_bar"));
if (bar == nullptr) return gtk_header_bar_new();
app.toolbar = bar; app.toolbar = bar;
// The control already *is* the window's name: a `window` label in front of it said nothing the // The control already *is* the window's name: a `window` label in front of it said nothing the
// `2 s` beside it did not, and the bar is the busiest row in the window. // `2 s` beside it did not, and the bar is the busiest row in the window. It goes into the file's slot
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), build_window_control(app)); // rather than straight onto the bar: `gtk_header_bar_pack_start()` appends *after* whatever the file
// has already packed, so a control added here would land right of the whole group.
if (GtkWidget* slot = GTK_WIDGET(gtk_builder_get_object(ui, "window_control_slot"))) {
gtk_box_append(GTK_BOX(slot), build_window_control(app));
}
refresh_window_limits(app); refresh_window_limits(app);
// The end group reads, left to right: cursors, measurements, math, menu -- with the // The end group reads, left to right: cursors, measurements, math, menu -- with the
@@ -4280,85 +4289,34 @@ GtkWidget* build_toolbar(App& app) {
gtk_box_append(GTK_BOX(menu_box), about_item); gtk_box_append(GTK_BOX(menu_box), about_item);
// --- the pane toggles, which live on the bar rather than in the menu ------ // --- the pane toggles, which live on the bar rather than in the menu ------
// Created here because they share the popover's styling helpers; packed onto the header bar // The three pane toggles, and the rest of the bar, are the file's now; their handlers are wired where
// below, where the pack order (not this order) decides where they sit. // the bar's ids are resolved, below. The popover above still shares this function's styling helpers
app.measures_toggle = gtk_toggle_button_new(); // (`menu_button()`, `menu_toggle()`), which is why it is built here.
gtk_button_set_icon_name(GTK_BUTTON(app.measures_toggle), "udpscope-measure-symbolic");
gtk_widget_set_tooltip_text(app.measures_toggle,
"compute a value over the visible window of a signal, and "
"optionally mark it on the plot");
g_signal_connect(app.measures_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b,
gpointer user) {
App* a = static_cast<App*>(user);
a->measures_revealed = gtk_toggle_button_get_active(b);
if (a->measures_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(a->measures_revealer),
a->measures_revealed);
}
}),
&app);
app.math_toggle = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.math_toggle), "udpscope-math-symbolic");
gtk_widget_set_tooltip_text(app.math_toggle,
"channels derived from the others by a graph of operations: gain, "
"absolute value, filters, sums and the rest. An enabled one is "
"acquired, plotted and measured exactly like a signal");
g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b,
gpointer user) {
App* a = static_cast<App*>(user);
if (a->math_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer),
gtk_toggle_button_get_active(b));
}
}),
&app);
app.cursors_toggle = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.cursors_toggle), "udpscope-cursors-symbolic");
gtk_widget_set_tooltip_text(app.cursors_toggle,
"the cursor instruments: two times, two levels, or the two read "
"off each trace where they cross");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.cursors_toggle), false);
g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
GtkWidget* popover = gtk_popover_new(); GtkWidget* popover = gtk_popover_new();
gtk_widget_add_css_class(popover, "udpscope-menu"); gtk_widget_add_css_class(popover, "udpscope-menu");
gtk_popover_set_child(GTK_POPOVER(popover), menu_box); gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
app.menu_button = gtk_menu_button_new(); app.menu_button = GTK_WIDGET(gtk_builder_get_object(ui, "menu_button"));
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(app.menu_button), "open-menu-symbolic"); app.sidebar_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "sidebar_toggle"));
app.trigger_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_toggle"));
app.events_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "events_toggle"));
app.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button"));
app.measures_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "measures_toggle"));
app.math_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "math_toggle"));
app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle"));
if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr ||
app.events_toggle == nullptr || app.pause_button == nullptr || app.measures_toggle == nullptr ||
app.math_toggle == nullptr || app.cursors_toggle == nullptr) {
// A missing id is a mistake in the `.ui`: the bar comes up with the controls it got rather than taking
// the window with it, and `[udpscope] header:` reads whatever geometry is left.
return bar;
}
gtk_menu_button_set_popover(GTK_MENU_BUTTON(app.menu_button), popover); gtk_menu_button_set_popover(GTK_MENU_BUTTON(app.menu_button), popover);
gtk_widget_set_tooltip_text(app.menu_button, "view options");
// `pack_end()` lays its children out right to left -- the first packed is the *rightmost* -- so
// the menu is packed first to sit last on screen, and the pane toggles before it in reverse of the
// order they should read in: cursors, measurements, math, menu.
// --- what stays on the bar (packed end: first call is rightmost) ---------- // What each control *does*, which a file cannot hold.
app.sidebar_toggle = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.sidebar_toggle), "sidebar-show-symbolic");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), true);
gtk_widget_set_tooltip_text(app.sidebar_toggle, "show the side panel");
g_signal_connect(app.sidebar_toggle, "toggled", G_CALLBACK(on_sidebar_toggled), &app); g_signal_connect(app.sidebar_toggle, "toggled", G_CALLBACK(on_sidebar_toggled), &app);
app.trigger_toggle = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.trigger_toggle), "udpscope-trigger-symbolic");
gtk_widget_set_tooltip_text(app.trigger_toggle,
"trigger mode: freeze the view and show one captured event "
"at a time");
g_signal_connect(app.trigger_toggle, "toggled", G_CALLBACK(on_trigger_toggled), &app); g_signal_connect(app.trigger_toggle, "toggled", G_CALLBACK(on_trigger_toggled), &app);
// Packed with the window control instead of at the far end: the trigger and the pause are read // The event history is a *trigger* thing, and one click from the mode that produces it.
// against the *span* on screen (a window change moves the trigger's event window with it), so they
// belong next to the control that sets it.
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), app.trigger_toggle);
// Immediately right of the trigger, and only while it is on: the event history is a *trigger*
// thing, and asking for it is one click from the mode that produces it.
app.events_toggle = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.events_toggle), "udpscope-events-symbolic");
gtk_widget_set_tooltip_text(app.events_toggle,
"the event history: the triggered acquisitions kept in the cache, "
"with a picture of each");
gtk_widget_set_visible(app.events_toggle, FALSE);
g_signal_connect(app.events_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { g_signal_connect(app.events_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
const bool open = gtk_toggle_button_get_active(b); const bool open = gtk_toggle_button_get_active(b);
@@ -4369,31 +4327,26 @@ GtkWidget* build_toolbar(App& app) {
if (open) refresh_events_pane(*a); if (open) refresh_events_pane(*a);
}), }),
&app); &app);
app.pause_button = gtk_toggle_button_new();
gtk_button_set_icon_name(GTK_BUTTON(app.pause_button), "media-playback-pause-symbolic");
gtk_widget_set_tooltip_text(app.pause_button, "pause acquisition");
g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app); g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app);
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), app.pause_button); g_signal_connect(app.measures_toggle, "toggled",
// A separator, because the events button is not another mode on the plot: what stands to its left is G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
// what the *view* is doing (the window, the trigger, the pause) and what follows is the cache of what App* a = static_cast<App*>(user);
// those produced. a->measures_revealed = gtk_toggle_button_get_active(b);
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); if (a->measures_revealer != nullptr) {
gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), app.events_toggle); gtk_revealer_set_reveal_child(GTK_REVEALER(a->measures_revealer),
a->measures_revealed);
// Packed here, after every widget above has been created: the *pack* order is what decides the }
// layout, not the creation order, and packing before the widget exists passes NULL to the header }),
// bar (five `gtk_box_prepend: assertion` criticals, and the controls simply absent). &app);
// The **first** packed is the rightmost, and the dump settled it: its tree order is the visual g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
// order left to right, and it reads the *reverse* of this list. (Both of this comment's earlier App* a = static_cast<App*>(user);
// versions were wrong in turn; the arrangement they produced is what got reported.) Packed in this if (a->math_revealer != nullptr) {
// order the row reads, left to right: signal list, cursors, measurements, math, sidebar, menu. gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer),
gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.menu_button); gtk_toggle_button_get_active(b));
gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.sidebar_toggle); }
gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.math_toggle); }),
gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.measures_toggle); &app);
gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.cursors_toggle); g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
return bar; return bar;
} }
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The header bar: the time-window control's slot, the trigger, pause and events group with the separator that
marks where the *view's* controls end, and the five at the far end.
The bar carries only the controls you reach for *while watching a signal*; everything you set up once lives
in the overflow menu, which is code — its rows act on the `RowSet` directly and two of them are toggles
whose state the rest of the interface reads, so a `GMenu` model would only add a second copy of that state.
The order is the file's, and it is the one thing here that is not self-evident: `pack_end()` lays its
children out right to left, so a `<child type="end">` is added leftwards-from-the-right in document order
and the screen reads the reverse of it — cursors, measurements, math, sidebar, menu.
The first start child is a slot and not the control: the time-window control is code (its menu's rows come
from the buffer's limits), and `gtk_header_bar_pack_start()` appends *after* whatever a file has already
packed, so a control added by code would land to the right of this whole group instead of at its head.
The events toggle starts hidden: the event history is a *trigger* thing, and asking for it is one click
from the mode that produces it.
-->
<interface>
<object class="GtkHeaderBar" id="header_bar">
<child type="start">
<object class="GtkBox" id="window_control_slot">
<property name="orientation">horizontal</property>
</object>
</child>
<child type="start">
<object class="GtkToggleButton" id="trigger_toggle">
<property name="icon-name">udpscope-trigger-symbolic</property>
<property name="tooltip-text" translatable="yes">trigger mode: freeze the view and show one captured event at a time</property>
</object>
</child>
<child type="start">
<object class="GtkToggleButton" id="pause_button">
<property name="icon-name">media-playback-pause-symbolic</property>
<property name="tooltip-text" translatable="yes">pause acquisition</property>
</object>
</child>
<child type="start">
<object class="GtkSeparator">
<property name="orientation">vertical</property>
</object>
</child>
<child type="start">
<object class="GtkToggleButton" id="events_toggle">
<property name="icon-name">udpscope-events-symbolic</property>
<property name="tooltip-text" translatable="yes">the event history: the triggered acquisitions kept in the cache, with a picture of each</property>
<property name="visible">0</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">view options</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="sidebar_toggle">
<property name="icon-name">sidebar-show-symbolic</property>
<property name="tooltip-text" translatable="yes">show the side panel</property>
<property name="active">1</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="math_toggle">
<property name="icon-name">udpscope-math-symbolic</property>
<property name="tooltip-text" translatable="yes">channels derived from the others by a graph of operations: gain, absolute value, filters, sums and the rest. An enabled one is acquired, plotted and measured exactly like a signal</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="measures_toggle">
<property name="icon-name">udpscope-measure-symbolic</property>
<property name="tooltip-text" translatable="yes">compute a value over the visible window of a signal, and optionally mark it on the plot</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="cursors_toggle">
<property name="icon-name">udpscope-cursors-symbolic</property>
<property name="tooltip-text" translatable="yes">the cursor instruments: two times, two levels, or the two read off each trace where they cross</property>
<property name="active">0</property>
</object>
</child>
</object>
</interface>
+1
View File
@@ -20,6 +20,7 @@
<file preprocess="xml-stripblanks">math-panel.ui</file> <file preprocess="xml-stripblanks">math-panel.ui</file>
<file preprocess="xml-stripblanks">math-editor.ui</file> <file preprocess="xml-stripblanks">math-editor.ui</file>
<file preprocess="xml-stripblanks">events-pane.ui</file> <file preprocess="xml-stripblanks">events-pane.ui</file>
<file preprocess="xml-stripblanks">header-bar.ui</file>
<file preprocess="xml-stripblanks">trigger-panel.ui</file> <file preprocess="xml-stripblanks">trigger-panel.ui</file>
<file preprocess="xml-stripblanks">signals-panel.ui</file> <file preprocess="xml-stripblanks">signals-panel.ui</file>
</gresource> </gresource>