From cbc3f2016f8af409591a756c0511e0c60c424216 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Thu, 21 Aug 2025 18:08:21 +0000 Subject: [PATCH] Move keyboardOptions to useKeyboardLayouts Manage state to eliminate rerenders by judicious use of useMemo. Also removed the extraneous resetKeyboardState. --- ui/src/components/MacroForm.tsx | 6 ++-- ui/src/components/MacroStepCard.tsx | 28 ++++++++++--------- ui/src/components/VirtualKeyboard.tsx | 23 +++++++++------ ui/src/components/popovers/PasteModal.tsx | 14 +++++----- ui/src/hooks/useKeyboardLayout.ts | 20 +++++++++---- ui/src/keyboardLayouts.ts | 14 +--------- .../routes/devices.$id.settings.keyboard.tsx | 10 +++---- ui/src/routes/devices.$id.settings.macros.tsx | 12 ++++---- ui/src/routes/devices.$id.settings.tsx | 5 +--- 9 files changed, 66 insertions(+), 66 deletions(-) diff --git a/ui/src/components/MacroForm.tsx b/ui/src/components/MacroForm.tsx index 6240a8a..1aafe9c 100644 --- a/ui/src/components/MacroForm.tsx +++ b/ui/src/components/MacroForm.tsx @@ -12,7 +12,7 @@ import { MAX_KEYS_PER_STEP, } from "@/constants/macros"; import { KeySequence } from "@/hooks/stores"; -import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; interface ValidationErrors { name?: string; @@ -45,7 +45,7 @@ export function MacroForm({ const [keyQueries, setKeyQueries] = useState>({}); const [errors, setErrors] = useState({}); const [errorMessage, setErrorMessage] = useState(null); - const { keyboard } = useKeyboardLayout(); + const { selectedKeyboard } = useKeyboardLayout(); const showTemporaryError = (message: string) => { setErrorMessage(message); @@ -236,7 +236,7 @@ export function MacroForm({ } onDelayChange={delay => handleDelayChange(stepIndex, delay)} isLastStep={stepIndex === (macro.steps?.length || 0) - 1} - keyboard={keyboard} + keyboard={selectedKeyboard} /> ))} diff --git a/ui/src/components/MacroStepCard.tsx b/ui/src/components/MacroStepCard.tsx index c9d3822..cf22468 100644 --- a/ui/src/components/MacroStepCard.tsx +++ b/ui/src/components/MacroStepCard.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; import { Button } from "@/components/Button"; @@ -12,15 +13,6 @@ import { keys, modifiers } from "@/keyboardMappings"; // Filter out modifier keys since they're handled in the modifiers section const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; -const keyOptions = (keyDisplayMap: Record) => { - return Object.keys(keys) - .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) - .map(key => ({ - value: key, - label: keyDisplayMap[key] || key, - })); -} - const modifierOptions = Object.keys(modifiers).map(modifier => ({ value: modifier, label: modifier.replace(/^(Control|Alt|Shift|Meta)(Left|Right)$/, "$1 $2"), @@ -93,16 +85,26 @@ export function MacroStepCard({ }: MacroStepCardProps) { const { keyDisplayMap } = keyboard; - const getFilteredKeys = () => { + const keyOptions = useMemo(() => + Object.keys(keys) + .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) + .map(key => ({ + value: key, + label: keyDisplayMap[key] || key, + })), + [keyDisplayMap] + ); + + const filteredKeys = useMemo(() => { const selectedKeys = ensureArray(step.keys); - const availableKeys = keyOptions(keyDisplayMap).filter(option => !selectedKeys.includes(option.value)); + const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); if (keyQuery === '') { return availableKeys; } else { return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); } - }; + }, [keyOptions, keyQuery, step.keys]); return ( @@ -211,7 +213,7 @@ export function MacroStepCard({ }} displayValue={() => keyQuery} onInputChange={onKeyQueryChange} - options={getFilteredKeys} + options={() => filteredKeys} disabledMessage="Max keys reached" size="SM" immediate diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx index 1d41f94..876c7bc 100644 --- a/ui/src/components/VirtualKeyboard.tsx +++ b/ui/src/components/VirtualKeyboard.tsx @@ -14,7 +14,7 @@ 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 useKeyboardLayout from "@/hooks/useKeyboardLayout"; import { keys, modifiers, latchingKeys, decodeModifiers } from "@/keyboardMappings"; export const DetachIcon = ({ className }: { className?: string }) => { @@ -30,12 +30,19 @@ function KeyboardWrapper() { const { isAttachedVirtualKeyboardVisible, setAttachedVirtualKeyboardVisibility } = useUiStore(); const { keysDownState, /* keyboardLedState,*/ isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); const { handleKeyPress, executeMacro } = useKeyboard(); + const { selectedKeyboard } = useKeyboardLayout(); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState({ x: 0, y: 0 }); const [newPosition, setNewPosition] = useState({ x: 0, y: 0 }); - const { keyboard } = useKeyboardLayout(); + const keyDisplayMap = useMemo(() => { + return selectedKeyboard.keyDisplayMap; + }, [selectedKeyboard]); + + const virtualKeyboard = useMemo(() => { + return selectedKeyboard.virtualKeyboard; + }, [selectedKeyboard]); //const isCapsLockActive = useMemo(() => { // return (keyboardLedState.caps_lock); @@ -269,8 +276,8 @@ function KeyboardWrapper() { buttons: keyNamesForDownKeys.join(" "), }, ]} - display={keyboard.keyDisplayMap} - layout={keyboard.virtualKeyboard.main} + display={keyDisplayMap} + layout={virtualKeyboard.main} disableButtonHold={true} enableLayoutCandidates={false} preventMouseDownDefault={true} @@ -290,8 +297,8 @@ function KeyboardWrapper() { layoutName="default" onKeyPress={onKeyDown} onKeyReleased={onKeyUp} - display={keyboard.keyDisplayMap} - layout={keyboard.virtualKeyboard.control} + display={keyDisplayMap} + layout={virtualKeyboard.control} disableButtonHold={true} enableLayoutCandidates={false} preventMouseDownDefault={true} @@ -308,8 +315,8 @@ function KeyboardWrapper() { theme="simple-keyboard hg-theme-default hg-layout-default" onKeyPress={onKeyDown} onKeyReleased={onKeyUp} - display={keyboard.keyDisplayMap} - layout={keyboard.virtualKeyboard.arrows} + display={keyDisplayMap} + layout={virtualKeyboard.arrows} disableButtonHold={true} enableLayoutCandidates={false} preventMouseDownDefault={true} diff --git a/ui/src/components/popovers/PasteModal.tsx b/ui/src/components/popovers/PasteModal.tsx index 5aebd25..1e4314b 100644 --- a/ui/src/components/popovers/PasteModal.tsx +++ b/ui/src/components/popovers/PasteModal.tsx @@ -11,7 +11,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; import { KeyStroke } from "@/keyboardLayouts"; -import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import notifications from "@/notifications"; const hidKeyboardPayload = (modifier: number, keys: number[]) => { @@ -36,7 +36,7 @@ export default function PasteModal() { const close = useClose(); const { setKeyboardLayout } = useSettingsStore(); - const { keyboard } = useKeyboardLayout(); + const { selectedKeyboard } = useKeyboardLayout(); useEffect(() => { send("getKeyboardLayout", {}, resp => { @@ -56,13 +56,13 @@ export default function PasteModal() { setDisableVideoFocusTrap(false); if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return; - if (!keyboard) return; + if (!selectedKeyboard) return; const text = TextAreaRef.current.value; try { for (const char of text) { - const keyprops = keyboard.chars[char]; + const keyprops = selectedKeyboard.chars[char]; if (!keyprops) continue; const { key, shift, altRight, deadKey, accentKey } = keyprops; @@ -102,7 +102,7 @@ export default function PasteModal() { ); }); } - }, [keyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]); + }, [selectedKeyboard, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteModeEnabled]); useEffect(() => { if (TextAreaRef.current) { @@ -152,7 +152,7 @@ export default function PasteModal() { // @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments [...new Intl.Segmenter().segment(value)] .map(x => x.segment) - .filter(char => !keyboard.chars[char]), + .filter(char => !selectedKeyboard.chars[char]), ), ]; @@ -173,7 +173,7 @@ export default function PasteModal() {

- Sending text using keyboard layout: {keyboard.isoCode}-{keyboard.name} + Sending text using keyboard layout: {selectedKeyboard.isoCode}-{selectedKeyboard.name}

diff --git a/ui/src/hooks/useKeyboardLayout.ts b/ui/src/hooks/useKeyboardLayout.ts index d1b1b65..c1d0557 100644 --- a/ui/src/hooks/useKeyboardLayout.ts +++ b/ui/src/hooks/useKeyboardLayout.ts @@ -1,11 +1,17 @@ import { useMemo } from "react"; import { useSettingsStore } from "@/hooks/stores"; -import { KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts"; +import { keyboards } from "@/keyboardLayouts"; -export function useKeyboardLayout(): { keyboard: KeyboardLayout } { +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 we don't have a specific layout, default to "en-US" because that was the original layout // developed so it is a good fallback. Additionally, we replace "en_US" with "en-US" because @@ -13,15 +19,17 @@ export function useKeyboardLayout(): { keyboard: KeyboardLayout } { // ISO code for English/United State. To ensure we remain backward compatible with devices that // have not had their Keyboard Layout selected by the user, we want to treat "en_US" as if it was // "en-US" to match the ISO standard codes now used in the keyboardLayouts. - console.log("Current keyboard layout from store:", keyboardLayout); + console.debug("Current keyboard layout from store:", keyboardLayout); if (keyboardLayout && keyboardLayout.length > 0) return keyboardLayout.replace("en_US", "en-US"); return "en-US"; }, [keyboardLayout]); - const keyboard = useMemo(() => { - return selectedKeyboard(isoCode); + const selectedKeyboard = useMemo(() => { + // fallback to original behaviour of en-US if no isoCode given or matching layout not found + return keyboards.find(keyboard => keyboard.isoCode === isoCode) + ?? keyboards.find(keyboard => keyboard.isoCode === "en-US")!; }, [isoCode]); - return { keyboard }; + return { keyboardOptions, isoCode, selectedKeyboard }; } \ No newline at end of file diff --git a/ui/src/keyboardLayouts.ts b/ui/src/keyboardLayouts.ts index 2260983..4ae8970 100644 --- a/ui/src/keyboardLayouts.ts +++ b/ui/src/keyboardLayouts.ts @@ -14,7 +14,7 @@ export interface KeyboardLayout { }; } -// to add a new layout, create a file like the above and add it to the list +// To add a new layout, create a file like the above and add it to the list import { cs_CZ } from "@/keyboardLayouts/cs_CZ" import { de_CH } from "@/keyboardLayouts/de_CH" import { de_DE } from "@/keyboardLayouts/de_DE" @@ -29,15 +29,3 @@ import { nb_NO } from "@/keyboardLayouts/nb_NO" import { sv_SE } from "@/keyboardLayouts/sv_SE" export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ]; - -export const selectedKeyboard = (isoCode: string): KeyboardLayout => { - // fallback to original behaviour of en-US if no isoCode given or matching layout not found - return keyboards.find(keyboard => keyboard.isoCode == isoCode) - ?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!; -}; - -export const keyboardOptions = () => { - return keyboards.map((keyboard) => { - return { label: keyboard.name, value: keyboard.isoCode } - }); -} diff --git a/ui/src/routes/devices.$id.settings.keyboard.tsx b/ui/src/routes/devices.$id.settings.keyboard.tsx index bdc65cd..8a158a7 100644 --- a/ui/src/routes/devices.$id.settings.keyboard.tsx +++ b/ui/src/routes/devices.$id.settings.keyboard.tsx @@ -2,11 +2,10 @@ import { useCallback, useEffect } from "react"; import { useSettingsStore } from "@/hooks/stores"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import { Checkbox } from "@/components/Checkbox"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; -import { keyboardOptions } from "@/keyboardLayouts"; import notifications from "@/notifications"; import { SettingsItem } from "./devices.$id.settings"; @@ -14,8 +13,7 @@ import { SettingsItem } from "./devices.$id.settings"; export default function SettingsKeyboardRoute() { const { setKeyboardLayout } = useSettingsStore(); const { showPressedKeys, setShowPressedKeys } = useSettingsStore(); - const { keyboard } = useKeyboardLayout(); - const layoutOptions = keyboardOptions(); + const { selectedKeyboard, keyboardOptions } = useKeyboardLayout(); const { send } = useJsonRpc(); @@ -62,9 +60,9 @@ export default function SettingsKeyboardRoute() { size="SM" label="" fullWidth - value={keyboard.isoCode} + value={selectedKeyboard.isoCode} onChange={onKeyboardLayoutChange} - options={layoutOptions} + options={keyboardOptions} />

diff --git a/ui/src/routes/devices.$id.settings.macros.tsx b/ui/src/routes/devices.$id.settings.macros.tsx index 7e147f4..734f17e 100644 --- a/ui/src/routes/devices.$id.settings.macros.tsx +++ b/ui/src/routes/devices.$id.settings.macros.tsx @@ -20,7 +20,7 @@ import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros import notifications from "@/notifications"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import LoadingSpinner from "@/components/LoadingSpinner"; -import { useKeyboardLayout } from "@/hooks/useKeyboardLayout"; +import useKeyboardLayout from "@/hooks/useKeyboardLayout"; const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => { return macros.map((macro, index) => ({ @@ -35,7 +35,7 @@ export default function SettingsMacrosRoute() { const [actionLoadingId, setActionLoadingId] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [macroToDelete, setMacroToDelete] = useState(null); - const { keyboard } = useKeyboardLayout(); + const { selectedKeyboard } = useKeyboardLayout(); const isMaxMacrosReached = useMemo( () => macros.length >= MAX_TOTAL_MACROS, @@ -186,7 +186,7 @@ export default function SettingsMacrosRoute() { step.modifiers.map((modifier, idx) => ( - {keyboard.modifierDisplayMap[modifier] || modifier} + {selectedKeyboard.modifierDisplayMap[modifier] || modifier} {idx < step.modifiers.length - 1 && ( @@ -211,7 +211,7 @@ export default function SettingsMacrosRoute() { step.keys.map((key, idx) => ( - {keyboard.keyDisplayMap[key] || key} + {selectedKeyboard.keyDisplayMap[key] || key} {idx < step.keys.length - 1 && ( @@ -298,8 +298,8 @@ export default function SettingsMacrosRoute() { actionLoadingId, handleDeleteMacro, handleMoveMacro, - keyboard.modifierDisplayMap, - keyboard.keyDisplayMap, + selectedKeyboard.modifierDisplayMap, + selectedKeyboard.keyDisplayMap, handleDuplicateMacro, navigate ], diff --git a/ui/src/routes/devices.$id.settings.tsx b/ui/src/routes/devices.$id.settings.tsx index 6c9314c..5a61778 100644 --- a/ui/src/routes/devices.$id.settings.tsx +++ b/ui/src/routes/devices.$id.settings.tsx @@ -20,7 +20,6 @@ import { LinkButton } from "@/components/Button"; import { FeatureFlag } from "@/components/FeatureFlag"; import LoadingSpinner from "@/components/LoadingSpinner"; import { useUiStore } from "@/hooks/stores"; -import useKeyboard from "@/hooks/useKeyboard"; import { cx } from "../cva.config"; @@ -28,7 +27,6 @@ import { cx } from "../cva.config"; export default function SettingsRoute() { const location = useLocation(); const { setDisableVideoFocusTrap } = useUiStore(); - const { resetKeyboardState } = useKeyboard(); const scrollContainerRef = useRef(null); const [showLeftGradient, setShowLeftGradient] = useState(false); const [showRightGradient, setShowRightGradient] = useState(false); @@ -66,13 +64,12 @@ export default function SettingsRoute() { useEffect(() => { setTimeout(() => { setDisableVideoFocusTrap(true); - resetKeyboardState(); }, 500); return () => { setDisableVideoFocusTrap(false); }; - }, [resetKeyboardState, setDisableVideoFocusTrap]); + }, [setDisableVideoFocusTrap]); return (