Skip to content

Commit

Permalink
EnglishStringToCodeMap values are an array of the keys that could be …
Browse files Browse the repository at this point in the history
…pressed for that english string key, see #1520
  • Loading branch information
jessegreenberg committed Jul 19, 2023
1 parent e5741c3 commit 90c27f4
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 126 deletions.
145 changes: 66 additions & 79 deletions js/accessibility/EnglishStringToCodeMap.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,80 @@
// Copyright 2022-2023, University of Colorado Boulder

/**
* Maps the english key you want to use to the associated KeyboardEvent.codes for usage in listeners.
* If a key has multiple code values, listener behavior will fire if either are pressed.
*
* @author Jesse Greenberg (PhET Interactive Simulations)
*/

import { KeyboardUtils, scenery } from '../imports.js';

//
const EnglishStringToCodeMap = {
q: KeyboardUtils.KEY_Q,
w: KeyboardUtils.KEY_W,
e: KeyboardUtils.KEY_E,
r: KeyboardUtils.KEY_R,
t: KeyboardUtils.KEY_T,
y: KeyboardUtils.KEY_Y,
u: KeyboardUtils.KEY_U,
i: KeyboardUtils.KEY_I,
o: KeyboardUtils.KEY_O,
p: KeyboardUtils.KEY_P,
a: KeyboardUtils.KEY_A,
s: KeyboardUtils.KEY_S,
d: KeyboardUtils.KEY_D,
f: KeyboardUtils.KEY_F,
g: KeyboardUtils.KEY_G,
h: KeyboardUtils.KEY_H,
j: KeyboardUtils.KEY_J,
k: KeyboardUtils.KEY_K,
l: KeyboardUtils.KEY_L,
z: KeyboardUtils.KEY_Z,
x: KeyboardUtils.KEY_X,
c: KeyboardUtils.KEY_C,
v: KeyboardUtils.KEY_V,
b: KeyboardUtils.KEY_B,
n: KeyboardUtils.KEY_N,
m: KeyboardUtils.KEY_M,
0: KeyboardUtils.KEY_0,
1: KeyboardUtils.KEY_1,
2: KeyboardUtils.KEY_2,
3: KeyboardUtils.KEY_3,
4: KeyboardUtils.KEY_4,
5: KeyboardUtils.KEY_5,
6: KeyboardUtils.KEY_6,
7: KeyboardUtils.KEY_7,
8: KeyboardUtils.KEY_8,
9: KeyboardUtils.KEY_9,
[ KeyboardUtils.KEY_NUMPAD_0 ]: KeyboardUtils.KEY_NUMPAD_0,
[ KeyboardUtils.KEY_NUMPAD_1 ]: KeyboardUtils.KEY_NUMPAD_1,
[ KeyboardUtils.KEY_NUMPAD_2 ]: KeyboardUtils.KEY_NUMPAD_2,
[ KeyboardUtils.KEY_NUMPAD_3 ]: KeyboardUtils.KEY_NUMPAD_3,
[ KeyboardUtils.KEY_NUMPAD_4 ]: KeyboardUtils.KEY_NUMPAD_4,
[ KeyboardUtils.KEY_NUMPAD_5 ]: KeyboardUtils.KEY_NUMPAD_5,
[ KeyboardUtils.KEY_NUMPAD_6 ]: KeyboardUtils.KEY_NUMPAD_6,
[ KeyboardUtils.KEY_NUMPAD_7 ]: KeyboardUtils.KEY_NUMPAD_7,
[ KeyboardUtils.KEY_NUMPAD_8 ]: KeyboardUtils.KEY_NUMPAD_8,
[ KeyboardUtils.KEY_NUMPAD_9 ]: KeyboardUtils.KEY_NUMPAD_9,
[ KeyboardUtils.KEY_NUMPAD_DECIMAL ]: KeyboardUtils.KEY_NUMPAD_DECIMAL,
[ KeyboardUtils.KEY_NUMPAD_DECIMAL ]: KeyboardUtils.KEY_NUMPAD_DECIMAL,
[ KeyboardUtils.KEY_NUMPAD_PLUS ]: KeyboardUtils.KEY_NUMPAD_PLUS,
[ KeyboardUtils.KEY_NUMPAD_MINUS ]: KeyboardUtils.KEY_NUMPAD_MINUS,
const EnglishStringToCodeMap: Record<string, string[]> = {

ctrl: KeyboardUtils.KEY_CONTROL,
alt: KeyboardUtils.KEY_ALT,
shift: KeyboardUtils.KEY_SHIFT,
ctrlLeft: KeyboardUtils.KEY_CONTROL_LEFT,
ctrlRight: KeyboardUtils.KEY_CONTROL_RIGHT,
shiftLeft: KeyboardUtils.KEY_SHIFT_LEFT,
shiftRight: KeyboardUtils.KEY_SHIFT_RIGHT,
altLeft: KeyboardUtils.KEY_ALT_LEFT,
altRight: KeyboardUtils.KEY_ALT_RIGHT,
// Letter keys
q: [ KeyboardUtils.KEY_Q ],
w: [ KeyboardUtils.KEY_W ],
e: [ KeyboardUtils.KEY_E ],
r: [ KeyboardUtils.KEY_R ],
t: [ KeyboardUtils.KEY_T ],
y: [ KeyboardUtils.KEY_Y ],
u: [ KeyboardUtils.KEY_U ],
i: [ KeyboardUtils.KEY_I ],
o: [ KeyboardUtils.KEY_O ],
p: [ KeyboardUtils.KEY_P ],
a: [ KeyboardUtils.KEY_A ],
s: [ KeyboardUtils.KEY_S ],
d: [ KeyboardUtils.KEY_D ],
f: [ KeyboardUtils.KEY_F ],
g: [ KeyboardUtils.KEY_G ],
h: [ KeyboardUtils.KEY_H ],
j: [ KeyboardUtils.KEY_J ],
k: [ KeyboardUtils.KEY_K ],
l: [ KeyboardUtils.KEY_L ],
z: [ KeyboardUtils.KEY_Z ],
x: [ KeyboardUtils.KEY_X ],
c: [ KeyboardUtils.KEY_C ],
v: [ KeyboardUtils.KEY_V ],
b: [ KeyboardUtils.KEY_B ],
n: [ KeyboardUtils.KEY_N ],
m: [ KeyboardUtils.KEY_M ],

enter: KeyboardUtils.KEY_ENTER,
tab: KeyboardUtils.KEY_TAB,
equals: KeyboardUtils.KEY_EQUALS,
plus: KeyboardUtils.KEY_PLUS,
minus: KeyboardUtils.KEY_MINUS,
period: KeyboardUtils.KEY_PERIOD,
escape: KeyboardUtils.KEY_ESCAPE,
delete: KeyboardUtils.KEY_DELETE,
backspace: KeyboardUtils.KEY_BACKSPACE,
page_up: KeyboardUtils.KEY_PAGE_UP,
page_down: KeyboardUtils.KEY_PAGE_DOWN,
end: KeyboardUtils.KEY_END,
home: KeyboardUtils.KEY_HOME,
// number keys - number and numpad
0: [ KeyboardUtils.KEY_0, KeyboardUtils.KEY_NUMPAD_0 ],
1: [ KeyboardUtils.KEY_1, KeyboardUtils.KEY_NUMPAD_1 ],
2: [ KeyboardUtils.KEY_2, KeyboardUtils.KEY_NUMPAD_2 ],
3: [ KeyboardUtils.KEY_3, KeyboardUtils.KEY_NUMPAD_3 ],
4: [ KeyboardUtils.KEY_4, KeyboardUtils.KEY_NUMPAD_4 ],
5: [ KeyboardUtils.KEY_5, KeyboardUtils.KEY_NUMPAD_5 ],
6: [ KeyboardUtils.KEY_6, KeyboardUtils.KEY_NUMPAD_6 ],
7: [ KeyboardUtils.KEY_7, KeyboardUtils.KEY_NUMPAD_7 ],
8: [ KeyboardUtils.KEY_8, KeyboardUtils.KEY_NUMPAD_8 ],
9: [ KeyboardUtils.KEY_9, KeyboardUtils.KEY_NUMPAD_9 ],

space: KeyboardUtils.KEY_SPACE,
arrowLeft: KeyboardUtils.KEY_LEFT_ARROW,
arrowRight: KeyboardUtils.KEY_RIGHT_ARROW,
arrowUp: KeyboardUtils.KEY_UP_ARROW,
arrowDown: KeyboardUtils.KEY_DOWN_ARROW
// various command keys
enter: [ KeyboardUtils.KEY_ENTER ],
tab: [ KeyboardUtils.KEY_TAB ],
equals: [ KeyboardUtils.KEY_EQUALS ],
plus: [ KeyboardUtils.KEY_PLUS, KeyboardUtils.KEY_NUMPAD_PLUS ],
minus: [ KeyboardUtils.KEY_MINUS, KeyboardUtils.KEY_NUMPAD_MINUS ],
period: [ KeyboardUtils.KEY_PERIOD, KeyboardUtils.KEY_NUMPAD_DECIMAL ],
escape: [ KeyboardUtils.KEY_ESCAPE ],
delete: [ KeyboardUtils.KEY_DELETE ],
backspace: [ KeyboardUtils.KEY_BACKSPACE ],
page_up: [ KeyboardUtils.KEY_PAGE_UP ],
page_down: [ KeyboardUtils.KEY_PAGE_DOWN ],
end: [ KeyboardUtils.KEY_END ],
home: [ KeyboardUtils.KEY_HOME ],
space: [ KeyboardUtils.KEY_SPACE ],
arrowLeft: [ KeyboardUtils.KEY_LEFT_ARROW ],
arrowRight: [ KeyboardUtils.KEY_RIGHT_ARROW ],
arrowUp: [ KeyboardUtils.KEY_UP_ARROW ],
arrowDown: [ KeyboardUtils.KEY_DOWN_ARROW ],

// modifier keys
ctrl: [ KeyboardUtils.KEY_CONTROL_LEFT, KeyboardUtils.KEY_CONTROL_RIGHT ],
alt: [ KeyboardUtils.KEY_ALT_LEFT, KeyboardUtils.KEY_ALT_RIGHT ],
shift: [ KeyboardUtils.KEY_SHIFT_LEFT, KeyboardUtils.KEY_SHIFT_RIGHT ]
};

scenery.register( 'EnglishStringToCodeMap', EnglishStringToCodeMap );
Expand Down
23 changes: 0 additions & 23 deletions js/accessibility/KeyboardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,29 +299,6 @@ const KeyboardUtils = {
return MODIFIER_KEYS.includes( key );
},

/**
* Returns true if the provided KeyboardEvent.code/KeyboardEvent.key is equivalent to the provided KeyboardEvent.code.
* Specifically comparing modifier keys. If both are `code`, returns true when they are equal. If first value is
* a `key` for alt/control/shift modifier key, then it returns true when the code is one of the matching
* left/right `codes` for that `key`. For example
*
* `keyOrCode` = 'Shift', `code` = 'ShiftLeft -> true
* `keyOrCode = 'Alt', `code` = 'AltRight' -> true
* `keyOrCode = 'Control`, `code` = 'KeyR' -> false
*
* @param keyOrCode - KeyboardEvent.key OR KeyboardEvent.code
* @param code - KeyboardEvent.code
*/
areKeysEquivalent( keyOrCode: string, code: string ): boolean {
const equivalentModifierKeys = KeyboardUtils.MODIFIER_KEY_TO_CODE_MAP.get( keyOrCode );
if ( equivalentModifierKeys ) {
return equivalentModifierKeys.includes( code );
}
else {
return keyOrCode === code;
}
},

ALL_KEYS: ALL_KEY_CODES
};

