Skip to content

Commit cfc810a

Browse files
committed
impr(keymap): dynamic legends now show alt layer if supported by the layout
1 parent 6513e64 commit cfc810a

File tree

4 files changed

+159
-108
lines changed

4 files changed

+159
-108
lines changed

frontend/src/ts/elements/keymap.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import * as Hangul from "hangul-js";
99
import * as Notifications from "../elements/notifications";
1010
import * as ActivePage from "../states/active-page";
1111
import * as TestWords from "../test/test-words";
12+
import { capsState } from "../test/caps-warning";
13+
import * as ShiftTracker from "../test/shift-tracker";
14+
import * as AltTracker from "../test/alt-tracker";
15+
import * as KeyConverter from "../utils/key-converter";
1216

1317
const stenoKeys: JSONData.Layout = {
1418
keymapShowTopRow: true,
@@ -402,6 +406,112 @@ export async function refresh(
402406
}
403407
}
404408

409+
const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
410+
const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u;
411+
type KeymapLegendStates = [letters: 0 | 1 | 2 | 3, symbols: 0 | 1 | 2 | 3];
412+
let keymapLegendStates: KeymapLegendStates = [0, 0];
413+
414+
function getLegendStates(): KeymapLegendStates | undefined {
415+
// MacOS has different CapsLock and Shift logic than other operating systems
416+
// Windows and Linux only capitalize letters if either Shift OR CapsLock are
417+
// pressed, but not both at once.
418+
// MacOS instead capitalizes when either or both are pressed,
419+
// so we have to check for that.
420+
const shiftState = ShiftTracker.leftState || ShiftTracker.rightState;
421+
const altState = AltTracker.leftState || AltTracker.rightState;
422+
423+
const osDependentLettersState = isMacLike
424+
? shiftState || capsState
425+
: shiftState !== capsState;
426+
427+
const lettersState = (osDependentLettersState ? 1 : 0) + (altState ? 2 : 0);
428+
const symbolsState = (shiftState ? 1 : 0) + (altState ? 2 : 0);
429+
430+
const [previousLettersState, previousSymbolsState] = keymapLegendStates;
431+
432+
if (
433+
previousLettersState === lettersState &&
434+
previousSymbolsState === symbolsState
435+
) {
436+
return;
437+
}
438+
439+
keymapLegendStates = [
440+
lettersState as 0 | 1 | 2 | 3,
441+
symbolsState as 0 | 1 | 2 | 3,
442+
];
443+
return keymapLegendStates;
444+
}
445+
446+
async function updateLegends(): Promise<void> {
447+
const states = getLegendStates();
448+
if (states === undefined) return;
449+
450+
const keymapKeys = [...document.getElementsByClassName("keymapKey")].filter(
451+
(el) => {
452+
const isKeymapKey = el.classList.contains("keymapKey");
453+
const isNotSpace = !el.classList.contains("keySpace");
454+
455+
return isKeymapKey && isNotSpace;
456+
}
457+
) as HTMLElement[];
458+
459+
const layoutKeys = keymapKeys.map((el) => el.dataset["key"]);
460+
if (layoutKeys.includes(undefined)) return;
461+
462+
const keys = keymapKeys.map((el) => el.childNodes[1]);
463+
464+
const [lettersState, symbolsState] = states;
465+
466+
const layoutName =
467+
Config.keymapLayout === "overrideSync"
468+
? Config.layout === "default"
469+
? "qwerty"
470+
: Config.layout
471+
: Config.keymapLayout;
472+
473+
const layout = await JSONData.getLayout(layoutName).catch(() => undefined);
474+
if (layout === undefined) {
475+
Notifications.add("Failed to load keymap layout", -1);
476+
477+
return;
478+
}
479+
480+
for (let i = 0; i < layoutKeys.length; i++) {
481+
const layoutKey = layoutKeys[i] as string;
482+
const key = keys[i];
483+
const lowerCaseCharacter = layoutKey[0];
484+
const upperCaseCharacter = layoutKey[1];
485+
486+
if (
487+
key === undefined ||
488+
layoutKey === undefined ||
489+
lowerCaseCharacter === undefined ||
490+
upperCaseCharacter === undefined
491+
)
492+
continue;
493+
494+
const keyIsSymbol = [lowerCaseCharacter, upperCaseCharacter].some(
495+
(character) => symbolsPattern.test(character ?? "")
496+
);
497+
498+
const keycode = KeyConverter.layoutKeyToKeycode(lowerCaseCharacter, layout);
499+
if (keycode === undefined) {
500+
return;
501+
}
502+
const oppositeShift = ShiftTracker.isUsingOppositeShift(keycode);
503+
504+
const state = keyIsSymbol ? symbolsState : lettersState;
505+
const characterIndex = oppositeShift ? state : 0;
506+
507+
//if the character at the index is undefined, try without alt
508+
const character =
509+
layoutKey[characterIndex] ?? layoutKey[characterIndex - 2];
510+
511+
key.textContent = character ?? "";
512+
}
513+
}
514+
405515
ConfigEvent.subscribe((eventKey, newValue) => {
406516
if (eventKey === "layout" && Config.keymapLayout === "overrideSync") {
407517
void refresh(Config.keymapLayout);
@@ -427,3 +537,27 @@ KeymapEvent.subscribe((mode, key, correct) => {
427537
void flashKey(key, correct);
428538
}
429539
});
540+
541+
$(document).on("keydown", (e) => {
542+
if (
543+
Config.keymapLegendStyle === "dynamic" &&
544+
(e.code === "ShiftLeft" ||
545+
e.code === "ShiftRight" ||
546+
e.code === "AltRight" ||
547+
e.code === "AltLeft")
548+
) {
549+
void updateLegends();
550+
}
551+
});
552+
553+
$(document).on("keyup", (e) => {
554+
if (
555+
Config.keymapLegendStyle === "dynamic" &&
556+
(e.code === "ShiftLeft" ||
557+
e.code === "ShiftRight" ||
558+
e.code === "AltRight" ||
559+
e.code === "AltLeft")
560+
) {
561+
void updateLegends();
562+
}
563+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export let leftState = false;
2+
export let rightState = false;
3+
4+
$(document).on("keydown", (e) => {
5+
if (e.code === "AltLeft") {
6+
leftState = true;
7+
} else if (e.code === "AltRight") {
8+
rightState = true;
9+
}
10+
});
11+
12+
$(document).on("keyup", (e) => {
13+
if (e.code === "AltLeft") {
14+
leftState = false;
15+
} else if (e.code === "AltRight") {
16+
rightState = false;
17+
}
18+
});
19+
20+
export function reset(): void {
21+
leftState = false;
22+
rightState = false;
23+
}

frontend/src/ts/test/shift-tracker.ts

Lines changed: 0 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,9 @@
11
import Config from "../config";
2-
import * as JSONData from "../utils/json-data";
3-
import { capsState } from "./caps-warning";
4-
import * as Notifications from "../elements/notifications";
52
import * as KeyConverter from "../utils/key-converter";
63

74
export let leftState = false;
85
export let rightState = false;
96

10-
type KeymapLegendStates = [letters: boolean, symbols: boolean];
11-
12-
const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u;
13-
14-
const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
15-
16-
let keymapLegendStates: KeymapLegendStates = [false, false];
17-
function getLegendStates(): KeymapLegendStates | undefined {
18-
const symbolsState = leftState || rightState;
19-
// MacOS has different CapsLock and Shift logic than other operating systems
20-
// Windows and Linux only capitalize letters if either Shift OR CapsLock are
21-
// pressed, but not both at once.
22-
// MacOS instead capitalizes when either or both are pressed,
23-
// so we have to check for that.
24-
const lettersState = isMacLike
25-
? symbolsState || capsState
26-
: symbolsState !== capsState;
27-
28-
const [previousLettersState, previousSymbolsState] = keymapLegendStates;
29-
30-
if (
31-
previousLettersState === lettersState &&
32-
previousSymbolsState === symbolsState
33-
) {
34-
return;
35-
}
36-
37-
return (keymapLegendStates = [lettersState, symbolsState]);
38-
}
39-
40-
async function updateKeymapLegendCasing(): Promise<void> {
41-
const states = getLegendStates();
42-
if (states === undefined) return;
43-
44-
const keymapKeys = [...document.getElementsByClassName("keymapKey")].filter(
45-
(el) => {
46-
const isKeymapKey = el.classList.contains("keymapKey");
47-
const isNotSpace = !el.classList.contains("keySpace");
48-
49-
return isKeymapKey && isNotSpace;
50-
}
51-
) as HTMLElement[];
52-
53-
const layoutKeys = keymapKeys.map((el) => el.dataset["key"]);
54-
if (layoutKeys.includes(undefined)) return;
55-
56-
const keys = keymapKeys.map((el) => el.childNodes[1]);
57-
58-
const [lettersState, symbolsState] = states;
59-
60-
const layoutName =
61-
Config.keymapLayout === "overrideSync"
62-
? Config.layout === "default"
63-
? "qwerty"
64-
: Config.layout
65-
: Config.keymapLayout;
66-
67-
const layout = await JSONData.getLayout(layoutName).catch(() => undefined);
68-
if (layout === undefined) {
69-
Notifications.add("Failed to load keymap layout", -1);
70-
71-
return;
72-
}
73-
74-
for (let i = 0; i < layoutKeys.length; i++) {
75-
const layoutKey = layoutKeys[i] as string;
76-
const key = keys[i];
77-
const lowerCaseCharacter = layoutKey[0];
78-
const upperCaseCharacter = layoutKey[1];
79-
80-
if (
81-
key === undefined ||
82-
layoutKey === undefined ||
83-
lowerCaseCharacter === undefined ||
84-
upperCaseCharacter === undefined
85-
)
86-
continue;
87-
88-
const keyIsSymbol = [lowerCaseCharacter, upperCaseCharacter].some(
89-
(character) => symbolsPattern.test(character ?? "")
90-
);
91-
92-
const keycode = KeyConverter.layoutKeyToKeycode(lowerCaseCharacter, layout);
93-
if (keycode === undefined) {
94-
return;
95-
}
96-
const oppositeShift = isUsingOppositeShift(keycode);
97-
98-
const state = keyIsSymbol ? symbolsState : lettersState;
99-
const capitalize = oppositeShift && state;
100-
const keyIndex = Number(capitalize);
101-
const character = layoutKey[keyIndex];
102-
103-
key.textContent = character ?? "";
104-
}
105-
}
106-
1077
$(document).on("keydown", (e) => {
1088
if (e.code === "ShiftLeft") {
1099
leftState = true;
@@ -112,21 +12,13 @@ $(document).on("keydown", (e) => {
11212
leftState = false;
11313
rightState = true;
11414
}
115-
116-
if (Config.keymapLegendStyle === "dynamic") {
117-
void updateKeymapLegendCasing();
118-
}
11915
});
12016

12117
$(document).on("keyup", (e) => {
12218
if (e.code === "ShiftLeft" || e.code === "ShiftRight") {
12319
leftState = false;
12420
rightState = false;
12521
}
126-
127-
if (Config.keymapLegendStyle === "dynamic") {
128-
void updateKeymapLegendCasing();
129-
}
13022
});
13123

13224
export function reset(): void {

frontend/src/ts/test/test-logic.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as CustomTextState from "../states/custom-text-name";
1313
import * as TestStats from "./test-stats";
1414
import * as PractiseWords from "./practise-words";
1515
import * as ShiftTracker from "./shift-tracker";
16+
import * as AltTracker from "./alt-tracker";
1617
import * as Focus from "./focus";
1718
import * as Funbox from "./funbox/funbox";
1819
import * as Keymap from "../elements/keymap";
@@ -255,6 +256,7 @@ export function restart(options = {} as RestartOptions): void {
255256
TestInput.restart();
256257
TestInput.corrected.reset();
257258
ShiftTracker.reset();
259+
AltTracker.reset();
258260
Caret.hide();
259261
TestState.setActive(false);
260262
Replay.stopReplayRecording();

0 commit comments

Comments
 (0)