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:
1 parent
0e256764d5
commit
4c56172005
11 files changed
+403
-247
No files matched your search
@@ -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
|
||||
|
||||
@@ -903,7 +903,7 @@ popover row reads `failed: <strerror>` — one stderr line, no retry storm again
|
||||
## `[sources] prefs`
|
||||
|
||||
```
|
||||
[sources] prefs: opened=1 apply=1
|
||||
[sources] prefs: opened=1 rows=1
|
||||
[sources] prefs: dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark cfg_relative=1
|
||||
```
|
||||
|
||||
@@ -1042,6 +1042,40 @@ library measures and draws the rect in tests of its own.
|
||||
|
||||
## `[prefs-probe]`
|
||||
|
||||
```
|
||||
[prefs-probe] opened=1 rows=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark live=1
|
||||
[prefs-probe] default restored: file=''
|
||||
[prefs-probe] browse: cancelled=1 icon_ok=1 field='/tmp/k5/cache/udpscope/events'
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_PREFS_MS` opens the preferences, changes every row, and reads the settings file *back
|
||||
from disk* after each change -- not from `App::settings`, because a dialog that edits its own copy and
|
||||
never writes is exactly the failure this has to catch. **A change applies as it is made**: the rows are
|
||||
libadwaita's (`AdwEntryRow`, `AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow`), and the `preferences.ui`
|
||||
dialog has no Apply button for the probe to press.
|
||||
|
||||
- `opened=1 rows=1` is the dialog having come up *and* its rows having resolved by id. The file is
|
||||
checked by the build (`xml-stripblanks`) and by `GtkBuilder` at load; a missing **id** is the one
|
||||
mistake neither sees, and reading a null row on the first change is what it costs.
|
||||
- `dir` is the directory the *row* is offering -- the preference if there is one, otherwise the default
|
||||
written out in full -- and `store` is the directory the cache is really using. **`store` must be `dir`
|
||||
plus the source's folder name**, which is the reading that says the row shows the path in force rather
|
||||
than a plausible string.
|
||||
- The second line must echo the probe's own input: directory `/tmp/udpscope-prefs-events`, keep `7`,
|
||||
relative time `1`, theme `dark`. Any field that differs means the row, the write or the parse dropped
|
||||
it. `live=1` is the theme having reached the running style manager rather than only the file.
|
||||
- `default restored: file=''` closes the loop from the other side: the probe puts the default path back
|
||||
into the row, and the file must end up with **no** `event_cache_dir` at all. A path in the file is a
|
||||
user's choice, and leaving the default is not one -- pinning it there would stop the setting following
|
||||
the cache directory it is named after.
|
||||
- `browse: cancelled=1 icon_ok=1 field='...'` is the folder chooser. `cancelled=1` is its cancellable
|
||||
having been cancelled, which is the *cancel* path a picker invites (a user's own Cancel button arrives
|
||||
in the same callback with the same null answer); `icon_ok=1` is the icon theme knowing
|
||||
`folder-open-symbolic`; and the field must still read the directory it read before -- **a cancelled
|
||||
picker changes nothing**.
|
||||
|
||||
|
||||
```
|
||||
[prefs-probe] opened=1 apply=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark live=1
|
||||
|
||||
Reference in new issue
Block a user