diff options
Diffstat (limited to 'chromium/chrome/browser/resources/chromeos/chromevox/common/key_sequence.js')
-rw-r--r-- | chromium/chrome/browser/resources/chromeos/chromevox/common/key_sequence.js | 627 |
1 files changed, 627 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/common/key_sequence.js b/chromium/chrome/browser/resources/chromeos/chromevox/common/key_sequence.js new file mode 100644 index 00000000000..f0d09239368 --- /dev/null +++ b/chromium/chrome/browser/resources/chromeos/chromevox/common/key_sequence.js @@ -0,0 +1,627 @@ +// 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; + } 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; +}; |