Files
udpscope/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md
T
Martino Ferrari ed82abd405 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.
2026-10-12 10:47:34 +02:00

81 lines
5.7 KiB
Markdown

# UI files and current GTK practice
Where the application's interface should be a `.ui` resource instead of widget construction, what is
already there, and the order to finish it in. Written 2026-10-11.
## Why
Three reasons, in the order they bite.
1. **Some things can only be said in a UI file.** The shortcut overview is the proof: GTK's
`GtkShortcutsShortcut` has exactly one gesture type that draws an `icon`, and libadwaita's
`AdwShortcutsItem` has no icon property at all -- so a mouse gesture can only be shown as a glyph
from a `.ui`. `src/ui/shortcuts.ui` is that, and it is the reason the file exists.
2. **Structure is data.** A window's arrangement rewritten in C++ is a hundred lines of
`gtk_box_append` that no one can read at a glance, cannot be compared with a diff, and cannot be
previewed. A UI file is the same arrangement in the notation the GNOME ecosystem reads.
3. **Current GTK and libadwaita have better widgets than this app uses.** The forms here are
`GtkEntry`/`GtkSpinButton` in hand-built label rows. The modern equivalents -- `AdwEntryRow`,
`AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow`, inside `AdwPreferencesPage`/`AdwPreferencesGroup` in an
`AdwPreferencesDialog` -- are what the preferences dialog should be, and they come from the same
move.
## Conventions (established, and in `docs/claude/invariants-gtk-and-chrome.md`)
- `.ui` files live in `src/ui/` and are compiled by the third GResource (`udpscope-ui.gresource.xml`,
`prefix=/org/udpscope/ui`), loaded with `gtk_builder_new_from_resource()`.
- Every file carries `preprocess="xml-stripblanks"`, which is the only thing that catches a malformed
file at build time.
- A file that is valid XML but wrong for GTK **aborts the process** (`Gtk-ERROR`). The probe that opens
every window is what catches that; there is no build-time substitute.
- A `GIcon` property takes a nested object, not a name.
- A `.ui` over 4095 characters needs `-Wno-overlength-strings` on its generated source.
- A window built from a builder is replaced (not reused) on the next opening: it belongs to the
builder's object list, and a destroyed `GtkWindow` cannot be presented again.
- A deprecated *widget* is acceptable when no deprecated *function* is called.
## Acceptance for every slice
The same as any other change here: the build stays at **four warnings**, a run stays at **no
`Gtk-CRITICAL`**, and the slice's own surface is read back by a probe rather than asserted. A new `.ui`
needs a probe that proves it *loads* and that its contents match whatever drives it -- `[help]`'s
`ui=` against `tables=` is the model.
## Already done
- `src/ui/main-window.ui` -- the main window's **skeleton**: the window, its two panes, the plot column
with the status line under it, and the sidebar's scroller. The four subtrees that depend on the stream
(the events pane, the two plot areas, the sidebar's panels, the status popover) are still built by
code and put into the slots by id.
- `src/ui/source-form.ui` -- the form a source is added or edited with: an `AdwDialog` of
`AdwPreferencesGroup` rows, with its action in the header bar where the label says which mode it is in.
- `src/ui/help.ui` -- the help window's chrome: an `AdwWindow` with a header bar and an
`AdwNavigationSplitView` whose sidebar is the page list. The rows stay in C++ because the fourteen
pages are data, not template.
- `src/ui/preferences.ui` -- the preferences, as an `AdwPreferencesDialog` with `AdwEntryRow`,
`AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow` and `AdwButtonRow` rows. The Apply button is gone: a row
applies as it changes, which is what those widgets are designed around, and `[prefs-probe]` reads the
file back after every change.
- `src/ui/shortcuts.ui` -- the shortcut overview, with the mouse rows drawn as an icon cell.
- The `src/ui/` GResource and its build block.
- `[help]`'s `ui=`/`ui_accel=`/`ui_icon=`/`ui_bare=` reading, which checks the file against the tables
and asserts that no row is left without a visual.
## Remaining, smallest first
| 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 |
| `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.