diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 014616a..68f36a2 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -14,7 +14,8 @@ import DetachIconRaw from "@/assets/detach-icon.svg"; import { cx } from "@/cva.config"; import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; import useKeyboard from "@/hooks/useKeyboard"; -import { keyDisplayMap, keyDisplayMap2, keys, modifiers, sKeyDisplayMap } from "@/keyboardMappings"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; +import { keyDisplayMap2, keys, modifiers, sKeyDisplayMap, latchingKeys } from "@/keyboardMappings"; import { dark_bg2_style} from "@/layout/theme_color"; import GoBottomSvg from "@/assets/second/gobottom.svg?react"; @@ -33,6 +34,15 @@ function KeyboardWrapper() { ); const { sendKeyboardEvent, resetKeyboardState } = useKeyboard(); + const { selectedKeyboard } = useKeyboardLayout(); + + const keyDisplayMap = useMemo(() => { + return selectedKeyboard.keyDisplayMap; + }, [selectedKeyboard]); + + const virtualKeyboardLayout = useMemo(() => { + return selectedKeyboard.virtualKeyboard; + }, [selectedKeyboard]); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); @@ -837,24 +847,7 @@ function KeyboardWrapper() { ? [{ class: "modifier-locked", buttons: modifierLockButtons }] : [] } - layout={{ - default: [ - "Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12", - "Backquote Digit1 Digit2 Digit3 Digit4 Digit5 Digit6 Digit7 Digit8 Digit9 Digit0 Minus Equal Backspace", - "Tab KeyQ KeyW KeyE KeyR KeyT KeyY KeyU KeyI KeyO KeyP BracketLeft BracketRight Backslash", - "CapsLock KeyA KeyS KeyD KeyF KeyG KeyH KeyJ KeyK KeyL Semicolon Quote Enter", - "ShiftLeft KeyZ KeyX KeyC KeyV KeyB KeyN KeyM Comma Period Slash ShiftRight", - "ControlLeft AltLeft MetaLeft Space MetaRight AltRight", - ], - shift: [ - "Escape F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12", - "(Backquote) (Digit1) (Digit2) (Digit3) (Digit4) (Digit5) (Digit6) (Digit7) (Digit8) (Digit9) (Digit0) (Minus) (Equal) (Backspace)", - "Tab (KeyQ) (KeyW) (KeyE) (KeyR) (KeyT) (KeyY) (KeyU) (KeyI) (KeyO) (KeyP) (BracketLeft) (BracketRight) (Backslash)", - "CapsLock (KeyA) (KeyS) (KeyD) (KeyF) (KeyG) (KeyH) (KeyJ) (KeyK) (KeyL) (Semicolon) (Quote) Enter", - "ShiftLeft (KeyZ) (KeyX) (KeyC) (KeyV) (KeyB) (KeyN) (KeyM) (Comma) (Period) (Slash) ShiftRight", - "ControlLeft AltLeft MetaLeft Space MetaRight AltRight", - ], - }} + layout={virtualKeyboardLayout.main} disableButtonHold={true} syncInstanceInputs={true} debug={false} @@ -874,10 +867,7 @@ function KeyboardWrapper() { layoutName={layoutName} onKeyPress={onKeyDown} display={keyDisplayMap} - layout={{ - default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"], - shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"], - }} + layout={virtualKeyboardLayout.control} syncInstanceInputs={true} debug={false} /> @@ -902,7 +892,7 @@ function KeyboardWrapper() { onKeyPress={onKeyDown} display={keyDisplayMap} layout={{ - default: ["ArrowUp"], + default: [virtualKeyboardLayout.arrows?.default?.[0] || "ArrowUp"], }} syncInstanceInputs={true} debug={false} @@ -916,7 +906,7 @@ function KeyboardWrapper() { onKeyPress={onKeyDown} display={keyDisplayMap} layout={{ - default: ["ArrowLeft ArrowDown ArrowRight"], + default: [virtualKeyboardLayout.arrows?.default?.[1] || "ArrowLeft ArrowDown ArrowRight"], }} syncInstanceInputs={true} debug={false} diff --git a/ui/src/hooks/useKeyboardLayout.ts b/ui/src/hooks/useKeyboardLayout.ts new file mode 100644 index 0000000..2d42a1b --- /dev/null +++ b/ui/src/hooks/useKeyboardLayout.ts @@ -0,0 +1,29 @@ +import { useMemo } from "react"; + +import { useSettingsStore } from "@/hooks/stores"; +import { keyboards } from "@/keyboardLayouts"; + +export default function useKeyboardLayout() { + const { keyboardLayout } = useSettingsStore(); + + const keyboardOptions = useMemo(() => { + return keyboards.map(keyboard => { + return { label: keyboard.name, value: keyboard.isoCode }; + }); + }, []); + + const isoCode = useMemo(() => { + if (keyboardLayout && keyboardLayout.length > 0) + return keyboardLayout.replace("en_US", "en-US"); + return "en-US"; + }, [keyboardLayout]); + + const selectedKeyboard = useMemo(() => { + return ( + keyboards.find(keyboard => keyboard.isoCode === isoCode) ?? + keyboards.find(keyboard => keyboard.isoCode === "en-US")! + ); + }, [isoCode]); + + return { keyboardOptions, isoCode, selectedKeyboard }; +} \ No newline at end of file diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 81aed8e..02c8e7a 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -233,6 +233,17 @@ export const keyDisplayMap: Record = { }; +export const latchingKeys = ["CapsLock", "ScrollLock", "NumLock", "MetaLeft", "MetaRight", "Compose", "Kana"]; + +export function decodeModifiers(modifier: number) { + return { + isShiftActive: (modifier & (modifiers.ShiftLeft | modifiers.ShiftRight)) !== 0, + isControlActive: (modifier & (modifiers.ControlLeft | modifiers.ControlRight)) !== 0, + isAltActive: (modifier & (modifiers.AltLeft | modifiers.AltRight)) !== 0, + isMetaActive: (modifier & (modifiers.MetaLeft | modifiers.MetaRight)) !== 0, + }; +} + export const keyDisplayMap2: Record = { ...keyDisplayMap, ...{ diff --git a/ui/src/utils/shortcuts.ts b/ui/src/utils/shortcuts.ts new file mode 100644 index 0000000..bc503e0 --- /dev/null +++ b/ui/src/utils/shortcuts.ts @@ -0,0 +1,118 @@ +const MODIFIER_KEYS = new Set([ + "Control", + "Shift", + "Alt", + "Meta", +]); + +const SPECIAL_CODE_TO_KEY: Record = { + Space: "Space", + Enter: "Enter", + Escape: "Esc", + Tab: "Tab", + Backspace: "Backspace", + Delete: "Delete", + ArrowUp: "Up", + ArrowDown: "Down", + ArrowLeft: "Left", + ArrowRight: "Right", +}; + +type ShortcutSpec = { + ctrl: boolean; + shift: boolean; + alt: boolean; + meta: boolean; + key: string; +}; + +function normalizeShortcutToken(token: string) { + return token.trim().toLowerCase(); +} + +function normalizeKeyName(key: string) { + const trimmed = key.trim(); + if (trimmed.length === 1) return trimmed.toUpperCase(); + const lower = trimmed.toLowerCase(); + if (lower === "escape") return "Esc"; + if (lower === " ") return "Space"; + if (lower === "arrowup") return "Up"; + if (lower === "arrowdown") return "Down"; + if (lower === "arrowleft") return "Left"; + if (lower === "arrowright") return "Right"; + return trimmed; +} + +function keyFromEvent(e: KeyboardEvent) { + const { code, key } = e; + if (code.startsWith("Key")) return code.slice(3).toUpperCase(); + if (code.startsWith("Digit")) return code.slice(5); + if (SPECIAL_CODE_TO_KEY[code]) return SPECIAL_CODE_TO_KEY[code]; + return normalizeKeyName(key); +} + +function parseShortcut(shortcut: string): ShortcutSpec | null { + if (!shortcut) return null; + const tokens = shortcut + .split("+") + .map(token => token.trim()) + .filter(Boolean); + if (tokens.length === 0) return null; + + const spec: ShortcutSpec = { + ctrl: false, + shift: false, + alt: false, + meta: false, + key: "", + }; + + for (const token of tokens) { + const normalized = normalizeShortcutToken(token); + if (normalized === "ctrl" || normalized === "control") { + spec.ctrl = true; + continue; + } + if (normalized === "shift") { + spec.shift = true; + continue; + } + if (normalized === "alt" || normalized === "option") { + spec.alt = true; + continue; + } + if (normalized === "meta" || normalized === "cmd" || normalized === "command") { + spec.meta = true; + continue; + } + spec.key = normalizeKeyName(token); + } + + if (!spec.key) return null; + return spec; +} + +export function eventMatchesShortcut(e: KeyboardEvent, shortcut: string) { + const spec = parseShortcut(shortcut); + if (!spec) return false; + const eventKey = keyFromEvent(e); + return ( + e.ctrlKey === spec.ctrl + && e.shiftKey === spec.shift + && e.altKey === spec.alt + && e.metaKey === spec.meta + && eventKey === spec.key + ); +} + +export function shortcutFromKeyboardEvent(e: KeyboardEvent) { + if (MODIFIER_KEYS.has(e.key)) return null; + const key = keyFromEvent(e); + const modifiers: string[] = []; + if (e.ctrlKey) modifiers.push("Ctrl"); + if (e.shiftKey) modifiers.push("Shift"); + if (e.altKey) modifiers.push("Alt"); + if (e.metaKey) modifiers.push("Meta"); + if (modifiers.length === 0) return null; + return [...modifiers, key].join("+"); +}