/** * @fileoverview This file implements the C1Pjs Keyboard component. * @author Jeff Parsons * @version 1.0 * Created 2012-Jun-20 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCjs. If not, * see . * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen * that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ "use strict"; if (NODE) { var str = require("../../shared/lib/strlib"); var web = require("../../shared/lib/weblib"); var Component = require("../../shared/lib/component"); } /** * C1PKeyboard(parmsKbd) * * The Keyboard component can be configured with the following (parmsKbd) properties: * * model: model number (one of: 542 or 600; 600 is the default) * * Its main purpose is to receive binding requests for various keyboard events, * and to use those events to simulate the C1P's keyboard hardware; specifically, * an OSI model 600 board (NOT the model 542). * * Keys on the C1P keyboard that differ from modern keyboards, along with their * closest modern counterpart: * * C1P PC * --- -- * 2" 2@ * 6& 6^ * 7' 7& * 8( 8* * 9) 9( * 0 0) * :* -_ * -= =+ * ;+ ;: * LINEFEED CTRL-J * RETURN ENTER (or CTRL-M) * SHIFT-O DELETE (or CTRL-H) * SHIFT-N ^ * SHIFT-P @ (used by the BASIC-IN-ROM to abandon the current line) * SHIFT-L \ * SHIFT-K [ * SHIFT-M ] * CTRL-C Same (used by the BASIC-IN-ROM to interrupt RUN and LIST, unless disabled with POKE 530,1) * CTRL-O Same (used by the BASIC-IN-ROM to suppress output until another CTRL-O is typed) * RUB-OUT (no mapping chosen for this key yet) * REPEAT (no mapping chosen for this key yet) * * Problems with iOS Devices * ------------------------- * The keyboard pops up with the SHIFT key depressed, which is not the initial keyboard state that the C1P expects. * I tried to fix that by adding an 'autocapitalize="off"' attribute alongside the 'contenteditable="true"' attribute * on the element, but apparently Safari v5 honors that only inside certain elements (eg, ). The simplest * work-around is to tap the iOS device's SHIFT key before starting to type, but I'd prefer an automatic solution. * * Another work-around might be to NEVER pass the real CAPS-LOCK state to the virtual machine, and whenever CAPS-LOCK is * actually down, automatically "uncapitalize" all letters. * * However, the current work-around is for keyPressSimulate() to ALWAYS convert all alphabetic charCodes to their * lower-case equivalents, and simply let the C1P's own shift-key logic do its thing. Note that we do this ONLY for * iOS devices, so that on all other devices, you can still use special shifted key combinations like SHIFT-O and SHIFT-P; * this work-around breaks those key combinations for iOS devices, but that seems like a reasonable trade-off. * * Simple C1P Virtual Hardware Test * -------------------------------- * Using the following code: * * 10 POKE 530,1 * 20 KEY=57088 * 25 Q=-1 * 30 POKE KEY,64 * 40 P=PEEK(KEY) * 50 IF P<>Q THEN PRINT P * 60 Q=P * 70 GOTO 30 * * The value 64 (0x40) should have enabled every row except R6. Here were the results for keys * on row R7: * * 1 2 3 4 5 6 7 * --- --- --- --- --- --- --- * 126 190 222 238 246 250 252 * * Sure enough, none of the keys on R6 worked, and all the keys on rows R0-R5 generated the same * values as R7. But why were the values read all EVEN instead of ODD (ie, why was bit 0 cleared as * well?) Answer: because 0x40 also enables keys on row R0, where the SHIFT-LOCK key resides, * and since the SHIFT-LOCK is normally locked AND also sits in column C0, bit 0 will be clear as well. * * This explains why the "STAR WARS" game (SAMPLE4.BAS) expected values "126,190,222,238,246,250" * for keys 1-6 after POKE'ing 64 into location 57088 (0xdf00), instead of the more typical values * "127,191,223,239,247,251." This also means that anyone who happened to unlock their SHIFT-LOCK * would have trouble playing that game. * * @constructor * @extends Component */ function C1PKeyboard(parmsKbd) { Component.call(this, "C1PKeyboard", parmsKbd); this.flags.fPowered = false; this.nDefaultModel = parmsKbd['model']; /* * keyCodes that I must pay particular attention to */ this.KEYCODE_DELETE = 0x08; this.KEYCODE_TAB = 0x09; this.KEYCODE_LF = 0x0A; this.KEYCODE_CR = 0x0D; this.KEYCODE_SHIFT = 0x10; // I map this to CHARCODE_LSHIFT this.KEYCODE_CONTROL = 0x11; this.KEYCODE_ALT = 0x12; // I map this to CHARCODE_RSHIFT (since the C1P keyboard has no ALT key) this.KEYCODE_CAPSLOCK = 0x14; this.KEYCODE_ESC = 0x1B; // NOTE: for some reason, this comes in via keyDown/keyUp only, not keyPress this.KEYCODE_COMMAND = 0x5B; /* * The following charCodes are the same as the corresponding keyCodes */ this.CHARCODE_DELETE = this.KEYCODE_DELETE; this.CHARCODE_LF = this.KEYCODE_LF; this.CHARCODE_CR = this.KEYCODE_CR; this.CHARCODE_ESC = this.KEYCODE_ESC; /* * The following charCodes are NOT the same as the corresponding keyCodes, hence the bias (PSEUDO_CHARCODE); * I've deliberately chosen a bias that still produces values in the byte range (0x00-0xFF) and will therefore * fit into aCharCodeMap, but which shouldn't conflict with any actual, type-able keys. */ this.PSEUDO_CHARCODE = 0xE0; this.CHARCODE_LSHIFT = this.KEYCODE_SHIFT + this.PSEUDO_CHARCODE; this.CHARCODE_CTRL = this.KEYCODE_CONTROL + this.PSEUDO_CHARCODE; this.CHARCODE_RSHIFT = this.KEYCODE_ALT + this.PSEUDO_CHARCODE; this.CHARCODE_SHIFTLOCK = this.KEYCODE_CAPSLOCK + this.PSEUDO_CHARCODE; /* * Other common character codes, pseudo (like the C1P's "BREAK" key, which has no modern analog) or otherwise */ this.CHARCODE_BREAK = 0x00 + this.PSEUDO_CHARCODE; this.CHARCODE_CTRLC = 0x03; this.CHARCODE_CTRLO = 0x0F; /* * These are "shift key" bits I store in bitsShift, and with the exception of BIT_COMMAND (because * the C1P doesn't have a COMMAND key), they all match the bit position of the corresponding shift key's * column on row 0 (abKbdCols[0]) of the simulated keyboard hardware. * * NOTE: Whenever shift key bits need to be restored from bitsShift to abKbdCols[0] (eg, when restoring * the current shift state at the completion of a simulated key), be sure to mask bitsShift with BITS_SIMULATE * before propagating them. */ this.BIT_SHIFTLOCK = 0x01; this.BIT_RSHIFT = 0x02; this.BIT_LSHIFT = 0x04; this.BIT_COMMAND = 0x08; // the C1P has no key "wired" to this column, so I can use this bit for COMMAND this.BIT_CTRL = 0x40; this.BITS_SIMULATE = (this.BIT_RSHIFT | this.BIT_LSHIFT | this.BIT_CTRL); this.SIMCODE_KEYPRESS = 0; this.SIMCODE_KEYRELEASE = 1; this.SIMCODE_KEYEVENT = 2; this.SIMCODE_KEYTIMEOUT = 3; this.SIMCODE_AUTOCLEAR = 4; this.aSimCodeDescs = ["keyPress","keyRelease","keyEvent","keyTimeout","autoClear"]; /* * From "OSI C1P Technical Report" p.19 regarding the Model 600 Board: * * "By holding down any key, one will first get one character output, and after approximately * a half second delay a repeat rate of approximately 5 characters per second." */ this.nCyclesThreshold = 8192; // number of virtual CPU cycles required before aKbdStates is propagated this.msReleaseDelay = 250; // number of milliseconds before a down key is "forced" up (unless we see it go up) this.msReleaseRepeat = 100; // number of milliseconds before a held key is "forced" up (assuming auto-repeat) this.msInjectDelay = 300; // number of milliseconds between injected keystrokes this.aButtonCodeMap = []; this.aButtonCodeMap['break'] = this.CHARCODE_BREAK; this.aButtonCodeMap['esc'] = this.CHARCODE_ESC; this.aButtonCodeMap['ctrl-c'] = this.CHARCODE_CTRLC; this.aButtonCodeMap['ctrl-o'] = this.CHARCODE_CTRLO; /* * This array is used by keyEventSimulate() to lookup a given charCode and convert it to the appropriate * row/col bit combination that the C1P requires. I assign each supported charCode a 16-bit value, where * the high byte contains the row/col pair (in the high and low nibbles, respectively), and the low byte * contains any required shift-key code. * * For example, an apostrophe (0x27) is an unshifted key on a modern keyboard, but it is a SHIFT-7 on the * C1P keyboard, so when I simulate the 7, I must also simulate a SHIFT (I always choose the LEFT shift * key -- CHARCODE_LSHIFT -- but that choice is completely arbitrary). * * Using charCodes (from keyPress events) proved to be more robust than using keyCodes (from keyDown and * keyUp events), in part because of differences between the C1P keyboard's layout and modern keyboards, * and also because of differences in the way browsers generate the keyDown and keyUp events. For * example, Safari on iOS devices will not generate up/down events for shift keys, and for other keys, * the up/down events are usually generated after the actual press is complete, and in rapid succession, * which doesn't give the slow C1P virtual machine enough time to detect the key. * * There are still a few times that I call keyEventSimulate() from keyEvent(), and for those occasions, * I create a pseudo-charCode value by adding PSEUDO_CHARCODE (0xE0) to the keyCode value, to avoid any * confusion with real charCodes: * * CHARCODE_LSHIFT (originally 0x10, which also looks like CTRL-P, so converted to 0xF0) * CHARCODE_CTRL (originally 0x11, which also looks like CTRL-Q, so converted to 0xF1) * CHARCODE_RSHIFT (originally 0x12, which also looks like CTRL-R, so converted to 0xF2) * CHARCODE_SHIFTLOCK (originally 0x14, which also looks like CTRL-T, so converted to 0xF4) * * Again, as things currently stand, iOS devices will never generate the above charCodes, so any C1P software * that relies detecting on shift-key state changes will not work on those devices. * * For reference purposes, I've left some parenthetical references to corresponding keyCodes in the comments * below. Relying on keyCodes is problematic, which is why I've tried to eliminate most dependencies on them, * but still, they're all you get on keyDown/keyUp events. */ this.aCharCodeMap = []; this.aCharCodeMap[0x31] = 0x7700; this.aCharCodeMap[0x21] = 0x7700 + this.CHARCODE_LSHIFT; // 1 (0x31) ! (0x31) this.aCharCodeMap[0x32] = 0x7600; this.aCharCodeMap[0x22] = 0x7600 + this.CHARCODE_LSHIFT; // 2 (0x32) " (0xDE) this.aCharCodeMap[0x33] = 0x7500; this.aCharCodeMap[0x23] = 0x7500 + this.CHARCODE_LSHIFT; // 3 (0x33) # (0x33) this.aCharCodeMap[0x34] = 0x7400; this.aCharCodeMap[0x24] = 0x7400 + this.CHARCODE_LSHIFT; // 4 (0x34) $ (0x34) this.aCharCodeMap[0x35] = 0x7300; this.aCharCodeMap[0x25] = 0x7300 + this.CHARCODE_LSHIFT; // 5 (0x35) % (0x35) this.aCharCodeMap[0x36] = 0x7200; this.aCharCodeMap[0x26] = 0x7200 + this.CHARCODE_LSHIFT; // 6 (0x36) & (0x37) this.aCharCodeMap[0x37] = 0x7100; this.aCharCodeMap[0x27] = 0x7100 + this.CHARCODE_LSHIFT; // 7 (0x37) ' (0xDE) this.aCharCodeMap[0x38] = 0x6700; this.aCharCodeMap[0x28] = 0x6700 + this.CHARCODE_LSHIFT; // 8 (0x38) ( (0x39) this.aCharCodeMap[0x39] = 0x6600; this.aCharCodeMap[0x29] = 0x6600 + this.CHARCODE_LSHIFT; // 9 (0x39) ) (0x30) this.aCharCodeMap[0x30] = 0x6500; // 0 this.aCharCodeMap[0x3A] = 0x6400; this.aCharCodeMap[0x2A] = 0x6400 + this.CHARCODE_LSHIFT; // : (0xBA) * (0x38) this.aCharCodeMap[0x2D] = 0x6300; this.aCharCodeMap[0x3D] = 0x6300 + this.CHARCODE_LSHIFT; // - (0xBD) = (0xBB) //this.aCharCodeMap[0x00] = 0x6200; // RUB-OUT (no mapping chosen for this key yet) this.aCharCodeMap[0x2E] = 0x5700; this.aCharCodeMap[0x3E] = 0x5700 + this.CHARCODE_LSHIFT; // . (0xBE) > (0xBE) this.aCharCodeMap[0x6C] = 0x5600; this.aCharCodeMap[0x4C] = 0x5600 + this.CHARCODE_LSHIFT; this.aCharCodeMap[0x5C] = 0x5600 + this.CHARCODE_LSHIFT; // l L \ this.aCharCodeMap[0x6F] = 0x5500; this.aCharCodeMap[0x4F] = 0x5500 + this.CHARCODE_LSHIFT; this.aCharCodeMap[this.CHARCODE_DELETE] = 0x5500 + this.CHARCODE_LSHIFT; // o O DELETE this.aCharCodeMap[this.CHARCODE_LF] = 0x5400; // LINE-FEED this.aCharCodeMap[this.CHARCODE_CR] = 0x5300; // RETURN this.aCharCodeMap[0x77] = 0x4700; this.aCharCodeMap[0x57] = 0x4700 + this.CHARCODE_LSHIFT; // w W this.aCharCodeMap[0x65] = 0x4600; this.aCharCodeMap[0x45] = 0x4600 + this.CHARCODE_LSHIFT; // e E this.aCharCodeMap[0x72] = 0x4500; this.aCharCodeMap[0x52] = 0x4500 + this.CHARCODE_LSHIFT; // r R this.aCharCodeMap[0x74] = 0x4400; this.aCharCodeMap[0x54] = 0x4400 + this.CHARCODE_LSHIFT; // t T this.aCharCodeMap[0x79] = 0x4300; this.aCharCodeMap[0x59] = 0x4300 + this.CHARCODE_LSHIFT; // y Y this.aCharCodeMap[0x75] = 0x4200; this.aCharCodeMap[0x55] = 0x4200 + this.CHARCODE_LSHIFT; // u U this.aCharCodeMap[0x69] = 0x4100; this.aCharCodeMap[0x49] = 0x4100 + this.CHARCODE_LSHIFT; // i I this.aCharCodeMap[0x73] = 0x3700; this.aCharCodeMap[0x53] = 0x3700 + this.CHARCODE_LSHIFT; // s S this.aCharCodeMap[0x64] = 0x3600; this.aCharCodeMap[0x44] = 0x3600 + this.CHARCODE_LSHIFT; // d D this.aCharCodeMap[0x66] = 0x3500; this.aCharCodeMap[0x46] = 0x3500 + this.CHARCODE_LSHIFT; // f F this.aCharCodeMap[0x67] = 0x3400; this.aCharCodeMap[0x47] = 0x3400 + this.CHARCODE_LSHIFT; // g G this.aCharCodeMap[0x68] = 0x3300; this.aCharCodeMap[0x48] = 0x3300 + this.CHARCODE_LSHIFT; // h H this.aCharCodeMap[0x6A] = 0x3200; this.aCharCodeMap[0x4A] = 0x3200 + this.CHARCODE_LSHIFT; // j J this.aCharCodeMap[0x6B] = 0x3100; this.aCharCodeMap[0x4B] = 0x3100 + this.CHARCODE_LSHIFT; this.aCharCodeMap[0x5B] = 0x3100 + this.CHARCODE_LSHIFT; // k K [ this.aCharCodeMap[0x78] = 0x2700; this.aCharCodeMap[0x58] = 0x2700 + this.CHARCODE_LSHIFT; // x X this.aCharCodeMap[0x63] = 0x2600; this.aCharCodeMap[0x43] = 0x2600 + this.CHARCODE_LSHIFT; // c C this.aCharCodeMap[0x76] = 0x2500; this.aCharCodeMap[0x56] = 0x2500 + this.CHARCODE_LSHIFT; // v V this.aCharCodeMap[0x62] = 0x2400; this.aCharCodeMap[0x42] = 0x2400 + this.CHARCODE_LSHIFT; // b B this.aCharCodeMap[0x6E] = 0x2300; this.aCharCodeMap[0x4E] = 0x2300 + this.CHARCODE_LSHIFT; this.aCharCodeMap[0x5E] = 0x2300 + this.CHARCODE_LSHIFT; // n N ^ this.aCharCodeMap[0x6D] = 0x2200; this.aCharCodeMap[0x4D] = 0x2200 + this.CHARCODE_LSHIFT; this.aCharCodeMap[0x5D] = 0x2200 + this.CHARCODE_LSHIFT; // m M ] this.aCharCodeMap[0x2C] = 0x2100; this.aCharCodeMap[0x3C] = 0x2100 + this.CHARCODE_LSHIFT; // , (0xBC) < (0xBC) this.aCharCodeMap[0x71] = 0x1700; this.aCharCodeMap[0x51] = 0x1700 + this.CHARCODE_LSHIFT; // q Q this.aCharCodeMap[0x61] = 0x1600; this.aCharCodeMap[0x41] = 0x1600 + this.CHARCODE_LSHIFT; // a A this.aCharCodeMap[0x7A] = 0x1500; this.aCharCodeMap[0x5A] = 0x1500 + this.CHARCODE_LSHIFT; // z Z this.aCharCodeMap[0x20] = 0x1400; // SPACE this.aCharCodeMap[0x2F] = 0x1300; this.aCharCodeMap[0x3F] = 0x1300 + this.CHARCODE_LSHIFT; // / (0xBF) ? (0xBF) this.aCharCodeMap[0x3B] = 0x1200; this.aCharCodeMap[0x2B] = 0x1200 + this.CHARCODE_LSHIFT; // ; (0xBA) + (0xBB) this.aCharCodeMap[0x70] = 0x1100; this.aCharCodeMap[0x50] = 0x1100 + this.CHARCODE_LSHIFT; this.aCharCodeMap[0x40] = 0x1100 + this.CHARCODE_LSHIFT; // p P @ //this.aCharCodeMap[0x00] = 0x0700; // REPEAT (no mapping chosen for this key yet) this.aCharCodeMap[this.CHARCODE_CTRL] = 0x0600; // CTRL this.aCharCodeMap[this.CHARCODE_ESC] = 0x0500; // ESC this.aCharCodeMap[this.CHARCODE_LSHIFT] = 0x0200; // LEFT-SHIFT this.aCharCodeMap[this.CHARCODE_RSHIFT] = 0x0100; // RIGHT-SHIFT this.aCharCodeMap[this.CHARCODE_SHIFTLOCK] = 0x0000; // SHIFT-LOCK this.reset(); } Component.subclass(C1PKeyboard); /** * @this {C1PKeyboard} */ C1PKeyboard.prototype.reset = function() { this.setModel(this.nDefaultModel); /* * The physical (not virtual) state of various shift keys, * with the exception of SHIFT-LOCK, which needs to start in the * "locked" position, regardless of the physical CAPS-LOCK state. * * QUESTION: In JavaScript, how do you query initial key states? */ this.bitsShift = this.BIT_SHIFTLOCK; /* * Every SET bit of bKbdRows represents an enabled row (this convention * is the REVERSE of the C1P hardware, but I prefer it). */ this.bKbdRows = 0x00; /* * Every SET bit of abKbdCols represents an enabled column; again, this is * the REVERSE of the C1P hardware, but I compensate for that difference with * a quick XOR in updateMemory(). * * Like bitsShift, this 8x8 array (8 byte values, each with 8 bits) represents * the physical state of the keyboard, encoded in C1P format; the C1P won't * actually see data this until updateMemory() decides it's time to propagate it. */ this.abKbdCols = [this.BIT_SHIFTLOCK,0x00,0x00,0x00,0x00,0x00,0x00,0x00]; /* * After a new key event has updated abKbdCols, we "push" a copy of that * updated keyboard state onto this array. updateMemory() will then "shift" * the next copy off, update its own copy (abKbdColsLast), and then propagate * that to the C1P's keyboard memory, once the CPU has had enough time to * process the previous event (see nCyclesThreshold). */ this.aKbdStates = []; /* * When a key "down" is simulated on behalf of some charCode, I save * the timer object responsible for simulating the key "up" here, so that * if I detect the actual key going up sooner, I can cancel the timer and * simulate the "up" immediately. Similarly, if another press for the same * key arrives before last one expired (eg, auto-repeat), I need to cancel * the previous timer for that key before setting another. * * NOTE: If this is anything other than an initial reset, then we need to * make sure there are no outstanding timers before we blow the array away. */ if (this.aKeyTimers) { for (var i in this.aKeyTimers) { if (isNaN(+i)) continue; // ignore any non-numeric properties, if any if (this.aKeyTimers[i]) clearTimeout(this.aKeyTimers[i]); } } this.aKeyTimers = []; this.prevCharDown = 0; this.prevKeyDown = 0; /* * These save the last values written to keyboard memory, so that I can * avoid rewriting the memory if the values haven't changed since the last update. */ this.bWriteLast = -1; this.abKbdColsLast = this.abKbdCols; /* * Due to the way the C1P scans its keyboard rows (from R0 up to R7 *or* to the * highest row for which a "down" key has just been detected), if we get back-to-back * key events for, say, "I" and then "S", the C1P will see only the "S", never the "I", * because "I" is on a higher row. * * That's why we have aKbdStates, which relies on the following CPU activity variables. */ this.nReadsSinceLastEvent = 0; this.nWritesSinceLastEvent = 0; this.nCyclesSinceLastEvent = 0; /* * Make sure the auto-injection buffer is empty, too (an injection could have been * in progress on any reset after the first). */ this.sInjectBuffer = ""; }; /** * @this {C1PKeyboard} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc", "ctrl-c") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ C1PKeyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue) { /* * I want to bind to the first caller (ie, the Screen), not subsequent ones (eg, the Panel) */ if (this.bindings[sBinding] === undefined) { switch(sBinding) { case "keyDown": this.bindings[sBinding] = control; control.onkeydown = function(kbd) { return function(event) { return kbd.keyEvent(event, true); }; }(this); return true; case "keyPress": this.bindings[sBinding] = control; control.onkeypress = function(kbd) { return function(event) { return kbd.keyPress(event); }; }(this); return true; case "keyUp": this.bindings[sBinding] = control; control.onkeyup = function(kbd) { return function(event) { return kbd.keyEvent(event, false); }; }(this); return true; case "break": /* * The BREAK key is unusual: it requires us forcing the equivalent of someone pressing * our "Reset" and "Run" buttons. As things stand, the Computer component is responsible * for end-user "reset" requests, so we can simply arrange to call this.cmp.reset(true). * * NOTE: At the risk of making keyPressSimulate() a bit uglier, I also permit BREAK * there, in case someone wants to "inject" the BREAK key; however, if it's followed by * other injected keys, I'll need to avoid clearing the injection buffer on a reset; * currently, reset() resets everything. */ this.bindings[sBinding] = control; control.onclick = function(kbd) { return function(event) { if (DEBUG) kbd.println("keyPressSimulate(break)"); if (kbd.cmp) kbd.cmp.reset(true); }; }(this); return true; default: if (this.aButtonCodeMap[sBinding] !== undefined) { this.bindings[sBinding] = control; control.onclick = function(kbd, sButton, charCode) { return function(event) { if (DEBUG) kbd.println("keyPressSimulate(" + sButton + ")"); if (kbd.cpu) kbd.cpu.setFocus(); return !kbd.keyPressSimulate(charCode); }; }(this, sBinding, this.aButtonCodeMap[sBinding]); return true; } break; } } return false; }; /** * @this {C1PKeyboard} * @param {Array} abMemory * @param {number} start * @param {number} end * @param {C1PCPU} cpu */ C1PKeyboard.prototype.setBuffer = function(abMemory, start, end, cpu) { this.abMem = abMemory; this.offKbd = start; this.cbKbd = end - start + 1; this.offKbdLimit = this.offKbd + this.cbKbd; if (cpu) { this.cpu = cpu; if (DEBUG) cpu.addReadNotify(start, end, this, this.getByte); cpu.addWriteNotify(start, end, this, this.setByte); } this.setReady(); }; /** * @this {C1PKeyboard} * @param {number} nModel */ C1PKeyboard.prototype.setModel = function(nModel) { this.nModel = nModel; /* * Default to Model 600 behavior, where the keyboard status lines are inverted * (ie, a zero bit indicates a key press). */ this.bInvert = 0xff; if (this.nModel != 600) { /* * No inversion for model 542 */ this.bInvert = 0x00; this.println("updated keyboard model: " + this.nModel); } }; /** * @this {C1PKeyboard} * @param {boolean} fOn * @param {C1PComputer} cmp * * We make a note of the Computer component, so that we can invoke its reset() method when our simulated * BREAK key is pressed, and we query the Debugger component so that we can use its info() and halt() functions, * which we use to buffer information without adversely affecting timing and then dump later using the Debugger's * "info" command. */ C1PKeyboard.prototype.setPower = function(fOn, cmp) { if (fOn && !this.flags.fPowered) { this.flags.fPowered = true; this.cmp = cmp; if (DEBUGGER) this.dbg = cmp.getComponentByType("debugger"); } }; /** * @this {C1PKeyboard} * @param {boolean} [fReady] is assumed to indicate "ready" unless EXPLICITLY set to false */ C1PKeyboard.prototype.setReady = function(fReady) { this.iOS = web.isUserAgent("iOS"); this.fMobile = (this.iOS || web.isUserAgent("Android")); if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message("mobile keyboard support: " + (this.fMobile? "true" : "false") + " (" + window.navigator.userAgent + ")"); } Component.prototype.setReady.call(this); }; /** * calcReleaseDelay(fRepeat) * * This attempts to scale our default "release" delay appropriately for the current CPU speed. * * Note that if the effective CPU speed exceeds 16Mhz, it becomes very difficult to rely on timer-driven key events * (even the shortest available timer delay still gives the CPU too much time, so it thinks that even the briefest key * press represents a held key, resulting in multiple keystrokes). We deal with this by artificially limiting the top * speed in the CPU component (the current limit for "fast" mode is 8Mhz; see CPU.mhzFast) * * @this {C1PKeyboard} * @param {boolean} fRepeat is true if a timeout had already been active for the current key * @return {number} */ C1PKeyboard.prototype.calcReleaseDelay = function(fRepeat) { /* * NOTE: This delay affects only the "up" delay, not repeat delay, but it's useful to have an initial * "up" delay that's sufficiently large to ensure the native machine's auto-repeat behavior cooperates * with the virtual machine's auto-repeat behavior. msReleaseDelay is the initial delay, msReleaseRepeat * is the subsequent delay. * * Unfortunately, with a large initial delay, we need to enable the auto-clear code in the keyEvent() * handler, otherwise doing things like pressing ENTER repeatedly will result in sluggish behavior * (because you can generally press/release/repress keys faster than they will auto-repeat). */ var msDelay = (fRepeat? this.msReleaseRepeat: this.msReleaseDelay); if (this.cpu && this.cpu.mhz) { msDelay /= this.cpu.mhz; } return msDelay; }; /** * isShift() * * @this {C1PKeyboard} * @param {number} charCode * @return {boolean} * C1PKeyboard.prototype.isShift = function(charCode) { return charCode == this.CHARCODE_LSHIFT || charCode == this.CHARCODE_RSHIFT || charCode == this.CHARCODE_CTRL || charCode == this.CHARCODE_SHIFTLOCK; }; */ /** * @this {C1PKeyboard} * @param {number} [notCharCode] */ C1PKeyboard.prototype.autoClear = function(notCharCode) { if (this.prevCharDown && (notCharCode === undefined || notCharCode != this.prevCharDown)) { if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message("autoClear(" + str.toHexByte(this.prevCharDown) + ")"); } Component.assert(this.aKeyTimers[this.prevCharDown]); clearTimeout(this.aKeyTimers[this.prevCharDown]); this.keyEventSimulate(this.prevCharDown, false, this.SIMCODE_AUTOCLEAR); } }; /** * @this {C1PKeyboard} * @param {string} sKeyCodes * @param {number} [msDelay] is an optional injection delay (default is msInjectDelay) */ C1PKeyboard.prototype.injectKeys = function(sKeyCodes, msDelay) { this.sInjectBuffer = sKeyCodes; if (DEBUG) this.log("injectKeys(" + this.sInjectBuffer.split("\n").join("\\n") + ")"); this.injectKeysFromBuffer(msDelay || this.msInjectDelay); }; /** * @this {C1PKeyboard} * @param {number} msDelay is the delay between injected keys */ C1PKeyboard.prototype.injectKeysFromBuffer = function(msDelay) { if (this.sInjectBuffer.length > 0) { var ch = this.sInjectBuffer.charCodeAt(0); /* * I could require all callers to supply CRs instead of LFs, but this is friendlier. */ if (ch == 0x0a) ch = 0x0d; /* * Also, if upper-case characters are being injected, convert them to lower-case, and rely * on the virtual SHIFT-LOCK remaining locked for the duration; otherwise, we'd have to simulate * SHIFT key presses around every character (or around the entire set of characters) as well. * * UPDATE: Even though keyPressSimulate() currently has some code to do this automatically now, * it's really intended as a work-around for a SHIFT-related problem on iOS devices only, so * we can't rely on that in the general case. */ if (ch >= 0x41 && ch <= 0x5A) ch += 0x20; this.sInjectBuffer = this.sInjectBuffer.substr(1); this.keyPressSimulate(ch); } if (this.sInjectBuffer.length > 0) { setTimeout(function(kbd) { return function() {kbd.injectKeysFromBuffer(msDelay);}; }(this), msDelay); } }; /** * @this {C1PKeyboard} * @param {Object} event * @param {boolean} fDown is true if called for a keyDown event, false if called for a keyUp event * @return {boolean} true to pass the event along, false to consume it */ C1PKeyboard.prototype.keyEvent = function(event, fDown) { var fPass; var fAutoClear = !fDown; var keyCode = event.keyCode; if (fDown) this.prevKeyDown = keyCode; if (keyCode == this.CHARCODE_LSHIFT - this.PSEUDO_CHARCODE) { this.bitsShift &= ~this.BIT_LSHIFT; if (fDown) this.bitsShift |= this.BIT_LSHIFT; keyCode += this.PSEUDO_CHARCODE; fAutoClear = false; } else if (keyCode == this.CHARCODE_RSHIFT - this.PSEUDO_CHARCODE) { this.bitsShift &= ~this.BIT_RSHIFT; if (fDown) this.bitsShift |= this.BIT_RSHIFT; keyCode += this.PSEUDO_CHARCODE; fAutoClear = false; } else if (keyCode == this.CHARCODE_CTRL - this.PSEUDO_CHARCODE) { this.bitsShift &= ~this.BIT_CTRL; if (fDown) this.bitsShift |= this.BIT_CTRL; keyCode += this.PSEUDO_CHARCODE; fAutoClear = false; } else if (keyCode == this.CHARCODE_SHIFTLOCK - this.PSEUDO_CHARCODE) { /* * FYI, this generates a "down" event ONLY when getting locked, and an "up" event ONLY * when getting unlocked--which is exactly what I want, even though that may seem a little * counter-intuitive (since the key itself actually went down AND up for each event). * * Moreover, since most people do NOT have CAPS-LOCK enabled, whereas the C1P needs it * enabled by default, we invert fDown, so that if the user enables CAPS-LOCK for some * reason, we treat is as *disabling* SHIFT-LOCK, and vice versa. */ fDown = !fDown; this.bitsShift &= ~this.BIT_SHIFTLOCK; if (fDown) this.bitsShift |= this.BIT_SHIFTLOCK; keyCode += this.PSEUDO_CHARCODE; fAutoClear = false; } else if (keyCode == this.KEYCODE_COMMAND) { /* * Avoid interfering with useful Browser key commands, like COMMAND-Q, COMMAND-T, etc. */ this.bitsShift &= ~this.BIT_COMMAND; if (fDown) this.bitsShift |= this.BIT_COMMAND; fAutoClear = false; fPass = true; } else if (keyCode == this.KEYCODE_TAB) { /* * If I don't consume TAB on the "down" event, then that's all I'll see, because the * browser will see it and give focus to the next control. But the "down" side is that * that no "press" event will be generated. This puts it in the same category as ESC, * which also generates "down" and "up" events (LOTS of "down" events for that matter), * but no "press" event. However, the C1P has no TAB key, so it's safe to completely ignore. */ fPass = fAutoClear = false; } else if (keyCode == this.KEYCODE_ESC || keyCode == this.KEYCODE_DELETE) { /* * I don't get keyPress events for ESC (why?) and I never want the browser to act on DELETE * (which does double-duty as the "Back" button and leaves the current page), so I have to * simulate them now. * * Note that I call the "press" simulate method and NOT the "event" simulate method, because * the former takes care of simulating both individual "down" and "up" events. */ if (DEBUG && DEBUGGER && keyCode == this.KEYCODE_ESC && this.dbg) this.dbg.halt(); fPass = (fDown? !this.keyPressSimulate(keyCode) : false); } else { /* * Pass on everything else; I'll take care of this key at the keyPress stage, not the * the keyDown or keyUp stage. */ fPass = true; /* * At this point, I have a difficult choice to make: leave fAutoClear true for any remaining * "up" events, so that keys will repeat immediately when released/pressed repeatedly (most * noticeable with the Enter key), or set fAutoClear to false to ensure that polling apps have * enough time to see every key press. * * I've decided that the former is more important than the latter, so if polling apps are still * missing keystrokes, then perhaps nCyclesThreshold needs to be supplemented in some way. * * fAutoClear = false; */ } if (fAutoClear) { /* * When you use a command like COMMAND-T, I see the COMMAND key going down, but not going up, * so I think the COMMAND key is still down and ignore all input; to easily get out of that state, * I clear our internal BIT_COMMAND whenever I see ANY key go up (well, ALMOST any key; cases * above that explicitly clear fAutoClear -- such as the COMMAND key itself -- are exceptions * to the rule). */ this.bitsShift &= ~this.BIT_COMMAND; /* * I don't reliably get keyDown/keyUp events for all keys on all devices, but for those devices that * I DO, it seems like a good idea to cancel any pending key "up" simulation on receipt of the actual * keyUp event. * * However, the following code is problematic for Safari on iOS devices, which as noted above, doesn't * generate keyDown/keyUp events until after the press operation is complete, and then they are generated * in rapid succession, which doesn't give the C1P enough time to detect the key. So I simply don't do * this on iOS devices. */ if (!this.fMobile && keyCode == this.prevKeyDown) this.autoClear(); } if (fPass === undefined) { fPass = !this.keyEventSimulate(keyCode, fDown, this.SIMCODE_KEYEVENT); } if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message(/*(fDown?"\n":"") +*/ "key" + (fDown?"Down":"Up") + "(" + str.toHexByte(keyCode) + "): " + (fPass? "pass" : "consume")); } return fPass; }; /** * @this {C1PKeyboard} * @param {Object} event * @return {boolean} true to pass the event along, false to consume it * * We've stopped relying on keyPress for keyboard emulation purposes, but it's still handy to hook and monitor * when debugging. */ C1PKeyboard.prototype.keyPress = function(event) { var fPass = true; /* * Browser-independent charCode extraction... */ event = event || window.event; var charCode = event.which || event.keyCode; /* * Let's stop any injection currently in progress, too */ this.sInjectBuffer = ""; if (this.bitsShift & this.BIT_COMMAND) this.bitsShift &= ~this.BIT_COMMAND; else fPass = !this.keyPressSimulate(charCode); if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message("keyPress(" + str.toHexByte(charCode) + "): " + (fPass? "pass" : "consume")); } return fPass; }; /** * @this {C1PKeyboard} * @param {number} charCode * @return {boolean} true if successfully simulated, false if unrecognized/unsupported key */ C1PKeyboard.prototype.keyPressSimulate = function(charCode) { var fSimulated = false; if (charCode == this.CHARCODE_BREAK) { /* * The BREAK key is not wired up to the keyboard like the other keys are, but we simulate * it here, so that it can be injected like any other key. */ if (this.cmp) { this.cmp.reset(true); fSimulated = true; } } else { /* * WARNING: The next line is why you cannot use SHIFT-N, SHIFT-O, SHIFT-P, etc. But without it, * iOS devices with the annoying "autocapitalization" feature enabled make the keyboard unusable * by default. The trade-off is: either require all iOS users to first tap the shift key to turn * "autocapitalization" off, or lose the ability to type any of the special shifted alphabetic keys. * I choose the latter, because I have friendlier aliases already defined for those keys (eg, * ^, DELETE, and @). * * Furthermore, by doing this for iOS (and Android) devices ONLY, other platforms retain the ability * to use those special key combos. */ if (this.fMobile) { if (charCode >= 0x41 && charCode <= 0x5A) charCode += 0x20; } /* * Auto-clear any previous down key EXCEPT for charCode (because it may be held and repeating). */ this.autoClear(charCode); if (this.keyEventSimulate(charCode, true, this.SIMCODE_KEYPRESS)) { /* * If CPU speed is unlimited, then we switch to an alternate approach, which is to immediately * queue a "release" event as well. The problem with the original timer-based approach at high * speeds is that the the CPU may get lucky and execute a LOT of instructions between delivery * of the keyPress event and the "keyTimeout" event. In that case, even enabling keyboard polling * detection in updateMemory() won't entirely help -- although we do that, too -- because JavaScript * events are delivered synchronously, so it may simply take too long for the "keyTimeout" event * to arrive. * * Why don't we ALWAYS do this? Because in the normal case (SPEED_SLOW, and even SPEED_FAST) we want * to faithfully simulate how long a key is held, so that features like auto-repeat work properly. * You'll notice in the SPEED_MAX case, holding a key no longer has any effect; even though multiple * keyPress events WILL arrive, if we simulate a release immediately after each one, then repeat * is defeated. Also, the keyboard polling detection code in updateMemory() doesn't work well for * all apps. */ if (this.cpu.speed == this.cpu.SPEED_MAX) { this.keyEventSimulate(charCode, false, this.SIMCODE_KEYRELEASE); } else { var fRepeat = false; if (this.aKeyTimers[charCode]) { clearTimeout(this.aKeyTimers[charCode]); fRepeat = true; } var msDelay = this.calcReleaseDelay(fRepeat); this.aKeyTimers[this.prevCharDown = charCode] = setTimeout(function(kbd) { return function() {kbd.keyEventSimulate(charCode, false, kbd.SIMCODE_KEYTIMEOUT);}; }(this), msDelay); if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message("keyPressSimulate(" + str.toHexByte(charCode) + "): setTimeout()"); } } fSimulated = true; } } if (DEBUGGER && this.dbg && this.dbg.messageEnabled(this.dbg.MESSAGE_KBD)) { this.dbg.message("keyPressSimulate(" + str.toHexByte(charCode) + "): " + (fSimulated? "true" : "false")); } return fSimulated; }; /** * @this {C1PKeyboard} * @param {number} charCode * @param {boolean} fDown * @param {number} simCode indicates the origin of the event * @return {boolean} true if successfully simulated, false if unrecognized/unsupported key */ C1PKeyboard.prototype.keyEventSimulate = function(charCode, fDown, simCode) { var fSimulated = false; if (!fDown) { this.aKeyTimers[charCode] = null; if (this.prevCharDown == charCode) this.prevCharDown = 0; } var bShift = 0; var bCode = this.aCharCodeMap[charCode]; if (bCode === undefined) { /* * Perhaps we're dealing with a CTRL variation of an alphabetic key; this won't * affect non-CTRL-key combos like CR or LF, because they're defined in aCharCodeMap, * and this bit of code relieves us from having to explicitly define every CTRL-letter * possibility in aCharCodeMap. However, CTRL-anything-else is a different matter. */ if (charCode >= 0x01 && charCode <= 0x1A) { charCode += 0x40; bShift = this.CHARCODE_CTRL; } bCode = this.aCharCodeMap[charCode]; } if (bCode !== undefined) { var iRow = bCode >> 12; var iCol = (bCode >> 8) & 0xf; if (!bShift) bShift = bCode & 0xff; if (fDown) { this.abKbdCols[iRow] |= 1 << iCol; if (bShift == this.CHARCODE_CTRL) this.abKbdCols[0] |= this.BIT_CTRL; else if (bShift == this.CHARCODE_LSHIFT) this.abKbdCols[0] |= this.BIT_LSHIFT; else if (bShift == this.CHARCODE_RSHIFT) this.abKbdCols[0] |= this.BIT_RSHIFT; else this.abKbdCols[0] &= ~this.BITS_SIMULATE; } else { this.abKbdCols[iRow] &= ~(1 << iCol); this.abKbdCols[0] &= ~this.BITS_SIMULATE; this.abKbdCols[0] |= (this.bitsShift & this.BITS_SIMULATE); } var fPropagate = (simCode == this.SIMCODE_KEYPRESS && !this.aKbdStates.length); this.aKbdStates.push(this.abKbdCols.slice()); this.updateMemory(fPropagate); fSimulated = true; } if (DEBUG && this.dbg) this.dbg.info("keyEventSimulate(" + str.toHexByte(charCode) + "," + (fDown?"down":"up") + "," + this.aSimCodeDescs[simCode] + "): " + (fSimulated? "true" : "false")); return fSimulated; }; /** * @this {C1PKeyboard} * @param {number} addr * @param {number|undefined} addrFrom (not defined whenever the Debugger tries to read the specified addr) * * NOTE: As long as we rely on the CPU processing a certain number of cycles (nCyclesThreshold) before * propagating the next kbd state, and not how many reads and/or writes the CPU has performed, we could * eliminate the overhead of this read-notification handler. * * It's useful for diagnostic purposes, which is why it's still here. */ C1PKeyboard.prototype.getByte = function(addr, addrFrom) { /* * Don't trigger any further hardware emulation (beyond what we've already stored in memory) if * the Debugger performed this read (need a special Debugger I/O command if/when you really want to do that). */ if (DEBUG) { if (addrFrom !== undefined) { this.nReadsSinceLastEvent++; if (DEBUG && this.dbg) this.dbg.info("reading kbd " + str.toHexWord(addr) + " @" + this.cpu.getCycles() + " cycles"); } } }; /** * @this {C1PKeyboard} * @param {number} addr * @param {number|undefined} addrFrom (not defined whenever the Debugger tries to write the specified addr) * * NOTE: Ordinarily, I wouldn't allow Debugger writes (addrFrom === undefined) to interfere with the simulated * hardware state, but for now, I find it useful to be able to prod the simulation code directly from the Debugger. */ C1PKeyboard.prototype.setByte = function(addr, addrFrom) { var b = this.cpu.getByte(addr); this.bKbdRows = b ^ this.bInvert; this.nWritesSinceLastEvent++; this.updateMemory(false, addr, b); }; /** * @this {C1PKeyboard} * @param {boolean} fPropagate is true to propagate immediately, false to use normal propagation * @param {number} [addr] is the memory address to update; default is the entire memory range * @param {number} [bWrite] is the value of any immediately preceding write, or undefined if none * * Update emulated keyboard memory. By updating the keyboard memory whenever it's written to, * as well as whenever a key is pressed or released, I avoid the hit of a read-notification handler. * Besides, read-notification handlers are called only AFTER the read has been performed, so it * would be too late to update the memory at that point. * * WARNING: There is a slight risk of an application reading from a different keyboard memory address * than it just wrote to. That would be legal, but very odd, and we save valuable time by not updating * every other byte in the entire memory range every time a different keyboard "row" is selected. */ C1PKeyboard.prototype.updateMemory = function(fPropagate, addr, bWrite) { /* * First, we determine if it's time for event propagation... */ var nCycles = this.cpu.getCycles(); /* * Monitoring the C1P's keyboard polling activity is problematic, because not all apps monitor * the keyboard in the same way. It's better to simply wait for a certain amount of CPU activity to * occur (nCyclesThreshold); it's more reliable and it scales well, because it's not affected by * how many cycles we're executing in real time. The trick is finding a value for nCyclesThreshold * that works well across the board. */ if (!fPropagate) { if (this.cpu.speed == this.cpu.SPEED_MAX) fPropagate = (addr !== undefined && this.nWritesSinceLastEvent >= 32); else { /* * We have to handle the delta being less than zero, in case the user changed the speed, thereby * resetting the cycle count returned by getCycles(). */ var nCycleDelta = nCycles - this.nCyclesSinceLastEvent; fPropagate = (nCycleDelta < 0 || nCycleDelta >= this.nCyclesThreshold); } } /* * Next, we propagate any buffered state (in abKbdStates) as appropriate */ if (fPropagate) { var abKbdCols = this.aKbdStates.shift(); if (abKbdCols !== undefined) { if (DEBUG && this.dbg) this.dbg.info("kbd update @" + nCycles + " cycles, " + this.nWritesSinceLastEvent + " writes"); this.abKbdColsLast = abKbdCols; } this.nReadsSinceLastEvent = this.nWritesSinceLastEvent = 0; this.nCyclesSinceLastEvent = nCycles; } /* * Then we calculate the value (which may or may not have just been propagated), * based on the currently selected keyboard row(s) (bKbdRows). */ var b = 0; for (var iRow=0; iRow < 8; iRow++) { if (!(this.bKbdRows & (1 << iRow))) continue; b |= this.abKbdColsLast[iRow]; } /* * Now invert all the bits, since I SET the column bit of an "active" key, * whereas the C1P Model 600 keyboard expects "active" column bits to be CLEAR. */ b ^= this.bInvert; if (addr !== undefined) { this.abMem[addr] = b; } else { addr = this.offKbd; if (b != this.bWriteLast) { for (var offset=addr; offset < this.offKbdLimit; offset++) this.abMem[offset] = b; } } this.bWriteLast = b; if (DEBUG && this.dbg) this.dbg.info("updating kbd " + str.toHexWord(addr) + " with " + str.toHexByte(b) + (bWrite !== undefined? (" following write " + str.toHexByte(bWrite)) : "") + " @" + nCycles + " cycles"); }; /** * C1PKeyboard.init() * * This function operates on every HTML element of class "keyboard", extracting the * JSON-encoded parameters for the C1PKeyboard constructor from the element's "data-value" * attribute, invoking the constructor to create a C1PKeyboard component, and then binding * any associated HTML controls to the new component. */ C1PKeyboard.init = function() { var aeKbd = Component.getElementsByClass(document, C1PJS.APPCLASS, "keyboard"); for (var iKbd=0; iKbd < aeKbd.length; iKbd++) { var eKbd = aeKbd[iKbd]; var parmsKbd = Component.getComponentParms(eKbd); var kbd = new C1PKeyboard(parmsKbd); Component.bindComponentControls(kbd, eKbd, C1PJS.APPCLASS); } }; /* * Initialize every Keyboard module on the page. */ web.onInit(C1PKeyboard.init);