feat: the preferences are a UI file, applying as they change

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.
This commit is contained in:
Martino Ferrari committed 2026-10-11 15:30:10 +02:00
1 parent 0e256764d5
commit 4c56172005
11 files changed
+403 -247

No files matched your search

+13
View File
@@ -231,6 +231,19 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
from a `.ui` and presenting it as a `GtkWindow` names no deprecated function, so the build stays at its
four warnings. When such a window closes, its builder is **replaced on the next opening**, not kept: the
window belongs to the builder's object list and a destroyed `GtkWindow` cannot be presented again.
- **An `AdwDialog` is not a `GtkWindow`, and not every toplevel is a widget either.** The preferences
are an `AdwDialog`, which is a `GObject` that a *window* presents, so it cannot parent a
`GtkFileDialog`: the form keeps the window it was opened with (`PrefsForm::parent`, the caller's) and
hands that to the picker. Two things follow, and each cost a run. In its floating presentation mode an
`AdwDialog` adds a **native surface** to `gtk_window_get_toplevels()`, so that list holds things that
are neither `GtkWindow`s nor widgets -- `GTK_WINDOW()` on one is a segfault inside `gtk_window_close()`,
and `g_object_unref()` on one is a `GLib-GObject-CRITICAL`. Anything walking it must test
`GTK_IS_WIDGET()` *and* `GTK_IS_WINDOW()` first, and a probe should not walk it at all where a
cancellable will do.
- **A folder chooser outlives the dialog that opened it unless something cancels it.** The picker's
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.
- **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