feat: the host form, and the help window, are UI files

src/ui/source-form.ui is the form a source is added or edited with: an AdwDialog of
AdwPreferencesGroup rows (AdwEntryRow, AdwSpinRow, AdwSwitchRow) with its action in the header bar,
where the label says which mode the form is in -- Connect for a new host, Save for an existing
configuration. The grid of labels and bare entries it replaces had its hints in placeholders, which
vanish as soon as there is a value; they are the groups' descriptions now.

src/ui/help.ui is 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.

Four UI files now, 653 lines of template that used to be widget construction. Two libadwaita traps
cost a run each and are in docs/claude/invariants-gtk-and-chrome.md: an AdwDialog has no content
property (its child is a plain <child>, unlike an AdwToolbarView), and AdwEntryRow has no subtitle
(it is an AdwPreferencesRow, not an AdwActionRow). The unused form_field() helper went with them.

Every probe reading is unchanged -- [sources] form:, [sources] edit form:, [help] -- which is what
refactoring a shape rather than a behaviour should look like.
This commit is contained in:
Martino Ferrari committed 2026-10-11 17:27:05 +02:00
1 parent 4c56172005
commit 68743a7d7f
8 files changed
+388 -184

No files matched your search

+12
View File
@@ -213,6 +213,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **A UI file is data, and a mistake in it is a runtime abort, not a build failure.** `.ui` files live in
`src/ui/` and are compiled into the binary by the third GResource (`udpscope-ui.gresource.xml`,
`prefix=/org/udpscope/ui`), loaded with `gtk_builder_new_from_resource()`. `preprocess="xml-stripblanks"`
catches a *malformed* file at build time — and that includes an XML rule easy to break by hand: **a
comment may not contain a double hyphen**, so prose written the way every C++ comment in this project
is written (` -- like this -- `) fails the build with a caret under the offending character. Use an em
dash in a UI file. `preprocess="xml-stripblanks"`
catches a *malformed* file at build time -- and nothing else does, which is why it is on every file in
every bundle. What it cannot catch is a file that is valid XML but wrong for GTK: `GtkBuilder` reports
that with a **`Gtk-ERROR`, which aborts the process** by default, so a window built by hand from bad
@@ -244,6 +248,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
answer writes a row, and the row is gone once the dialog closes. `PrefsForm::browse_cancel` is
cancelled by the `closed` handler and replaced by the next chooser: one open picker at a time, and none
that can call back into a cleared form. It is also how a probe takes the cancel path without a mouse.
- **libadwaita's rows do not share one property set, and a `.ui` is where that shows.** An
`AdwEntryRow` is an `AdwPreferencesRow`: it has a `title` and **no `subtitle`**. `AdwSpinRow` and
`AdwSwitchRow` are `AdwActionRow`s and have both. A hint about an entry -- "empty = unicast" --
therefore belongs in the *group's* `description`, which is the better place for it anyway: a
placeholder vanishes as soon as there is a value, and a description does not. And an `AdwDialog` has
**no `content` property**: its child is a plain `<child>`, where an `AdwToolbarView` does take
`content`. Both mistakes are a `Gtk-ERROR` at load, so both abort the process, and both are caught by
the probe that opens every window rather than by anything at build time.
- **A dialog reachable from two windows cannot own its state in either.** The preferences open from
the main window's menu *and* from the source chooser, which exists before `App` does (`main()`
chooses a source, then builds the model and the UI). The widgets therefore live in a `PrefsForm` the