feat: the main window's skeleton is a UI file
src/ui/main-window.ui holds the window, its two panes, the plot column with the status line under it, and the sidebar's scroller. What stays in code is what depends on the stream: the events pane, the two plot areas, the sidebar's panels and the status popover, each put into a slot by id. The header bar is deliberately not in the file. It is the window's own titlebar, every button on it is wired to an action, and an AdwApplicationWindow refuses a titlebar outright -- a fatal Adwaita-ERROR rather than a warning that leaves the window merely ugly -- so the window is a GtkApplicationWindow and the header bar stays where it was. gtk_paned_set_position() is the other thing that had to stay in code, and the reason is order rather than shape: it clamps against its children *now*, so stating it before the sidebar's panels exist moved the plot column by ~180 px. The layout, events and box-zoom probes read what they read before this change, and the header reads window_button_w=130 bar_w=756 as it always has -- checked by building the previous commit and running the same probe against it, because the layout numbers in the notes turned out to be stale from an earlier session. Five UI files now.
This commit is contained in:
1 parent
68743a7d7f
commit
8073e7b3fd
8 files changed
+196
-70
No files matched your search
+50
-69
@@ -4368,7 +4368,32 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
|
||||
install_css();
|
||||
app->window = gtk_application_window_new(application);
|
||||
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status
|
||||
// line under it, and the sidebar's scroller. Everything that depends on the stream -- the events
|
||||
// pane, the two plot areas, the sidebar's panels -- is built below and put into the slots by id.
|
||||
g_clear_object(&app->window_ui);
|
||||
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
|
||||
if (app->window_ui == nullptr) return;
|
||||
app->window = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "main_window"));
|
||||
GtkWidget* paned = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "paned"));
|
||||
GtkWidget* events_divider = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "events_divider"));
|
||||
app->plot_box = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_box"));
|
||||
app->plot_split = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "plot_split"));
|
||||
app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge"));
|
||||
app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label"));
|
||||
app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info"));
|
||||
app->side_scroll = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_scroll"));
|
||||
GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column"));
|
||||
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
|
||||
// be a null pointer.
|
||||
if (app->window == nullptr || paned == nullptr || events_divider == nullptr ||
|
||||
app->plot_box == nullptr || app->plot_split == nullptr || app->status_badge == nullptr ||
|
||||
app->status_label == nullptr || app->status_info_button == nullptr ||
|
||||
app->side_scroll == nullptr || side == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The window's actions live on the application, so it has to know which one it belongs to.
|
||||
gtk_window_set_application(GTK_WINDOW(app->window), application);
|
||||
// A window closed by hand (a WM close button) must stop the frame tick: it holds
|
||||
// pointers to widgets that are finalized with the window.
|
||||
g_signal_connect(app->window, "destroy", G_CALLBACK(+[](GtkWidget*, gpointer user) {
|
||||
@@ -4383,45 +4408,25 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
a->window = nullptr;
|
||||
}),
|
||||
app);
|
||||
gtk_window_set_title(GTK_WINDOW(app->window), "udpscope");
|
||||
// The shell takes a window's icon from this name, and GTK takes the WM class from the
|
||||
// application ID -- which is the same string, which is what associates this window with the
|
||||
// desktop entry.
|
||||
gtk_window_set_icon_name(GTK_WINDOW(app->window), "org.udpscope.app");
|
||||
gtk_window_set_default_size(GTK_WINDOW(app->window), 1280, 800);
|
||||
// The header bar is the window's own titlebar, and it is *not* in the `.ui`: every button on it is
|
||||
// wired to an action the application owns, which is code by nature. Its title and the icon the shell
|
||||
// shows are the window's, and those are in the file.
|
||||
gtk_window_set_titlebar(GTK_WINDOW(app->window), build_toolbar(*app));
|
||||
|
||||
GtkWidget* paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
|
||||
app->plot_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_box_set_homogeneous(GTK_BOX(app->plot_box), true);
|
||||
gtk_widget_set_hexpand(app->plot_box, true);
|
||||
gtk_widget_set_vexpand(app->plot_box, true);
|
||||
|
||||
GtkWidget* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_box_append(GTK_BOX(left), app->plot_box);
|
||||
gtk_widget_set_vexpand(app->plot_box, true);
|
||||
|
||||
// The badge and the text share one row so the badge sits at the line's head. It is a
|
||||
// sibling rather than part of the label's own markup because the label's text is
|
||||
// rewritten every frame and its markup is ellipsized -- a blinking span inside it would
|
||||
// be reparsed sixty times a second and could be the character that gets elided.
|
||||
GtkWidget* status_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app->status_badge = gtk_label_new(nullptr);
|
||||
// The badge's markup is the one value the `.ui` cannot carry (it is a phase, not a string), and its
|
||||
// tooltip says which phase the badge is *not* reporting.
|
||||
gtk_label_set_markup(GTK_LABEL(app->status_badge), badge_markup(BadgePhase::Idle));
|
||||
gtk_widget_set_tooltip_text(app->status_badge, "waiting for the stream");
|
||||
gtk_box_append(GTK_BOX(status_row), app->status_badge);
|
||||
|
||||
app->status_label = gtk_label_new("starting...");
|
||||
// The line is rewritten every frame and its text changes width as the counters
|
||||
// grow. A label's *minimum* width is its text, so an unellipsized one pushes the
|
||||
// paned divider around as the numbers change -- which resizes the plot, which
|
||||
// re-lays out the axes and their ticks. Ellipsizing gives it a minimum of "…" and
|
||||
// makes it fill whatever width it is given instead.
|
||||
gtk_label_set_ellipsize(GTK_LABEL(app->status_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_widget_set_hexpand(app->status_label, true);
|
||||
gtk_widget_set_halign(app->status_label, GTK_ALIGN_FILL);
|
||||
gtk_box_append(GTK_BOX(status_row), app->status_label);
|
||||
|
||||
// Guard the construction so the counter means something: if status_popover is already
|
||||
// non-null (e.g. run_ui called twice), count the extra build and avoid a leak.
|
||||
@@ -4488,9 +4493,6 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
gtk_box_append(GTK_BOX(popover_box), app->capture_button);
|
||||
gtk_popover_set_child(GTK_POPOVER(app->status_popover), popover_box);
|
||||
|
||||
app->status_info_button = gtk_menu_button_new();
|
||||
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(app->status_info_button),
|
||||
"help-about-symbolic");
|
||||
// Above the button, always. The status row is the last thing in the window, so "below"
|
||||
// is off the window's bottom edge and, on a window that reaches the bottom of the screen,
|
||||
// off the screen: the bubble's top edge and its arrow stayed at the button while the body
|
||||
@@ -4502,21 +4504,14 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
// where a menu button in a header bar wants it), and it is the *last* word that counts.
|
||||
gtk_popover_set_position(GTK_POPOVER(app->status_popover), GTK_POS_TOP);
|
||||
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
|
||||
gtk_widget_set_tooltip_text(app->status_info_button, "Stream and buffer detail");
|
||||
gtk_widget_set_has_tooltip(app->status_info_button, TRUE);
|
||||
gtk_box_append(GTK_BOX(status_row), app->status_info_button);
|
||||
}
|
||||
|
||||
gtk_widget_set_margin_top(status_row, 2);
|
||||
gtk_widget_set_margin_bottom(status_row, 2);
|
||||
gtk_widget_set_margin_start(status_row, 6);
|
||||
gtk_box_append(GTK_BOX(left), status_row);
|
||||
|
||||
// The right-hand side: two independently collapsible panels, each revealed by
|
||||
// its toolbar toggle. No outer scrolled window -- the signal list scrolls
|
||||
// inside its own panel -- so neither panel can be squeezed to nothing by the
|
||||
// other's size request.
|
||||
GtkWidget* side = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
// `side` is the sidebar column from the `.ui`, resolved above with the rest of the skeleton; the
|
||||
// app keeps it because the panel geometry, the divide and the revealers all ask about it.
|
||||
app->side_box = side;
|
||||
|
||||
// The two panels share a vertical box, so they collapse *vertically*: a
|
||||
@@ -4562,47 +4557,33 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->signal_revealer), app->signal_panel);
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer), TRUE);
|
||||
gtk_box_append(GTK_BOX(side), app->signal_revealer);
|
||||
gtk_widget_set_size_request(side, 300, -1);
|
||||
|
||||
// The column scrolls as a whole, so a long signal list or a revealed trigger
|
||||
// panel can never push either panel out of the window.
|
||||
app->side_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(app->side_scroll), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(app->side_scroll), side);
|
||||
gtk_widget_set_size_request(app->side_scroll, 320, -1);
|
||||
// Its size request and the scroller around it are the `.ui`'s: the column scrolls as a whole, so a
|
||||
// long signal list or a revealed trigger panel can never push either panel out of the window.
|
||||
|
||||
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
||||
// (the item's "collapsable pane on the left"), revealed by trigger mode.
|
||||
// A *paned*: the pane's width is the user's to choose, and the plot takes what is left.
|
||||
GtkWidget* left_row = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_paned_set_start_child(GTK_PANED(left_row), build_events_pane(*app));
|
||||
// Once, now that both the button and the pane exist: trigger mode is off, so the pane is closed
|
||||
// and its button is not on the bar -- stated by the one function that decides it rather than by
|
||||
// whichever widget happened to be built with which default.
|
||||
// The events pane is the one subtree of the inner divider that is built: whether it exists at all
|
||||
// depends on trigger mode.
|
||||
gtk_paned_set_start_child(GTK_PANED(events_divider), build_events_pane(*app));
|
||||
// Once, now that both the button and the pane exist: trigger mode is off, so the pane is closed and
|
||||
// its button is not on the bar -- stated by the one function that decides it rather than by whichever
|
||||
// widget happened to be built with which default.
|
||||
sync_events_availability(*app);
|
||||
gtk_widget_set_hexpand(left, TRUE);
|
||||
gtk_paned_set_end_child(GTK_PANED(left_row), left);
|
||||
// Shrinkable, or the pane's own minimum would be reserved as an empty column even while it is
|
||||
// closed, and the plot would pay for a pane nobody opened.
|
||||
gtk_paned_set_shrink_start_child(GTK_PANED(left_row), TRUE);
|
||||
gtk_paned_set_resize_start_child(GTK_PANED(left_row), FALSE);
|
||||
app->events_divider = left_row;
|
||||
gtk_paned_set_start_child(GTK_PANED(paned), left_row);
|
||||
gtk_paned_set_end_child(GTK_PANED(paned), app->side_scroll);
|
||||
gtk_paned_set_resize_end_child(GTK_PANED(paned), FALSE);
|
||||
gtk_paned_set_shrink_end_child(GTK_PANED(paned), FALSE);
|
||||
gtk_window_set_child(GTK_WINDOW(app->window), paned);
|
||||
// After the children exist, or the position is clamped against an empty pane.
|
||||
gtk_paned_set_position(GTK_PANED(paned), 940);
|
||||
app->events_divider = events_divider;
|
||||
|
||||
// Two plots, in a paned so a comparison's divider can move: the primary takes everything while the
|
||||
// second is hidden, and the width is the user's once it is not.
|
||||
app->plot_split = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
// After the children exist -- the sidebar's panels are appended above -- or the position is clamped
|
||||
// against an empty pane and the divider lands wherever the empty width happened to be. This is the
|
||||
// one thing about a `GtkPaned` that cannot live in the `.ui`: it has to be stated after both sides
|
||||
// have been filled, which is a fact about this function's order, not about the window's shape.
|
||||
gtk_paned_set_position(GTK_PANED(paned), 940);
|
||||
|
||||
// The split is in the `.ui`; what goes in it is not, because an area is a GL surface with a figure
|
||||
// handed to it.
|
||||
gtk_paned_set_start_child(GTK_PANED(app->plot_split), make_area(*app));
|
||||
gtk_paned_set_end_child(GTK_PANED(app->plot_split), make_compare_area(*app));
|
||||
gtk_paned_set_shrink_end_child(GTK_PANED(app->plot_split), TRUE);
|
||||
gtk_box_append(GTK_BOX(app->plot_box), app->plot_split);
|
||||
|
||||
// A waveform opened from a file is already in the model by the time the UI is built -- there is no
|
||||
// drop and no CONFIG to wait for -- so its channels are bound to the rows here: four traces to a
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
The main window's skeleton: the window, the two panes that make its three columns, the plot column
|
||||
with the status line under it, and the sidebar's scroller.
|
||||
|
||||
What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of
|
||||
the window does not depend on the stream, the trigger or the layout — while the four subtrees that
|
||||
do are built by code and put in place:
|
||||
|
||||
- the events pane, into the inner paned's start side;
|
||||
- the two plot areas, into the split inside `plot_box`;
|
||||
- the sidebar's panels, appended to `side_column`;
|
||||
- the status popover, attached to `status_info`.
|
||||
|
||||
A `GtkApplicationWindow` and not an `AdwApplicationWindow`, deliberately: the header bar is the
|
||||
window's own titlebar and Adw's window refuses one outright (`gtk_window_set_titlebar() is not
|
||||
supported for AdwApplicationWindow`, which is a fatal `Adwaita-ERROR`, not a warning). An Adw window
|
||||
would want its header as an `AdwToolbarView`'s top bar instead, and this one is a `GtkHeaderBar`
|
||||
packed by hand — a change for its own sake, and one that would take the window controls away from it
|
||||
(a GtkHeaderBar draws those only as a titlebar).
|
||||
|
||||
The header bar is *not* in this file: it is the window's own titlebar,
|
||||
built by `build_toolbar()` because every button on it is wired to an action. The status line is not a
|
||||
bottom bar either — it belongs to the plot column, not to the window, so it stays inside `left_column`
|
||||
where the paned divider can move past it.
|
||||
|
||||
The `GtkPaned`s are here rather than in code because their *rules* are the skeleton: the events pane
|
||||
is shrinkable and does not resize (so a closed pane costs the plot nothing), and the sidebar is
|
||||
neither. `gtk_paned_set_position()` is not, because it has to run after the children exist.
|
||||
-->
|
||||
<interface>
|
||||
<object class="GtkApplicationWindow" id="main_window">
|
||||
<property name="title">udpscope</property>
|
||||
<property name="icon-name">org.udpscope.app</property>
|
||||
<property name="default-width">1280</property>
|
||||
<property name="default-height">800</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="resize-end-child">0</property>
|
||||
<property name="shrink-end-child">0</property>
|
||||
<child type="start">
|
||||
<object class="GtkPaned" id="events_divider">
|
||||
<property name="orientation">horizontal</property>
|
||||
<!-- Shrinkable, or the pane's own minimum is reserved as an empty column even while it is
|
||||
closed and the plot pays for a pane nobody opened. -->
|
||||
<property name="shrink-start-child">1</property>
|
||||
<property name="resize-start-child">0</property>
|
||||
<child type="end">
|
||||
<object class="GtkBox" id="left_column">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="hexpand">1</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="plot_box">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="homogeneous">1</property>
|
||||
<property name="spacing">4</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="plot_split">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="shrink-end-child">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_row">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="margin-bottom">2</property>
|
||||
<property name="margin-start">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="status_badge"/>
|
||||
</child>
|
||||
<child>
|
||||
<!-- The line is rewritten every frame and its counters grow, so it is
|
||||
ellipsized: a label's minimum width is its text, and an unellipsized one
|
||||
would push the paned divider around as the numbers change. -->
|
||||
<object class="GtkLabel" id="status_label">
|
||||
<property name="label">starting…</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="halign">fill</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="status_info">
|
||||
<property name="icon-name">help-about-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">Stream and buffer detail</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<!-- The column scrolls as a whole, so a long signal list or a revealed trigger panel can
|
||||
never push either panel out of the window. -->
|
||||
<object class="GtkScrolledWindow" id="side_scroll">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="vscrollbar-policy">automatic</property>
|
||||
<property name="width-request">320</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="side_column">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="width-request">300</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -13,5 +13,6 @@
|
||||
<file preprocess="xml-stripblanks">preferences.ui</file>
|
||||
<file preprocess="xml-stripblanks">help.ui</file>
|
||||
<file preprocess="xml-stripblanks">source-form.ui</file>
|
||||
<file preprocess="xml-stripblanks">main-window.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
@@ -399,6 +399,9 @@ struct App {
|
||||
plot::Figure* area_b_figure = nullptr;
|
||||
std::unique_ptr<RowSet> rows_b;
|
||||
GtkWidget* plot_split = nullptr;
|
||||
/// The main window's builder. Held because the window belongs to its object list: it is replaced on
|
||||
/// the next build rather than reused, and everything found by id comes out of it.
|
||||
GtkBuilder* window_ui = nullptr;
|
||||
bool comparing = false;
|
||||
/// The last x/y range seen on each side of a comparison, one entry per row: a frame compares them
|
||||
/// with the plots' own to tell *which* side the user moved, and copies from that side to the other.
|
||||
|
||||
Reference in new issue
Block a user