Refactor key handling to use `event.key` (#152)

Before, keyboard input used key codes to map events to Apple II keys.
This worked reasonably well, but `event.keyCode` was deprecated and
slated to be removed.

The refactored code now uses `event.key` which returns the localized,
keyboard-mapped key that the user pressed, which may be a letter or a
"symbolic" key.  This is then transformed into an Apple II key.

One side effect of the refactoring is that the keys now light up as
you type and that combinations of mouse clicks on modifiers and plain
keys will take the modifiers into account.
This commit is contained in:
Ian Flanigan 2022-08-25 03:23:22 +02:00 committed by GitHub
parent e1e8eec218
commit 4688cae5b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 145 additions and 187 deletions

View File

@ -5,9 +5,9 @@ import { Apple2 as Apple2Impl } from '../apple2';
import {
keys2,
keys2e,
mapKeyEvent,
mapMouseEvent,
keysAsTuples
keysAsTuples,
mapKeyboardEvent
} from './util/keyboard';
import styles from './css/Keyboard.module.css';
@ -115,22 +115,55 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
// Set global keystroke handler
useEffect(() => {
const keyDown = (event: KeyboardEvent) => {
if (!apple2) {
return;
}
if (document.activeElement && document.activeElement !== document.body) {
return;
}
event.preventDefault();
const key = mapKeyEvent(event, active.includes('LOCK'));
if (key !== 0xff) {
// CTRL-SHIFT-DELETE for reset
if (key === 0x7F && event.shiftKey && event.ctrlKey) {
apple2?.reset();
} else {
apple2?.getIO().keyDown(key);
}
const {key, keyCode, keyLabel} = mapKeyboardEvent(event, active.includes('LOCK'), active.includes('CTRL'));
setPressed(pressed => pressed.concat([keyLabel]));
setActive(active => active.concat([keyLabel]));
if (key === 'RESET') {
apple2.reset();
return;
}
const io = apple2.getIO();
if (key === 'OPEN_APPLE') {
io.buttonDown(0, true);
return;
}
if (key === 'CLOSED_APPLE') {
io.buttonDown(1, true);
return;
}
if (keyCode !== 0xff) {
apple2.getIO().keyDown(keyCode);
}
};
const keyUp = () => {
apple2?.getIO().keyUp();
const keyUp = (event: KeyboardEvent) => {
if (!apple2) {
return;
}
const {key, keyLabel} = mapKeyboardEvent(event);
setPressed(pressed => pressed.filter(k => k !== keyLabel));
setActive(active => active.filter(k => k !== keyLabel));
const io = apple2.getIO();
if (key === 'OPEN_APPLE') {
io.buttonDown(0, false);
}
if (key === 'CLOSED_APPLE') {
io.buttonDown(1, false);
}
apple2.getIO().keyUp();
};
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup', keyUp);
@ -146,6 +179,8 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
if (!apple2) {
return;
}
// Sometimes control-clicking will open a menu, so don't do that.
event.preventDefault();
const toggleActive = (key: string) => {
if (!active.includes(key)) {
setActive([...active, key]);
@ -212,7 +247,7 @@ export const Keyboard = ({ apple2, e }: KeyboardProps) => {
lower={lower}
upper={upper}
active={active.includes(lower)}
pressed={pressed.includes(upper)}
pressed={pressed.includes(lower)}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>;

View File

@ -1,168 +1,44 @@
import { JSX } from 'preact';
import { byte, DeepMemberOf, KnownKeys } from '../../types';
import { debug, toHex } from '../../util';
import { DeepMemberOf, KnownKeys } from '../../types';
/**
* Map of KeyboardEvent.keyCode to ASCII, for normal,
* shifted and control states.
*/
// keycode: [plain, ctrl, shift]
export const keymap = {
// Most of these won't happen
0x00: [0x00, 0x00, 0x00], //
0x01: [0x01, 0x01, 0x01], //
0x02: [0x02, 0x02, 0x02], //
0x03: [0x03, 0x03, 0x03], //
0x04: [0x04, 0x04, 0x04], //
0x05: [0x05, 0x05, 0x05], //
0x06: [0x06, 0x06, 0x06], //
0x07: [0x07, 0x07, 0x07], //
0x08: [0x7F, 0x7F, 0x7F], // BS/DELETE
0x09: [0x09, 0x09, 0x09], // TAB
0x0A: [0x0A, 0x0A, 0x0A], //
0x0B: [0x0B, 0x0B, 0x0B], //
0x0C: [0x0C, 0x0C, 0x0C], //
0x0D: [0x0D, 0x0D, 0x0D], // CR
0x0E: [0x0E, 0x0E, 0x0E], //
0x0F: [0x0F, 0x0F, 0x0F], //
0x10: [0xff, 0xff, 0xff], // SHIFT
0x11: [0xff, 0xff, 0xff], // CTRL
0x12: [0xff, 0xff, 0xff], // ALT/OPTION
0x13: [0x13, 0x13, 0x13], //
0x14: [0x14, 0x14, 0x14], //
0x15: [0x15, 0x15, 0x15], //
0x16: [0x16, 0x16, 0x16], //
0x17: [0x17, 0x17, 0x18], //
0x18: [0x18, 0x18, 0x18], //
0x19: [0x19, 0x19, 0x19], //
0x1A: [0x1A, 0x1A, 0x1A], //
0x1B: [0x1B, 0x1B, 0x1B], // ESC
0x1C: [0x1C, 0x1C, 0x1C], //
0x1D: [0x1D, 0x1D, 0x1D], //
0x1E: [0x1E, 0x1E, 0x1E], //
0x1F: [0x1F, 0x1F, 0x1F], //
// Most of these besides space won't happen
0x20: [0x20, 0x20, 0x20], //
0x21: [0x21, 0x21, 0x21], //
0x22: [0x22, 0x22, 0x22], //
0x23: [0x23, 0x23, 0x23], //
0x24: [0x24, 0x24, 0x24], //
0x25: [0x08, 0x08, 0x08], // <- left
0x26: [0x0B, 0x0B, 0x0B], // ^ up
0x27: [0x15, 0x15, 0x15], // -> right
0x28: [0x0A, 0x0A, 0x0A], // v down
0x29: [0x29, 0x29, 0x29], // )
0x2A: [0x2A, 0x2A, 0x2A], // *
0x2B: [0x2B, 0x2B, 0x2B], // +
0x2C: [0x2C, 0x2C, 0x3C], // , - <
0x2D: [0x2D, 0x2D, 0x5F], // - - _
0x2E: [0x2E, 0x2E, 0x3E], // . - >
0x2F: [0x2F, 0x2F, 0x3F], // / - ?
0x30: [0x30, 0x30, 0x29], // 0 - )
0x31: [0x31, 0x31, 0x21], // 1 - !
0x32: [0x32, 0x00, 0x40], // 2 - @
0x33: [0x33, 0x33, 0x23], // 3 - #
0x34: [0x34, 0x34, 0x24], // 4 - $
0x35: [0x35, 0x35, 0x25], // 5 - %
0x36: [0x36, 0x36, 0x5E], // 6 - ^
0x37: [0x37, 0x37, 0x26], // 7 - &
0x38: [0x38, 0x38, 0x2A], // 8 - *
0x39: [0x39, 0x39, 0x28], // 9 - (
0x3A: [0x3A, 0x3A, 0x3A], // :
0x3B: [0x3B, 0x3B, 0x3A], // ; - :
0x3C: [0x3C, 0x3C, 0x3C], // <
0x3D: [0x3D, 0x3D, 0x2B], // = - +
0x3E: [0x3E, 0x3E, 0x3E], // >
0x3F: [0x3F, 0x3F, 0x3F], // ?
// Alpha and control
0x40: [0x40, 0x00, 0x40], // @
0x41: [0x61, 0x01, 0x41], // A
0x42: [0x62, 0x02, 0x42], // B
0x43: [0x63, 0x03, 0x43], // C - BRK
0x44: [0x64, 0x04, 0x44], // D
0x45: [0x65, 0x05, 0x45], // E
0x46: [0x66, 0x06, 0x46], // F
0x47: [0x67, 0x07, 0x47], // G - BELL
0x48: [0x68, 0x08, 0x48], // H
0x49: [0x69, 0x09, 0x49], // I - TAB
0x4A: [0x6A, 0x0A, 0x4A], // J - NL
0x4B: [0x6B, 0x0B, 0x4B], // K - VT
0x4C: [0x6C, 0x0C, 0x4C], // L
0x4D: [0x6D, 0x0D, 0x4D], // M - CR
0x4E: [0x6E, 0x0E, 0x4E], // N
0x4F: [0x6F, 0x0F, 0x4F], // O
0x50: [0x70, 0x10, 0x50], // P
0x51: [0x71, 0x11, 0x51], // Q
0x52: [0x72, 0x12, 0x52], // R
0x53: [0x73, 0x13, 0x53], // S
0x54: [0x74, 0x14, 0x54], // T
0x55: [0x75, 0x15, 0x55], // U
0x56: [0x76, 0x16, 0x56], // V
0x57: [0x77, 0x17, 0x57], // W
0x58: [0x78, 0x18, 0x58], // X
0x59: [0x79, 0x19, 0x59], // Y
0x5A: [0x7A, 0x1A, 0x5A], // Z
0x5B: [0xFF, 0xFF, 0xFF], // Left window
0x5C: [0xFF, 0xFF, 0xFF], // Right window
0x5D: [0xFF, 0xFF, 0xFF], // Select
0x5E: [0x5E, 0x1E, 0x5E], //
0x5F: [0x5F, 0x1F, 0x5F], // _
// Numeric pad
0x60: [0x30, 0x30, 0x30], // 0
0x61: [0x31, 0x31, 0x31], // 1
0x62: [0x32, 0x32, 0x32], // 2
0x63: [0x33, 0x33, 0x33], // 3
0x64: [0x34, 0x34, 0x34], // 4
0x65: [0x35, 0x35, 0x35], // 5
0x66: [0x36, 0x36, 0x36], // 6
0x67: [0x37, 0x37, 0x37], // 7
0x68: [0x38, 0x38, 0x38], // 8
0x69: [0x39, 0x39, 0x39], // 9
0x6A: [0x2A, 0x2A, 0x2A], // *
0x6B: [0x2B, 0x2B, 0x2B], // +
0x6D: [0x2D, 0x2D, 0x2D], // -
0x6E: [0x2E, 0x2E, 0x2E], // .
0x6F: [0x2F, 0x2F, 0x39], // /
// Stray keys
0xAD: [0x2D, 0x2D, 0x5F], // - - _
0xBA: [0x3B, 0x3B, 0x3A], // ; - :
0xBB: [0x3D, 0x3D, 0x2B], // = - +
0xBC: [0x2C, 0x2C, 0x3C], // , - <
0xBD: [0x2D, 0x2D, 0x5F], // - - _
0xBE: [0x2E, 0x2E, 0x3E], // . - >
0xBF: [0x2F, 0x2F, 0x3F], // / - ?
0xC0: [0x60, 0x60, 0x7E], // ` - ~
0xDB: [0x5B, 0x1B, 0x7B], // [ - {
0xDC: [0x5C, 0x1C, 0x7C], // \ - |
0xDD: [0x5D, 0x1D, 0x7D], // ] - }
0xDE: [0x27, 0x22, 0x22], // ' - '
0xFF: [0xFF, 0xFF, 0xFF] // No comma line
export const SPECIAL_KEY_MAP = {
'Shift': 'SHIFT',
'Enter': 'RETURN',
'CapsLock': 'LOCK',
'Control': 'CTRL',
'Escape': 'ESC',
'Delete': 'RESET',
'Tab': 'TAB',
'Backspace': 'DELETE',
'ArrowUp': '&uarr;',
'ArrowDown': '&darr;',
'ArrowRight': '&rarr;',
'ArrowLeft': '&larr;',
// UiKit symbols
'UIKeyInputLeftArrow': '&larr;',
'UIKeyInputRightArrow': '&rarr;',
'UIKeyInputUpArrow': '&uarr;',
'UIKeyInputDownArrow': '&darr;',
'UIKeyInputEscape': 'ESC',
} as const;
export const isKeyboardCode = (code: number): code is KnownKeys<typeof keymap> => {
return code in keymap;
export const isSpecialKey = (k: string): k is KnownKeys<typeof SPECIAL_KEY_MAP> => {
return k in SPECIAL_KEY_MAP;
};
const uiKitMap = {
'Dead': 0xFF,
'UIKeyInputLeftArrow': 0x08,
'UIKeyInputRightArrow': 0x15,
'UIKeyInputUpArrow': 0x0B,
'UIKeyInputDownArrow': 0x0A,
'UIKeyInputEscape': 0x1B
export const SPECIAL_KEY_CODE = {
'TAB': 9,
'RETURN': 13,
'ESC': 27,
'&uarr;': 11,
'&darr;': 10,
'&rarr;': 21,
'&larr;': 8,
'DELETE': 127,
} as const;
export const isUiKitKey = (k: string): k is KnownKeys<typeof uiKitMap> => {
return k in uiKitMap;
export const hasSpecialKeyCode = (k: string): k is KnownKeys<typeof SPECIAL_KEY_CODE> => {
return k in SPECIAL_KEY_CODE;
};
/**
@ -205,6 +81,35 @@ export const keys2e = [
]
] as const;
/** Shifted */
const SHIFTED = {
'!' : '1' ,
'@' : '2' ,
'#' : '3' ,
'$' : '4' ,
'%' : '5' ,
'^' : '6' ,
'&' : '7' ,
'*' : '8' ,
'(' : '9' ,
')' : '0' ,
'_' : '-' ,
'+' : '=' ,
'{' : '[' ,
'}' : ']' ,
'|' : '\\',
':' : ';' ,
'\'' : '"',
'<' : ',' ,
'>' : '.' ,
'?' : '/' ,
'~' : '`' ,
} as const;
export const isShiftyKey = (k: string): k is KnownKeys<typeof SHIFTED> => {
return k in SHIFTED;
};
export type Key2e = DeepMemberOf<typeof keys2e>;
export type Key = Key2 | Key2e;
@ -213,30 +118,48 @@ export type KeyFunction = (key: KeyboardEvent) => void;
/**
* Convert a DOM keyboard event into an ASCII equivalent that
* an Apple // can recognize.
* an Apple II can recognize.
*
* @param evt Event to convert
* @param caps Caps Lock state
* @returns ASCII character
* @returns a tuple of:
* * `key`: the symbol of the key
* * `keyLabel`: the label on the keycap
* * `keyCode`: the corresponding byte for the Apple II
*/
export const mapKeyEvent = (evt: KeyboardEvent, caps: boolean) => {
// TODO(whscullin): Find replacement for deprecated keycode
const code = evt.keyCode;
let key: byte = 0xff;
if (isUiKitKey(evt.key)) {
key = uiKitMap[evt.key];
} else if (isKeyboardCode(code)) {
key = keymap[code][evt.shiftKey ? 2 : (evt.ctrlKey ? 1 : 0)];
if (caps && key >= 0x61 && key <= 0x7A) {
key -= 0x20;
}
export const mapKeyboardEvent = (event: KeyboardEvent, caps: boolean = false, control: boolean = false) => {
let key: string;
if (isSpecialKey(event.key)) {
key = SPECIAL_KEY_MAP[event.key];
} else if (event.key === 'Alt') {
key = event.location === 1 ? 'OPEN_APPLE' : 'CLOSED_APPLE';
} else {
debug(`Unhandled key = ${toHex(code)}`);
key = event.key;
}
return key;
let keyLabel = key;
if (key.length === 1) {
if (isShiftyKey(key)) {
keyLabel = SHIFTED[key];
} else {
keyLabel = key.toUpperCase();
}
}
let keyCode = 0xff;
if (hasSpecialKeyCode(key)) {
keyCode = SPECIAL_KEY_CODE[key];
} else if (key.length === 1) {
keyCode = key.charCodeAt(0);
}
if ((caps || control) && keyCode >= 0x61 && keyCode <= 0x7A) {
keyCode -= 0x20;
}
if (control && keyCode >= 0x40 && keyCode < 0x60) {
keyCode -= 0x40;
}
return { key, keyLabel, keyCode};
};
/**