Files
udpscope/src/ui_prefs.hpp
T
Martino Ferrari f00d161900 fix: row titles mean their mnemonics instead of drawing the underscore
Fifteen display strings in the two preference forms were written as GTK mnemonics -- `_Directory`,
`_Server`, `Multicast _group`, `_Theme` and the rest -- but the property that makes GTK read them was
never set, so GTK drew the underscore literally: `_Directory` in the row, not a D with a line under it.

Two switches are involved, FALSE by default in each case. `GtkLabel`/`GtkButton` take `use-underline`;
the rows take `AdwPreferencesRow:use-underline`, "whether an embedded underline in the title indicates a
mnemonic". The two buttons in the source form already set it (so their mnemonics worked and showed no
underscore); the thirteen rows did not. All thirteen now do.

The row classes that carry the property are not all related -- AdwEntryRow has its own, while AdwSpinRow,
AdwComboRow and AdwSwitchRow get it through AdwActionRow -- so the probe reads it by name on a GObject
(g_object_class_find_property, then g_object_get) rather than casting to one row type; a cast to a
`GtkWidget*` where the type is only forward-declared does not compile at all.

`_Cancel` and `_Connect` both claimed C in the same form, so Alt+C could only reach one of them: that
marker was unreachable, which is worse than absent because it is in the source to be trusted. `_Connect`
becomes `Conn_ect`, freeing C for Cancel.

[prefs-probe] reads the count (4/4) on the real widgets; check_ui validates the property against the GIR
for every object in every .ui file, inherited properties included.
2026-10-12 15:10:22 +02:00

115 lines
6.0 KiB
C++

// 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 <functional>
#include <string>
#include <adwaita.h>
#include <gtk/gtk.h>
#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;
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<void(const Settings&, bool written)> 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<void(const Settings&, bool written)> 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