mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 09:01:22 +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 { cx } from "@/cva.config";
|
||||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
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 { dark_bg2_style} from "@/layout/theme_color";
|
||||||
|
|
||||||
import GoBottomSvg from "@/assets/second/gobottom.svg?react";
|
import GoBottomSvg from "@/assets/second/gobottom.svg?react";
|
||||||
@@ -33,6 +34,15 @@ function KeyboardWrapper() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
|
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 [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
@@ -837,24 +847,7 @@ function KeyboardWrapper() {
|
|||||||
? [{ class: "modifier-locked", buttons: modifierLockButtons }]
|
? [{ class: "modifier-locked", buttons: modifierLockButtons }]
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
layout={{
|
layout={virtualKeyboardLayout.main}
|
||||||
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",
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
disableButtonHold={true}
|
disableButtonHold={true}
|
||||||
syncInstanceInputs={true}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
debug={false}
|
||||||
@@ -874,10 +867,7 @@ function KeyboardWrapper() {
|
|||||||
layoutName={layoutName}
|
layoutName={layoutName}
|
||||||
onKeyPress={onKeyDown}
|
onKeyPress={onKeyDown}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={virtualKeyboardLayout.control}
|
||||||
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
|
|
||||||
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
|
|
||||||
}}
|
|
||||||
syncInstanceInputs={true}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
debug={false}
|
||||||
/>
|
/>
|
||||||
@@ -902,7 +892,7 @@ function KeyboardWrapper() {
|
|||||||
onKeyPress={onKeyDown}
|
onKeyPress={onKeyDown}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={{
|
||||||
default: ["ArrowUp"],
|
default: [virtualKeyboardLayout.arrows?.default?.[0] || "ArrowUp"],
|
||||||
}}
|
}}
|
||||||
syncInstanceInputs={true}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
debug={false}
|
||||||
@@ -916,7 +906,7 @@ function KeyboardWrapper() {
|
|||||||
onKeyPress={onKeyDown}
|
onKeyPress={onKeyDown}
|
||||||
display={keyDisplayMap}
|
display={keyDisplayMap}
|
||||||
layout={{
|
layout={{
|
||||||
default: ["ArrowLeft ArrowDown ArrowRight"],
|
default: [virtualKeyboardLayout.arrows?.default?.[1] || "ArrowLeft ArrowDown ArrowRight"],
|
||||||
}}
|
}}
|
||||||
syncInstanceInputs={true}
|
syncInstanceInputs={true}
|
||||||
debug={false}
|
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> = {
|
export const keyDisplayMap2: Record<string, string> = {
|
||||||
...keyDisplayMap,
|
...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