Skip to content

feat(ui): Enhances virtual keyboard with sticky modifier key support #500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from

Conversation

IDisposable
Copy link
Contributor

@IDisposable IDisposable commented May 22, 2025

Adds support for Shift, Ctrl, Alt, Meta, and AltGr keys to the virtual keyboard, treating them as "sticky" keys when entered on the Virtual Keyboard. This allows the user to click the Shift or Ctrl or Alt or Meta or AltGr button and then click another button to emit the combo (e.g. Ctrl-Alt-Del would be done using exactly that click sequence).

  • Tracks and displays the active state of modifier keys (Shift, Ctrl, Alt, Meta, AltGr, CapsLock, NumLock, ScrollLock) in the InfoBar.
  • Updates the virtual keyboard to reflect the state of these modifier keys, including a "depressed" visual style.
  • Changes the virtual keyboard layout based on current Shift and CapsLock states.
  • Improves keyboard event handling to correctly send keycodes with appropriate modifiers.
  • Updated the InfoBar to show the status of all sticky keys
  • Added code to the WebRTCVideo.tsx to ensure that physical keys are also kept in sync so you can use have the Virtual Keyboard up, use the physical Ctrl (or other) key and click the desired character. For example Ctrl-C

@IDisposable
Copy link
Contributor Author

@ym @adamshiervani This is ready for review and works really well ;)

I am not sure about the CSS style of the "depressed" buttons, I made something up and welcome other suggestions.

@IDisposable
Copy link
Contributor Author

The sticky keys and InfoBar look like this
image

@@ -37,6 +37,11 @@ export default function InfoBar() {
}, [rpcDataChannel]);

const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
const isShiftActive = useHidStore(state => state.isShiftActive);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State us kept in sync between the physical keyboard (in WebRTCVideo.tsx) and the virtual keyboard (in VirtualKeyboard.tsx)

@@ -118,6 +123,56 @@ export default function InfoBar() {
Relayed by Cloudflare
</div>
)}
<div
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to hide some of these if the browser window get smaller eventually.

@@ -26,6 +26,7 @@ const AttachIcon = ({ className }: { className?: string }) => {

function KeyboardWrapper() {
const [layoutName, setLayoutName] = useState("default");
const [depressedButtons, setDepressedButtons] = useState("");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tracks which buttons are "sticky down" to add them to the main keyboard's depressed-key class... so they get a different CSS style.

useEffect(() => {
// if you have the CapsLock "down", then the shift state is inverted
const effectiveShift = isCapsLockActive ? false === isShiftActive : isShiftActive;
setLayoutName(effectiveShift ? "shift" : "default");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a useEffect makes this so much easier... and it stays in sync :)

[keys["Escape"]],
[modifiers["MetaLeft"], modifiers["AltLeft"]],
);
// this causes the buttons to look depressed/clicked depending on the sticky state
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we build up the button names of the ones that should be stuck down.

const cleanKey = key.replace(/[()]/g, "");

// if it's one of the special keys, just pass it through immediately and return
const passthrough = ["PrintScreen", "SystemRequest", "Pause", "Break", "ScrollLock", "Enter", "Space"].find((value) => value === cleanKey);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the keys that should just get emitted without further massaging, we have a preemptive emit and return

return;
}

// adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we take the key click and turn it into toggling the modifier states.

emitkeycode(cleanKey);

function emitkeycode(key: string) {
const effectiveMods: number[] = [];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now, since we're tracking HID wide, the state of the modifiers we can just compute it!

sendKeyboardEvent([keycode], effectiveMods);
}

// release the key (if one pressed), but retain the modifiers
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the biggest gain... we can leave the modifier state still reflecting what we're tracking it to be so that the remote knows we still have the corresponding keys down. This is especially important for CapsLock because we want the remote to know we've got that on so it can do the warnings and such.

display={keyDisplayMap}
layout={{
default: ["PrintScreen ScrollLock Pause", "Insert Home Pageup", "Delete End Pagedown"],
shift: ["(PrintScreen) ScrollLock (Pause)", "Insert Home Pageup", "Delete End Pagedown"],
default: ["PrintScreen ScrollLock Pause", "Insert Home PageUp", "Delete End PageDown"],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, fixed the name

@@ -56,8 +56,16 @@ export default function WebRTCVideo() {
const isVideoLoading = !isPlaying;

// Keyboard related states
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
useHidStore();
const {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we're tracking the physical keyboard state to keep things in sync

@@ -222,6 +222,10 @@ video::-webkit-media-controls {
background: none;
}

.simple-keyboard .hg-button.depressed-key {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the "depressed button" style... I welcome better ideas.

Tab: "tab",
Backspace: "backspace",
"(Backspace)": "backspace",
Tab: "tab ⇥",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the cool unicode characters for the keys, so the keyboard looks nicer now.

@IDisposable IDisposable marked this pull request as ready for review May 22, 2025 10:45
@IDisposable IDisposable changed the title Enhances virtual keyboard with sticky modifier key support feat(ui): Enhances virtual keyboard with sticky modifier key support May 22, 2025
@IDisposable IDisposable force-pushed the feat/virtual-keyboard-sticky branch from 5be305a to 2020b6c Compare May 23, 2025 08:06
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,
@IDisposable IDisposable force-pushed the feat/virtual-keyboard-sticky branch from 2020b6c to 642f0a5 Compare May 23, 2025 08:12
@IDisposable
Copy link
Contributor Author

Rebased and ready for review @ym

}

// adjust the sticky state of the Shift/Ctrl/Alt/Meta/AltGr
if (key === "CapsLock" && !isKeyboardLedManagedByHost)
Copy link
Contributor Author

@IDisposable IDisposable May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant