diff --git a/packages/app/src/keyboard/shortcut-string.ts b/packages/app/src/keyboard/shortcut-string.ts index 2fc3e2e4a..0d5e8e669 100644 --- a/packages/app/src/keyboard/shortcut-string.ts +++ b/packages/app/src/keyboard/shortcut-string.ts @@ -43,6 +43,17 @@ KEY_MAP["ArrowLeft"] = { code: "ArrowLeft" }; KEY_MAP["ArrowRight"] = { code: "ArrowRight" }; KEY_MAP["ArrowUp"] = { code: "ArrowUp" }; KEY_MAP["ArrowDown"] = { code: "ArrowDown" }; +KEY_MAP["Tab"] = { code: "Tab" }; +KEY_MAP["Delete"] = { code: "Delete" }; +KEY_MAP["Home"] = { code: "Home" }; +KEY_MAP["End"] = { code: "End" }; +KEY_MAP["PageUp"] = { code: "PageUp" }; +KEY_MAP["PageDown"] = { code: "PageDown" }; +KEY_MAP["Insert"] = { code: "Insert" }; + +for (let i = 1; i <= 12; i++) { + KEY_MAP[`F${i}`] = { code: `F${i}` }; +} const CODE_TO_KEY: Record = {}; for (const [humanKey, mapping] of Object.entries(KEY_MAP)) { @@ -170,6 +181,15 @@ export function chordStringToShortcutKeys(s: string): ShortcutKey[][] { return s.split(" ").map(comboStringToShortcutKeys); } +export function heldModifiersFromEvent(event: KeyboardEvent): string | null { + const parts: string[] = []; + if (event.ctrlKey) parts.push("Ctrl"); + if (event.altKey) parts.push("Alt"); + if (event.shiftKey) parts.push("Shift"); + if (event.metaKey) parts.push("Cmd"); + return parts.length > 0 ? parts.join("+") : null; +} + export function keyboardEventToComboString(event: KeyboardEvent): string | null { if (MODIFIER_CODES.has(event.code)) { return null; diff --git a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx index 796b3f8cc..7f64c7e55 100644 --- a/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx +++ b/packages/app/src/screens/settings/keyboard-shortcuts-section.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { View, Text, Platform } from "react-native"; +import { useIsFocused } from "@react-navigation/native"; import { StyleSheet } from "react-native-unistyles"; import { settingsStyles } from "@/styles/settings"; import { Button } from "@/components/ui/button"; @@ -13,18 +14,30 @@ import { import { chordStringToShortcutKeys, comboStringToShortcutKeys, + heldModifiersFromEvent, keyboardEventToComboString, } from "@/keyboard/shortcut-string"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { getShortcutOs } from "@/utils/shortcut-platform"; import { getIsElectronRuntime } from "@/constants/layout"; -function ShortcutSequence({ chord }: { chord: string[] | null }) { - if (!chord || chord.length === 0) { +function ShortcutSequence({ + chord, + heldModifiers, +}: { + chord: string[] | null; + heldModifiers: string | null; +}) { + if ((!chord || chord.length === 0) && !heldModifiers) { return Press shortcut...; } - return ; + const displayCombos = [...(chord ?? [])]; + if (heldModifiers) { + displayCombos.push(heldModifiers); + } + + return ; } function ShortcutRow({ @@ -33,6 +46,7 @@ function ShortcutRow({ overrideCombo, isCapturing, capturedCombos, + heldModifiers, onRebind, onDone, onCancel, @@ -43,6 +57,7 @@ function ShortcutRow({ overrideCombo: string | undefined; isCapturing: boolean; capturedCombos: string[]; + heldModifiers: string | null; onRebind: () => void; onDone: () => void; onCancel: () => void; @@ -55,7 +70,7 @@ function ShortcutRow({ {row.label} {isCapturing ? ( - + ) : ( )} @@ -88,22 +103,32 @@ function ShortcutRow({ export function KeyboardShortcutsSection() { const [capturingBindingId, setCapturingBindingId] = useState(null); const [capturedCombos, setCapturedCombos] = useState([]); + const [heldModifiers, setHeldModifiers] = useState(null); const { overrides, hasOverrides, setOverride, removeOverride, resetAll } = useKeyboardShortcutOverrides(); const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut); + const isFocused = useIsFocused(); const isMac = getShortcutOs() === "mac"; const isDesktopApp = getIsElectronRuntime(); const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp }); + useEffect(() => { + if (!isFocused && capturingBindingId !== null) { + cancelCapture(); + } + }, [isFocused]); + function cancelCapture() { setCapturedCombos([]); + setHeldModifiers(null); setCapturingBindingId(null); setCapturingShortcut(false); } function startCapture(bindingId: string) { setCapturedCombos([]); + setHeldModifiers(null); setCapturingBindingId(bindingId); setCapturingShortcut(true); } @@ -132,9 +157,11 @@ export function KeyboardShortcutsSection() { const comboString = keyboardEventToComboString(event); if (comboString === null) { + setHeldModifiers(heldModifiersFromEvent(event)); return; } + setHeldModifiers(null); setCapturedCombos((current) => [...current, comboString]); } @@ -194,6 +221,7 @@ export function KeyboardShortcutsSection() { overrideCombo={overrideCombo} isCapturing={capturingBindingId === bindingId} capturedCombos={capturingBindingId === bindingId ? capturedCombos : []} + heldModifiers={capturingBindingId === bindingId ? heldModifiers : null} onRebind={() => { if (bindingId) { startCapture(bindingId);