diff --git a/ui/src/assets/attach-icon.svg b/ui/src/assets/attach-icon.svg deleted file mode 100644 index 88deb80a..00000000 --- a/ui/src/assets/attach-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index 97fcc5f6..b7f09501 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -175,7 +175,7 @@ type ButtonPropsType = Pick< export const Button = React.forwardRef( ({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => { const classes = cx( - "group outline-hidden", + "group outline-hidden cursor-pointer", props.fullWidth ? "w-full" : "", loading ? "pointer-events-none" : "", ); diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index ce1bd83f..60bfa7b1 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -2,33 +2,31 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Keyboard from "react-simple-keyboard"; +import { LuKeyboard } from "react-icons/lu"; import Card from "@components/Card"; // eslint-disable-next-line import/order -import { Button } from "@components/Button"; +import { Button, LinkButton } from "@components/Button"; import "react-simple-keyboard/build/css/index.css"; -import AttachIconRaw from "@/assets/attach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg"; import { cx } from "@/cva.config"; import { useHidStore, useUiStore } from "@/hooks/stores"; import useKeyboard from "@/hooks/useKeyboard"; import useKeyboardLayout from "@/hooks/useKeyboardLayout"; -import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings"; +import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings"; export const DetachIcon = ({ className }: { className?: string }) => { return Detach Icon; }; -const AttachIcon = ({ className }: { className?: string }) => { - return Attach Icon; -}; - function KeyboardWrapper() { const keyboardRef = useRef(null); - const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore(); - const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); + const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = + useUiStore(); + const { keysDownState, isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = + useHidStore(); const { handleKeyPress, executeMacro } = useKeyboard(); const { selectedKeyboard } = useKeyboardLayout(); @@ -44,29 +42,28 @@ function KeyboardWrapper() { return selectedKeyboard.virtualKeyboard; }, [selectedKeyboard]); - //const isCapsLockActive = useMemo(() => { - // return (keyboardLedState.caps_lock); - //}, [keyboardLedState]); - - const { isShiftActive, /*isControlActive, isAltActive, isMetaActive, isAltGrActive*/ } = useMemo(() => { + const { isShiftActive } = useMemo(() => { return decodeModifiers(keysDownState.modifier); }, [keysDownState]); const mainLayoutName = useMemo(() => { - const layoutName = isShiftActive ? "shift": "default"; - return layoutName; + return isShiftActive ? "shift" : "default"; }, [isShiftActive]); const keyNamesForDownKeys = useMemo(() => { const activeModifierMask = keysDownState.modifier || 0; - const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name); + const modifierNames = Object.entries(modifiers) + .filter(([_, mask]) => (activeModifierMask & mask) !== 0) + .map(([name, _]) => name); const keysDown = keysDownState.keys || []; - const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name); + const keyNames = Object.entries(keys) + .filter(([_, value]) => keysDown.includes(value)) + .map(([name, _]) => name); - return [...modifierNames,...keyNames, ' ']; // we have to have at least one space to avoid keyboard whining + return [...modifierNames, ...keyNames, " "]; // we have to have at least one space to avoid keyboard whining }, [keysDownState]); - + const startDrag = useCallback((e: MouseEvent | TouchEvent) => { if (!keyboardRef.current) return; if (e instanceof TouchEvent && e.touches.length > 1) return; @@ -110,6 +107,9 @@ function KeyboardWrapper() { }, []); useEffect(() => { + // Is the keyboard detached or attached? + if (isAttachedVirtualKeyboardVisible) return; + const handle = keyboardRef.current; if (handle) { handle.addEventListener("touchstart", startDrag); @@ -134,15 +134,12 @@ function KeyboardWrapper() { document.removeEventListener("mousemove", onDrag); document.removeEventListener("touchmove", onDrag); }; - }, [endDrag, onDrag, startDrag]); + }, [isAttachedVirtualKeyboardVisible, endDrag, onDrag, startDrag]); - const onKeyUp = useCallback( - async (_: string, e: MouseEvent | undefined) => { - e?.preventDefault(); - e?.stopPropagation(); - }, - [] - ); + const onKeyUp = useCallback(async (_: string, e: MouseEvent | undefined) => { + e?.preventDefault(); + e?.stopPropagation(); + }, []); const onKeyDown = useCallback( async (key: string, e: MouseEvent | undefined) => { @@ -151,24 +148,30 @@ function KeyboardWrapper() { // handle the fake key-macros we have defined for common combinations if (key === "CtrlAltDelete") { - await executeMacro([ { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); + await executeMacro([ + { keys: ["Delete"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 }, + ]); return; } if (key === "AltMetaEscape") { - await executeMacro([ { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 } ]); + await executeMacro([ + { keys: ["Escape"], modifiers: ["AltLeft", "MetaLeft"], delay: 100 }, + ]); return; } if (key === "CtrlAltBackspace") { - await executeMacro([ { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 } ]); + await executeMacro([ + { keys: ["Backspace"], modifiers: ["ControlLeft", "AltLeft"], delay: 100 }, + ]); return; } // if they press any of the latching keys, we send a keypress down event and the release it automatically (on timer) if (latchingKeys.includes(key)) { console.debug(`Latching key pressed: ${key} sending down and delayed up pair`); - handleKeyPress(keys[key], true) + handleKeyPress(keys[key], true); setTimeout(() => handleKeyPress(keys[key], false), 100); return; } @@ -176,8 +179,10 @@ function KeyboardWrapper() { // if they press any of the dynamic keys, we send a keypress down event but we don't release it until they click it again if (Object.keys(modifiers).includes(key)) { const currentlyDown = keyNamesForDownKeys.includes(key); - console.debug(`Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`); - handleKeyPress(keys[key], !currentlyDown) + console.debug( + `Dynamic key pressed: ${key} was currently down: ${currentlyDown}, toggling state`, + ); + handleKeyPress(keys[key], !currentlyDown); return; } @@ -211,7 +216,7 @@ function KeyboardWrapper() {
-
+
{isAttachedVirtualKeyboardVisible ? (
-

+

Virtual Keyboard

-
+
+
+ +
+
+
- { /* TODO add optional number pad */ } + {/* TODO add optional number pad */}
diff --git a/ui/src/index.css b/ui/src/index.css index db03b427..ae23db2b 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -325,6 +325,20 @@ video::-webkit-media-controls { @apply mr-[2px]! md:mr-[5px]!; } +/* Reduce font size for selected keys when keyboard is detached */ +.keyboard-detached .simple-keyboard-main.simple-keyboard { + min-width: calc(14 * 7ch); +} + +.keyboard-detached .simple-keyboard.hg-theme-default div.hg-button { + text-wrap: auto; + text-align: center; + min-width: 6ch; +} +.keyboard-detached .simple-keyboard.hg-theme-default .hg-button span { + font-size: 50%; +} + /* Hide the scrollbar by setting the scrollbar color to the background color */ .xterm .xterm-viewport { scrollbar-color: var(--color-gray-900) #002b36; diff --git a/ui/src/keyboardLayouts/en_US.ts b/ui/src/keyboardLayouts/en_US.ts index 872d3569..2076ce64 100644 --- a/ui/src/keyboardLayouts/en_US.ts +++ b/ui/src/keyboardLayouts/en_US.ts @@ -144,33 +144,33 @@ export const keyDisplayMap: Record = { AltMetaEscape: "Alt + Meta + Escape", CtrlAltBackspace: "Ctrl + Alt + Backspace", AltGr: "AltGr", - AltLeft: "Alt", - AltRight: "Alt", + AltLeft: "Alt ⌥", + AltRight: "⌥ Alt", ArrowDown: "↓", ArrowLeft: "←", ArrowRight: "→", ArrowUp: "↑", Backspace: "Backspace", "(Backspace)": "Backspace", - CapsLock: "Caps Lock", + CapsLock: "Caps Lock ⇪", Clear: "Clear", - ControlLeft: "Ctrl", - ControlRight: "Ctrl", - Delete: "Delete", + ControlLeft: "Ctrl ⌃", + ControlRight: "⌃ Ctrl", + Delete: "Delete ⌦", End: "End", Enter: "Enter", Escape: "Esc", Home: "Home", Insert: "Insert", Menu: "Menu", - MetaLeft: "Meta", - MetaRight: "Meta", + MetaLeft: "Meta ⌘", + MetaRight: "⌘ Meta", PageDown: "PgDn", PageUp: "PgUp", - ShiftLeft: "Shift", - ShiftRight: "Shift", + ShiftLeft: "Shift ⇧", + ShiftRight: "⇧ Shift", Space: " ", - Tab: "Tab", + Tab: "Tab ⇥", // Letters KeyA: "a", KeyB: "b", KeyC: "c", KeyD: "d", KeyE: "e", diff --git a/ui/src/keyboardMappings.ts b/ui/src/keyboardMappings.ts index 14b0c606..1ffc8d78 100644 --- a/ui/src/keyboardMappings.ts +++ b/ui/src/keyboardMappings.ts @@ -81,12 +81,6 @@ export const keys = { Help: 0x75, Home: 0x4a, Insert: 0x49, - International1: 0x87, - International2: 0x88, - International3: 0x89, - International4: 0x8a, - International5: 0x8b, - International6: 0x8c, International7: 0x8d, International8: 0x8e, International9: 0x8f, @@ -117,14 +111,20 @@ export const keys = { KeyX: 0x1b, KeyY: 0x1c, KeyZ: 0x1d, + KeyRO: 0x87, + KatakanaHiragana: 0x88, + Yen: 0x89, + Henkan: 0x8a, + Muhenkan: 0x8b, + KPJPComma: 0x8c, + Hangeul: 0x90, + Hanja: 0x91, + Katakana: 0x92, + Hiragana: 0x93, + ZenkakuHankaku:0x94, LockingCapsLock: 0x82, LockingNumLock: 0x83, LockingScrollLock: 0x84, - Lang1: 0x90, // Hangul/English toggle on Korean keyboards - Lang2: 0x91, // Hanja conversion on Korean keyboards - Lang3: 0x92, // Katakana on Japanese keyboards - Lang4: 0x93, // Hiragana on Japanese keyboards - Lang5: 0x94, // Zenkaku/Hankaku toggle on Japanese keyboards Lang6: 0x95, Lang7: 0x96, Lang8: 0x97, @@ -157,7 +157,7 @@ export const keys = { NumpadClearEntry: 0xd9, NumpadColon: 0xcb, NumpadComma: 0x85, - NumpadDecimal: 0x63, + NumpadDecimal: 0x63, // and Delete NumpadDecimalBase: 0xdc, NumpadDelete: 0x63, NumpadDivide: 0x54, @@ -211,7 +211,7 @@ export const keys = { PageUp: 0x4b, Paste: 0x7d, Pause: 0x48, - Period: 0x37, + Period: 0x37, // aka Dot Power: 0x66, PrintScreen: 0x46, Prior: 0x9d, @@ -226,7 +226,7 @@ export const keys = { Slash: 0x38, Space: 0x2c, Stop: 0x78, - SystemRequest: 0x9a, + SystemRequest: 0x9a, // aka Attention Tab: 0x2b, ThousandsSeparator: 0xb2, Tilde: 0x35, diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index abd72bf7..6f5c2e86 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -53,7 +53,7 @@ export default function SettingsKeyboardRoute() {

- Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system. + The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.