feat: the cursors panel is a UI file
src/ui/cursors-panel.ui holds the panel's shape -- the header with the mode dropdown and the reset button, and the grid the two cursor times are read in. What is not in it is the grid's contents, which follow the cursor mode and the rows and are rebuilt whenever either changes. Panels need a builder that outlives them, unlike the dialogs, which are rebuilt on every opening: a widget belongs to the builder that built it, so App::ui_builders holds each panel's for the life of the window. ui_load() is that, in ui_widgets.cpp. make test-ui also checks that udpscope-ui.gresource.xml lists every .ui file now. That was the third trap of this refactor -- a file on disk but not in the resource is a Gtk-ERROR at load, which aborts -- and it is a compile-time mistake that nothing else in the build could see. Verified by unlisting the new file and watching the check fail, then restoring it. [cursors-probe] reads as it did: all four modes, pane=1, the timings and the drag cases.
This commit is contained in:
1 parent
7a982da6c4
commit
ed82abd405
8 files changed
+118
-25
No files matched your search
@@ -66,6 +66,15 @@ needs a probe that proves it *loads* and that its contents match whatever drives
|
|||||||
| where | what | why it is the right next step |
|
| where | what | why it is the right next step |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `src/source_dialog.cpp` (the chooser's own window) | the chooser: `.ui` for the window, its two pages and its action row. Its **host form is done**; what is left is the list, the notebook and the buttons around it | the last hand-built UI outside `ui.cpp`, and the file is already open |
|
| `src/source_dialog.cpp` (the chooser's own window) | the chooser: `.ui` for the window, its two pages and its action row. Its **host form is done**; what is left is the list, the notebook and the buttons around it | the last hand-built UI outside `ui.cpp`, and the file is already open |
|
||||||
| `src/ui.cpp` (the panels inside the skeleton) | the trigger panel, the signal panel, the cursors/measurements/math panels, the layout editor's overlay, the menus and the popovers: `.ui` for each, in that order, with the probe that reads it run after each | each is self-contained inside the skeleton that is now in place, and each has a probe of its own |
|
| `build_measures_panel` (`src/ui.cpp`, ~82 lines) | the measurements panel: `.ui` + `tools/check_ui.py` after | small, self-contained, and its own probe |
|
||||||
|
| `src/ui/ui_trigger.cpp` (~22 kB) | the trigger panel: the `.ui` holds the panel and its groups; the signal chips and the condition rows stay in code because they follow CONFIG | the largest user-visible panel |
|
||||||
|
| `src/ui/ui_signals.cpp` (~20 kB) | the signals panel | mostly a list of data-driven rows, so the `.ui` holds the frame |
|
||||||
|
| `src/ui/ui_math.cpp` (~48 kB) | the math panel and the node editor's chrome (the canvas itself is drawn, not declared) | the largest file, and the one where least of it is template |
|
||||||
|
| `src/ui/ui_events.cpp` (~44 kB) | the events pane | the rows are events, the frame is not |
|
||||||
|
| `src/ui/ui_layout.cpp` (~20 kB) | the layout editor's overlay buttons, which are positioned by code | only the buttons are template |
|
||||||
|
| `build_toolbar` (`src/ui.cpp`, ~212 lines) | the header bar's layout, with every button still wired in code | delicate: the `[udpscope] header:` probe reads its geometry |
|
||||||
|
|
||||||
|
Each of these is one change on the pattern `src/ui/ui_cursors.cpp` now follows: `ui_load()` the file, resolve
|
||||||
|
the ids, keep every value, signal and rebuild in code, and run the panel's own probe before committing.
|
||||||
|
|
||||||
Nothing in this list should be done in one pass. Each row is its own change, verified on its own.
|
Nothing in this list should be done in one pass. Each row is its own change, verified on its own.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- SPDX-License-Identifier: MIT -->
|
||||||
|
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||||
|
<!--
|
||||||
|
The cursors panel: a header with the mode's dropdown and the reset button, and the grid the two
|
||||||
|
cursor times are read in, one line per row of the figure.
|
||||||
|
|
||||||
|
The grid's *contents* are not here: they follow the cursor mode and the rows, and they are rebuilt
|
||||||
|
whenever either changes (`rebuild_cursors_pane()`). What is here is the panel's shape, the dropdown
|
||||||
|
and the button, and the grid they sit above.
|
||||||
|
-->
|
||||||
|
<interface>
|
||||||
|
<object class="GtkBox" id="cursors_panel">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">4</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
<property name="margin-bottom">6</property>
|
||||||
|
<property name="margin-start">6</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox" id="head">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">8</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="title">
|
||||||
|
<property name="label" translatable="yes">Cursors</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">1</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkDropDown" id="mode_drop"/>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="reset">
|
||||||
|
<property name="icon-name">edit-clear-symbolic</property>
|
||||||
|
<property name="valign">center</property>
|
||||||
|
<property name="css-classes">flat</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkGrid" id="body">
|
||||||
|
<property name="row-spacing">2</property>
|
||||||
|
<property name="column-spacing">4</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</interface>
|
||||||
@@ -14,5 +14,6 @@
|
|||||||
<file preprocess="xml-stripblanks">help.ui</file>
|
<file preprocess="xml-stripblanks">help.ui</file>
|
||||||
<file preprocess="xml-stripblanks">source-form.ui</file>
|
<file preprocess="xml-stripblanks">source-form.ui</file>
|
||||||
<file preprocess="xml-stripblanks">main-window.ui</file>
|
<file preprocess="xml-stripblanks">main-window.ui</file>
|
||||||
|
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
@@ -399,6 +399,10 @@ struct App {
|
|||||||
plot::Figure* area_b_figure = nullptr;
|
plot::Figure* area_b_figure = nullptr;
|
||||||
std::unique_ptr<RowSet> rows_b;
|
std::unique_ptr<RowSet> rows_b;
|
||||||
GtkWidget* plot_split = nullptr;
|
GtkWidget* plot_split = nullptr;
|
||||||
|
/// The builders of the panels inside the window. A widget belongs to the builder that built it, and a
|
||||||
|
/// panel lives as long as the window, so these do too -- unlike the dialogs', which are replaced.
|
||||||
|
std::vector<GtkBuilder*> ui_builders;
|
||||||
|
|
||||||
/// The main window's builder. Held because the window belongs to its object list: it is replaced on
|
/// 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.
|
/// the next build rather than reused, and everything found by id comes out of it.
|
||||||
GtkBuilder* window_ui = nullptr;
|
GtkBuilder* window_ui = nullptr;
|
||||||
@@ -1038,6 +1042,9 @@ GtkWidget* menu_button(const char* label, const char* accel);
|
|||||||
GtkWidget* menu_toggle(const char* label, const char* accel, bool active);
|
GtkWidget* menu_toggle(const char* label, const char* accel, bool active);
|
||||||
GtkWidget* menu_separator();
|
GtkWidget* menu_separator();
|
||||||
GtkWidget* labeled_box(const char* label, GtkWidget* child);
|
GtkWidget* labeled_box(const char* label, GtkWidget* child);
|
||||||
|
/// Loads a UI file into the window's builders and returns the builder, so the caller can resolve the
|
||||||
|
/// objects it needs by id. See `ui_widgets.cpp`.
|
||||||
|
GtkBuilder* ui_load(App& app, const char* resource);
|
||||||
LayoutState collect_layout(App& app);
|
LayoutState collect_layout(App& app);
|
||||||
void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
|
void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
|
||||||
void on_config_forget_clicked(GtkButton* button, gpointer user);
|
void on_config_forget_clicked(GtkButton* button, gpointer user);
|
||||||
|
|||||||
+21
-24
@@ -78,41 +78,38 @@ void update_cursors_pane(App& app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GtkWidget* build_cursors_panel(App& app) {
|
GtkWidget* build_cursors_panel(App& app) {
|
||||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
// The panel's shape is a UI file; the *contents* of its grid are not, because they follow the cursor
|
||||||
gtk_widget_set_margin_top(box, 6);
|
// mode and the rows and are rebuilt whenever either changes.
|
||||||
gtk_widget_set_margin_bottom(box, 6);
|
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/cursors-panel.ui");
|
||||||
gtk_widget_set_margin_start(box, 6);
|
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_panel"));
|
||||||
gtk_widget_set_margin_end(box, 6);
|
app.cursors_mode_drop = GTK_WIDGET(gtk_builder_get_object(ui, "mode_drop"));
|
||||||
|
app.cursors_reset = GTK_WIDGET(gtk_builder_get_object(ui, "reset"));
|
||||||
|
app.cursors_body = GTK_WIDGET(gtk_builder_get_object(ui, "body"));
|
||||||
|
if (box == nullptr || app.cursors_mode_drop == nullptr || app.cursors_reset == nullptr ||
|
||||||
|
app.cursors_body == nullptr) {
|
||||||
|
// A missing id is a mistake in the `.ui`. The panel comes up empty rather than taking the window
|
||||||
|
// with it, and `[cursors]` reads differently rather than the application failing to start.
|
||||||
|
return box != nullptr ? box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
// The mode names are the dropdown's model, and they are a C array, so they are set here.
|
||||||
GtkWidget* title = gtk_label_new("Cursors");
|
|
||||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
|
||||||
gtk_widget_set_hexpand(title, true);
|
|
||||||
gtk_box_append(GTK_BOX(head), title);
|
|
||||||
const char* names[5] = {kCursorModeNames[0], kCursorModeNames[1], kCursorModeNames[2],
|
const char* names[5] = {kCursorModeNames[0], kCursorModeNames[1], kCursorModeNames[2],
|
||||||
kCursorModeNames[3], nullptr};
|
kCursorModeNames[3], nullptr};
|
||||||
app.cursors_mode_drop = gtk_drop_down_new_from_strings(names);
|
gtk_drop_down_set_model(GTK_DROP_DOWN(app.cursors_mode_drop),
|
||||||
|
G_LIST_MODEL(gtk_string_list_new(names)));
|
||||||
gtk_widget_set_tooltip_text(app.cursors_mode_drop,
|
gtk_widget_set_tooltip_text(app.cursors_mode_drop,
|
||||||
"vertical: two times, shared by every row\n"
|
"vertical: two times, shared by every row\n"
|
||||||
"horizontal: two rulers per row\n"
|
"horizontal: one value per row, at the two times\n"
|
||||||
"both: the two together\n"
|
"both: the two times and one value per row\n"
|
||||||
"mix: the two times, read off each trace where they cross");
|
"mix: the two times, read off each trace where they cross");
|
||||||
g_signal_connect(app.cursors_mode_drop, "notify::selected",
|
g_signal_connect(app.cursors_mode_drop, "notify::selected",
|
||||||
G_CALLBACK(on_cursor_mode_changed), &app);
|
G_CALLBACK(on_cursor_mode_changed), &app);
|
||||||
gtk_box_append(GTK_BOX(head), app.cursors_mode_drop);
|
|
||||||
// Reset lives here, beside the mode it resets, rather than in the overflow menu: it acts on this
|
// Reset lives beside the mode it resets rather than in the overflow menu: it acts on this pane's
|
||||||
// pane's instruments and is only visible while they are on, both of which are this pane's facts.
|
// instruments and is only visible while they are on, both of which are this pane's facts.
|
||||||
app.cursors_reset = gtk_button_new_from_icon_name("edit-clear-symbolic");
|
|
||||||
gtk_widget_add_css_class(app.cursors_reset, "flat");
|
|
||||||
gtk_widget_set_tooltip_text(app.cursors_reset, "re-place the cursors at the window's edges");
|
gtk_widget_set_tooltip_text(app.cursors_reset, "re-place the cursors at the window's edges");
|
||||||
g_signal_connect(app.cursors_reset, "clicked", G_CALLBACK(on_cursors_reset), &app);
|
g_signal_connect(app.cursors_reset, "clicked", G_CALLBACK(on_cursors_reset), &app);
|
||||||
gtk_box_append(GTK_BOX(head), app.cursors_reset);
|
|
||||||
gtk_box_append(GTK_BOX(box), head);
|
|
||||||
|
|
||||||
app.cursors_body = gtk_grid_new();
|
|
||||||
gtk_grid_set_row_spacing(GTK_GRID(app.cursors_body), 2);
|
|
||||||
gtk_grid_set_column_spacing(GTK_GRID(app.cursors_body), 4);
|
|
||||||
gtk_box_append(GTK_BOX(box), app.cursors_body);
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,22 @@
|
|||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
namespace ui_impl {
|
namespace ui_impl {
|
||||||
|
|
||||||
|
/// Loads one UI file and keeps its builder for the life of the window.
|
||||||
|
///
|
||||||
|
/// A widget belongs to the builder that built it -- drop the builder and the widgets it made go with
|
||||||
|
/// it -- so the panels' builders are kept in `App` beside the widgets they built. Unlike the dialogs
|
||||||
|
/// (`prefs_open()`, `show_help()`), a panel is built once and lives as long as the window, so these are
|
||||||
|
/// appended and never replaced.
|
||||||
|
///
|
||||||
|
/// A resource that is not in the GResource aborts here rather than returning null: `GtkBuilder` reports
|
||||||
|
/// a missing resource with a `Gtk-ERROR`, which is fatal. `make test-ui` checks that the xml lists every
|
||||||
|
/// `.ui` file, which is the only way to make that a build-time failure instead.
|
||||||
|
GtkBuilder* ui_load(App& app, const char* resource) {
|
||||||
|
GtkBuilder* builder = gtk_builder_new_from_resource(resource);
|
||||||
|
app.ui_builders.push_back(builder);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
/// Installs the one style rule the app needs: a signal row that is as short as its
|
/// Installs the one style rule the app needs: a signal row that is as short as its
|
||||||
/// contents.
|
/// contents.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -30,5 +30,6 @@
|
|||||||
- the help window's chrome is **done** (`src/ui/help.ui`: `AdwWindow` + header bar + `AdwNavigationSplitView`)
|
- the help window's chrome is **done** (`src/ui/help.ui`: `AdwWindow` + header bar + `AdwNavigationSplitView`)
|
||||||
- the host form is **done** (`src/ui/source-form.ui`); the source chooser's own window is next
|
- the host form is **done** (`src/ui/source-form.ui`); the source chooser's own window is next
|
||||||
- the main window's skeleton is **done** (`src/ui/main-window.ui`: the window, the two panes, the plot column and the status line, the sidebar's scroller); the panels inside it are the next slices
|
- the main window's skeleton is **done** (`src/ui/main-window.ui`: the window, the two panes, the plot column and the status line, the sidebar's scroller); the panels inside it are the next slices
|
||||||
|
- the side panels are being extracted one per commit on that pattern: the cursors panel is **done** (`src/ui/cursors-panel.ui`), and the measurements panel, the trigger panel, the signals panel, the math panel, the events pane, the layout editor's buttons and the header bar are queued in the plan
|
||||||
- next: the preferences (`AdwPreferencesDialog` + `AdwEntryRow`/`AdwSpinRow`/`AdwSwitchRow`), the help window's chrome, the source chooser and its host form, then the main window's skeleton
|
- next: the preferences (`AdwPreferencesDialog` + `AdwEntryRow`/`AdwSpinRow`/`AdwSwitchRow`), the help window's chrome, the source chooser and its host form, then the main window's skeleton
|
||||||
- plan, conventions and acceptance for each slice: `docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md`
|
- plan, conventions and acceptance for each slice: `docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md`
|
||||||
@@ -43,6 +43,7 @@ answering "fine" without having looked is the one answer it must not give.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -113,6 +114,17 @@ def main() -> int:
|
|||||||
for line in messages:
|
for line in messages:
|
||||||
print(f" {line}")
|
print(f" {line}")
|
||||||
|
|
||||||
|
# Every file has to be in the GResource. One that is not is a `Gtk-ERROR` at load -- an abort rather
|
||||||
|
# than a window that fails to open -- and nothing else in the build notices, because the xml is what
|
||||||
|
# `glib-compile-resources` reads and the file on disk is not it.
|
||||||
|
gresource = directory / "udpscope-ui.gresource.xml"
|
||||||
|
if gresource.exists():
|
||||||
|
listed = set(re.findall(r"<file[^>]*>([^<]+)</file>", gresource.read_text()))
|
||||||
|
for ui in files:
|
||||||
|
if ui.name not in listed:
|
||||||
|
print(f" FAIL {ui.name} is not listed in udpscope-ui.gresource.xml")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
print(f"check_ui: {len(files) - failed} ok, {failed} failed")
|
print(f"check_ui: {len(files) - failed} ok, {failed} failed")
|
||||||
if advice_count:
|
if advice_count:
|
||||||
print(f"check_ui: {advice_count} pieces of advice — not failures; --lint shows them")
|
print(f"check_ui: {advice_count} pieces of advice — not failures; --lint shows them")
|
||||||
|
|||||||
Reference in new issue
Block a user