diff --git a/CLAUDE.md b/CLAUDE.md index 78e5aac..c1039df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,18 @@ form for a new host. Every probe below passes `--server`, so none of them sees i (`PLOT_BUILD_GTK4`); its tests and examples are forced off. `deps/udpsclient` is a vendored C99 source pair compiled into a static lib. +**libadwaita ≥ 1.8 is a system dependency** (`pkg_check_modules(ADWAITA ... libadwaita-1>=1.8)`), +for exactly one widget: the shortcut overview is an `AdwShortcutsDialog`. GTK4's own +`GtkShortcutsWindow` is deprecated as of 4.18 and that is its replacement. Two consequences +worth knowing before editing. `adw_init()` is called — in `run_ui()` and, since the chooser +runs first and must be styled the same, in `choose_source()` in place of its `gtk_init()` +(both are idempotent) — so **the whole app is drawn with libadwaita's stylesheet**, not +GTK's: a metric change there reaches every widget, and `layout over N frames` / +`axes.w changes` is what says whether the layout still settles. And an `AdwDialog` is +**not** a `GtkWindow`: `show_shortcuts()` keeps an `AdwDialog*`, clears it on `::closed` +(there is no `::close-request`) and closes it with `adw_dialog_close()`. Presenting it on +this app's plain `GtkApplicationWindow` is supported and warning-free; it was checked. + `deps/plotter` is upstream's own repository and this app's figure support was added there rather than worked around here: `Figure::set_min_y_gutter()` (a floor under the common left gutter), and in the GTK4 adapter `PlotArea`'s **figure mode** — @@ -1253,6 +1265,16 @@ far the largest file. `gtk_icon_theme_has_icon()` is checked for every toolbar button and reported by the dump (see `toolbar icons:` under Verification): a resource path that was never added does not raise an error, it draws `image-missing`. +- **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has + none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them + with `gtk_accelerator_parse()` — so they are `"space"`, `"e"`, `"slash"`, + `"plus minus"` (a space-separated pair is the "either of these" form the label draws as + `+ / -`), never the prose `"Ctrl+E"` or `"+ / -"`. Neither GTK's syntax nor libadwaita's + knows a **wheel or a drag**: `"Wheel"` costs a `Failed to parse` warning and draws an empty + cell. Those six rows therefore carry `accel = ""` and put the prose in `subtitle`, which is + the one field that is free text. A row added with a parse error is otherwise silent — the + overview still opens and the row is simply blank — so read the probe run's stderr for + `Failed to parse` after touching either table. - **The chooser runs before `ScopeModel` exists, and `--server` is what decides.** `Config` carries `server_specified`, set only by the `--server` branch, and `main()` asks only when it is false — so declining costs no buffer and no socket, and `ScopeModel`, `Acquisition`, `RowSet` and diff --git a/CMakeLists.txt b/CMakeLists.txt index c6d7974..9594037 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,15 @@ endif() find_package(Threads REQUIRED) +# --- libadwaita: the shortcut overview --------------------------------------- +# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's +# `AdwShortcutsDialog` (1.8) is its replacement, so the overview is built from the +# platform widget rather than from a hand-rolled grid. This is the only thing libadwaita +# is used for; `adw_init()` is called for it, which also means the app picks up +# libadwaita's stylesheet. +find_package(PkgConfig REQUIRED) +pkg_check_modules(ADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.8) + # --- deps/plotter: built from the submodule, GTK4 adapter only ---------------- set(PLOT_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(PLOT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) @@ -109,7 +118,8 @@ add_executable(udpscope ) target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) -target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads) +target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads + PkgConfig::ADWAITA) add_dependencies(udpscope udpscope_build_info) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic) diff --git a/src/help_window.cpp b/src/help_window.cpp index 1982f5f..a9065f3 100644 --- a/src/help_window.cpp +++ b/src/help_window.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT #include "help_window.hpp" +#include + #include #include @@ -15,7 +17,10 @@ namespace { /// One pointer per window, file-scope because there is one application. GtkWidget* g_about = nullptr; GtkWidget* g_help = nullptr; -GtkWidget* g_shortcuts = nullptr; + +/// The shortcut overview is an `AdwDialog`, not a window, so it has its own pointer and +/// its own clearing signal (`::closed` rather than `::close-request`). +AdwDialog* g_shortcuts = nullptr; gboolean on_window_close(GtkWindow* window, gpointer slot) { (void)window; @@ -23,6 +28,12 @@ gboolean on_window_close(GtkWindow* window, gpointer slot) { return FALSE; // let the default handler destroy it } +void on_shortcuts_closed(AdwDialog* dialog, gpointer user) { + (void)dialog; + (void)user; + g_shortcuts = nullptr; +} + struct HelpPage { const char* section; ///< the group heading in the sidebar const char* title; ///< the row's label and the page's heading @@ -216,91 +227,65 @@ void show_help(GtkWindow* parent) { namespace { -/// One `accel description` pair appended to `grid` at `row`. The accel column is -/// monospaced and right-aligned so the descriptions line up down the window. -void append_shortcut_row(GtkWidget* grid, int row, const char* accel_text, - const char* description) { - GtkWidget* keys = gtk_label_new(nullptr); - char markup[256]; - std::snprintf(markup, sizeof(markup), "%s", accel_text); - gtk_label_set_markup(GTK_LABEL(keys), markup); - gtk_label_set_xalign(GTK_LABEL(keys), 1.0f); - gtk_widget_set_margin_end(keys, 14); - gtk_grid_attach(GTK_GRID(grid), keys, 0, row, 1, 1); - - GtkWidget* what = gtk_label_new(description); - gtk_label_set_xalign(GTK_LABEL(what), 0.0f); - gtk_grid_attach(GTK_GRID(grid), what, 1, row, 1, 1); -} - -/// A section heading spanning both columns. -void append_shortcut_heading(GtkWidget* grid, int row, const char* section) { - GtkWidget* heading = gtk_label_new(nullptr); - char markup[128]; - std::snprintf(markup, sizeof(markup), "%s", section); - gtk_label_set_markup(GTK_LABEL(heading), markup); - gtk_label_set_xalign(GTK_LABEL(heading), 0.0f); - gtk_widget_set_margin_top(heading, row == 0 ? 0 : 14); - gtk_widget_set_margin_bottom(heading, 4); - gtk_grid_attach(GTK_GRID(grid), heading, 0, row, 2, 1); +/// Appends one table row to `dialog`, opening a new section whenever the row's `section` +/// string differs from the last one. `*current` is the section being filled, `*title` the +/// name it carries; both are owned by the caller so the two tables share one run. +/// +/// `accel` is handed to `AdwShortcutsItem` as an accelerator when it is non-empty and +/// `subtitle` is used when it is not, which is how a wheel or a drag gets a row: neither +/// GTK's nor libadwaita's accelerator syntax can name one, and asking them to parse +/// "Wheel" costs a parse warning and draws nothing. +void append_shortcut(AdwShortcutsDialog* dialog, AdwShortcutsSection** current, + const char** title, const char* section, const char* accel, + const char* subtitle, const char* description) { + if (*current == nullptr || std::string(*title) != section) { + *current = adw_shortcuts_section_new(section); + *title = section; + adw_shortcuts_dialog_add(dialog, *current); + } + AdwShortcutsItem* item = adw_shortcuts_item_new(description, accel); + if (subtitle != nullptr && *subtitle != '\0') { + adw_shortcuts_item_set_subtitle(item, subtitle); + } + adw_shortcuts_section_add(*current, item); } } // namespace void show_shortcuts(GtkWindow* parent) { if (g_shortcuts != nullptr) { - gtk_window_present(GTK_WINDOW(g_shortcuts)); + adw_dialog_present(g_shortcuts, GTK_WIDGET(parent)); return; } - GtkWidget* window = gtk_window_new(); - g_shortcuts = window; - gtk_window_set_title(GTK_WINDOW(window), "Keyboard and mouse shortcuts"); - gtk_window_set_default_size(GTK_WINDOW(window), 560, 680); - gtk_window_set_transient_for(GTK_WINDOW(window), parent); + AdwDialog* dialog = adw_shortcuts_dialog_new(); + g_shortcuts = dialog; + adw_dialog_set_title(dialog, "Keyboard and mouse shortcuts"); - GtkWidget* grid = gtk_grid_new(); - gtk_grid_set_row_spacing(GTK_GRID(grid), 2); - gtk_widget_set_margin_start(grid, 20); - gtk_widget_set_margin_end(grid, 20); - gtk_widget_set_margin_top(grid, 16); - gtk_widget_set_margin_bottom(grid, 16); - - int row = 0; - const char* section = nullptr; + AdwShortcutsSection* section = nullptr; + const char* section_title = nullptr; int accel_count = 0; const Accel* accels = app_accels(&accel_count); for (int i = 0; i < accel_count; ++i) { - if (section == nullptr || std::string(section) != accels[i].section) { - section = accels[i].section; - append_shortcut_heading(grid, row++, section); - } - append_shortcut_row(grid, row++, accels[i].accel_text, accels[i].description); + append_shortcut(ADW_SHORTCUTS_DIALOG(dialog), §ion, §ion_title, + accels[i].section, accels[i].accel, "", accels[i].description); } int gesture_count = 0; const Gesture* gestures = plot_gestures(&gesture_count); for (int i = 0; i < gesture_count; ++i) { - if (section == nullptr || std::string(section) != gestures[i].section) { - section = gestures[i].section; - append_shortcut_heading(grid, row++, section); - } - append_shortcut_row(grid, row++, gestures[i].accel_text, gestures[i].description); + append_shortcut(ADW_SHORTCUTS_DIALOG(dialog), §ion, §ion_title, + gestures[i].section, gestures[i].accel, gestures[i].subtitle, + gestures[i].description); } - GtkWidget* scroll = gtk_scrolled_window_new(); - gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, - GTK_POLICY_AUTOMATIC); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), grid); - gtk_window_set_child(GTK_WINDOW(window), scroll); - - g_signal_connect(window, "close-request", G_CALLBACK(on_window_close), &g_shortcuts); - gtk_window_present(GTK_WINDOW(window)); + g_signal_connect(dialog, "closed", G_CALLBACK(on_shortcuts_closed), nullptr); + adw_dialog_present(dialog, GTK_WIDGET(parent)); } void close_help_windows() { if (g_help != nullptr) gtk_window_close(GTK_WINDOW(g_help)); if (g_about != nullptr) gtk_window_close(GTK_WINDOW(g_about)); - if (g_shortcuts != nullptr) gtk_window_close(GTK_WINDOW(g_shortcuts)); + if (g_shortcuts != nullptr) adw_dialog_close(g_shortcuts); } } // namespace udpscope diff --git a/src/shortcuts.cpp b/src/shortcuts.cpp index a0c54d2..d225189 100644 --- a/src/shortcuts.cpp +++ b/src/shortcuts.cpp @@ -9,47 +9,52 @@ namespace { /// Bare letters are toggles, Ctrl is for files and windows. Nothing here shadows a key the /// plot's own controller handles ('+', '-', 'f', the arrows, Escape). constexpr Accel kAccels[] = { - {GDK_KEY_space, 0, AppAction::PauseToggle, "Space", "Pause or resume the display", + {GDK_KEY_space, 0, AppAction::PauseToggle, "space", "Pause or resume the display", "Acquisition"}, - {GDK_KEY_a, 0, AppAction::TriggerArm, "A", "Arm or disarm the trigger", "Acquisition"}, - {GDK_KEY_t, 0, AppAction::TriggerPanel, "T", "Show or hide the trigger panel", + {GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"}, + {GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Show or hide the trigger panel", "Acquisition"}, - {GDK_KEY_c, 0, AppAction::CursorsPane, "C", "Show or hide the cursors pane", + {GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Show or hide the cursors pane", "Panes and editors"}, - {GDK_KEY_m, 0, AppAction::MeasuresPane, "M", "Show or hide the measurements pane", + {GDK_KEY_m, 0, AppAction::MeasuresPane, "m", "Show or hide the measurements pane", "Panes and editors"}, - {GDK_KEY_g, 0, AppAction::MathPane, "G", "Show or hide the math pane", + {GDK_KEY_g, 0, AppAction::MathPane, "g", "Show or hide the math pane", "Panes and editors"}, - {GDK_KEY_l, 0, AppAction::LayoutEditor, "L", "Show or hide the layout editor", + {GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor", "Panes and editors"}, - {GDK_KEY_s, 0, AppAction::SidePanel, "S", "Show or hide the side panel", + {GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel", "Panes and editors"}, - {GDK_KEY_e, GDK_CONTROL_MASK, AppAction::Export, "Ctrl+E", + {GDK_KEY_e, GDK_CONTROL_MASK, AppAction::Export, "e", "Export the visible range", "Files and windows"}, {GDK_KEY_F1, 0, AppAction::Help, "F1", "Open the help window", "Files and windows"}, - {GDK_KEY_slash, GDK_CONTROL_MASK, AppAction::Shortcuts, "Ctrl+/", + {GDK_KEY_slash, GDK_CONTROL_MASK, AppAction::Shortcuts, "slash", "Open this shortcut overview", "Files and windows"}, - {GDK_KEY_q, GDK_CONTROL_MASK, AppAction::Quit, "Ctrl+Q", "Quit udpscope", + {GDK_KEY_q, GDK_CONTROL_MASK, AppAction::Quit, "q", "Quit udpscope", "Files and windows"}, }; constexpr int kAccelCount = static_cast(sizeof(kAccels) / sizeof(kAccels[0])); /// The plot's own interactions, documentation only. The keyboard's modifiers are not the /// wheel's: '+' is shift+'=' on common layouts, so Shift could not mean anything there. +/// +/// A space-separated accelerator is the two-key alternative form the shortcut label draws +/// as `+ / -`. The mouse rows carry no accelerator at all: neither GTK's accelerator +/// syntax nor libadwaita's knows a wheel or a drag, and asking either to parse "Wheel" +/// is a parse warning and an empty cell. Those rows are prose in the subtitle instead. constexpr Gesture kGestures[] = { - {"+ / -", "Zoom in or out, both axes", "Plot keyboard"}, - {"Ctrl + / Ctrl -", "Zoom the time axis only", "Plot keyboard"}, - {"Alt + / Alt -", "Zoom the value axis only", "Plot keyboard"}, - {"Left / Right", "Pan along time", "Plot keyboard"}, - {"Up / Down", "Pan along values", "Plot keyboard"}, - {"F", "Fit the view to the data", "Plot keyboard"}, - {"Escape", "Cancel the gesture in progress", "Plot keyboard"}, - {"Wheel", "Zoom about the pointer, both axes", "Plot mouse"}, - {"Shift+wheel", "Zoom the time axis only", "Plot mouse"}, - {"Ctrl+wheel", "Zoom the value axis only", "Plot mouse"}, - {"Drag", "Pan", "Plot mouse"}, - {"Ctrl+drag", "Box zoom on time", "Plot mouse"}, - {"Alt+drag", "Box zoom on values", "Plot mouse"}, + {"plus minus", "", "Zoom in or out, both axes", "Plot keyboard"}, + {"plus minus", "", "Zoom the time axis only", "Plot keyboard"}, + {"plus minus", "", "Zoom the value axis only", "Plot keyboard"}, + {"Left Right", "", "Pan along time", "Plot keyboard"}, + {"Up Down", "", "Pan along values", "Plot keyboard"}, + {"f", "", "Fit the view to the data", "Plot keyboard"}, + {"Escape", "", "Cancel the gesture in progress", "Plot keyboard"}, + {"", "Wheel", "Zoom about the pointer, both axes", "Plot mouse"}, + {"", "Shift and wheel", "Zoom the time axis only", "Plot mouse"}, + {"", "Ctrl and wheel", "Zoom the value axis only", "Plot mouse"}, + {"", "Drag", "Pan", "Plot mouse"}, + {"", "Ctrl and drag", "Box zoom on time", "Plot mouse"}, + {"", "Alt and drag", "Box zoom on values", "Plot mouse"}, }; constexpr int kGestureCount = static_cast(sizeof(kGestures) / sizeof(kGestures[0])); diff --git a/src/shortcuts.hpp b/src/shortcuts.hpp index 407d948..bf9fec1 100644 --- a/src/shortcuts.hpp +++ b/src/shortcuts.hpp @@ -22,10 +22,10 @@ enum class AppAction { /// One application accelerator: what to press, what it does, and where the overview lists it. struct Accel { - unsigned keyval; ///< GDK keyval - unsigned mods; ///< GdkModifierType mask, 0 for a bare key + unsigned keyval; ///< GDK keyval + unsigned mods; ///< GdkModifierType mask, 0 for a bare key AppAction action; - const char* accel_text; ///< what the overview prints + const char* accel; ///< GTK accelerator syntax, as `gtk_accelerator_parse()` reads it const char* description; const char* section; }; @@ -33,7 +33,8 @@ struct Accel { /// One documented plot interaction. These have no AppAction: the plotter's own controller /// implements them, and this table exists so the overview can list them. struct Gesture { - const char* accel_text; + const char* accel; ///< GTK accelerator syntax, or "" for a pointer gesture + const char* subtitle; ///< prose for what no accelerator can express ("Wheel"), else "" const char* description; const char* section; }; diff --git a/src/source_dialog.cpp b/src/source_dialog.cpp index 0111f65..7bc15e3 100644 --- a/src/source_dialog.cpp +++ b/src/source_dialog.cpp @@ -5,6 +5,7 @@ #include "layout_store.hpp" +#include #include #include @@ -351,7 +352,10 @@ bool choose_source(Config& cfg) { return false; } - gtk_init(); + // `adw_init()` rather than `gtk_init()`: it does everything the latter does and also + // installs libadwaita's stylesheet, which the shortcut overview's `AdwShortcutsDialog` + // is drawn with. Both are idempotent, so `run_ui()` calling it again costs nothing. + adw_init(); Chooser c; c.cfg = &cfg; diff --git a/src/ui.cpp b/src/ui.cpp index baa045a..eabfe86 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -25,6 +25,7 @@ #include "model.hpp" #include "plot_host.hpp" +#include #include #include @@ -7293,6 +7294,9 @@ gboolean help_probe(gpointer user) { int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq, RowSet& rows) { (void)argc; + // Installs libadwaita's stylesheet, which the shortcut overview's `AdwShortcutsDialog` + // needs to be drawn at all. Idempotent, and `choose_source()` may already have run it. + adw_init(); App app; app.model = &model; app.acq = &acq;