refactor: build the shortcut overview from AdwShortcutsDialog

The hand-rolled grid existed to avoid GTK's deprecated GtkShortcutsWindow;
libadwaita 1.8's AdwShortcutsDialog is the supported replacement, so use the
platform widget and get its search, sectioning and shortcut labels for free.

The table strings become GTK accelerator syntax, since the shortcut label parses
them. The six mouse rows cannot: no accelerator syntax names a wheel or a drag,
so they carry an empty accelerator and prose in the new subtitle field.

adw_init() is called in run_ui() and in choose_source() (replacing its
gtk_init()), which means the whole app is drawn with libadwaita's stylesheet.
Verified settled: viewport.w 870..870, axes.w changes 2, x-desync 0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-04 15:34:03 +02:00
1 parent 018b98eddf
commit d65a121a9a
7 files changed
+123 -92

No files matched your search

+47 -62
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
#include "help_window.hpp"
#include <adwaita.h>
#include <cstdio>
#include <string>
@@ -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), "<tt>%s</tt>", 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), "<b>%s</b>", 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), &section, &section_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), &section, &section_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
+29 -24
View File
@@ -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, "<Control>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, "<Control>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, "<Control>q", "Quit udpscope",
"Files and windows"},
};
constexpr int kAccelCount = static_cast<int>(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"},
{"<Control>plus <Control>minus", "", "Zoom the time axis only", "Plot keyboard"},
{"<Alt>plus <Alt>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<int>(sizeof(kGestures) / sizeof(kGestures[0]));
+5 -4
View File
@@ -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;
};
+5 -1
View File
@@ -5,6 +5,7 @@
#include "layout_store.hpp"
#include <adwaita.h>
#include <gtk/gtk.h>
#include <cstdio>
@@ -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;
+4
View File
@@ -25,6 +25,7 @@
#include "model.hpp"
#include "plot_host.hpp"
#include <adwaita.h>
#include <gtk/gtk.h>
#include <algorithm>
@@ -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;