From 28d80f2e90270640f353a43b4fa1e77ce78f240a Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 12 Oct 2026 16:33:25 +0200 Subject: [PATCH] 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 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. --- src/ui.cpp | 149 +++++++++++-------------------- src/ui/header-bar.ui | 87 ++++++++++++++++++ src/ui/udpscope-ui.gresource.xml | 1 + 3 files changed, 139 insertions(+), 98 deletions(-) create mode 100644 src/ui/header-bar.ui diff --git a/src/ui.cpp b/src/ui.cpp index 20e4b4d..2db9862 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -4186,12 +4186,21 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi /// /// [ 2 s ▾ ] [plots ▾] ............... [pause] [trigger] [panel] [menu] 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; // 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. - gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), build_window_control(app)); + // `2 s` beside it did not, and the bar is the busiest row in the window. It goes into the file's slot + // 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); // 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); // --- 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 - // below, where the pack order (not this order) decides where they sit. - app.measures_toggle = gtk_toggle_button_new(); - 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(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(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); + // The three pane toggles, and the rest of the bar, are the file's now; their handlers are wired where + // the bar's ids are resolved, below. The popover above still shares this function's styling helpers + // (`menu_button()`, `menu_toggle()`), which is why it is built here. GtkWidget* popover = gtk_popover_new(); gtk_widget_add_css_class(popover, "udpscope-menu"); gtk_popover_set_child(GTK_POPOVER(popover), menu_box); - app.menu_button = gtk_menu_button_new(); - gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(app.menu_button), "open-menu-symbolic"); + app.menu_button = GTK_WIDGET(gtk_builder_get_object(ui, "menu_button")); + 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_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) ---------- - 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"); + // What each control *does*, which a file cannot hold. 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); - // Packed with the window control instead of at the far end: the trigger and the pause are read - // 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); + // The event history is a *trigger* thing, and one click from the mode that produces it. g_signal_connect(app.events_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { App* a = static_cast(user); const bool open = gtk_toggle_button_get_active(b); @@ -4369,31 +4327,26 @@ GtkWidget* build_toolbar(App& app) { if (open) refresh_events_pane(*a); }), &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); - gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), app.pause_button); - // A separator, because the events button is not another mode on the plot: what stands to its left is - // what the *view* is doing (the window, the trigger, the pause) and what follows is the cache of what - // those produced. - gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), gtk_separator_new(GTK_ORIENTATION_VERTICAL)); - gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), app.events_toggle); - - // 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). - // The **first** packed is the rightmost, and the dump settled it: its tree order is the visual - // order left to right, and it reads the *reverse* of this list. (Both of this comment's earlier - // versions were wrong in turn; the arrangement they produced is what got reported.) Packed in this - // order the row reads, left to right: signal list, cursors, measurements, math, sidebar, menu. - gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.menu_button); - 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); - gtk_header_bar_pack_end(GTK_HEADER_BAR(bar), app.cursors_toggle); + g_signal_connect(app.measures_toggle, "toggled", + G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { + App* a = static_cast(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); + g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) { + App* a = static_cast(user); + if (a->math_revealer != nullptr) { + gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer), + gtk_toggle_button_get_active(b)); + } + }), + &app); + g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app); return bar; } diff --git a/src/ui/header-bar.ui b/src/ui/header-bar.ui new file mode 100644 index 0000000..b1bd8ef --- /dev/null +++ b/src/ui/header-bar.ui @@ -0,0 +1,87 @@ + + + + + + + + + horizontal + + + + + udpscope-trigger-symbolic + trigger mode: freeze the view and show one captured event at a time + + + + + media-playback-pause-symbolic + pause acquisition + + + + + vertical + + + + + udpscope-events-symbolic + the event history: the triggered acquisitions kept in the cache, with a picture of each + 0 + + + + + open-menu-symbolic + view options + + + + + sidebar-show-symbolic + show the side panel + 1 + + + + + udpscope-math-symbolic + 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 + + + + + udpscope-measure-symbolic + compute a value over the visible window of a signal, and optionally mark it on the plot + + + + + udpscope-cursors-symbolic + the cursor instruments: two times, two levels, or the two read off each trace where they cross + 0 + + + + diff --git a/src/ui/udpscope-ui.gresource.xml b/src/ui/udpscope-ui.gresource.xml index 7fd5c92..6dc9310 100644 --- a/src/ui/udpscope-ui.gresource.xml +++ b/src/ui/udpscope-ui.gresource.xml @@ -20,6 +20,7 @@ math-panel.ui math-editor.ui events-pane.ui + header-bar.ui trigger-panel.ui signals-panel.ui