From 775f71def0d73cb8d20e9174795f794d07289116 Mon Sep 17 00:00:00 2001 From: lenisko <10072920+lenisko@users.noreply.github.com> Date: Sat, 23 May 2026 01:58:40 +0200 Subject: [PATCH 1/7] feat: user-editable keyboard shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Settings → Actions pane that rebinds every keyboard shortcut in FlowVision (~90 actions, ~120 chord slots). Defaults are bit-identical to the previous hardcoded dispatcher. Architecture - ShortcutAction: enum of every rebindable behavior. - ShortcutDef: single-source metadata table (category, scope, English name, default chords, firesDuringSearch, isSystemConventional, hasLockedPrimary). Built once at first access, cached. - KeyChord: layout-independent (keyCode, modifiers); character is display-only and ignored by hash/eq so the same physical key matches across layouts. - ShortcutStore: bindings + reverse-index, persisted to UserDefaults under shortcutBindings.v2. Posts .shortcutsChanged on mutation. v1 (single-chord) blobs auto-migrate; legacy key only dropped after a successful v2 write. - ShortcutRecorderView: NSView capturing a chord via first-responder. Bare Esc clears; right-click → Clear. - KeyShortcutManager (rewritten): window gate → non-rebindable text input → text-system fallthrough → 0.1s debounce → non-rebindable navigation → reverse-index chord lookup gated by scope + state. - AppDelegate.syncMenuShortcuts: walks main menu, applies current keyEquivalents to items whose identifier matches an action rawValue. Bare-letter and Fn-only chords skip menu sync to avoid AppKit hijacking text input. Scope model - browserOnly fires only when \!isInLargeView. - viewerOnly fires only in viewer. - both fires always. - Cross-scope collisions are intentional (W = browser up + viewer zoom-in, A/D = browser nav + viewer prev/next, F = sidebar + mirror, etc.). Conflict handling - Same-scope duplicate → "Shortcut conflict — reassign?" with one-pass strip of the blocker. - macOS HIG convention chord on non-system-conventional action → override confirm. - Locked primary default of another action (currently only Cmd+Q for quitApp) → hard reject, drop pending slot. - hasLockedPrimary actions render the primary recorder disabled; extras remain bindable. Non-rebindable surfaces (by design) - Quick-search letter input. - Cmd+ACVXZ + Home/End in OCR / rename text fields (delegated to the text system). - F2 / Enter rename, Tab focus swap, outline + grid arrow nav (context-dynamic, not keystroke-driven). Adding a new action: case in enum, case in buildDef, case in dispatch, optional storyboard identifier. Compiler enforces all of them via exhaustive switches. --- FlowVision.xcodeproj/project.pbxproj | 24 + .../Resources/Base.lproj/Main.storyboard | 46 +- FlowVision/Sources/AppDelegate.swift | 58 +- .../ActionsSettingsViewController.swift | 579 +++++- FlowVision/Sources/Shortcuts/KeyChord.swift | 233 +++ .../Sources/Shortcuts/ShortcutAction.swift | 628 +++++++ .../Shortcuts/ShortcutRecorderView.swift | 153 ++ .../Sources/Shortcuts/ShortcutStore.swift | 276 +++ .../ViewControllerExtension/KeyShortcut.swift | 1641 +++++++---------- 9 files changed, 2669 insertions(+), 969 deletions(-) create mode 100644 FlowVision/Sources/Shortcuts/KeyChord.swift create mode 100644 FlowVision/Sources/Shortcuts/ShortcutAction.swift create mode 100644 FlowVision/Sources/Shortcuts/ShortcutRecorderView.swift create mode 100644 FlowVision/Sources/Shortcuts/ShortcutStore.swift diff --git a/FlowVision.xcodeproj/project.pbxproj b/FlowVision.xcodeproj/project.pbxproj index 1f540927..17653e6c 100644 --- a/FlowVision.xcodeproj/project.pbxproj +++ b/FlowVision.xcodeproj/project.pbxproj @@ -81,6 +81,10 @@ F7F600482E1E095500ED8536 /* FinderTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F600472E1E094A00ED8536 /* FinderTag.swift */; }; F7FAE0012F6BA00000FAE001 /* FavoritesPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FAE0002F6BA00000FAE000 /* FavoritesPopoverViewController.swift */; }; F7FED5F32F7D0B1100E35164 /* Tagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FED5F22F7D0B0E00E35164 /* Tagging.swift */; }; + F7CD001C2F0E000000C9AB85 /* KeyChord.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CD001B2F0E000000C9AB85 /* KeyChord.swift */; }; + F7CD001E2F0E000000C9AB85 /* ShortcutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CD001D2F0E000000C9AB85 /* ShortcutAction.swift */; }; + F7CD00202F0E000000C9AB85 /* ShortcutStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CD001F2F0E000000C9AB85 /* ShortcutStore.swift */; }; + F7CD00222F0E000000C9AB85 /* ShortcutRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CD00212F0E000000C9AB85 /* ShortcutRecorderView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -197,6 +201,10 @@ F7F600472E1E094A00ED8536 /* FinderTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinderTag.swift; sourceTree = ""; }; F7FAE0002F6BA00000FAE000 /* FavoritesPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPopoverViewController.swift; sourceTree = ""; }; F7FED5F22F7D0B0E00E35164 /* Tagging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tagging.swift; sourceTree = ""; }; + F7CD001B2F0E000000C9AB85 /* KeyChord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChord.swift; sourceTree = ""; }; + F7CD001D2F0E000000C9AB85 /* ShortcutAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutAction.swift; sourceTree = ""; }; + F7CD001F2F0E000000C9AB85 /* ShortcutStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutStore.swift; sourceTree = ""; }; + F7CD00212F0E000000C9AB85 /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutRecorderView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -274,6 +282,7 @@ F7790ECB2BA5CA9200406D35 /* WindowController.swift */, F7A075552BA1D716009C47A6 /* ViewController.swift */, F770988F2F0E3C5400E7D164 /* ViewControllerExtension */, + F7CD001A2F0E000000C9AB85 /* Shortcuts */, F73865DC2C37BD7F00837FE4 /* Common */, F7C2DEB92C4E6BB2003DF765 /* SettingsViews */, F73865E92C37C3EA00837FE4 /* Views */, @@ -281,6 +290,17 @@ path = Sources; sourceTree = ""; }; + F7CD001A2F0E000000C9AB85 /* Shortcuts */ = { + isa = PBXGroup; + children = ( + F7CD001B2F0E000000C9AB85 /* KeyChord.swift */, + F7CD001D2F0E000000C9AB85 /* ShortcutAction.swift */, + F7CD001F2F0E000000C9AB85 /* ShortcutStore.swift */, + F7CD00212F0E000000C9AB85 /* ShortcutRecorderView.swift */, + ); + path = Shortcuts; + sourceTree = ""; + }; F754F3C62C10393700A25D12 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -526,6 +546,10 @@ F74D554E2F0E459C00C9AB85 /* WindowManagement.swift in Sources */, F73865D32C37B97500837FE4 /* CustomCollectionView.swift in Sources */, F74D555E2F0E52F800C9AB85 /* ArrowKeyLocate.swift in Sources */, + F7CD001C2F0E000000C9AB85 /* KeyChord.swift in Sources */, + F7CD001E2F0E000000C9AB85 /* ShortcutAction.swift in Sources */, + F7CD00202F0E000000C9AB85 /* ShortcutStore.swift in Sources */, + F7CD00222F0E000000C9AB85 /* ShortcutRecorderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FlowVision/Resources/Base.lproj/Main.storyboard b/FlowVision/Resources/Base.lproj/Main.storyboard index 4edffd50..7457a96c 100644 --- a/FlowVision/Resources/Base.lproj/Main.storyboard +++ b/FlowVision/Resources/Base.lproj/Main.storyboard @@ -54,7 +54,7 @@ - + @@ -66,22 +66,22 @@ - - + @@ -211,7 +211,7 @@ Gw - + @@ -670,14 +670,14 @@ Gw - + - + @@ -691,33 +691,33 @@ Gw - + - + - + - + - + @@ -743,19 +743,19 @@ Gw - + - + - + @@ -767,7 +767,7 @@ Gw - + @@ -818,25 +818,25 @@ Gw - + - + - + -