From b4e8eecfb87b1080653ef8a8d4951e49eed4eb04 Mon Sep 17 00:00:00 2001 From: Evan <64989416+Ferotiq@users.noreply.github.com> Date: Mon, 3 Apr 2023 05:18:31 -0500 Subject: [PATCH] Update `dynamic` Keymap Legend Style to More Accurately Reflect Casing (#4113) Ferotiq * use `.on` * update symbols and letters seperately This is based off how Shift and Caps Lock are fundamentally different. Shift changes every key while Caps Lock just changes letters. In addition, MacOS and Windows handle Caps Lock differently. In Windows, Shift can revert the letters change of Caps Lock, and on MacOS it cannot. So I have also added in an edge case for MacOS. * reduce the diff * explain why there's a platform check * remove export * update `isUsingOppositeShift` to accept keycodes This allows the keymap to update only what can be capitalized when `oppositeShiftMode` is on * simplify * fix support for iso layouts * fix `oppositeShiftMode`: `keymap` not working * should default to true * whoops * error handling --- .../src/ts/controllers/input-controller.ts | 24 +- frontend/src/ts/test/shift-tracker.ts | 316 +++++++++++------- 2 files changed, 212 insertions(+), 128 deletions(-) diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 411bdacae..4316fa51d 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -18,6 +18,7 @@ import * as Focus from "../test/focus"; import * as ShiftTracker from "../test/shift-tracker"; import * as Replay from "../test/replay"; import * as MonkeyPower from "../elements/monkey-power"; +import * as Notifications from "../elements/notifications"; import * as WeakSpot from "../test/weak-spot"; import * as ActivePage from "../states/active-page"; import * as TestState from "../test/test-state"; @@ -906,8 +907,27 @@ $(document).keydown(async (event) => { } if (Config.oppositeShiftMode !== "off") { - correctShiftUsed = - (await ShiftTracker.isUsingOppositeShift(event)) !== false; + if ( + Config.oppositeShiftMode === "keymap" && + Config.keymapLayout !== "overrideSync" + ) { + const keymapLayout = await Misc.getLayout(Config.keymapLayout).catch( + () => undefined + ); + if (keymapLayout === undefined) { + Notifications.add("Failed to load keymap layout", -1); + + return; + } + const keycode = ShiftTracker.layoutKeyToKeycode(event.key, keymapLayout); + + correctShiftUsed = + keycode === undefined + ? true + : ShiftTracker.isUsingOppositeShift(keycode); + } else { + correctShiftUsed = ShiftTracker.isUsingOppositeShift(event.code); + } } const funbox = FunboxList.get(Config.funbox).find( diff --git a/frontend/src/ts/test/shift-tracker.ts b/frontend/src/ts/test/shift-tracker.ts index 875518121..6d4a270e4 100644 --- a/frontend/src/ts/test/shift-tracker.ts +++ b/frontend/src/ts/test/shift-tracker.ts @@ -5,107 +5,153 @@ import * as Notifications from "../elements/notifications"; export let leftState = false; export let rightState = false; -let caseState = false; -interface KeymapStrings { - left: string[] | null; - right: string[] | null; - keymap: string | null; -} +type KeymapLegendStates = [letters: boolean, symbols: boolean]; -const keymapStrings: KeymapStrings = { - left: null, - right: null, - keymap: null, -}; +const qwertyKeycodeKeymap = [ + [ + "Backquote", + "Digit1", + "Digit2", + "Digit3", + "Digit4", + "Digit5", + "Digit6", + "Digit7", + "Digit8", + "Digit9", + "Digit0", + "Minus", + "Equal", + ], + [ + "KeyQ", + "KeyW", + "KeyE", + "KeyR", + "KeyT", + "KeyY", + "KeyU", + "KeyI", + "KeyO", + "KeyP", + "BracketLeft", + "BracketRight", + "Backslash", + ], + [ + "KeyA", + "KeyS", + "KeyD", + "KeyF", + "KeyG", + "KeyH", + "KeyJ", + "KeyK", + "KeyL", + "Semicolon", + "Quote", + ], + [ + "KeyZ", + "KeyX", + "KeyC", + "KeyV", + "KeyB", + "KeyN", + "KeyM", + "Comma", + "Period", + "Slash", + ], + ["Space"], +]; -function dynamicKeymapLegendStyle(uppercase: boolean): void { - const keymapKeys = [ - ...document.getElementsByClassName("keymapKey"), - ]; +const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u; - const layoutKeys = keymapKeys.map((el) => el.dataset["key"]); +const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform); - const keys = keymapKeys.map((el) => el.childNodes[1]); +let keymapLegendStates: KeymapLegendStates = [false, false]; +function getLegendStates(): KeymapLegendStates | undefined { + const symbolsState = leftState || rightState; + // MacOS has different CapsLock and Shift logic than other operating systems + // Windows and Linux only capitalize letters if either Shift OR CapsLock are + // pressed, but not both at once. + // MacOS instead capitalizes when either or both are pressed, + // so we have to check for that. + const lettersState = isMacLike + ? symbolsState || capsState + : symbolsState !== capsState; - if (capsState) uppercase = !uppercase; + const [previousLettersState, previousSymbolsState] = keymapLegendStates; - if (layoutKeys.filter((v) => v === undefined).length > 2) return; - - if ((uppercase && caseState) || (!uppercase && !caseState)) return; - - caseState = uppercase; - - const index = caseState ? 1 : 0; - - for (let i = 0; i < layoutKeys.length; i++) { - const layoutKey = layoutKeys[i], - key = keys[i]; - - if (key === undefined || layoutKey === undefined) continue; - - key.textContent = layoutKey[index]; - } -} - -async function buildKeymapStrings(): Promise { - if (keymapStrings.keymap === Config.keymapLayout) return; - - const layoutName = - Config.keymapLayout === "overrideSync" - ? Config.layout - : Config.keymapLayout; - - let layout; - try { - layout = await Misc.getLayout(layoutName); - } catch (e) { - Notifications.add( - Misc.createErrorMessage(e, "Failed to track shift state"), - -1 - ); + if ( + previousLettersState === lettersState && + previousSymbolsState === symbolsState + ) { return; } - const layoutKeys = layout.keys; - const layoutKeysEntries = Object.entries(layoutKeys) as [string, string[]][]; + return (keymapLegendStates = [lettersState, symbolsState]); +} - keymapStrings.keymap = Config.keymapLayout; +async function updateKeymapLegendCasing(): Promise { + const states = getLegendStates(); + if (states === undefined) return; - if (!layout) { - keymapStrings.left = null; - keymapStrings.right = null; - } else { - keymapStrings.left = layoutKeysEntries - .map(([rowName, row]) => - row - // includes "6" and "y" (buttons on qwerty) into the left hand - .slice( - 0, - ["row1", "row2"].includes(rowName) - ? rowName === "row1" - ? 7 - : 6 - : 5 - ) - .map((key) => key.split("")) - ) - .flat(2); + const keymapKeys = ( + [...document.getElementsByClassName("keymapKey")].filter( + (el) => el.className === "keymapKey" + ) + ); - keymapStrings.right = layoutKeysEntries - .map(([rowName, row]) => - row - // includes "b" (buttons on qwerty) into the right hand - .slice( - ["row1", "row4"].includes(rowName) - ? rowName === "row1" - ? 6 - : 4 - : 5 - ) - .map((key) => key.split("")) - ) - .flat(2); + const layoutKeys = keymapKeys.map((el) => el.dataset["key"]); + if (layoutKeys.includes(undefined)) return; + + const keys = keymapKeys.map((el) => el.childNodes[1]); + + const [lettersState, symbolsState] = states; + + const layoutName = + Config.keymapLayout === "overrideSync" + ? Config.layout === "default" + ? "qwerty" + : Config.layout + : Config.keymapLayout; + + const layout = await Misc.getLayout(layoutName).catch(() => undefined); + if (layout === undefined) { + Notifications.add("Failed to load keymap layout", -1); + + return; + } + + for (let i = 0; i < layoutKeys.length; i++) { + const layoutKey = layoutKeys[i]; + const key = keys[i]; + + if (key === undefined || layoutKey === undefined) continue; + + const lowerCaseCharacter = layoutKey[0]; + const upperCaseCharacter = layoutKey[1]; + + const keyIsSymbol = [lowerCaseCharacter, upperCaseCharacter].some( + (character) => symbolsPattern.test(character) + ); + + const keycode = layoutKeyToKeycode(lowerCaseCharacter, layout); + if (keycode === undefined) { + return; + } + + const oppositeShift = isUsingOppositeShift(keycode); + + const state = keyIsSymbol ? symbolsState : lettersState; + const capitalize = oppositeShift && state; + const keyIndex = Number(capitalize); + const character = layoutKey[keyIndex]; + + key.textContent = character; } } @@ -119,18 +165,18 @@ $(document).on("keydown", (e) => { } if (Config.keymapLegendStyle === "dynamic") { - dynamicKeymapLegendStyle(leftState || rightState); + updateKeymapLegendCasing(); } }); -$(document).keyup((e) => { +$(document).on("keyup", (e) => { if (e.code === "ShiftLeft" || e.code === "ShiftRight") { leftState = false; rightState = false; } if (Config.keymapLegendStyle === "dynamic") { - dynamicKeymapLegendStyle(leftState || rightState); + updateKeymapLegendCasing(); } }); @@ -145,6 +191,7 @@ const leftSideKeys = [ "KeyE", "KeyR", "KeyT", + "KeyY", "KeyA", "KeyS", @@ -152,10 +199,12 @@ const leftSideKeys = [ "KeyF", "KeyG", + "IntlBackslash", "KeyZ", "KeyX", "KeyC", "KeyV", + "KeyB", "Backquote", "Digit1", @@ -163,9 +212,11 @@ const leftSideKeys = [ "Digit3", "Digit4", "Digit5", + "Digit6", ]; const rightSideKeys = [ + "KeyY", "KeyU", "KeyI", "KeyO", @@ -176,13 +227,17 @@ const rightSideKeys = [ "KeyK", "KeyL", + "KeyB", "KeyN", "KeyM", + "Digit6", "Digit7", "Digit8", "Digit9", "Digit0", + "Minus", + "Equal", "Backslash", "BracketLeft", @@ -194,46 +249,55 @@ const rightSideKeys = [ "Slash", ]; -export async function isUsingOppositeShift( - event: JQuery.KeyDownEvent -): Promise { - if (!leftState && !rightState) return null; +export function isUsingOppositeShift(keycode: string): boolean { + if (!leftState && !rightState) { + return true; + } - if ( - Config.oppositeShiftMode === "on" || - (Config.oppositeShiftMode === "keymap" && - Config.keymapLayout === "overrideSync" && - Config.layout === "default") - ) { - if ( - !rightSideKeys.includes(event.code) && - !leftSideKeys.includes(event.code) - ) { - return null; - } + if (Config.oppositeShiftMode === "off") { + return true; + } - if ( - (leftState && rightSideKeys.includes(event.code)) || - (rightState && leftSideKeys.includes(event.code)) - ) { - return true; - } else { - return false; - } - } else if (Config.oppositeShiftMode === "keymap") { - await buildKeymapStrings(); + const isRight = rightSideKeys.includes(keycode); + const isLeft = leftSideKeys.includes(keycode); + if (!isRight && !isLeft) { + return true; + } - if (!keymapStrings.left || !keymapStrings.right) return null; + if ((leftState && isRight) || (rightState && isLeft)) { + return true; + } - if ( - (leftState && keymapStrings.right.includes(event.key)) || - (rightState && keymapStrings.left.includes(event.key)) - ) { - return true; - } else { - return false; + return false; +} + +export function layoutKeyToKeycode( + key: string, + layout: MonkeyTypes.Layout +): string | undefined { + const rows: string[][] = Object.values(layout.keys); + + const rowIndex = rows.findIndex((row) => row.find((k) => k.includes(key))); + const row = rows[rowIndex]; + if (row === undefined) { + return; + } + + const keyIndex = row.findIndex((k) => k.includes(key)); + if (keyIndex === -1) { + return; + } + + let keycode = qwertyKeycodeKeymap[rowIndex][keyIndex]; + if (layout.type === "iso") { + if (rowIndex === 2 && keyIndex === 11) { + keycode = "Backslash"; + } else if (rowIndex === 3 && keyIndex === 0) { + keycode = "IntlBackslash"; + } else if (rowIndex === 3) { + keycode = qwertyKeycodeKeymap[3][keyIndex - 1]; } } - return true; + return keycode; }