feat(keyboard): integrate keyboard layout management and shortcuts functionality

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
luckfox-eng29
2026-05-06 18:55:57 +08:00
parent 4798bde987
commit 2a5c0e585a
4 changed files with 173 additions and 25 deletions

View File

@@ -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}

View File

@@ -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 };
}

View File

@@ -233,6 +233,17 @@ export const keyDisplayMap: Record<string, string> = {
};
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<string, string> = {
...keyDisplayMap,
...{

118
ui/src/utils/shortcuts.ts Normal file
View File

@@ -0,0 +1,118 @@
const MODIFIER_KEYS = new Set([
"Control",
"Shift",
"Alt",
"Meta",
]);
const SPECIAL_CODE_TO_KEY: Record<string, string> = {
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("+");
}