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; }