fix: show modifier keys and missing keys during shortcut rebinding
Add Tab, F1-F12, Delete, Home, End, PageUp, PageDown, Insert to the key map so they can be captured during rebinding. Show held modifier keys (Ctrl, Alt, Shift, Cmd) as live feedback matching VS Code's keydown-only approach — modifiers persist after release until the next keypress. Cancel capture when navigating away from settings.
This commit is contained in:
@@ -43,6 +43,17 @@ KEY_MAP["ArrowLeft"] = { code: "ArrowLeft" };
|
|||||||
KEY_MAP["ArrowRight"] = { code: "ArrowRight" };
|
KEY_MAP["ArrowRight"] = { code: "ArrowRight" };
|
||||||
KEY_MAP["ArrowUp"] = { code: "ArrowUp" };
|
KEY_MAP["ArrowUp"] = { code: "ArrowUp" };
|
||||||
KEY_MAP["ArrowDown"] = { code: "ArrowDown" };
|
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<string, string> = {};
|
const CODE_TO_KEY: Record<string, string> = {};
|
||||||
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
|
for (const [humanKey, mapping] of Object.entries(KEY_MAP)) {
|
||||||
@@ -170,6 +181,15 @@ export function chordStringToShortcutKeys(s: string): ShortcutKey[][] {
|
|||||||
return s.split(" ").map(comboStringToShortcutKeys);
|
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 {
|
export function keyboardEventToComboString(event: KeyboardEvent): string | null {
|
||||||
if (MODIFIER_CODES.has(event.code)) {
|
if (MODIFIER_CODES.has(event.code)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { View, Text, Platform } from "react-native";
|
import { View, Text, Platform } from "react-native";
|
||||||
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { settingsStyles } from "@/styles/settings";
|
import { settingsStyles } from "@/styles/settings";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -13,18 +14,30 @@ import {
|
|||||||
import {
|
import {
|
||||||
chordStringToShortcutKeys,
|
chordStringToShortcutKeys,
|
||||||
comboStringToShortcutKeys,
|
comboStringToShortcutKeys,
|
||||||
|
heldModifiersFromEvent,
|
||||||
keyboardEventToComboString,
|
keyboardEventToComboString,
|
||||||
} from "@/keyboard/shortcut-string";
|
} from "@/keyboard/shortcut-string";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
import { getIsElectronRuntime } from "@/constants/layout";
|
import { getIsElectronRuntime } from "@/constants/layout";
|
||||||
|
|
||||||
function ShortcutSequence({ chord }: { chord: string[] | null }) {
|
function ShortcutSequence({
|
||||||
if (!chord || chord.length === 0) {
|
chord,
|
||||||
|
heldModifiers,
|
||||||
|
}: {
|
||||||
|
chord: string[] | null;
|
||||||
|
heldModifiers: string | null;
|
||||||
|
}) {
|
||||||
|
if ((!chord || chord.length === 0) && !heldModifiers) {
|
||||||
return <Text style={styles.capturingText}>Press shortcut...</Text>;
|
return <Text style={styles.capturingText}>Press shortcut...</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Shortcut chord={chord.map(comboStringToShortcutKeys)} />;
|
const displayCombos = [...(chord ?? [])];
|
||||||
|
if (heldModifiers) {
|
||||||
|
displayCombos.push(heldModifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Shortcut chord={displayCombos.map(comboStringToShortcutKeys)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShortcutRow({
|
function ShortcutRow({
|
||||||
@@ -33,6 +46,7 @@ function ShortcutRow({
|
|||||||
overrideCombo,
|
overrideCombo,
|
||||||
isCapturing,
|
isCapturing,
|
||||||
capturedCombos,
|
capturedCombos,
|
||||||
|
heldModifiers,
|
||||||
onRebind,
|
onRebind,
|
||||||
onDone,
|
onDone,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -43,6 +57,7 @@ function ShortcutRow({
|
|||||||
overrideCombo: string | undefined;
|
overrideCombo: string | undefined;
|
||||||
isCapturing: boolean;
|
isCapturing: boolean;
|
||||||
capturedCombos: string[];
|
capturedCombos: string[];
|
||||||
|
heldModifiers: string | null;
|
||||||
onRebind: () => void;
|
onRebind: () => void;
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -55,7 +70,7 @@ function ShortcutRow({
|
|||||||
<Text style={styles.rowLabel}>{row.label}</Text>
|
<Text style={styles.rowLabel}>{row.label}</Text>
|
||||||
<View style={styles.rowActions}>
|
<View style={styles.rowActions}>
|
||||||
{isCapturing ? (
|
{isCapturing ? (
|
||||||
<ShortcutSequence chord={capturedCombos} />
|
<ShortcutSequence chord={capturedCombos} heldModifiers={heldModifiers} />
|
||||||
) : (
|
) : (
|
||||||
<Shortcut chord={displayChord} />
|
<Shortcut chord={displayChord} />
|
||||||
)}
|
)}
|
||||||
@@ -88,22 +103,32 @@ function ShortcutRow({
|
|||||||
export function KeyboardShortcutsSection() {
|
export function KeyboardShortcutsSection() {
|
||||||
const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null);
|
const [capturingBindingId, setCapturingBindingId] = useState<string | null>(null);
|
||||||
const [capturedCombos, setCapturedCombos] = useState<string[]>([]);
|
const [capturedCombos, setCapturedCombos] = useState<string[]>([]);
|
||||||
|
const [heldModifiers, setHeldModifiers] = useState<string | null>(null);
|
||||||
const { overrides, hasOverrides, setOverride, removeOverride, resetAll } =
|
const { overrides, hasOverrides, setOverride, removeOverride, resetAll } =
|
||||||
useKeyboardShortcutOverrides();
|
useKeyboardShortcutOverrides();
|
||||||
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
|
const setCapturingShortcut = useKeyboardShortcutsStore((s) => s.setCapturingShortcut);
|
||||||
|
|
||||||
|
const isFocused = useIsFocused();
|
||||||
const isMac = getShortcutOs() === "mac";
|
const isMac = getShortcutOs() === "mac";
|
||||||
const isDesktopApp = getIsElectronRuntime();
|
const isDesktopApp = getIsElectronRuntime();
|
||||||
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
|
const sections = buildKeyboardShortcutHelpSections({ isMac, isDesktop: isDesktopApp });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFocused && capturingBindingId !== null) {
|
||||||
|
cancelCapture();
|
||||||
|
}
|
||||||
|
}, [isFocused]);
|
||||||
|
|
||||||
function cancelCapture() {
|
function cancelCapture() {
|
||||||
setCapturedCombos([]);
|
setCapturedCombos([]);
|
||||||
|
setHeldModifiers(null);
|
||||||
setCapturingBindingId(null);
|
setCapturingBindingId(null);
|
||||||
setCapturingShortcut(false);
|
setCapturingShortcut(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCapture(bindingId: string) {
|
function startCapture(bindingId: string) {
|
||||||
setCapturedCombos([]);
|
setCapturedCombos([]);
|
||||||
|
setHeldModifiers(null);
|
||||||
setCapturingBindingId(bindingId);
|
setCapturingBindingId(bindingId);
|
||||||
setCapturingShortcut(true);
|
setCapturingShortcut(true);
|
||||||
}
|
}
|
||||||
@@ -132,9 +157,11 @@ export function KeyboardShortcutsSection() {
|
|||||||
|
|
||||||
const comboString = keyboardEventToComboString(event);
|
const comboString = keyboardEventToComboString(event);
|
||||||
if (comboString === null) {
|
if (comboString === null) {
|
||||||
|
setHeldModifiers(heldModifiersFromEvent(event));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHeldModifiers(null);
|
||||||
setCapturedCombos((current) => [...current, comboString]);
|
setCapturedCombos((current) => [...current, comboString]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +221,7 @@ export function KeyboardShortcutsSection() {
|
|||||||
overrideCombo={overrideCombo}
|
overrideCombo={overrideCombo}
|
||||||
isCapturing={capturingBindingId === bindingId}
|
isCapturing={capturingBindingId === bindingId}
|
||||||
capturedCombos={capturingBindingId === bindingId ? capturedCombos : []}
|
capturedCombos={capturingBindingId === bindingId ? capturedCombos : []}
|
||||||
|
heldModifiers={capturingBindingId === bindingId ? heldModifiers : null}
|
||||||
onRebind={() => {
|
onRebind={() => {
|
||||||
if (bindingId) {
|
if (bindingId) {
|
||||||
startCapture(bindingId);
|
startCapture(bindingId);
|
||||||
|
|||||||
Reference in New Issue
Block a user