Expand Down
107 changes: 83 additions & 24 deletions js/listeners/KeyboardListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ import KeyboardUtils from '../accessibility/KeyboardUtils.js';
type ModifierKey = 'q' | 'w' | 'e' | 'r' | 't' | 'y' | 'u' | 'i' | 'o' | 'p' | 'a' | 's' | 'd' |
'f' | 'g' | 'h' | 'j' | 'k' | 'l' | 'z' | 'x' | 'c' |
'v' | 'b' | 'n' | 'm' | 'ctrl' | 'alt' | 'shift' | 'tab';

// Keys of the EnglishStringToCodeMap, Extract makes sure they are strings (which they are declared to be, but
// TypeScript doesn't know that for some reason).
type AllowedKeys = keyof typeof EnglishStringToCodeMap;

export type OneKeyStroke = `${AllowedKeys}` |
Expand Down Expand Up @@ -123,13 +126,10 @@ type KeyboardListenerOptions<Keys extends readonly OneKeyStroke[ ]> = {
type KeyGroup<Keys extends readonly OneKeyStroke[]> = {

// All must be pressed fully before the key is pressed to activate the command.
modifierKeys: string[];

// the final key that is pressed (after modifier keys) to trigger the listener
key: string;
modifierKeys: string[][];

// all keys in this KeyGroup as a KeyboardEvent.code
allKeys: string[];
// Contains the triggering key for the listener. One of these keys must be pressed to activate callbacks.
keys: string[];

// All keys in this KeyGroup using the readable form
naturalKeys: Keys[number];
Expand Down Expand Up @@ -241,8 +241,8 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi
this._keyGroups.forEach( keyGroup => {

if ( !this._activeKeyGroups.includes( keyGroup ) ) {
if ( this.areKeysDownForListener( keyGroup.allKeys ) &&
KeyboardUtils.areKeysEquivalent( keyGroup.key, globalKeyStateTracker.getLastKeyDown()! ) ) {
if ( this.areKeysDownForListener( keyGroup ) &&
keyGroup.keys.includes( globalKeyStateTracker.getLastKeyDown()! ) ) {

this._activeKeyGroups.push( keyGroup );

Expand All @@ -269,7 +269,7 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi

if ( this._activeKeyGroups.length > 0 ) {
this._activeKeyGroups.forEach( ( activeKeyGroup, index ) => {
if ( !this.areKeysDownForListener( activeKeyGroup.allKeys ) ) {
if ( !this.areKeysDownForListener( activeKeyGroup ) ) {
if ( activeKeyGroup.timer ) {
activeKeyGroup.timer.stop( false );
}
Expand All @@ -285,8 +285,8 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi
// happens, see https://github.com/phetsims/scenery/issues/1534.
if ( eventCode ) {
this._keyGroups.forEach( keyGroup => {
if ( this.areKeysDownForListener( keyGroup.modifierKeys ) &&
KeyboardUtils.areKeysEquivalent( keyGroup.key, eventCode ) ) {
if ( this.areModifierKeysDownForListener( keyGroup ) &&
keyGroup.keys.includes( eventCode ) ) {
this.keysDown = false;
this.fireCallback( event, keyGroup );
}
Expand All @@ -298,12 +298,72 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi
}

/**
* Are the provided keys currently pressed in a way that should start or stop firing callbacks? If this listener
* allows other keys to be pressed, returns true if the keys are down. If not, it returns true if ONLY the
* provided keys are down.
* Returns an array of KeyboardEvent.codes from the provided key group that are currently pressed down.
*/
private getDownModifierKeys( keyGroup: KeyGroup<Keys> ): string[] {

// Remember, this is a 2D array. The inner array is the list of 'equivalent' keys to be pressed for the required
// modifier key. For example [ 'shiftLeft', 'shiftRight' ]. If any of the keys in that inner array are pressed,
// that set of modifier keys is considered pressed.
const modifierKeysCollection = keyGroup.modifierKeys;

// The list of modifier keys that are actually pressed
const downModifierKeys: string[] = [];
modifierKeysCollection.forEach( modifierKeys => {
for ( const modifierKey of modifierKeys ) {
if ( globalKeyStateTracker.isKeyDown( modifierKey ) ) {
downModifierKeys.push( modifierKey );

// One modifier key from this inner set is down, stop looking
break;
}
}
} );

return downModifierKeys;
}

/**
* Returns true if keys are pressed such that the listener should fire. In order to fire, all modifier keys
* should be down and the final key of the group should be down. If allowOtherKeys is false then ONLY the
* keys of this keyGroup are allowed to be pressed. Used to determine if callbacks of this listener should fire.
*/
private areKeysDownForListener( keyGroup: KeyGroup<Keys> ): boolean {
const downModifierKeys = this.getDownModifierKeys( keyGroup );

// modifier keys are down if one key from each inner array is down
const areModifierKeysDown = downModifierKeys.length === keyGroup.modifierKeys.length;

// The final key of the group is down if any of them are pressed
const finalDownKey = keyGroup.keys.find( key => globalKeyStateTracker.isKeyDown( key ) );

if ( areModifierKeysDown && !!finalDownKey ) {

// All keys are down.
const allKeys = [ ...downModifierKeys, finalDownKey ];
return this._allowOtherKeys ? true : globalKeyStateTracker.areKeysExclusivelyDown( allKeys );
}
else {
return false;
}
}

/**
* Returns true if the modifier keys of the provided key group are currently down. If allowOtherKeys is false then
* ONLY the modifier keys can be pressed. Used to determine if callbacks of this listener should fire.
*/
private areKeysDownForListener( keys: string[] ): boolean {
return this._allowOtherKeys ? globalKeyStateTracker.areKeysDown( keys ) : globalKeyStateTracker.areKeysExclusivelyDown( keys );
private areModifierKeysDownForListener( keyGroup: KeyGroup<Keys> ): boolean {
const downModifierKeys = this.getDownModifierKeys( keyGroup );

// modifier keys are down if one key from each inner array is down
const modifierKeysDown = downModifierKeys.length === keyGroup.modifierKeys.length;

if ( modifierKeysDown ) {
return this._allowOtherKeys ? true : globalKeyStateTracker.areKeysExclusivelyDown( downModifierKeys );
}
else {
return false;
}
}

/**
Expand Down Expand Up @@ -425,15 +485,15 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi
assert && assert( groupKeys.length > 0, 'no keys provided?' );

const naturalKey = groupKeys.slice( -1 )[ 0 ];
const key = EnglishStringToCodeMap[ naturalKey ];
assert && assert( key, `Key not found, do you need to add it to EnglishStringToCodeMap? ${naturalKey}` );
const keys = EnglishStringToCodeMap[ naturalKey ]!;
assert && assert( keys, `Codes were not found, do you need to add it to EnglishStringToCodeMap? ${naturalKey}` );

let modifierKeys: string[] = [];
let modifierKeys: string[][] = [];
if ( groupKeys.length > 1 ) {
modifierKeys = groupKeys.slice( 0, groupKeys.length - 1 ).map( naturalModifierKey => {
const modifierKey = EnglishStringToCodeMap[ naturalModifierKey ];
assert && assert( modifierKey, `Key not found, do you need to add it to EnglishStringToCodeMap? ${naturalModifierKey}` );
return modifierKey;
const modifierKeys = EnglishStringToCodeMap[ naturalModifierKey ]!;
assert && assert( modifierKeys, `Key not found, do you need to add it to EnglishStringToCodeMap? ${naturalModifierKey}` );
return modifierKeys;
} );
}

Expand All @@ -445,10 +505,9 @@ class KeyboardListener<Keys extends readonly OneKeyStroke[]> implements TInputLi
} ) : null;

const keyGroup: KeyGroup<Keys> = {
key: key,
keys: keys,
modifierKeys: modifierKeys,
naturalKeys: naturalKeys,
allKeys: modifierKeys.concat( key ),
timer: timer
};
return keyGroup;
Expand Down

0 comments on commit 90c27f4

Please sign in to comment.