/** * @fileoverview Implements the PCx86 Keyboard component. * @author Jeff Parsons * @copyright © Jeff Parsons 2012-2017 * * 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 modified copy of this work * and to display that copyright notice when the software starts running; see COPYRIGHT in * . * * 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 Usr = require("../../shared/lib/usrlib"); var Web = require("../../shared/lib/weblib"); var Component = require("../../shared/lib/component"); var Keys = require("../../shared/lib/keys"); var State = require("../../shared/lib/state"); var PCX86 = require("./defines"); var Interrupts = require("./interrupts"); var Messages = require("./messages"); var ChipSet = require("./chipset"); var CPU = require("./cpu"); } /** * TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default, * which would force us to declare all class properties in the constructor, as well as prevent * us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'. * * @unrestricted */ class Keyboard extends Component { /** * Keyboard(parmsKbd) * * The Keyboard component can be configured with the following (parmsKbd) properties: * * model: keyboard model string, which must match one of the values listed in Keyboard.MODELS: * * "US83" (default) * "US84" * "US101" * * autoType: string of keys to automatically inject when the machine is ready (undefined if none) * * Its main purpose is to receive binding requests for various keyboard events, and to use those events * to simulate the PC's keyboard hardware. * * @this {Keyboard} * @param {Object} parmsKbd */ constructor(parmsKbd) { super("Keyboard", parmsKbd, Messages.KEYBOARD); this.setModel(parmsKbd['model']); this.fMobile = Web.isMobile(); this.fMSIE = Web.isUserAgent("MSIE"); this.printMessage("mobile keyboard support: " + (this.fMobile? "true" : "false")); /* * This is count of the number of "soft keyboard" keys present. At the moment, its only * purpose is to signal findBinding() whether to waste any time looking for SOFTCODE matches. */ this.cSoftCodes = 0; /* * Updated by onFocusChange() */ this.fHasFocus = true; /* * This is true whenever the physical Escape key is disabled (eg, by pointer locking code), * giving us the opportunity to map a different physical key to machine's virtual Escape key. */ this.fEscapeDisabled = false; /* * This is set whenever we notice a discrepancy between our internal CAPS_LOCK state and its * apparent state; we check whenever aKeysActive has been emptied. */ this.fToggleCapsLock = false; /* * New unified approach to key event processing: When we process a key on the "down" event, * we check the aKeysActive array: if the key is already active, do nothing; otherwise, insert * it into the table, generate the "make" scan code(s), and set a timeout for "repeat" if it's * a repeatable key (most are). * * Similarly, when a key goes "up", if it's already not active, do nothing; otherwise, generate * the "break" scan code(s), cancel any pending timeout, and remove it from the active key table. * * If a "press" event is received, then if the key is already active, remove it and (re)insert * it at the head of the table, generate the "make" scan code(s), set nRepeat to -1, and set a * timeout for "break". * * This requires an aKeysActive array that keeps track of the status of every active key; only the * first entry in the array is allowed to repeat. Each entry is a key object with the following * properties: * * simCode: our simulated keyCode from onKeyDown, onKeyUp, or onKeyPress * fDown: next state to simulate (true for down, false for up) * nRepeat: > 0 if timer should generate more "make" scan code(s), -1 for "break" scan code(s) * timer: timer for next key operation, if any * * Keys are inserted at the head of aKeysActive, using splice(0, 0, key), but not before zeroing * nRepeat of any repeating key that already occupies the head (index 0), so that at most only one * key (ie, the most recent) will ever be in a repeating state. * * IBM PC keyboard repeat behavior: when pressing CTRL, then C, and then releasing CTRL while still * holding C, the repeated CTRL_C characters turn into 'c' characters. We emulate that behavior. * However, when pressing C, then CTRL, all repeating stops: not a single CTRL_C is generated, and * even if the CTRL is released before the C, no more more 'c' characters are generated either. * We do NOT fully emulate that behavior -- we DO stop the repeating, but we also generate one CTRL_C. * More investigation is required, because I need to confirm whether the IBM keyboard automatically * "breaks" all non-shift keys before it "makes" the CTRL. */ this.aKeysActive = []; this.msAutoRepeat = 500; this.msNextRepeat = 100; this.msAutoRelease = 50; this.msInjectDefault = 150; // number of milliseconds between injected keystrokes this.msInjectDelay = 0; // set by the initial injectKeys() call /* * autoType records the machine's specified autoType sequence, if any. At the appropriate signal(s), * autoType will be copied to autoInject, and injection will commence. */ this.autoInject = null; this.autoType = parmsKbd['autoType']; this.fDOSReady = false; this.fnDOSReady = this.fnInjectReady = null; /* * HACK: We set fAllDown to false to ignore all down/up events for keys not explicitly marked as ONDOWN; * even though that prevents those keys from being repeated properly (ie, at the simulation's repeat rate * rather than the browser's repeat rate), it's the safest thing to do when dealing with international keyboards, * because our mapping tables are designed for US keyboards, and testing all the permutations of international * keyboards and web browsers is more work than I can take on right now. TODO: Dig into this some day. */ this.fAllDown = false; this['exports'] = { 'type': this.injectKeys, 'wait': this.waitReady }; this.setReady(); } /** * setBinding(sHTMLType, sBinding, control, sValue) * * @this {Keyboard} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc") * @param {HTMLElement} 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 */ setBinding(sHTMLType, sBinding, control, sValue) { /* * There's a special binding that the Video component uses ("screen") to effectively bind its * screen to the entire keyboard, in Video.powerUp(); ie: * * video.kbd.setBinding("canvas", "screen", video.canvasScreen); * or: * video.kbd.setBinding("textarea", "screen", video.textareaScreen); * * However, it's also possible for the keyboard XML definition to define a control that serves * a similar purpose; eg: * * Keyboard * * The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on * certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will * still be enabled (there's currently no way to configure the Video component to not bind its screen, * but we could certainly add one if the need ever arose). */ var kbd = this; var id = sHTMLType + '-' + sBinding; var controlText = /** @type {HTMLTextAreaElement} */ (control); if (this.bindings[id] === undefined) { switch (sBinding) { case "kbd": case "screen": /* * Recording the binding ID prevents multiple controls (or components) from attempting to erroneously * bind a control to the same ID, but in the case of a "dual display" configuration, we actually want * to allow BOTH video components to call setBinding() for "screen", so that it doesn't matter which * display the user gives focus to. * * this.bindings[id] = control; */ controlText.onkeydown = function onKeyDown(event) { return kbd.onKeyDown(event, true); }; controlText.onkeypress = function onKeyPressKbd(event) { return kbd.onKeyPress(event); }; controlText.onkeyup = function onKeyUp(event) { return kbd.onKeyDown(event, false); }; return true; case "caps-lock": this.bindings[id] = control; control.onclick = function onClickCapsLock(event) { event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work return kbd.toggleCapsLock(); }; return true; case "num-lock": this.bindings[id] = control; control.onclick = function onClickNumLock(event) { event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work return kbd.toggleNumLock(); }; return true; case "scroll-lock": this.bindings[id] = control; control.onclick = function onClickScrollLock(event) { event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work return kbd.toggleScrollLock(); }; return true; default: /* * Maintain support for older button codes; eg, map button code "ctrl-c" to CLICKCODE "CTRL_C" */ var sCode = sBinding.toUpperCase().replace(/-/g, '_'); if (Keyboard.CLICKCODES[sCode] !== undefined && sHTMLType == "button") { this.bindings[id] = controlText; controlText.onclick = function(kbd, sKey, simCode) { return function onKeyboardBindingClick(event) { if (!COMPILED && kbd.messageEnabled()) kbd.printMessage(sKey + " clicked", Messages.KEYS); event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work kbd.sInjectBuffer = ""; // key events should stop any injection currently in progress kbd.updateShiftState(simCode, true); // future-proofing if/when any LOCK keys are added to CLICKCODES kbd.addActiveKey(simCode, true); }; }(this, sCode, Keyboard.CLICKCODES[sCode]); return true; } else if (Keyboard.SOFTCODES[sBinding] !== undefined) { this.cSoftCodes++; this.bindings[id] = controlText; var fnDown = function(kbd, sKey, simCode) { return function onKeyboardBindingDown(event) { event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work kbd.sInjectBuffer = ""; // key events should stop any injection currently in progress kbd.addActiveKey(simCode); }; }(this, sBinding, Keyboard.SOFTCODES[sBinding]); var fnUp = function(kbd, sKey, simCode) { return function onKeyboardBindingUp(event) { kbd.removeActiveKey(simCode); }; }(this, sBinding, Keyboard.SOFTCODES[sBinding]); if ('ontouchstart' in window) { controlText.ontouchstart = fnDown; controlText.ontouchend = fnUp; } else { controlText.onmousedown = fnDown; controlText.onmouseup = controlText.onmouseout = fnUp; } return true; } else if (sValue) { /* * Instead of just having a dedicated "test" control, we now treat any unrecognized control with * a "value" attribute as a test control. The only caveat is that such controls must have binding IDs * that do not conflict with predefined controls (which, of course, is the only way you can get here). */ this.bindings[id] = control; control.onclick = function onClickTest(event) { event.preventDefault(); // preventDefault() is necessary... if (kbd.cmp) kbd.cmp.updateFocus(); // ...for the updateFocus() call to actually work return kbd.injectKeys(sValue); }; return true; } break; } } return false; } /** * findBinding(simCode, sType, fDown) * * TODO: This function is woefully inefficient, because the SOFTCODES table is designed for converting * soft key presses into SIMCODES, whereas this function is doing the reverse: looking for the soft key, * if any, that corresponds to a SIMCODE, simply so we can provide visual feedback of keys activated * by other means (eg, real keyboard events, button clicks that generate key sequences like CTRL_ALT_DEL, * etc). * * To minimize this function's cost, we would want to dynamically create a reverse-lookup table after * all the setBinding() calls for the soft keys have been established; note that the reverse-lookup table * would contain MORE entries than the SOFTCODES table, because there are multiple simCodes that correspond * to a given soft key (eg, '1' and '!' both map to the same soft key). * * @this {Keyboard} * @param {number} simCode * @param {string} sType is the type of control (eg, "button" or "key") * @param {boolean} [fDown] is true if the key is going down, false if up, or undefined if unchanged * @return {Object} is the HTML control DOM object (eg, HTMLButtonElement), or undefined if no such control exists */ findBinding(simCode, sType, fDown) { var control; if (this.cSoftCodes) { for (var code in Keys.SHIFTED_KEYCODES) { if (simCode == Keys.SHIFTED_KEYCODES[code]) { simCode = +code; code = Keys.NONASCII_KEYCODES[code]; if (code) simCode = code; break; } } for (var sBinding in Keyboard.SOFTCODES) { if (Keyboard.SOFTCODES[sBinding] == simCode || Keyboard.SOFTCODES[sBinding] == this.toUpperKey(simCode)) { var id = sType + '-' + sBinding; control = this.bindings[id]; if (control && fDown !== undefined) { this.setSoftKeyState(control, fDown); } break; } } } return control; } /** * initBus(cmp, bus, cpu, dbg) * * @this {Keyboard} * @param {Computer} cmp * @param {Bus} bus * @param {X86CPU} cpu * @param {DebuggerX86} dbg */ initBus(cmp, bus, cpu, dbg) { this.cmp = cmp; this.bus = bus; this.cpu = cpu; this.dbg = dbg; var kbd = this; this.timerInject = this.cpu.addTimer(this.id + ".inject", function() { kbd.injectKeysFromBuffer(); }); this.chipset = cmp.getMachineComponent("ChipSet"); this.autoType = cmp.getMachineParm('autoType') || this.autoType; cpu.addIntNotify(Interrupts.DOS, this.intDOS.bind(this)); } /** * intDOS() * * Monitors selected DOS interrupts for signals to initialize 'autoType' injection. * * @this {Keyboard} * @param {number} addr * @return {boolean} true to proceed with the INT 0x21 software interrupt, false to skip */ intDOS(addr) { var AH = (this.cpu.regEAX >> 8) & 0xff; if (AH == 0x0A) { this.fDOSReady = true; if (this.fnDOSReady) { this.fnDOSReady(); this.fnDOSReady = null; this.fDOSReady = false; } else { this.injectInit(this.autoType); } } return true; } /** * notifyEscape(fDisabled, fAllDown) * * When ESC is used by the browser to disable pointer lock, this gives us the option of mapping a different key to ESC. * * @this {Keyboard} * @param {boolean} fDisabled * @param {boolean} [fAllDown] (an experimental option to re-enable processing of all onkeydown/onkeyup events) */ notifyEscape(fDisabled, fAllDown) { this.fEscapeDisabled = fDisabled; if (fAllDown !== undefined) this.fAllDown = fAllDown; } /** * parseKeys(sKeys) * * The following special sequences are recognized: * * $date: converted to MM-DD-YYYY * $time: converted to HH:MM * * If you want any of those sequences to be typed as-is, then you must specify two "$" (ie, "$$date"). * Pairs of dollar signs will be automatically converted to single dollar signs, and single dollar signs * will be used as-is. * * WARNING: the JavaScript replace() function ALWAYS interprets "$" specially in replacement strings, * even when the search string is NOT a RegExp; specifically: * * $$ Inserts a "$" * $& Inserts the matched substring * $` Inserts the portion of the string that precedes the matched substring * $' Inserts the portion of the string that follows the matched substring * $n Where n is a positive integer less than 100, inserts the nth parenthesized sub-match string, * provided the first argument was a RegExp object * * Since we build machine definitions on a page from a potentially indeterminate number of string replace() * operations, multiple dollar signs could eventually get reduced to a single dollar sign BEFORE we get here. * * To compensate, I've changed a few replace() methods, like MarkOut's convertMDMachineLinks() and HTMLOut's * addFilesToHTML(), from the conventional string replace() to my own Str.replace(), and for situations like the * embed.js parseXML() function, which needs to use a RegExp-style replace(), I've added a preliminary * replace(/\$/g, "$$$$") to the replacement string. * * Unfortunately, this is something that will be extremely difficult to prevent from breaking down the road. * So, heads up to future me.... * * @this {Keyboard} * @param {string|undefined} sKeys * @return {string|undefined} */ parseKeys(sKeys) { if (sKeys) { var match, reSpecial = /(?:^|[^$])\$([a-z]+)/g; while (match = reSpecial.exec(sKeys)) { var sReplace = ""; switch (match[1]) { case 'date': sReplace = Usr.formatDate("n-j-Y"); break; case 'time': sReplace = Usr.formatDate("h:i:s"); break; default: // // Let's just leave any unrecognized sequences alone.... // // this.notice("unrecognized autoType sequence: $" + match[1]); // break; continue; } sKeys = sKeys.replace('$' + match[1], sReplace); /* * Even though we did just modify the string that reSpecial is iterating over, we aren't * going to muck with lastIndex, because 1) the replacement strings are always longer than * original strings, and 2) any unrecognized sequences that we now leave in place would cause * us to loop indefinitely. So, if you really want to do this, you will have to carefully * set lastIndex to the next unexamined character, not back to the beginning. * * reSpecial.lastIndex = 0; */ } /* * Any lingering "$$" sequences are now converted to "$"; as discussed above, replace() interprets * any "$$" in the replacement string as "$", so to the casual observer, it might not look like we're * downsizing the dollar signs, but we actually are. */ sKeys = sKeys.replace(/\$\$/g, "$$"); } return sKeys; } /** * setModel(sModel) * * This breaks a model string (eg, "US83") into two parts: modelCountry (eg, "US") and modelKeys (eg, 83). * If the model string isn't recognized, we use Keyboard.MODELS[0] (ie, the first entry in the model array). * * @this {Keyboard} * @param {string|undefined} sModel */ setModel(sModel) { var iModel = 0; this.model = null; if (typeof sModel == "string") { this.model = sModel.toUpperCase(); iModel = Keyboard.MODELS.indexOf(this.model); if (iModel < 0) iModel = 0; } sModel = Keyboard.MODELS[iModel]; if (sModel) { this.modelCountry = sModel.substr(0, 2); this.modelKeys = parseInt(sModel.substr(2), 10); } } /** * resetDevice(fNotify) * * @this {Keyboard} * @param {boolean} [fNotify] */ resetDevice(fNotify) { /* * TODO: There's more to reset, like LED indicators, default type rate, and emptying the scan code buffer. */ this.printMessage("keyboard reset", Messages.KEYBOARD | Messages.PORT); this.abBuffer = []; this.setResponse(Keyboard.CMDRES.BAT_OK); } /** * setEnabled(fData, fClock) * * This is the ChipSet's primary interface for toggling keyboard "data" and "clock" lines. * For MODEL_5150 and MODEL_5160 machines, this function is called from the ChipSet's PPI_B * output handler. For MODEL_5170 machines, this function is called when selected CMD * "data bytes" have been written. * * @this {Keyboard} * @param {boolean} fData is true if the keyboard simulated data line should be enabled * @param {boolean} fClock is true if the keyboard's simulated clock line should be enabled * @return {boolean} true if keyboard was re-enabled, false if not (or no change) */ setEnabled(fData, fClock) { var fReset = false; if (this.fClock !== fClock) { if (!COMPILED && this.messageEnabled(Messages.KEYBOARD | Messages.PORT)) { this.printMessage("keyboard clock line changing to " + fClock, true); } /* * Toggling the clock line low and then high signals a "reset", which we acknowledge once the * data line is high as well. */ this.fClock = this.fResetOnEnable = fClock; /* * Allow the next buffered scan code, if any, to advance. */ if (fClock) this.fAdvance = true; } if (this.fData !== fData) { if (!COMPILED && this.messageEnabled(Messages.KEYBOARD | Messages.PORT)) { this.printMessage("keyboard data line changing to " + fData, true); } this.fData = fData; /* * TODO: Review this code; it was added during the early days of MODEL_5150 testing and may not be * *exactly* what's called for here. */ if (fData && !this.fResetOnEnable) { this.shiftScanCode(true); } } if (this.fData && this.fResetOnEnable) { this.resetDevice(true); this.fResetOnEnable = false; fReset = true; } return fReset; } /** * setLEDs(b) * * This processes the option byte received after a SET_LEDS command byte. * * @this {Keyboard} * @param {number} b */ setLEDs(b) { this.bLEDs = b; // TODO: Implement } /** * setRate(b) * * This processes the rate parameter byte received after a SET_RATE command byte. * * @this {Keyboard} * @param {number} b */ setRate(b) { this.bRate = b; // TODO: Implement } /** * setResponse(b) * * @this {Keyboard} * @param {number} b */ setResponse(b) { if (this.chipset) { this.abBuffer.unshift(b); this.fAdvance = true; this.chipset.notifyKbdData(b); } } /** * sendCmd(bCmd) * * This is the ChipSet's primary interface for controlling "Model M" keyboards (ie, those used * with MODEL_5170 machines). Commands are delivered through the ChipSet's 8042 Keyboard Controller. * * @this {Keyboard} * @param {number} bCmd should be one of the Keyboard.CMD.* command codes (Model M keyboards only) * @return {number} response should be one of the Keyboard.CMDRES.* response codes, or -1 if unrecognized */ sendCmd(bCmd) { var b = -1; if (this.messageEnabled()) this.printMessage("sendCmd(" + Str.toHexByte(bCmd) + ")"); switch(this.bCmdPending || bCmd) { case Keyboard.CMD.RESET: // 0xFF /* * TODO: Determine whether we really need to also return CMDRES.ACK. resetDevice() operates * like setResponse(CMDRES.BAT_OK). Do we need both the ACK and the BAT_OK? */ b = Keyboard.CMDRES.ACK; this.resetDevice(); break; case Keyboard.CMD.SET_RATE: // 0xF3 if (this.bCmdPending) { this.setRate(bCmd); bCmd = 0; } this.setResponse(Keyboard.CMDRES.ACK); this.bCmdPending = bCmd; break; case Keyboard.CMD.SET_LEDS: // 0xED if (this.bCmdPending) { this.setLEDs(bCmd); bCmd = 0; } this.setResponse(Keyboard.CMDRES.ACK); this.bCmdPending = bCmd; break; default: this.printMessage("sendCmd(): unrecognized command"); break; } return b; } /** * checkScanCode() * * This is the ChipSet's interface for checking data availability. * * Note that even if we have data, we don't provide it unless fAdvance is set as well. * This ensures that we wait until the ROM to disable and re-enable the controller before * making more data available. * * @this {Keyboard} * @return {number} next scan code, or 0 if none */ checkScanCode() { var b = 0; if (this.abBuffer.length && this.fAdvance) { b = this.abBuffer[0]; if (this.chipset) this.chipset.notifyKbdData(b); } if (this.messageEnabled()) { this.printMessage(b? ("scan code " + Str.toHexByte(b) + " available") : "no scan codes available"); } return b; } /** * readScanCode() * * This is the ChipSet's interface for reading scan codes. * * @this {Keyboard} * @return {number} next scan code, or 0 if none */ readScanCode() { var b = 0; if (this.abBuffer.length) { b = this.abBuffer[0]; } if (this.messageEnabled()) this.printMessage("scan code " + Str.toHexByte(b) + " delivered"); return b; } /** * flushScanCode() * * This is the ChipSet's interface to flush scan codes. * * @this {Keyboard} */ flushScanCode() { this.abBuffer = []; if (this.messageEnabled()) this.printMessage("scan codes flushed"); } /** * shiftScanCode(fNotify) * * This is the ChipSet's interface to advance scan codes. * * @this {Keyboard} * @param {boolean} [fNotify] is true to notify ChipSet if more data is available. */ shiftScanCode(fNotify) { if (this.abBuffer.length > 0) { /* * The keyboard interrupt service routine toggles the enable bit after reading a scan code, so * presumably this is the proper point at which to shift the last scan code out, and then assert * another interrupt if more scan codes exist. */ this.abBuffer.shift(); this.fAdvance = fNotify; if (fNotify) { if (!this.abBuffer.length || !this.chipset) { fNotify = false; } else { this.chipset.notifyKbdData(this.abBuffer[0]); } } if (this.messageEnabled()) this.printMessage("scan codes shifted, notify " + (fNotify? "true" : "false")); } } /** * powerUp(data, fRepower) * * @this {Keyboard} * @param {Object|null} data * @param {boolean} [fRepower] * @return {boolean} true if successful, false if failure */ powerUp(data, fRepower) { if (!fRepower) { /* * TODO: Save/restore support for Keyboard is the barest minimum. In fact, originally, I wasn't * saving/restoring anything, and that was OK, but if we don't at least re-initialize fClock/fData, * we can get a spurious reset following a restore. In an ideal world, we might choose to save/restore * abBuffer as well, but realistically, I think it's going to be safer to always start with an * empty buffer--and who's going to notice anyway? * * So, like Debugger, we deviate from the typical save/restore pattern: instead of reset OR restore, * we always reset and then perform a (very limited) restore. */ this.reset(); if (data && this.restore) { if (!this.restore(data)) return false; } } return true; } /** * powerDown(fSave, fShutdown) * * @this {Keyboard} * @param {boolean} [fSave] * @param {boolean} [fShutdown] * @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure */ powerDown(fSave, fShutdown) { return fSave? this.save() : true; } /** * reset() * * @this {Keyboard} */ reset() { /* * If no keyboard model was specified, our initial setModel() call will select the "US83" keyboard as the * default, but now that the ChipSet is initialized, we can pick a better default, based on the ChipSet model. */ if (!this.model && this.chipset) { switch(this.chipset.model) { case ChipSet.MODEL_5150: case ChipSet.MODEL_5160: this.setModel(Keyboard.MODELS[0]); break; case ChipSet.MODEL_5170: default: this.setModel(Keyboard.MODELS[1]); break; } } this.initState(); } /** * save() * * This implements save support for the Keyboard component. * * @this {Keyboard} * @return {Object} */ save() { var state = new State(this); state.set(0, this.saveState()); return state.data(); } /** * restore(data) * * This implements restore support for the Keyboard component. * * @this {Keyboard} * @param {Object} data * @return {boolean} true if successful, false if failure */ restore(data) { return this.initState(data[0]); } /** * initState(data) * * @this {Keyboard} * @param {Array} [data] * @return {boolean} true if successful, false if failure */ initState(data) { var i = 0; if (!data) { data = []; this.autoInject = null; } else { this.autoInject = this.autoType; } this.fClock = this.fAdvance = data[i++]; this.fData = data[i]; this.bCmdPending = 0; // when non-zero, a command is pending (eg, SET_LED or SET_RATE) /* * The current (assumed) physical (and simulated) states of the various shift/lock keys. * * TODO: Determine how (or whether) we can query the browser's initial shift/lock key states. */ this.bitsState = this.bitsStateSim = 0; /* * New scan codes are "pushed" onto abBuffer and then "shifted" off. */ this.abBuffer = []; this.fAdvance = true; this.prevCharDown = 0; this.prevKeyDown = 0; /* * Make sure the auto-injection buffer is empty (an injection could have been in progress on any reset after the first). */ this.sInjectBuffer = ""; return true; } /** * saveState() * * @this {Keyboard} * @return {Array} */ saveState() { var data = []; data[0] = this.fClock; data[1] = this.fData; return data; } /** * setSoftKeyState(control, f) * * @this {Keyboard} * @param {HTMLElement} control is an HTML control DOM object * @param {boolean} f is true if the key represented by e should be "on", false if "off" */ setSoftKeyState(control, f) { control.style.color = (f? "#ffffff" : "#000000"); control.style.backgroundColor = (f? "#000000" : "#ffffff"); } /** * addScanCode(bScan) * * @this {Keyboard} * @param {number} bScan */ addScanCode(bScan) { /* * Prepare for the possibility that our reset() function may not have been called yet. * * TODO: Determine whether we need to reset() the Keyboard sooner (ie, in the constructor), * or if we need to protect other methods from prematurely accessing certain Keyboard structures, * as a result of calls from any of the key event handlers established by setBinding(). */ if (this.abBuffer) { if (this.abBuffer.length < Keyboard.LIMIT.MAX_SCANCODES) { if (this.messageEnabled()) this.printMessage("scan code " + Str.toHexByte(bScan) + " buffered"); this.abBuffer.push(bScan); if (this.abBuffer.length == 1) { if (this.chipset) this.chipset.notifyKbdData(bScan); } return; } if (this.abBuffer.length == Keyboard.LIMIT.MAX_SCANCODES) { this.abBuffer.push(Keyboard.CMDRES.BUFF_FULL); } this.printMessage("scan code buffer overflow"); } } /** * injectInit(sKeys) * * @this {Keyboard} * @param {string|undefined} sKeys * @return {boolean} */ injectInit(sKeys) { if (!this.autoInject && sKeys) { this.autoInject = sKeys; return this.injectKeys(this.autoInject); } return false; } /** * injectKeys(sKeys, msDelay) * * @this {Keyboard} * @param {string|undefined} sKeys * @param {number} [msDelay] is an optional injection delay (default is msInjectDefault) * @return {boolean} */ injectKeys(sKeys, msDelay) { if (sKeys && !this.sInjectBuffer) { this.sInjectBuffer = this.parseKeys(sKeys); if (!COMPILED) this.log("injectKeys(\"" + this.sInjectBuffer.split("\n").join("\\n") + "\")"); this.msInjectDelay = msDelay || this.msInjectDefault; this.injectKeysFromBuffer(); return true; } return false; } /** * injectKeysFromBuffer() * * @this {Keyboard} */ injectKeysFromBuffer() { var charCode = 0; while (this.sInjectBuffer.length > 0 && !charCode) { var ch = this.sInjectBuffer.charAt(0); this.sInjectBuffer = this.sInjectBuffer.substr(1); charCode = ch.charCodeAt(0); /* * charCodes 0xF1-0xFF establish a new delay of 100-1500ms between keys; 0xF0 reverts to * the default delay. For example: * * \r\rb:\rrt\r\xff\xf0test;\r * * performs two return key presses, then "b:" followed by return, "rt" followed by return, * then a delay of 1500ms, then a reversion to the default delay (normally 150ms), followed * by "test;" and return. */ if (charCode >= 0xF0) { this.msInjectDelay = ((charCode - 0xF0) * 100) || this.msInjectDefault; charCode = 0; break; } } if (charCode) { /* * I could require all callers to supply CRs instead of LFs, but this is friendlier. */ if (charCode == 0x0A) charCode = 0x0D; this.addActiveKey(charCode, true); } if (!this.sInjectBuffer.length) { if (this.fnInjectReady) { this.fnInjectReady(); this.fnInjectReady = null; } } else { this.cpu.setTimer(this.timerInject, this.msInjectDelay); } } /** * waitReady(fnCallReady, sOption) * * @this {Keyboard} * @param {function()|null} fnCallReady * @param {string} [sOption] * @return {boolean} false if wait required, true otherwise */ waitReady(fnCallReady, sOption) { var fReady = false; switch(sOption) { case "DOS": if (this.fDOSReady) { fReady = true; } else { this.fnDOSReady = fnCallReady; } break; default: if (!this.sInjectBuffer.length) { fReady = true; } else { this.fnInjectReady = fnCallReady; } break; } return fReady; } /** * setLED(control, f) * * @this {Keyboard} * @param {HTMLElement} control is an HTML control DOM object * @param {boolean} f is true if the LED represented by control should be "on", false if "off" */ setLED(control, f) { /* * TODO: Add support for user-definable LED colors */ control.style.backgroundColor = (f? "#00ff00" : "#000000"); } /** * updateLEDs(bitState) * * Updates any and all shift-related LEDs with the corresponding state in bitsStateSim. * * @this {Keyboard} * @param {number} [bitState] is the bit in bitsStateSim that may have changed, if known; undefined if not */ updateLEDs(bitState) { var control; for (var sBinding in Keyboard.LEDSTATES) { var id = "led-" + sBinding; var bitLED = Keyboard.LEDSTATES[sBinding]; if ((!bitState || bitState == bitLED) && (control = this.bindings[id])) { this.setLED(control, !!(this.bitsStateSim & bitLED)); } } } /** * toggleCapsLock() * * @this {Keyboard} */ toggleCapsLock() { this.addActiveKey(Keyboard.SIMCODE.CAPS_LOCK, true); } /** * toggleNumLock() * * @this {Keyboard} */ toggleNumLock() { this.addActiveKey(Keyboard.SIMCODE.NUM_LOCK, true); } /** * toggleScrollLock() * * @this {Keyboard} */ toggleScrollLock() { this.addActiveKey(Keyboard.SIMCODE.SCROLL_LOCK, true); } /** * updateShiftState(simCode, fSim, fDown) * * For non-locking shift keys, this function is straightforward: when fDown is true, the corresponding bitState * is set, and when fDown is false, it's cleared. However, for LOCK keys, fDown true means toggle, and fDown false * means no change. * * @this {Keyboard} * @param {number} simCode (includes any ONDOWN and/or ONRIGHT modifiers) * @param {boolean} [fSim] is true to update simulated state only * @param {boolean|null} [fDown] is true for down, false for up, undefined for toggle * @return {boolean} true if simCode was a shift key, false if not */ updateShiftState(simCode, fSim, fDown) { if (Keyboard.SIMCODES[simCode]) { var fRight = (Math.floor(simCode / 1000) & 2); var bitState = Keyboard.KEYSTATES[simCode] || 0; if (bitState) { if (fRight && !(bitState & Keyboard.STATE.ALL_RIGHT)) { bitState >>= 1; } if (bitState & Keyboard.STATE.ALL_LOCKS) { if (fDown === false) return true; fDown = null; } if (fDown == null) { // ie, null or undefined fDown = !((fSim? this.bitsStateSim : this.bitsState) & bitState); } else if (!fDown) { /* * In current webkit browsers, pressing and then releasing both left and right shift keys together * (or both alt keys, or both cmd/windows keys, or presumably both ctrl keys) results in 4 events, as * you would expect, but 3 of the 4 are "down" events; only the last of the 4 is an "up" event. * * Perhaps this is a browser accessibility feature (ie, deliberately suppressing the "up" event * of one of the shift keys to implement a "sticky shift mode"?), but in any case, to maintain our * internal consistency, if this is an "up" event and the shift state bit is any of ALL_SHIFT, then * we set it to ALL_SHIFT, so that we'll automatically clear ALL shift states. * * TODO: The only downside to this work-around is that the simulation will still think a shift key is * down. So in effect, we have enabled a "sticky shift mode" inside the simulation, whether or not that * was the browser's intent. To fix that, we would have to identify the shift key that never went up * and simulate the "up". That's more work than I think the problem merits. The user just needs to tap * a single shift key to get out that mode. */ if (bitState & Keyboard.STATE.ALL_SHIFT) bitState = Keyboard.STATE.ALL_SHIFT; } if (!fSim) { this.bitsState &= ~bitState; if (fDown) this.bitsState |= bitState; } else { this.bitsStateSim &= ~bitState; if (fDown) this.bitsStateSim |= bitState; this.updateLEDs(bitState); } return true; } } return false; } /** * addActiveKey(simCode, fPress) * * @this {Keyboard} * @param {number} simCode * @param {boolean} [fPress] */ addActiveKey(simCode, fPress) { var wCode = Keyboard.SIMCODES[simCode] || Keyboard.SIMCODES[simCode += Keys.KEYCODE.ONDOWN]; if (!wCode) { if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("addActiveKey(" + simCode + "," + (fPress? "press" : "down") + "): unrecognized", true); } return; } /* * Ignore all active keys if the CPU is not running. */ if (!this.cpu || !this.cpu.isRunning()) return; /* * If this simCode is in the KEYSTATE table, then stop all repeating. */ if (Keyboard.KEYSTATES[simCode] && this.aKeysActive.length) { if (this.aKeysActive[0].nRepeat > 0) this.aKeysActive[0].nRepeat = 0; } var key; for (var i = 0; i < this.aKeysActive.length; i++) { key = this.aKeysActive[i]; if (key.simCode == simCode) { /* * This key is already active, so if this a "down" request (or a "press" for a key we already * processed as a "down"), ignore it. */ if (!fPress || key.nRepeat >= 0) { i = -1; break; } if (i > 0) { if (this.aKeysActive[0].nRepeat > 0) this.aKeysActive[0].nRepeat = 0; this.aKeysActive.splice(i, 1); } break; } } if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("addActiveKey(" + simCode + "," + (fPress? "press" : "down") + "): " + (i < 0? "already active" : (i == this.aKeysActive.length? "adding" : "updating")), true); } if (i < 0) return; if (i == this.aKeysActive.length) { key = {}; key.simCode = simCode; key.bitsState = this.bitsState; this.findBinding(simCode, "key", true); i++; } if (i > 0) { this.aKeysActive.splice(0, 0, key); } key.fDown = true; key.nRepeat = (fPress? -1: (Keyboard.KEYSTATES[simCode]? 0 : 1)); this.updateActiveKey(key); } /** * checkActiveKey() * * @this {Keyboard} * @return {number} simCode of active key, 0 if none */ checkActiveKey() { return this.aKeysActive.length? this.aKeysActive[0].simCode : 0; } /** * isAlphaKey(code) * * @this {Keyboard} * @param {number} code * @returns {boolean} true if alpha key, false if not */ isAlphaKey(code) { return (code >= Keys.ASCII.A && code <= Keys.ASCII.Z || code >= Keys.ASCII.a && code <= Keys.ASCII.z); } /** * toUpperKey(code) * * @this {Keyboard} * @param {number} code * @returns {number} */ toUpperKey(code) { if (code >= Keys.ASCII.a && code <= Keys.ASCII.z) { code -= (Keys.ASCII.a - Keys.ASCII.A); } return code; } /** * clearActiveKeys() * * Force all active keys to "self-deactivate". * * TODO: Consider limiting this to non-shift keys only. * * @this {Keyboard} */ clearActiveKeys() { for (var i = 0; i < this.aKeysActive.length; i++) { var key = this.aKeysActive[i]; key.fDown = false; if (key.nRepeat > 0) key.nRepeat = 0; } } /** * removeActiveKey(simCode, fFlush) * * @param {number} simCode * @param {boolean} [fFlush] is true whenever the key must be removed, independent of other factors * @return {boolean} true if successfully removed, false if not */ removeActiveKey(simCode, fFlush) { if (!Keyboard.SIMCODES[simCode]) { if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("removeActiveKey(" + simCode + "): unrecognized", true); } return false; } /* * Ignore all active keys if the CPU is not running. */ if (!fFlush && (!this.cpu || !this.cpu.isRunning())) return false; var fRemoved = false; for (var i = 0; i < this.aKeysActive.length; i++) { var key = this.aKeysActive[i]; if (key.simCode == simCode || key.simCode == Keys.SHIFTED_KEYCODES[simCode]) { this.aKeysActive.splice(i, 1); if (key.timer) clearTimeout(key.timer); if (key.fDown && !fFlush) this.keySimulate(key.simCode, false); this.findBinding(simCode, "key", false); fRemoved = true; break; } } if (!COMPILED && !fFlush && this.messageEnabled(Messages.KEYS)) { this.printMessage("removeActiveKey(" + simCode + "): " + (fRemoved? "removed" : "not active"), true); } if (!this.aKeysActive.length && this.fToggleCapsLock) { if (!COMPILED) this.printMessage("removeActiveKey(): inverting caps-lock now", Messages.KEYS); this.updateShiftState(Keyboard.SIMCODE.CAPS_LOCK); this.fToggleCapsLock = false; } return fRemoved; } /** * updateActiveKey(key, msTimer) * * @param {Object} key * @param {number} [msTimer] */ updateActiveKey(key, msTimer) { /* * All active keys are automatically removed once the CPU stops running. */ if (!this.cpu || !this.cpu.isRunning()) { this.removeActiveKey(key.simCode, true); return; } if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage((msTimer? '\n' : "") + "updateActiveKey(" + key.simCode + (msTimer? "," + msTimer + "ms" : "") + "): " + (key.fDown? "down" : "up"), true); } if (!this.keySimulate(key.simCode, key.fDown)) return; if (!key.nRepeat) return; var ms; if (key.nRepeat < 0) { if (!key.fDown) { this.removeActiveKey(key.simCode); return; } key.fDown = false; ms = this.msAutoRelease; } else { ms = (key.nRepeat++ == 1? this.msAutoRepeat : this.msNextRepeat); } key.timer = setTimeout(function(kbd) { return function onUpdateActiveKey() { kbd.updateActiveKey(key, ms); }; }(this), ms); } /** * getSimCode(keyCode) * * @this {Keyboard} * @param {number} keyCode * @param {boolean} fShifted * @return {number} simCode */ getSimCode(keyCode, fShifted) { var code; var simCode = keyCode; if (keyCode >= Keys.ASCII.A && keyCode <= Keys.ASCII.Z) { if (!(this.bitsState & (Keyboard.STATE.SHIFT | Keyboard.STATE.RSHIFT | Keyboard.STATE.CAPS_LOCK)) == fShifted) { simCode = keyCode + (Keys.ASCII.a - Keys.ASCII.A); } } else if (keyCode >= Keys.ASCII.a && keyCode <= Keys.ASCII.z) { if (!!(this.bitsState & (Keyboard.STATE.SHIFT | Keyboard.STATE.RSHIFT | Keyboard.STATE.CAPS_LOCK)) == fShifted) { simCode = keyCode - (Keys.ASCII.a - Keys.ASCII.A); } } else if (!!(this.bitsState & (Keyboard.STATE.SHIFT | Keyboard.STATE.RSHIFT)) == fShifted) { if (code = Keys.SHIFTED_KEYCODES[keyCode]) { simCode = code; } } else { if (code = Keys.NONASCII_KEYCODES[keyCode]) { simCode = code; } } return simCode; } /** * onFocusChange(fFocus) * * @this {Keyboard} * @param {boolean} fFocus is true if gaining focus, false if losing it */ onFocusChange(fFocus) { if (this.fHasFocus != fFocus && !COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("onFocusChange(" + (fFocus? "true" : "false") + ")", true); } this.fHasFocus = fFocus; /* * Since we can't be sure of any shift states after losing focus, we clear them all. */ if (!fFocus) this.bitsState &= ~Keyboard.STATE.ALL_SHIFT; } /** * onKeyDown(event, fDown) * * @this {Keyboard} * @param {Object} event * @param {boolean} fDown is true for a keyDown event, false for a keyUp event * @return {boolean} true to pass the event along, false to consume it */ onKeyDown(event, fDown) { var fPass = true; var fPress = false; var fIgnore = false; var keyCode = event.keyCode; if (!this.cmp.notifyKbdEvent(event, fDown)) { return false; } this.sInjectBuffer = ""; // actual key events should stop any injection in progress Component.processScript(this.idMachine); // and any script, too /* * Although it would be nice to pay attention ONLY to these "up" and "down" events, and ignore "press" * events, iOS devices force us to process "press" events, because they don't give us shift-key events, * so we have to infer the shift state from the character code in the "press" event. * * So, to seamlessly blend "up" and "down" events with "press" events, we must convert any keyCodes we * receive here to a compatibly shifted simCode. */ var simCode = this.getSimCode(keyCode, true); if (this.fEscapeDisabled && simCode == Keys.ASCII['`']) { keyCode = simCode = Keys.KEYCODE.ESC; } if (Keyboard.SIMCODES[keyCode + Keys.KEYCODE.ONDOWN]) { simCode += Keys.KEYCODE.ONDOWN; if (event.location == Keys.LOCATION.RIGHT) { simCode += Keys.KEYCODE.ONRIGHT; } if (this.updateShiftState(simCode, false, fDown)) { if (keyCode == Keys.KEYCODE.CAPS_LOCK || keyCode == Keys.KEYCODE.NUM_LOCK || keyCode == Keys.KEYCODE.SCROLL_LOCK) { /* * FYI, "lock" keys generate a "down" event ONLY when getting locked and an "up" event ONLY * when getting unlocked--which is a little odd, since the key did go UP and DOWN each time. * * We must treat each event like a "down", and also as a "press", so that addActiveKey() will * automatically generate both the "make" and "break". * * Of course, there have to be exceptions, most notably MSIE, which sends both "up" and down" * on every press, so there's no need for trickery. */ if (!this.fMSIE) { fDown = fPress = true; } } /* * As a safeguard, whenever the CMD key goes up, clear all active keys, because there appear to be * cases where we don't always get notification of a CMD key's companion key going up (this probably * overlaps with most if not all situations where we also lose focus). */ if (!fDown && (keyCode == Keys.KEYCODE.CMD || keyCode == Keys.KEYCODE.RCMD)) { this.clearActiveKeys(); } } else { /* * Here we have all the non-shift keys in the ONDOWN category; eg, BS, TAB, ESC, UP, DOWN, LEFT, RIGHT, * and many more. * * For various reasons (some of which are discussed below), we don't want to pass these on (ie, we want * to suppress their "press" event), which means we must perform all key simulation on the "up" and "down" * events. * * Regarding BS: I never want the browser to act on BS, since it does double-duty as the BACK button, * leaving the current page. * * Regarding TAB: If I don't consume TAB on the "down" event, then that's all I'll see, because the browser * act on it by giving focus to the next control. * * Regarding ESC: This key generates "down" and "up" events (LOTS of "down" events for that matter), but no * "press" event. */ /* * HACK for simulating CTRL_BREAK using CTRL_DEL (Mac) or CTRL_BS (Windows) */ if (keyCode == Keys.KEYCODE.BS && (this.bitsState & (Keyboard.STATE.CTRL|Keyboard.STATE.ALT)) == Keyboard.STATE.CTRL) { simCode = Keyboard.SIMCODE.CTRL_BREAK; } /* * There are a number of other common key sequences that interfere with our machines; for example, * the up/down arrows have a "default" behavior of scrolling the web page up and down, which is * definitely NOT a behavior we want. Since we mark those keys as ONDOWN, we'll catch them all here. */ fPass = false; } } else { /* * When I have defined system-wide CTRL-key sequences to perform common editing operations (eg, CTRL_W * and CTRL_Z to scroll pages of text), the browser likes to act on those operations, so let's set fPass * to false to prevent that. * * Also, we don't want to set fIgnore in such cases, because the browser may not give us a press event for * these CTRL-key sequences, so we can't risk ignoring them. */ if (Keyboard.SIMCODES[simCode] && (this.bitsState & (Keyboard.STATE.CTRLS | Keyboard.STATE.ALTS))) { fPass = false; } /* * Don't simulate any key not explicitly marked ONDOWN, as well as any key sequence with the CMD key held. */ if (!this.fAllDown && fPass && fDown || !!(this.bitsState & Keyboard.STATE.CMDS)) fIgnore = true; } if (!fPass) { event.preventDefault(); } if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("\nonKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): " + (fIgnore? "ignore" : (fPass? "true" : "false")), true); } /* * Mobile (eg, iOS) keyboards don't fully support onKeyDown/onKeyUp events; for example, they usually * don't generate ANY events when a shift key is pressed, and even for normal keys, they seem to generate * rapid (ie, fake) "up" and "down" events around "press" events, probably more to satisfy compatibility * issues rather than making a serious effort to indicate when a key ACTUALLY went down or up. */ if (!fIgnore && (!this.fMobile || !fPass)) { if (fDown) { this.addActiveKey(simCode, fPress); } else { if (!this.removeActiveKey(simCode)) { var code = this.getSimCode(keyCode, false); if (code != simCode) this.removeActiveKey(code); } } } return fPass; } /** * onKeyPress(event) * * @this {Keyboard} * @param {Object} event * @return {boolean} true to pass the event along, false to consume it */ onKeyPress(event) { event = event || window.event; var keyCode = event.which || event.keyCode; if (!this.cmp.notifyKbdEvent(event)) { return false; } this.sInjectBuffer = ""; // actual key events should stop any injection currently in progress if (this.fAllDown) { var simCode = this.checkActiveKey(); if (simCode && this.isAlphaKey(simCode) && this.isAlphaKey(keyCode) && simCode != keyCode) { if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("onKeyPress(" + keyCode + ") out of sync with " + simCode + ", invert caps-lock", true); } this.fToggleCapsLock = true; keyCode = simCode; } } var fPass = !Keyboard.SIMCODES[keyCode] || !!(this.bitsState & Keyboard.STATE.CMD); if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("\nonKeyPress(" + keyCode + "): " + (fPass? "true" : "false"), true); } if (!fPass) { this.addActiveKey(keyCode, true); } return fPass; } /** * keySimulate(simCode, fDown) * * @this {Keyboard} * @param {number} simCode * @param {boolean} fDown * @return {boolean} true if successfully simulated, false if unrecognized/unsupported key */ keySimulate(simCode, fDown) { var fSimulated = false; this.updateShiftState(simCode, true, fDown); var wCode = Keyboard.SIMCODES[simCode] || Keyboard.SIMCODES[simCode + Keys.KEYCODE.ONDOWN]; if (wCode !== undefined) { /* * Hack to transform the IBM "BACKSPACE" key (which we normally map to KEYCODE_DELETE) to the IBM "DEL" key * whenever both CTRL and ALT are pressed as well, so that it's easier to simulate that old favorite: CTRL_ALT_DEL */ if (wCode == Keyboard.SCANCODE.BS) { if ((this.bitsState & (Keyboard.STATE.CTRL | Keyboard.STATE.ALT)) == (Keyboard.STATE.CTRL | Keyboard.STATE.ALT)) { wCode = Keyboard.SCANCODE.NUM_DEL; } } var abScanCodes = []; var bCode = wCode & 0xff; /* * TODO: Update the following restrictions to address 84-key and 101-key keyboard limitations. */ if (bCode > 83 && this.modelKeys == 83) { return false; } abScanCodes.push(bCode | (fDown? 0 : Keyboard.SCANCODE.BREAK)); var fAlpha = (simCode >= Keys.ASCII.A && simCode <= Keys.ASCII.Z || simCode >= Keys.ASCII.a && simCode <= Keys.ASCII.z); while (wCode >>>= 8) { var bShift = 0; var bScan = wCode & 0xff; /* * TODO: The handling of SIMCODE entries with "extended" codes still needs to be tested, and * moreover, if any of them need to perform any shift-state modifications, those modifications * may need to be encoded differently. */ if (bCode == Keyboard.SCANCODE.EXTEND1 || bCode == Keyboard.SCANCODE.EXTEND2) { abScanCodes.push(bCode | (fDown? 0 : Keyboard.SCANCODE.BREAK)); continue; } if (bScan == Keyboard.SCANCODE.SHIFT) { if (!(this.bitsStateSim & (Keyboard.STATE.SHIFT | Keyboard.STATE.RSHIFT))) { if (!(this.bitsStateSim & Keyboard.STATE.CAPS_LOCK) || !fAlpha) { bShift = bScan; } } } else if (bScan == Keyboard.SCANCODE.CTRL) { if (!(this.bitsStateSim & (Keyboard.STATE.CTRL | Keyboard.STATE.RCTRL))) { bShift = bScan; } } else if (bScan == Keyboard.SCANCODE.ALT) { if (!(this.bitsStateSim & (Keyboard.STATE.ALT | Keyboard.STATE.RALT))) { bShift = bScan; } } else { abScanCodes.push(bCode | (fDown? 0 : Keyboard.SCANCODE.BREAK)); } if (bShift) { if (fDown) abScanCodes.unshift(bShift); else abScanCodes.push(bShift | Keyboard.SCANCODE.BREAK); } } for (var i = 0; i < abScanCodes.length; i++) { this.addScanCode(abScanCodes[i]); } fSimulated = true; } if (!COMPILED && this.messageEnabled(Messages.KEYS)) { this.printMessage("keySimulate(" + simCode + "," + (fDown? "down" : "up") + "): " + (fSimulated? "true" : "false"), true); } return fSimulated; } /** * checkActiveKeyShift() * * @this {Keyboard} * @return {number|null} bitsState for active key, null if none * checkActiveKeyShift() { return this.aKeysActive.length? this.aKeysActive[0].bitsState : null; } */ /** * Keyboard.init() * * This function operates on every HTML element of class "keyboard", extracting the * JSON-encoded parameters for the Keyboard constructor from the element's "data-value" * attribute, invoking the constructor to create a Keyboard component, and then binding * any associated HTML controls to the new component. */ static init() { var aeKbd = Component.getElementsByClass(document, PCX86.APPCLASS, "keyboard"); for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) { var eKbd = aeKbd[iKbd]; var parmsKbd = Component.getComponentParms(eKbd); var kbd = new Keyboard(parmsKbd); Component.bindComponentControls(kbd, eKbd, PCX86.APPCLASS); } } } /* * Supported keyboard models (the first entry is the default if the specified model isn't recognized) */ Keyboard.MODELS = ["US83", "US84", "US101"]; Keyboard.SIMCODE = { BS: Keys.KEYCODE.BS + Keys.KEYCODE.ONDOWN, TAB: Keys.KEYCODE.TAB + Keys.KEYCODE.ONDOWN, SHIFT: Keys.KEYCODE.SHIFT + Keys.KEYCODE.ONDOWN, RSHIFT: Keys.KEYCODE.SHIFT + Keys.KEYCODE.ONDOWN + Keys.KEYCODE.ONRIGHT, CTRL: Keys.KEYCODE.CTRL + Keys.KEYCODE.ONDOWN, ALT: Keys.KEYCODE.ALT + Keys.KEYCODE.ONDOWN, CAPS_LOCK: Keys.KEYCODE.CAPS_LOCK + Keys.KEYCODE.ONDOWN, ESC: Keys.KEYCODE.ESC + Keys.KEYCODE.ONDOWN, /* * It seems that a recent change to Safari on iOS (first noticed in iOS 9.1) treats SPACE * differently now, at least with regard to