mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-26 08:05:08 +02:00
feat(keyboard): integrate keyboard layout management and shortcuts functionality
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
29
ui/src/hooks/useKeyboardLayout.ts
Normal file
29
ui/src/hooks/useKeyboardLayout.ts
Normal 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 };
|
||||
}
|
||||
@@ -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
118
ui/src/utils/shortcuts.ts
Normal 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("+");
|
||||
}
|
||||
Reference in New Issue
Block a user