mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
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:
parent
e1e8eec218
commit
4688cae5b2
@ -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}
|
||||
/>;
|
||||
|
@ -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': '↑',
|
||||
'ArrowDown': '↓',
|
||||
'ArrowRight': '→',
|
||||
'ArrowLeft': '←',
|
||||
// UiKit symbols
|
||||
'UIKeyInputLeftArrow': '←',
|
||||
'UIKeyInputRightArrow': '→',
|
||||
'UIKeyInputUpArrow': '↑',
|
||||
'UIKeyInputDownArrow': '↓',
|
||||
'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,
|
||||
'↑': 11,
|
||||
'↓': 10,
|
||||
'→': 21,
|
||||
'←': 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};
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user