Skip to content

Commit acbdd23

Browse files
committed
Treats all modifiers as "sticky" keys for use with just touch/mouse
Made the operation keys look more like a keyboard Add persistent state management for the Shift/Ctrl/Alt/Meta Add infoBar indicators for Shift/Ctr/Alt/Meta/AltGr Remove redundant Break it's actually on keyboard as (Pause) Add a style for the virtual keyboard "depressed" buttons. Delete the no-longer-needed button variants Added missing keycodes Added modifier tracking for all the other keys Ensures the InfoBar tracks for physical and virtual keyboard Shows what buttons are depressed for sticky keys Now treats all the Shift/Control/Alt/Meta/AltGr keys as if they were sticky keys so users can click the button and hit the next key,
1 parent b822b73 commit acbdd23

File tree

6 files changed

+329
-116
lines changed

6 files changed

+329
-116
lines changed

ui/src/components/InfoBar.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export default function InfoBar() {
4040
const keyboardLedState = useHidStore(state => state.keyboardLedState);
4141
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
4242
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
43+
44+
const isShiftActive = useHidStore(state => state.isShiftActive);
45+
const isCtrlActive = useHidStore(state => state.isCtrlActive);
46+
const isAltActive = useHidStore(state => state.isAltActive);
47+
const isMetaActive = useHidStore(state => state.isMetaActive);
48+
const isAltGrActive = useHidStore(state => state.isAltGrActive);
4349

4450
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
4551

@@ -135,6 +141,56 @@ export default function InfoBar() {
135141
{keyboardLedSync === "browser" ? "Browser" : "Host"}
136142
</div>
137143
) : null}
144+
<div
145+
className={cx(
146+
"shrink-0 p-1 px-1.5 text-xs",
147+
isShiftActive
148+
? "text-black dark:text-white"
149+
: "text-slate-800/20 dark:text-slate-300/20",
150+
)}
151+
>
152+
Shift
153+
</div>
154+
<div
155+
className={cx(
156+
"shrink-0 p-1 px-1.5 text-xs",
157+
isCtrlActive
158+
? "text-black dark:text-white"
159+
: "text-slate-800/20 dark:text-slate-300/20",
160+
)}
161+
>
162+
Ctrl
163+
</div>
164+
<div
165+
className={cx(
166+
"shrink-0 p-1 px-1.5 text-xs",
167+
isAltActive
168+
? "text-black dark:text-white"
169+
: "text-slate-800/20 dark:text-slate-300/20",
170+
)}
171+
>
172+
Alt
173+
</div>
174+
<div
175+
className={cx(
176+
"shrink-0 p-1 px-1.5 text-xs",
177+
isMetaActive
178+
? "text-black dark:text-white"
179+
: "text-slate-800/20 dark:text-slate-300/20",
180+
)}
181+
>
182+
Meta
183+
</div>
184+
<div
185+
className={cx(
186+
"shrink-0 p-1 px-1.5 text-xs",
187+
isAltGrActive
188+
? "text-black dark:text-white"
189+
: "text-slate-800/20 dark:text-slate-300/20",
190+
)}
191+
>
192+
AltGr
193+
</div>
138194
<div
139195
className={cx(
140196
"shrink-0 p-1 px-1.5 text-xs",

ui/src/components/VirtualKeyboard.tsx

Lines changed: 133 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const AttachIcon = ({ className }: { className?: string }) => {
2727

2828
function KeyboardWrapper() {
2929
const [layoutName, setLayoutName] = useState("default");
30+
const [depressedButtons, setDepressedButtons] = useState("");
3031

3132
const keyboardRef = useRef<HTMLDivElement>(null);
3233
const showAttachedVirtualKeyboard = useUiStore(
@@ -54,6 +55,21 @@ function KeyboardWrapper() {
5455

5556
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
5657

58+
const isShiftActive = useHidStore(state => state.isShiftActive);
59+
const setIsShiftActive = useHidStore(state => state.setIsShiftActive);
60+
61+
const isCtrlActive = useHidStore(state => state.isCtrlActive);
62+
const setIsCtrlActive = useHidStore(state => state.setIsCtrlActive);
63+
64+
const isAltActive = useHidStore(state => state.isAltActive);
65+
const setIsAltActive = useHidStore(state => state.setIsAltActive);
66+
67+
const isMetaActive = useHidStore(state => state.isMetaActive);
68+
const setIsMetaActive = useHidStore(state => state.setIsMetaActive);
69+
70+
const isAltGrActive = useHidStore(state => state.isAltGrActive);
71+
const setIsAltGrActive = useHidStore(state => state.setIsAltGrActive);
72+
5773
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
5874
if (!keyboardRef.current) return;
5975
if (e instanceof TouchEvent && e.touches.length > 1) return;
@@ -123,80 +139,123 @@ function KeyboardWrapper() {
123139
};
124140
}, [endDrag, onDrag, startDrag]);
125141

126-
const onKeyDown = useCallback(
127-
(key: string) => {
128-
const isKeyShift = key === "{shift}" || key === "ShiftLeft" || key === "ShiftRight";
129-
const isKeyCaps = key === "CapsLock";
130-
const cleanKey = key.replace(/[()]/g, "");
131-
const keyHasShiftModifier = key.includes("(");
132-
133-
// Handle toggle of layout for shift or caps lock
134-
const toggleLayout = () => {
135-
setLayoutName(prevLayout => (prevLayout === "default" ? "shift" : "default"));
136-
};
137-
138-
if (key === "CtrlAltDelete") {
139-
sendKeyboardEvent(
140-
[keys["Delete"]],
141-
[modifiers["ControlLeft"], modifiers["AltLeft"]],
142-
);
143-
setTimeout(resetKeyboardState, 100);
144-
return;
145-
}
142+
useEffect(() => {
143+
// if you have the CapsLock "down", then the shift state is inverted
144+
const effectiveShift = isCapsLockActive ? false === isShiftActive : isShiftActive;
145+
setLayoutName(effectiveShift ? "shift" : "default");
146+
},
147+
[setLayoutName, isCapsLockActive, isShiftActive]
148+
);
146149

147-
if (key === "AltMetaEscape") {
148-
sendKeyboardEvent(
149-
[keys["Escape"]],
150-
[modifiers["MetaLeft"], modifiers["AltLeft"]],
151-
);
150+
// this causes the buttons to look depressed/clicked depending on the sticky state
151+
useEffect(() => {
152+
let buttons = "None "; // make sure we name at least one (fake) button
153+
if (isCapsLockActive) buttons += "CapsLock ";
154+
if (isShiftActive) buttons += "ShiftLeft ShiftRight ";
155+
if (isCtrlActive) buttons += "ControlLeft ControlRight ";
156+
if (isAltActive) buttons += "AltLeft AltRight ";
157+
if (isMetaActive) buttons += "MetaLeft MetaRight ";
158+
setDepressedButtons(buttons.trimEnd());
159+
},
160+
[setDepressedButtons, isCapsLockActive, isShiftActive, isCtrlActive, isAltActive, isMetaActive, isAltGrActive]
161+
);
152162

153-
setTimeout(resetKeyboardState, 100);
154-
return;
155-
}
163+
const onKeyPress = useCallback((key: string) => {
164+
// handle the fake combo keys first
165+
if (key === "CtrlAltDelete") {
166+
sendKeyboardEvent(
167+
[keys["Delete"]],
168+
[modifiers["ControlLeft"], modifiers["AltLeft"]],
169+
);
170+
setTimeout(resetKeyboardState, 100);
171+
return;
172+
}
156173

157-
if (key === "CtrlAltBackspace") {
158-
sendKeyboardEvent(
159-
[keys["Backspace"]],
160-
[modifiers["ControlLeft"], modifiers["AltLeft"]],
161-
);
174+
if (key === "AltMetaEscape") {
175+
sendKeyboardEvent(
176+
[keys["Escape"]],
177+
[modifiers["MetaLeft"], modifiers["AltLeft"]],
178+
);
162179

163-
setTimeout(resetKeyboardState, 100);
164-
return;
165-
}
180+
setTimeout(resetKeyboardState, 100);
181+
return;
182+
}
166183

167-
if (isKeyShift || isKeyCaps) {
168-
toggleLayout();
184+
if (key === "CtrlAltBackspace") {
185+
sendKeyboardEvent(
186+
[keys["Backspace"]],
187+
[modifiers["ControlLeft"], modifiers["AltLeft"]],
188+
);
169189

170-
if (isCapsLockActive) {
171-
if (!isKeyboardLedManagedByHost) {
172-
setIsCapsLockActive(false);
173-
}
174-
sendKeyboardEvent([keys["CapsLock"]], []);
175-
return;
176-
}
177-
}
190+
setTimeout(resetKeyboardState, 100);
191+
return;
192+
}
178193

179-
// Handle caps lock state change
180-
if (isKeyCaps && !isKeyboardLedManagedByHost) {
181-
setIsCapsLockActive(!isCapsLockActive);
182-
}
194+
// strip away the parens for shifted characters
195+
const cleanKey = key.replace(/[()]/g, "");
196+
197+
const passthrough = ["PrintScreen", "SystemRequest", "Pause", "Break", "ScrollLock", "Enter", "Space"].find((value) => value === cleanKey);
198+
199+
if (passthrough) {
200+
emitkeycode(cleanKey);
201+
return;
202+
}
203+
204+
// adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr
205+
if (key === "CapsLock" && !isKeyboardLedManagedByHost)
206+
setIsCapsLockActive(!isCapsLockActive);
207+
else if (key === "ShiftLeft" || key === "ShiftRight")
208+
setIsShiftActive(!isShiftActive);
209+
else if (key === "ControlLeft" || key === "ControlRight")
210+
setIsCtrlActive(!isCtrlActive);
211+
else if (key === "AltLeft" || key === "AltRight")
212+
setIsAltActive(!isAltActive);
213+
else if (key === "MetaLeft" || key === "MetaRight")
214+
setIsMetaActive(!isMetaActive);
215+
else if (key === "AltGr")
216+
setIsAltGrActive(!isAltGrActive);
217+
218+
emitkeycode(cleanKey);
219+
220+
function emitkeycode(key: string) {
221+
const effectiveMods: number[] = [];
222+
223+
if (isShiftActive)
224+
effectiveMods.push(modifiers["ShiftLeft"]);
183225

184-
// Collect new active keys and modifiers
185-
const newKeys = keys[cleanKey] ? [keys[cleanKey]] : [];
186-
const newModifiers =
187-
keyHasShiftModifier && !isCapsLockActive ? [modifiers["ShiftLeft"]] : [];
226+
if (isCtrlActive)
227+
effectiveMods.push(modifiers["ControlLeft"]);
188228

189-
// Update current keys and modifiers
190-
sendKeyboardEvent(newKeys, newModifiers);
229+
if (isAltActive)
230+
effectiveMods.push(modifiers["AltLeft"]);
191231

192-
// If shift was used as a modifier and caps lock is not active, revert to default layout
193-
if (keyHasShiftModifier && !isCapsLockActive) {
194-
setLayoutName("default");
232+
if (isMetaActive)
233+
effectiveMods.push(modifiers["MetaLeft"]);
234+
235+
if (isAltGrActive) {
236+
effectiveMods.push(modifiers["MetaRight"]);
237+
effectiveMods.push(modifiers["CtrlLeft"]);
195238
}
196239

197-
setTimeout(resetKeyboardState, 100);
198-
},
199-
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
240+
const keycode = keys[key];
241+
if (keycode) {
242+
// send the keycode with modifiers
243+
sendKeyboardEvent([keycode], effectiveMods);
244+
}
245+
246+
// release the key (if one pressed), but retain the modifiers
247+
setTimeout(() => sendKeyboardEvent([], effectiveMods), 50);
248+
}
249+
},
250+
[isKeyboardLedManagedByHost,
251+
setIsCapsLockActive, isCapsLockActive,
252+
setIsShiftActive, isShiftActive,
253+
setIsCtrlActive, isCtrlActive,
254+
setIsAltActive, isAltActive,
255+
setIsMetaActive, isMetaActive,
256+
setIsAltGrActive, isAltGrActive,
257+
sendKeyboardEvent, resetKeyboardState
258+
],
200259
);
201260

202261
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
@@ -276,12 +335,16 @@ function KeyboardWrapper() {
276335
<Keyboard
277336
baseClass="simple-keyboard-main"
278337
layoutName={layoutName}
279-
onKeyPress={onKeyDown}
338+
onKeyPress={onKeyPress}
280339
buttonTheme={[
281340
{
282341
class: "combination-key",
283342
buttons: "CtrlAltDelete AltMetaEscape CtrlAltBackspace",
284343
},
344+
{
345+
class: "depressed-key",
346+
buttons: depressedButtons
347+
},
285348
]}
286349
display={keyDisplayMap}
287350
layout={{
@@ -305,34 +368,31 @@ function KeyboardWrapper() {
305368
],
306369
}}
307370
disableButtonHold={true}
308-
syncInstanceInputs={true}
309-
debug={false}
310371
/>
311372

312373
<div className="controlArrows">
313374
<Keyboard
314375
baseClass="simple-keyboard-control"
315376
theme="simple-keyboard hg-theme-default hg-layout-default"
316377
layoutName={layoutName}
317-
onKeyPress={onKeyDown}
378+
onKeyPress={onKeyPress}
318379
display={keyDisplayMap}
319380
layout={{
320-
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
321-
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
381+
default: ["PrintScreen ScrollLock Pause", "Insert Home PageUp", "Delete End PageDown"],
382+
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home PageUp", "Delete End PageDown"],
322383
}}
323-
syncInstanceInputs={true}
324-
debug={false}
384+
disableButtonHold={true}
325385
/>
326386
<Keyboard
327387
baseClass="simple-keyboard-arrows"
328388
theme="simple-keyboard hg-theme-default hg-layout-default"
329-
onKeyPress={onKeyDown}
389+
onKeyPress={onKeyPress}
330390
display={keyDisplayMap}
331391
layout={{
332392
default: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
393+
shift: ["ArrowUp", "ArrowLeft ArrowDown ArrowRight"],
333394
}}
334-
syncInstanceInputs={true}
335-
debug={false}
395+
disableButtonHold={true}
336396
/>
337397
</div>
338398
</div>

0 commit comments

Comments
 (0)