// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari // // The preferences, without an owner. // // The dialog is a `.ui` file (`src/ui/preferences.ui`) and its rows are libadwaita's own -- an // `AdwEntryRow`, an `AdwSpinRow`, an `AdwSwitchRow`, an `AdwComboRow` and an `AdwButtonRow` -- so this // file is only two things: how a `Settings` gets *into* the rows, and how a change to a row gets // written back out. // // **A change applies as it is made.** There is no Apply button: a preferences row that applies when it // changes is what these widgets are built around, and the two settings that cannot take effect at once // -- the cache directory and relative time, both of which are planned before any source is known -- say // so in their subtitles instead of being held back by a button. The writer is a few bytes to one file, // so a keystroke in the directory row is not worth batching. // // The form has no owner either: the application's menu opens it, and so does the source chooser, which // runs before the application's widgets exist. What a change *means* is therefore the caller's // business -- `PrefsForm::on_changed` -- because it means different things on the two sides. #ifndef UDPSCOPE_UI_PREFS_HPP #define UDPSCOPE_UI_PREFS_HPP #include #include #include #include #include "settings.hpp" /// Whether a row asks for `use-underline` -- the property (default FALSE) that makes a `_` in a row's title /// a mnemonic. Without it GTK draws the underscore as a literal character, which is what `_Directory` and the /// other row titles did. /// /// Asked for by *name* on a plain `GObject`: the row classes that carry the property are not all related /// (`AdwEntryRow`'s `use-underline` is its own, `AdwSpinRow`/`AdwComboRow`/`AdwSwitchRow`'s come through /// `AdwActionRow`), so a common getter or a cast to one row type would miss half of them -- and passing a /// row as a `GtkWidget*` where the type is only forward-declared does not even compile. A class without the /// property answers false rather than crashing. inline bool row_underlines(GObject* row) { if (row == nullptr) return false; if (g_object_class_find_property(G_OBJECT_GET_CLASS(row), "use-underline") == nullptr) return false; gboolean on = FALSE; g_object_get(row, "use-underline", &on, nullptr); return on != FALSE; } namespace udpscope { namespace ui_impl { /// One preferences dialog's widgets, owned by whoever opened it. /// /// Every pointer is valid only while `dialog` is non-null: the close path clears the lot, so a handler /// that fires after the dialog went away reads null rather than freed memory. struct PrefsForm { AdwDialog* dialog = nullptr; /// The builder the dialog came from, replaced on every opening: the dialog belongs to the builder's /// object list and closing destroys it, so the file is parsed again rather than a dead dialog fetched. GtkBuilder* builder = nullptr; /// The rows, typed. A switch is read with `adw_switch_row_get_active()` and not off a widget. AdwEntryRow* dir_row = nullptr; AdwSpinRow* keep_row = nullptr; AdwSwitchRow* relative_row = nullptr; AdwSpinRow* memory_row = nullptr; AdwComboRow* theme_row = nullptr; /// The folder chooser beside the directory row. A text field alone asks a user to know a path and /// type it correctly; the chooser is how they find one, and what it returns goes into that row -- so /// the text stays the single value and there is nothing to keep in step. GtkWidget* browse = nullptr; /// The window the file chooser is transient for, taken from whatever opened this form. /// /// Stored because an `AdwDialog` is **not** a `GtkWindow` -- it is a `GObject` that a window presents /// -- so the dialog cannot parent the chooser, and the caller's window is the only one there is. GtkWindow* parent = nullptr; /// True while the rows are being filled from the settings, so filling them cannot write them back: a /// row's change signal fires for a programmatic change exactly as it does for a typed one. /// The pending folder chooser's cancellable, so a chooser still open when this form goes away cannot /// call back into it. Cancelled by the `closed` handler, and replaced when another chooser opens -- /// two open pickers would each write the row when they answered. /// /// A probe cancels it to take the *cancel* path: a user's own Cancel button arrives in the same /// callback with the same null answer. GCancellable* browse_cancel = nullptr; /// True while the rows are being filled from the settings, so filling them cannot write them back: a /// row's change signal fires for a programmatic change exactly as it does for a typed one. bool filling = false; /// Called on every change, with what the rows now hold and whether it reached the file. /// /// It runs once per keystroke in the directory row, which is why it should report *failures* rather /// than announce every success. Empty is allowed. std::function on_changed; }; /// Opens the preferences for `form`, transient for `parent` (which may be null), starting from /// `current`. Called while the dialog is up it raises that dialog instead of building a second one. void prefs_open(PrefsForm& form, GtkWidget* parent, const Settings& current, std::function on_changed); /// Closes it. Safe to call when it is not open. void prefs_close(PrefsForm& form); /// Puts the interface into the named theme at once. Separate from the form because it is the one /// preference with nothing to plan: it applies to windows that are already on screen. void apply_theme(const std::string& name); /// Sets every row the way a user would, which applies each one as it is set: the self-check's two entry /// points both end up here, so the app's probe and the chooser's probe cannot drift apart in what they /// exercise. void prefs_probe_change(PrefsForm& form); } // namespace ui_impl } // namespace udpscope #endif // UDPSCOPE_UI_PREFS_HPP