diff --git a/src/spectrogram.c b/src/spectrogram.c index 7c3b068..ee513f7 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -129,6 +129,66 @@ void ResetForNewSignal(void) app.visibleTextureValid = false; } +// ============================================================================ +// Keymap — handlers + table + dispatcher. See spectrogram_types.h for the +// KeyBinding contract. Adding a global key = add one row here (and, if it needs +// to run at a specific point in the frame, leave action NULL and wire it inline). +// ============================================================================ + +static void ActionOpenBrowser(void) { app.showFileBrowser = true; ScanDirectory(GetWorkingDirectory()); } +static void ActionToggleScope(void) { app.showScope = !app.showScope; } +static void ActionToggleAbout(void) { app.showAbout = !app.showAbout; } +static void ActionToggleFullscreen(void){ ToggleFullscreen(); } +static void ActionExport(void) { ExportPNG(&app, app.exportDir); } + +static void ActionResetView(void) +{ + app.viewStart = 0.0f; app.viewEnd = 1.0f; + app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f; + app.visibleTextureValid = false; +} + +static void ActionZoomToStart(void) +{ + app.viewStart = 0.0f; + app.viewEnd = 0.1f; + app.visibleTextureValid = false; +} + +static const KeyBinding KEYMAP[] = { + { KEY_O, KEYGATE_MODAL, ActionOpenBrowser, "O", "open file browser" }, + { KEY_P, KEYGATE_NONE, ActionToggleScope, "P", "show / hide waveform scope" }, + { KEY_F1, KEYGATE_NONE, ActionToggleAbout, "F1", "about / help" }, + { KEY_F11, KEYGATE_NONE, ActionToggleFullscreen,"F11", "toggle fullscreen" }, + { KEY_HOME, KEYGATE_MODAL | KEYGATE_LOADED,ActionResetView, "Home", "reset view (fit all)" }, + { KEY_END, KEYGATE_MODAL | KEYGATE_LOADED,ActionZoomToStart, "End", "zoom to start" }, + { KEY_E, KEYGATE_MODAL | KEYGATE_STFT, ActionExport, "E", "export PNG" }, + // Order-sensitive: handled inline (see main loop), listed here for the overlay. + { KEY_SPACE, KEYGATE_NONE, NULL, "Space", "play / stop selection" }, + { KEY_ESCAPE,KEYGATE_NONE, NULL, "Esc", "clear selection / close dialog" }, +}; + +const KeyBinding* GetKeymap(int* count) +{ + *count = (int)(sizeof(KEYMAP) / sizeof(KEYMAP[0])); + return KEYMAP; +} + +// Run every gated, dispatchable binding whose key was pressed this frame. +static void DispatchKeymap(void) +{ + int n; + const KeyBinding* km = GetKeymap(&n); + for (int i = 0; i < n; i++) { + const KeyBinding* b = &km[i]; + if (!b->action) continue; + if ((b->gate & KEYGATE_MODAL) && UiModalOpen()) continue; + if ((b->gate & KEYGATE_LOADED) && !app.loaded) continue; + if ((b->gate & KEYGATE_STFT) && !app.stftComputed) continue; + if (IsKeyPressed(b->key)) b->action(); + } +} + // ============================================================================ // Main Application // ============================================================================ @@ -253,26 +313,9 @@ int main(int argc, char* argv[]) UnloadDroppedFiles(dropped); } - // File browser toggle - if (IsKeyPressed(KEY_O) && !UiModalOpen()) { - app.showFileBrowser = true; - ScanDirectory(GetWorkingDirectory()); - } - - // Scope view toggle (P key) - if (IsKeyPressed(KEY_P)) { - app.showScope = !app.showScope; - } - - // About / help dialog (F1) - if (IsKeyPressed(KEY_F1)) { - app.showAbout = !app.showAbout; - } - - // File browser update - if (app.showFileBrowser) { - // Browser handles its own input - } + // Global key bindings (table-driven; see KEYMAP/GetKeymap). The + // order-sensitive keys (Space, Esc) are handled inline further below. + DispatchKeymap(); // Check if playback finished naturally if (app.isPlaying && AudioPlaybackSound.frameCount > 0) { @@ -455,18 +498,6 @@ int main(int argc, char* argv[]) } } - // Home/End keys - if (IsKeyPressed(KEY_HOME)) { - app.viewStart = 0.0f; app.viewEnd = 1.0f; - app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f; - app.visibleTextureValid = false; - } - if (IsKeyPressed(KEY_END)) { - float newWidth = 0.1f; - app.viewStart = 0.0f; - app.viewEnd = newWidth; - app.visibleTextureValid = false; - } } // Keyboard shortcuts (SPACE for play/stop toggle, ESC for clear) @@ -490,11 +521,6 @@ int main(int argc, char* argv[]) } } - // Export PNG - if (IsKeyPressed(KEY_E) && !UiModalOpen() && app.stftComputed) { - ExportPNG(&app, app.exportDir); - } - if (IsKeyPressed(KEY_ESCAPE)) { if (app.showAbout) { app.showAbout = false; diff --git a/src/spectrogram_types.h b/src/spectrogram_types.h index 1b6b21e..a6c45a5 100644 --- a/src/spectrogram_types.h +++ b/src/spectrogram_types.h @@ -233,6 +233,32 @@ static inline bool UiModalOpen(void) return app.showFileBrowser || app.showAbout; } +// ============================================================================ +// Keymap — single source of truth for global key bindings. +// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose +// `action` is non-NULL and whose gate passes; the About/Help overlay renders +// the whole table so the on-screen key list can never drift from the bindings. +// Order-sensitive keys (Space, Esc) carry action==NULL and are handled inline +// where their frame ordering matters; they appear here for documentation only. +// ============================================================================ +typedef void (*KeyActionFn)(void); + +#define KEYGATE_NONE 0u +#define KEYGATE_MODAL 1u // skip while a modal overlay is open (!UiModalOpen()) +#define KEYGATE_LOADED 2u // require a loaded signal +#define KEYGATE_STFT 4u // require a computed STFT + +typedef struct { + int key; // raylib key code + unsigned gate; // KEYGATE_* bitmask + KeyActionFn action; // NULL = handled inline / documentation-only + const char* label; // shown in the help overlay, e.g. "O", "Home" + const char* help; // description for the help overlay +} KeyBinding; + +// Returns the keymap table and its entry count (defined in spectrogram.c). +const KeyBinding* GetKeymap(int* count); + // ============================================================================ // Small math helpers (header-inline so every module can use them) // ============================================================================ diff --git a/src/ui.c b/src/ui.c index b807fab..3990038 100644 --- a/src/ui.c +++ b/src/ui.c @@ -578,7 +578,7 @@ void DrawAboutDialog(void) DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.7f)); float pw = 560 * scale; - float ph = 470 * scale; + float ph = 540 * scale; Rectangle panel = { (sw - pw) / 2, (sh - ph) / 2, pw, ph }; DrawRectangleRec(panel, (Color){ 30, 30, 38, 255 }); DrawRectangleLinesEx(panel, 2, (Color){ 90, 90, 110, 255 }); @@ -586,7 +586,7 @@ void DrawAboutDialog(void) float px = panel.x + 24 * scale; float py = panel.y + 20 * scale; - DrawTextScaled("rspektrum — synchrosqueezed spectrogram viewer", px, py, 18, WHITE); + DrawTextScaled("rspektrum - synchrosqueezed spectrogram viewer", px, py, 18, WHITE); py += 34 * scale; // Body lines. NULL = blank spacer; headings start with no leading spaces. @@ -606,10 +606,6 @@ void DrawAboutDialog(void) " spectral magnitudes.", " - Pixels show synchrosqueezed (reassigned) energy, not raw FFT bins.", " Good to ~a dB for visualization; not lab-grade metrology.", - "", - "Keys", - " O open file P toggle scope Home/End reset / fit view", - " F1 / Esc close this dialog", }; int n = sizeof(lines) / sizeof(lines[0]); for (int i = 0; i < n; i++) { @@ -619,6 +615,20 @@ void DrawAboutDialog(void) py += (s[0] == '\0' ? 8 : 18) * scale; } + // Keys — rendered straight from the keymap table so this list can never + // drift from the actual bindings. + py += 10 * scale; + DrawTextScaled("Keys", px, py, 14, (Color){ 150, 200, 255, 255 }); + py += 20 * scale; + int kn; + const KeyBinding* km = GetKeymap(&kn); + for (int i = 0; i < kn; i++) { + DrawTextScaled(TextFormat(" %-5s %s", km[i].label, km[i].help), px, py, 13, LIGHTGRAY); + py += 16 * scale; + } + DrawTextScaled(" Mouse wheel = zoom, Alt+drag = pan, drag = select box", + px, py, 13, LIGHTGRAY); + DrawTextScaled("F1 / Esc / click to close", panel.x + pw - 196 * scale, panel.y + ph - 26 * scale, 12, GRAY); // Note: opening/closing is handled in the main input loop (not here) so the