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

+7 -10
View File
@@ -123,13 +123,10 @@ void chooser_finish(Chooser& c, bool chosen) {
c.form = nullptr; // before the destroy, so the `close-request` handler cannot re-enter
gtk_window_destroy(GTK_WINDOW(w));
}
// The preferences window is transient for the chooser, so it has to go first: a window destroyed
// with a transient still up is the lifetime this project has already got wrong once.
if (c.prefs.window != nullptr) {
GtkWidget* w = c.prefs.window;
c.prefs.window = nullptr;
gtk_window_destroy(GTK_WINDOW(w));
}
// The preferences go first: they are presented *on* this window, and a dialog still up when its
// window is destroyed is the lifetime this project has got wrong once already. `prefs_close()` is
// `adw_dialog_close()`, and the form's own `closed` handler clears it.
ui_impl::prefs_close(c.prefs);
if (c.window != nullptr) {
GtkWidget* w = c.window;
c.window = nullptr; // before the destroy: `close-request` must not re-enter this
@@ -628,9 +625,9 @@ gboolean pick_probe(gpointer user) {
// open: the pick below destroys the chooser with it still up, which is the lifetime at risk.
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_PREFS") != nullptr) {
open_chooser_preferences(c);
std::fprintf(stderr, "[sources] prefs: opened=%d apply=%d\n", c.prefs.window != nullptr ? 1 : 0,
c.prefs.apply != nullptr ? 1 : 0);
ui_impl::prefs_probe_fill_and_apply(c.prefs);
std::fprintf(stderr, "[sources] prefs: opened=%d rows=%d\n", c.prefs.dialog != nullptr ? 1 : 0,
c.prefs.dir_row != nullptr ? 1 : 0);
ui_impl::prefs_probe_change(c.prefs);
const Settings back = load_settings();
std::fprintf(stderr,
"[sources] prefs: dir=%s keep=%d rel=%d theme=%s cfg_relative=%d\n",