From 4688cae5b27209a24b86c0fbb3309dde676a8090 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Thu, 25 Aug 2022 03:23:22 +0200 Subject: [PATCH] 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. --- js/components/Keyboard.tsx | 61 ++++++-- js/components/util/keyboard.ts | 271 ++++++++++++--------------------- 2 files changed, 145 insertions(+), 187 deletions(-) diff --git a/js/components/Keyboard.tsx b/js/components/Keyboard.tsx index 98aafcc..4a4b126 100644 --- a/js/components/Keyboard.tsx +++ b/js/components/Keyboard.tsx @@ -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} />; diff --git a/js/components/util/keyboard.ts b/js/components/util/keyboard.ts index 48d75cc..c58a340 100644 --- a/js/components/util/keyboard.ts +++ b/js/components/util/keyboard.ts @@ -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 => { - return code in keymap; +export const isSpecialKey = (k: string): k is KnownKeys => { + 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 => { - return k in uiKitMap; +export const hasSpecialKeyCode = (k: string): k is KnownKeys => { + 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 => { + return k in SHIFTED; +}; + export type Key2e = DeepMemberOf; 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}; }; /**