| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview A JavaScript class that represents a sequence of keys entered |
| * by the user. |
| */ |
| |
| |
| goog.provide('cvox.KeySequence'); |
| |
| goog.require('cvox.ChromeVox'); |
| |
| |
| /** |
| * A class to represent a sequence of keys entered by a user or affiliated with |
| * a ChromeVox command. |
| * This class can represent the data from both types of key sequences: |
| * |
| * COMMAND KEYS SPECIFIED IN A KEYMAP: |
| * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc. Can |
| * specify one or both. |
| * - Modifiers (like ctrl, alt, meta, etc) |
| * - Whether or not the ChromeVox modifier key is required with the command. |
| * |
| * USER INPUT: |
| * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc. |
| * - Modifiers (like ctlr, alt, meta, etc) |
| * - Whether or not the ChromeVox modifier key was active when the keys were |
| * entered. |
| * - Whether or not a prefix key was entered before the discrete keys. |
| * - Whether sticky mode was active. |
| * @param {Event|Object} originalEvent The original key event entered by a user. |
| * The originalEvent may or may not have parameters stickyMode and keyPrefix |
| * specified. We will also accept an event-shaped object. |
| * @param {boolean=} opt_cvoxModifier Whether or not the ChromeVox modifier key |
| * is active. If not specified, we will try to determine whether the modifier |
| * was active by looking at the originalEvent. |
| * @param {boolean=} opt_skipStripping Skips stripping of ChromeVox modifiers |
| * from key events when the cvox modifiers are set. Defaults to false. |
| * @param {boolean=} opt_doubleTap Whether this is triggered via double tap. |
| * @constructor |
| */ |
| cvox.KeySequence = function( |
| originalEvent, opt_cvoxModifier, opt_skipStripping, opt_doubleTap) { |
| /** @type {boolean} */ |
| this.doubleTap = !!opt_doubleTap; |
| |
| if (opt_cvoxModifier == undefined) { |
| this.cvoxModifier = this.isCVoxModifierActive(originalEvent); |
| } else { |
| this.cvoxModifier = opt_cvoxModifier; |
| } |
| this.stickyMode = !!originalEvent['stickyMode']; |
| this.prefixKey = !!originalEvent['keyPrefix']; |
| this.skipStripping = !!opt_skipStripping; |
| |
| if (this.stickyMode && this.prefixKey) { |
| throw 'Prefix key and sticky mode cannot both be enabled: ' + originalEvent; |
| } |
| |
| var event = this.resolveChromeOSSpecialKeys_(originalEvent); |
| |
| // TODO (rshearer): We should take the user out of sticky mode if they |
| // try to use the CVox modifier or prefix key. |
| |
| /** |
| * Stores the key codes and modifiers for the keys in the key sequence. |
| * TODO(rshearer): Consider making this structure an array of minimal |
| * keyEvent-like objects instead so we don't have to worry about what happens |
| * when ctrlKey.length is different from altKey.length. |
| * |
| * NOTE: If a modifier key is pressed by itself, we will store the keyCode |
| * *and* set the appropriate modKey to be true. This mirrors the way key |
| * events are created on Mac and Windows. For example, if the Meta key was |
| * pressed by itself, the keys object will have: |
| * {metaKey: [true], keyCode:[91]} |
| * |
| * @type {Object} |
| */ |
| this.keys = { |
| ctrlKey: [], |
| searchKeyHeld: [], |
| altKey: [], |
| altGraphKey: [], |
| shiftKey: [], |
| metaKey: [], |
| keyCode: [] |
| }; |
| |
| this.extractKey_(event); |
| }; |
| |
| |
| // TODO(dtseng): This is incomplete; pull once we have appropriate libs. |
| /** |
| * Maps a keypress keycode to a keydown or keyup keycode. |
| * @type {Object.<number, number>} |
| */ |
| cvox.KeySequence.KEY_PRESS_CODE = { |
| 39: 222, |
| 44: 188, |
| 45: 189, |
| 46: 190, |
| 47: 191, |
| 59: 186, |
| 91: 219, |
| 92: 220, |
| 93: 221 |
| }; |
| |
| /** |
| * A cache of all key sequences that have been set as double-tappable. We need |
| * this cache because repeated key down computations causes ChromeVox to become |
| * less responsive. This list is small so we currently use an array. |
| * @type {!Array.<cvox.KeySequence>} |
| */ |
| cvox.KeySequence.doubleTapCache = []; |
| |
| |
| /** |
| * Adds an additional key onto the original sequence, for use when the user |
| * is entering two shortcut keys. This happens when the user presses a key, |
| * releases it, and then presses a second key. Those two keys together are |
| * considered part of the sequence. |
| * @param {Event|Object} additionalKeyEvent The additional key to be added to |
| * the original event. Should be an event or an event-shaped object. |
| * @return {boolean} Whether or not we were able to add a key. Returns false |
| * if there are already two keys attached to this event. |
| */ |
| cvox.KeySequence.prototype.addKeyEvent = function(additionalKeyEvent) { |
| if (this.keys.keyCode.length > 1) { |
| return false; |
| } |
| this.extractKey_(additionalKeyEvent); |
| return true; |
| }; |
| |
| |
| /** |
| * Check for equality. Commands are matched based on the actual key codes |
| * involved and on whether or not they both require a ChromeVox modifier key. |
| * |
| * If sticky mode or a prefix is active on one of the commands but not on |
| * the other, then we try and match based on key code first. |
| * - If both commands have the same key code and neither of them have the |
| * ChromeVox modifier active then we have a match. |
| * - Next we try and match with the ChromeVox modifier. If both commands have |
| * the same key code, and one of them has the ChromeVox modifier and the other |
| * has sticky mode or an active prefix, then we also have a match. |
| * @param {!cvox.KeySequence} rhs The key sequence to compare against. |
| * @return {boolean} True if equal. |
| */ |
| cvox.KeySequence.prototype.equals = function(rhs) { |
| // Check to make sure the same keys with the same modifiers were pressed. |
| if (!this.checkKeyEquality_(rhs)) { |
| return false; |
| } |
| |
| if (this.doubleTap != rhs.doubleTap) { |
| return false; |
| } |
| |
| // So now we know the actual keys are the same. |
| // If they both have the ChromeVox modifier, or they both don't have the |
| // ChromeVox modifier, then they are considered equal. |
| if (this.cvoxModifier === rhs.cvoxModifier) { |
| return true; |
| } |
| |
| // So only one of them has the ChromeVox modifier. If the one that doesn't |
| // have the ChromeVox modifier has sticky mode or the prefix key then the |
| // keys are still considered equal. |
| var unmodified = this.cvoxModifier ? rhs : this; |
| return unmodified.stickyMode || unmodified.prefixKey; |
| }; |
| |
| |
| /** |
| * Utility method that extracts the key code and any modifiers from a given |
| * event and adds them to the object map. |
| * @param {Event|Object} keyEvent The keyEvent or event-shaped object to extract |
| * from. |
| * @private |
| */ |
| cvox.KeySequence.prototype.extractKey_ = function(keyEvent) { |
| for (var prop in this.keys) { |
| if (prop == 'keyCode') { |
| var keyCode; |
| // TODO (rshearer): This is temporary until we find a library that can |
| // convert between ASCII charcodes and keycodes. |
| if (keyEvent.type == 'keypress' && keyEvent[prop] >= 97 && |
| keyEvent[prop] <= 122) { |
| // Alphabetic keypress. Convert to the upper case ASCII code. |
| keyCode = keyEvent[prop] - 32; |
| } else if (keyEvent.type == 'keypress') { |
| keyCode = cvox.KeySequence.KEY_PRESS_CODE[keyEvent[prop]]; |
| } |
| this.keys[prop].push(keyCode || keyEvent[prop]); |
| } else { |
| if (this.isKeyModifierActive(keyEvent, prop)) { |
| this.keys[prop].push(true); |
| } else { |
| this.keys[prop].push(false); |
| } |
| } |
| } |
| if (this.cvoxModifier) { |
| this.rationalizeKeys_(); |
| } |
| }; |
| |
| |
| /** |
| * Rationalizes the key codes and the ChromeVox modifier for this keySequence. |
| * This means we strip out the key codes and key modifiers stored for this |
| * KeySequence that are also present in the ChromeVox modifier. For example, if |
| * the ChromeVox modifier keys are Ctrl+Alt, and we've determined that the |
| * ChromeVox modifier is active (meaning the user has pressed Ctrl+Alt), we |
| * don't want this.keys.ctrlKey = true also because that implies that this |
| * KeySequence involves the ChromeVox modifier and the ctrl key being held down |
| * together, which doesn't make any sense. |
| * @private |
| */ |
| cvox.KeySequence.prototype.rationalizeKeys_ = function() { |
| if (this.skipStripping) { |
| return; |
| } |
| |
| // TODO (rshearer): This is a hack. When the modifier key becomes customizable |
| // then we will not have to deal with strings here. |
| var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g); |
| |
| var index = this.keys.keyCode.length - 1; |
| // For each modifier that is part of the CVox modifier, remove it from keys. |
| if (modifierKeyCombo.indexOf('Ctrl') != -1) { |
| this.keys.ctrlKey[index] = false; |
| } |
| if (modifierKeyCombo.indexOf('Alt') != -1) { |
| this.keys.altKey[index] = false; |
| } |
| if (modifierKeyCombo.indexOf('Shift') != -1) { |
| this.keys.shiftKey[index] = false; |
| } |
| var metaKeyName = this.getMetaKeyName_(); |
| if (modifierKeyCombo.indexOf(metaKeyName) != -1) { |
| if (metaKeyName == 'Search') { |
| this.keys.searchKeyHeld[index] = false; |
| // TODO(dmazzoni): http://crbug.com/404763 Get rid of the code that |
| // tracks the search key and just use meta everywhere. |
| this.keys.metaKey[index] = false; |
| } else if (metaKeyName == 'Cmd' || metaKeyName == 'Win') { |
| this.keys.metaKey[index] = false; |
| } |
| } |
| }; |
| |
| |
| /** |
| * Get the user-facing name for the meta key (keyCode = 91), which varies |
| * depending on the platform. |
| * @return {string} The user-facing string name for the meta key. |
| * @private |
| */ |
| cvox.KeySequence.prototype.getMetaKeyName_ = function() { |
| if (cvox.ChromeVox.isChromeOS) { |
| return 'Search'; |
| } else if (cvox.ChromeVox.isMac) { |
| return 'Cmd'; |
| } else { |
| return 'Win'; |
| } |
| }; |
| |
| |
| /** |
| * Utility method that checks for equality of the modifiers (like shift and alt) |
| * and the equality of key codes. |
| * @param {!cvox.KeySequence} rhs The key sequence to compare against. |
| * @return {boolean} True if the modifiers and key codes in the key sequence are |
| * the same. |
| * @private |
| */ |
| cvox.KeySequence.prototype.checkKeyEquality_ = function(rhs) { |
| for (var i in this.keys) { |
| for (var j = this.keys[i].length; j--;) { |
| if (this.keys[i][j] !== rhs.keys[i][j]) |
| return false; |
| } |
| } |
| return true; |
| }; |
| |
| |
| /** |
| * Gets first key code |
| * @return {number} The first key code. |
| */ |
| cvox.KeySequence.prototype.getFirstKeyCode = function() { |
| return this.keys.keyCode[0]; |
| }; |
| |
| |
| /** |
| * Gets the number of keys in the sequence. Should be 1 or 2. |
| * @return {number} The number of keys in the sequence. |
| */ |
| cvox.KeySequence.prototype.length = function() { |
| return this.keys.keyCode.length; |
| }; |
| |
| |
| |
| /** |
| * Checks if the specified key code represents a modifier key, i.e. Ctrl, Alt, |
| * Shift, Search (on ChromeOS) or Meta. |
| * |
| * @param {number} keyCode key code. |
| * @return {boolean} true if it is a modifier keycode, false otherwise. |
| */ |
| cvox.KeySequence.prototype.isModifierKey = function(keyCode) { |
| // Shift, Ctrl, Alt, Search/LWin |
| return keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 91 || |
| keyCode == 93; |
| }; |
| |
| |
| /** |
| * Determines whether the Cvox modifier key is active during the keyEvent. |
| * @param {Event|Object} keyEvent The keyEvent or event-shaped object to check. |
| * @return {boolean} Whether or not the modifier key was active during the |
| * keyEvent. |
| */ |
| cvox.KeySequence.prototype.isCVoxModifierActive = function(keyEvent) { |
| // TODO (rshearer): Update this when the modifier key becomes customizable |
| var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g); |
| |
| // For each modifier that is held down, remove it from the combo. |
| // If the combo string becomes empty, then the user has activated the combo. |
| if (this.isKeyModifierActive(keyEvent, 'ctrlKey')) { |
| modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { |
| return modifier != 'Ctrl'; |
| }); |
| } |
| if (this.isKeyModifierActive(keyEvent, 'altKey')) { |
| modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { |
| return modifier != 'Alt'; |
| }); |
| } |
| if (this.isKeyModifierActive(keyEvent, 'shiftKey')) { |
| modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { |
| return modifier != 'Shift'; |
| }); |
| } |
| if (this.isKeyModifierActive(keyEvent, 'metaKey') || |
| this.isKeyModifierActive(keyEvent, 'searchKeyHeld')) { |
| var metaKeyName = this.getMetaKeyName_(); |
| modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { |
| return modifier != metaKeyName; |
| }); |
| } |
| return (modifierKeyCombo.length == 0); |
| }; |
| |
| |
| /** |
| * Determines whether a particular key modifier (for example, ctrl or alt) is |
| * active during the keyEvent. |
| * @param {Event|Object} keyEvent The keyEvent or Event-shaped object to check. |
| * @param {string} modifier The modifier to check. |
| * @return {boolean} Whether or not the modifier key was active during the |
| * keyEvent. |
| */ |
| cvox.KeySequence.prototype.isKeyModifierActive = function(keyEvent, modifier) { |
| // We need to check the key event modifier and the keyCode because Linux will |
| // not set the keyEvent.modKey property if it is the modKey by itself. |
| // This bug filed as crbug.com/74044 |
| switch (modifier) { |
| case 'ctrlKey': |
| return (keyEvent.ctrlKey || keyEvent.keyCode == 17); |
| break; |
| case 'altKey': |
| return (keyEvent.altKey || (keyEvent.keyCode == 18)); |
| break; |
| case 'shiftKey': |
| return (keyEvent.shiftKey || (keyEvent.keyCode == 16)); |
| break; |
| case 'metaKey': |
| return (keyEvent.metaKey || |
| (!cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91)); |
| break; |
| case 'searchKeyHeld': |
| return ((cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91) || |
| keyEvent['searchKeyHeld']); |
| break; |
| } |
| return false; |
| }; |
| |
| /** |
| * Returns if any modifier is active in this sequence. |
| * @return {boolean} The result. |
| */ |
| cvox.KeySequence.prototype.isAnyModifierActive = function() { |
| for (var modifierType in this.keys) { |
| for (var i = 0; i < this.length(); i++) { |
| if (this.keys[modifierType][i] && modifierType != 'keyCode') { |
| return true; |
| } |
| } |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Creates a KeySequence event from a generic object. |
| * @param {Object} sequenceObject The object. |
| * @return {cvox.KeySequence} The created KeySequence object. |
| */ |
| cvox.KeySequence.deserialize = function(sequenceObject) { |
| var firstSequenceEvent = {}; |
| |
| firstSequenceEvent['stickyMode'] = (sequenceObject.stickyMode == undefined) ? |
| false : sequenceObject.stickyMode; |
| firstSequenceEvent['prefixKey'] = (sequenceObject.prefixKey == undefined) ? |
| false : sequenceObject.prefixKey; |
| |
| |
| var secondKeyPressed = sequenceObject.keys.keyCode.length > 1; |
| var secondSequenceEvent = {}; |
| |
| for (var keyPressed in sequenceObject.keys) { |
| firstSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][0]; |
| if (secondKeyPressed) { |
| secondSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][1]; |
| } |
| } |
| |
| var keySeq = new cvox.KeySequence(firstSequenceEvent, |
| sequenceObject.cvoxModifier, true, sequenceObject.doubleTap); |
| if (secondKeyPressed) { |
| cvox.ChromeVox.sequenceSwitchKeyCodes.push( |
| new cvox.KeySequence(firstSequenceEvent, sequenceObject.cvoxModifier)); |
| keySeq.addKeyEvent(secondSequenceEvent); |
| } |
| |
| if (sequenceObject.doubleTap) { |
| cvox.KeySequence.doubleTapCache.push(keySeq); |
| } |
| |
| return keySeq; |
| }; |
| |
| |
| /** |
| * Creates a KeySequence event from a given string. The string should be in the |
| * standard key sequence format described in keyUtil.keySequenceToString and |
| * used in the key map JSON files. |
| * @param {string} keyStr The string representation of a key sequence. |
| * @return {!cvox.KeySequence} The created KeySequence object. |
| */ |
| cvox.KeySequence.fromStr = function(keyStr) { |
| var sequenceEvent = {}; |
| var secondSequenceEvent = {}; |
| |
| var secondKeyPressed; |
| if (keyStr.indexOf('>') == -1) { |
| secondKeyPressed = false; |
| } else { |
| secondKeyPressed = true; |
| } |
| |
| var cvoxPressed = false; |
| sequenceEvent['stickyMode'] = false; |
| sequenceEvent['prefixKey'] = false; |
| |
| var tokens = keyStr.split('+'); |
| for (var i = 0; i < tokens.length; i++) { |
| var seqs = tokens[i].split('>'); |
| for (var j = 0; j < seqs.length; j++) { |
| if (seqs[j].charAt(0) == '#') { |
| var keyCode = parseInt(seqs[j].substr(1), 10); |
| if (j > 0) { |
| secondSequenceEvent['keyCode'] = keyCode; |
| } else { |
| sequenceEvent['keyCode'] = keyCode; |
| } |
| } |
| var keyName = seqs[j]; |
| if (seqs[j].length == 1) { |
| // Key is A/B/C...1/2/3 and we don't need to worry about setting |
| // modifiers. |
| if (j > 0) { |
| secondSequenceEvent['keyCode'] = seqs[j].charCodeAt(0); |
| } else { |
| sequenceEvent['keyCode'] = seqs[j].charCodeAt(0); |
| } |
| } else { |
| // Key is a modifier key |
| if (j > 0) { |
| cvox.KeySequence.setModifiersOnEvent_(keyName, secondSequenceEvent); |
| if (keyName == 'Cvox') { |
| cvoxPressed = true; |
| } |
| } else { |
| cvox.KeySequence.setModifiersOnEvent_(keyName, sequenceEvent); |
| if (keyName == 'Cvox') { |
| cvoxPressed = true; |
| } |
| } |
| } |
| } |
| } |
| var keySeq = new cvox.KeySequence(sequenceEvent, cvoxPressed); |
| if (secondKeyPressed) { |
| keySeq.addKeyEvent(secondSequenceEvent); |
| } |
| return keySeq; |
| }; |
| |
| |
| /** |
| * Utility method for populating the modifiers on an event object that will be |
| * used to create a KeySequence. |
| * @param {string} keyName A particular modifier key name (such as 'Ctrl'). |
| * @param {Object} seqEvent The event to populate. |
| * @private |
| */ |
| cvox.KeySequence.setModifiersOnEvent_ = function(keyName, seqEvent) { |
| if (keyName == 'Ctrl') { |
| seqEvent['ctrlKey'] = true; |
| seqEvent['keyCode'] = 17; |
| } else if (keyName == 'Alt') { |
| seqEvent['altKey'] = true; |
| seqEvent['keyCode'] = 18; |
| } else if (keyName == 'Shift') { |
| seqEvent['shiftKey'] = true; |
| seqEvent['keyCode'] = 16; |
| } else if (keyName == 'Search') { |
| seqEvent['searchKeyHeld'] = true; |
| seqEvent['keyCode'] = 91; |
| } else if (keyName == 'Cmd') { |
| seqEvent['metaKey'] = true; |
| seqEvent['keyCode'] = 91; |
| } else if (keyName == 'Win') { |
| seqEvent['metaKey'] = true; |
| seqEvent['keyCode'] = 91; |
| } else if (keyName == 'Insert') { |
| seqEvent['keyCode'] = 45; |
| } |
| }; |
| |
| |
| /** |
| * Used to resolve special ChromeOS keys (see link for more detail). |
| * http://crbug.com/162268 |
| * @param {Object} originalEvent The event. |
| * @return {Object} The resolved event. |
| * @private |
| */ |
| cvox.KeySequence.prototype.resolveChromeOSSpecialKeys_ = |
| function(originalEvent) { |
| if (!this.cvoxModifier || this.stickyMode || this.prefixKey || |
| !cvox.ChromeVox.isChromeOS) { |
| return originalEvent; |
| } |
| var evt = {}; |
| for (var key in originalEvent) { |
| evt[key] = originalEvent[key]; |
| } |
| switch (evt['keyCode']) { |
| case 33: // Page up. |
| evt['keyCode'] = 38; // Up arrow. |
| break; |
| case 34: // Page down. |
| evt['keyCode'] = 40; // Down arrow. |
| break; |
| case 35: // End. |
| evt['keyCode'] = 39; // Right arrow. |
| break; |
| case 36: // Home. |
| evt['keyCode'] = 37; // Left arrow. |
| break; |
| case 45: // Insert. |
| evt['keyCode'] = 190; // Period. |
| break; |
| case 46: // Delete. |
| evt['keyCode'] = 8; // Backspace. |
| break; |
| case 112: // F1. |
| evt['keyCode'] = 49; // 1. |
| break; |
| case 113: // F2. |
| evt['keyCode'] = 50; // 2. |
| break; |
| case 114: // F3. |
| evt['keyCode'] = 51; // 3. |
| break; |
| case 115: // F4. |
| evt['keyCode'] = 52; // 4. |
| break; |
| case 116: // F5. |
| evt['keyCode'] = 53; // 5. |
| break; |
| case 117: // F6. |
| evt['keyCode'] = 54; // 6. |
| break; |
| case 118: // F7. |
| evt['keyCode'] = 55; // 7. |
| break; |
| case 119: // F8. |
| evt['keyCode'] = 56; // 8. |
| break; |
| case 120: // F9. |
| evt['keyCode'] = 57; // 9. |
| break; |
| case 121: // F10. |
| evt['keyCode'] = 48; // 0. |
| break; |
| case 122: // F11 |
| evt['keyCode'] = 189; // Hyphen. |
| break; |
| case 123: // F12 |
| evt['keyCode'] = 187; // Equals. |
| break; |
| } |
| return evt; |
| }; |