src/ui/preferences.ui is an AdwPreferencesDialog whose rows are libadwaita's own -- AdwEntryRow, AdwSpinRow, AdwSwitchRow, AdwComboRow, AdwButtonRow -- instead of a label and a bare GtkEntry in a GtkBox. There is no Apply button: a row applies as it changes, which is what those widgets are designed around, and [prefs-probe] reads the settings file back after every change. The folder chooser is cancellable now, so a picker still open when the dialog closes cannot write into a row that is gone; the cancellable is also how the probe takes the cancel path without a mouse. Two traps cost a run each and are recorded in docs/claude/invariants-gtk-and-chrome.md: an AdwDialog is not a GtkWindow (it cannot parent a GtkFileDialog, and in floating mode it adds a native surface to gtk_window_get_toplevels() that is neither a widget nor a window), and a .ui that is not listed in the GResource is a Gtk-ERROR at runtime rather than a build failure -- which is exactly what the probe opening every window is for.
4.2 KiB
4.2 KiB
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.
- Some things can only be said in a UI file. The shortcut overview is the proof: GTK's
GtkShortcutsShortcuthas exactly one gesture type that draws anicon, and libadwaita'sAdwShortcutsItemhas no icon property at all -- so a mouse gesture can only be shown as a glyph from a.ui.src/ui/shortcuts.uiis that, and it is the reason the file exists. - Structure is data. A window's arrangement rewritten in C++ is a hundred lines of
gtk_box_appendthat 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. - Current GTK and libadwaita have better widgets than this app uses. The forms here are
GtkEntry/GtkSpinButtonin hand-built label rows. The modern equivalents --AdwEntryRow,AdwSpinRow,AdwSwitchRow,AdwComboRow, insideAdwPreferencesPage/AdwPreferencesGroupin anAdwPreferencesDialog-- 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)
.uifiles live insrc/ui/and are compiled by the third GResource (udpscope-ui.gresource.xml,prefix=/org/udpscope/ui), loaded withgtk_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
GIconproperty takes a nested object, not a name. - A
.uiover 4095 characters needs-Wno-overlength-stringson 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
GtkWindowcannot 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/preferences.ui-- the preferences, as anAdwPreferencesDialogwithAdwEntryRow,AdwSpinRow,AdwSwitchRow,AdwComboRowandAdwButtonRowrows. 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]'sui=/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/help_window.cpp (351 lines) |
the help window's chrome as a .ui (the pages themselves stay resources) |
small, self-contained, and its probe already opens it |
src/source_dialog.cpp (~900 lines) |
the source chooser and the host form: .ui + AdwDialog, with AdwEntryRow/AdwSpinRow for the form |
the form is the second-biggest hand-built surface, and its probes drive every branch of it |
src/ui.cpp (5774 lines) |
the main window's skeleton only -- the AdwApplicationWindow, the header bar, the plot area's placeholder, the panes' containers, the status bar -- with the custom widgets, popovers and dialogs left in code |
one file, one window: converting the skeleton is reviewable, converting the whole file is not. Do it in slices (header bar, then the panes, then the trigger panel, then the left panes) with the probe suite after each |
Nothing in this list should be done in one pass. Each row is its own change, verified on its own.