feat: keyboard and mouse shortcut overview window
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e0d0de325
commit
018b98eddf
6 files changed
+248
-3
No files matched your search
@@ -103,6 +103,7 @@ add_executable(udpscope
|
||||
src/source_dialog.cpp
|
||||
src/measure.cpp
|
||||
src/math_channel.cpp
|
||||
src/shortcuts.cpp
|
||||
src/help_window.cpp
|
||||
src/ui.cpp
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "build_info.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "shortcuts.hpp"
|
||||
|
||||
namespace udpscope {
|
||||
namespace {
|
||||
@@ -14,6 +15,7 @@ namespace {
|
||||
/// One pointer per window, file-scope because there is one application.
|
||||
GtkWidget* g_about = nullptr;
|
||||
GtkWidget* g_help = nullptr;
|
||||
GtkWidget* g_shortcuts = nullptr;
|
||||
|
||||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||||
(void)window;
|
||||
@@ -212,9 +214,93 @@ void show_help(GtkWindow* parent) {
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void show_shortcuts(GtkWindow* parent) {
|
||||
if (g_shortcuts != nullptr) {
|
||||
gtk_window_present(GTK_WINDOW(g_shortcuts));
|
||||
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);
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
@@ -28,6 +28,11 @@ struct HelpReport {
|
||||
/// Loads and validates every page without building a window. Safe to call headless.
|
||||
HelpReport help_self_check();
|
||||
|
||||
/// The shortcut overview: the application accelerators and the plot's own interactions,
|
||||
/// in the sections the tables name. Hand-rolled because GtkShortcutsWindow is deprecated
|
||||
/// as of GTK 4.18 and this project builds warning-free.
|
||||
void show_shortcuts(GtkWindow* parent);
|
||||
|
||||
/// Closes whichever of the three windows are open, via gtk_window_close() so the
|
||||
/// close-request handlers run and the pointers are nulled. Used by the probe.
|
||||
void close_help_windows();
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#include "shortcuts.hpp"
|
||||
|
||||
#include <gdk/gdk.h>
|
||||
|
||||
namespace udpscope {
|
||||
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",
|
||||
"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",
|
||||
"Acquisition"},
|
||||
{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",
|
||||
"Panes and editors"},
|
||||
{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",
|
||||
"Panes and editors"},
|
||||
{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",
|
||||
"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+/",
|
||||
"Open this shortcut overview", "Files and windows"},
|
||||
{GDK_KEY_q, GDK_CONTROL_MASK, AppAction::Quit, "Ctrl+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.
|
||||
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"},
|
||||
};
|
||||
constexpr int kGestureCount = static_cast<int>(sizeof(kGestures) / sizeof(kGestures[0]));
|
||||
|
||||
/// The only modifiers a table row may require. Lock, Num-Lock and the pointer buttons all
|
||||
/// arrive in the same mask and must be masked out or no row ever matches.
|
||||
constexpr unsigned kModMask = GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_ALT_MASK;
|
||||
|
||||
} // namespace
|
||||
|
||||
const Accel* app_accels(int* count) {
|
||||
if (count != nullptr) *count = kAccelCount;
|
||||
return kAccels;
|
||||
}
|
||||
|
||||
const Gesture* plot_gestures(int* count) {
|
||||
if (count != nullptr) *count = kGestureCount;
|
||||
return kGestures;
|
||||
}
|
||||
|
||||
const Accel* find_accel(unsigned keyval, unsigned mods) {
|
||||
const unsigned wanted = mods & kModMask;
|
||||
for (const Accel& accel : kAccels) {
|
||||
if (accel.keyval == keyval && accel.mods == wanted) return &accel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
#pragma once
|
||||
|
||||
namespace udpscope {
|
||||
|
||||
/// Everything a key can ask the application to do. The plot's own actions (zoom, pan, fit)
|
||||
/// are not here: they belong to the plotter's input controller, which handles its own keys.
|
||||
enum class AppAction {
|
||||
PauseToggle,
|
||||
TriggerArm,
|
||||
TriggerPanel,
|
||||
CursorsPane,
|
||||
MeasuresPane,
|
||||
MathPane,
|
||||
LayoutEditor,
|
||||
SidePanel,
|
||||
Export,
|
||||
Help,
|
||||
Shortcuts,
|
||||
Quit,
|
||||
};
|
||||
|
||||
/// 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
|
||||
AppAction action;
|
||||
const char* accel_text; ///< what the overview prints
|
||||
const char* description;
|
||||
const char* section;
|
||||
};
|
||||
|
||||
/// 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* description;
|
||||
const char* section;
|
||||
};
|
||||
|
||||
/// The application accelerator table. Writes the row count to `count`.
|
||||
const Accel* app_accels(int* count);
|
||||
|
||||
/// The plot keyboard and mouse table. Writes the row count to `count`.
|
||||
const Gesture* plot_gestures(int* count);
|
||||
|
||||
/// The accelerator for `keyval` with `mods`, or nullptr. `mods` is masked to
|
||||
/// Ctrl/Shift/Alt before comparing, because Lock, Num-Lock and the pointer buttons
|
||||
/// arrive in the same mask and would otherwise never match a table row.
|
||||
const Accel* find_accel(unsigned keyval, unsigned mods);
|
||||
|
||||
} // namespace udpscope
|
||||
+24
-3
@@ -20,6 +20,7 @@
|
||||
#include "export.hpp"
|
||||
#include "help_window.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "shortcuts.hpp"
|
||||
#include "measure.hpp"
|
||||
#include "model.hpp"
|
||||
#include "plot_host.hpp"
|
||||
@@ -4104,6 +4105,15 @@ void on_help_clicked(GtkButton*, gpointer user) {
|
||||
show_help(GTK_WINDOW(app->window));
|
||||
}
|
||||
|
||||
void on_shortcuts_clicked(GtkButton*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->menu_button != nullptr) {
|
||||
GtkPopover* popover = gtk_menu_button_get_popover(GTK_MENU_BUTTON(app->menu_button));
|
||||
if (popover != nullptr) gtk_popover_popdown(popover);
|
||||
}
|
||||
show_shortcuts(GTK_WINDOW(app->window));
|
||||
}
|
||||
|
||||
// --- right-click menu -------------------------------------------------------
|
||||
|
||||
void on_y_auto_clicked(GtkButton* button, gpointer user) {
|
||||
@@ -5570,6 +5580,12 @@ GtkWidget* build_toolbar(App& app) {
|
||||
g_signal_connect(help_item, "clicked", G_CALLBACK(on_help_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), help_item);
|
||||
|
||||
GtkWidget* shortcuts_item = gtk_button_new_with_label("Keyboard shortcuts (Ctrl+/)");
|
||||
gtk_widget_set_tooltip_text(shortcuts_item,
|
||||
"every key and mouse gesture, including the plot's own");
|
||||
g_signal_connect(shortcuts_item, "clicked", G_CALLBACK(on_shortcuts_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), shortcuts_item);
|
||||
|
||||
GtkWidget* about_item = gtk_button_new_with_label("About udpscope");
|
||||
gtk_widget_set_tooltip_text(about_item, "version, build timestamp and settings paths");
|
||||
g_signal_connect(about_item, "clicked", G_CALLBACK(on_about_clicked), &app);
|
||||
@@ -7257,14 +7273,19 @@ gboolean help_probe(gpointer user) {
|
||||
|
||||
show_help(GTK_WINDOW(a->window));
|
||||
show_about(GTK_WINDOW(a->window), session);
|
||||
show_shortcuts(GTK_WINDOW(a->window));
|
||||
close_help_windows();
|
||||
|
||||
int accel_count = 0;
|
||||
int gesture_count = 0;
|
||||
(void)app_accels(&accel_count);
|
||||
(void)plot_gestures(&gesture_count);
|
||||
std::fprintf(stderr,
|
||||
"[help] pages=%d parsed=%d shortest='%s' %zu B; "
|
||||
"[help] pages=%d parsed=%d shortest='%s' %zu B; shortcuts: app=%d plot=%d; "
|
||||
"about version=%s commit=%s built=%s settings=%s; closed ok\n",
|
||||
report.pages, report.parsed, report.shortest_title.c_str(),
|
||||
report.shortest_bytes, build_info::kVersion, build_info::kCommit,
|
||||
build_info::kTimestamp, config_dir().c_str());
|
||||
report.shortest_bytes, accel_count, gesture_count, build_info::kVersion,
|
||||
build_info::kCommit, build_info::kTimestamp, config_dir().c_str());
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user