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
|
||||
|
||||
@@ -43,6 +43,10 @@ needs a probe that proves it *loads* and that its contents match whatever drives
|
||||
|
||||
## Already done
|
||||
|
||||
- `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
|
||||
@@ -52,7 +56,6 @@ needs a probe that proves it *loads* and that its contents match whatever drives
|
||||
|
||||
| where | what | why it is the right next step |
|
||||
|---|---|---|
|
||||
| `src/ui_prefs.cpp` (~330 lines) | the preferences: `.ui` + `AdwPreferencesDialog`, `AdwEntryRow`, `AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow` | the most visible win per line: it replaces four hand-built rows with four rows that look and behave like every other GNOME application's, and the file is self-contained |
|
||||
| `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 |
|
||||
|
||||
Reference in new issue
Block a user