1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview A JavaScript class that represents a sequence of keys entered
11 goog.provide('cvox.KeySequence');
13 goog.require('cvox.ChromeVox');
14 goog.require('cvox.PlatformFilter');
18 * A class to represent a sequence of keys entered by a user or affiliated with
19 * a ChromeVox command.
20 * This class can represent the data from both types of key sequences:
22 * COMMAND KEYS SPECIFIED IN A KEYMAP:
23 * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc. Can
24 * specify one or both.
25 * - Modifiers (like ctrl, alt, meta, etc)
26 * - Whether or not the ChromeVox modifier key is required with the command.
29 * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc.
30 * - Modifiers (like ctlr, alt, meta, etc)
31 * - Whether or not the ChromeVox modifier key was active when the keys were
33 * - Whether or not a prefix key was entered before the discrete keys.
34 * - Whether sticky mode was active.
35 * @param {Event|Object} originalEvent The original key event entered by a user.
36 * The originalEvent may or may not have parameters stickyMode and keyPrefix
37 * specified. We will also accept an event-shaped object.
38 * @param {boolean=} opt_cvoxModifier Whether or not the ChromeVox modifier key
39 * is active. If not specified, we will try to determine whether the modifier
40 * was active by looking at the originalEvent.
41 * @param {boolean=} opt_skipStripping Skips stripping of ChromeVox modifiers
42 * from key events when the cvox modifiers are set. Defaults to false.
43 * @param {boolean=} opt_doubleTap Whether this is triggered via double tap.
46 cvox.KeySequence = function(
47 originalEvent, opt_cvoxModifier, opt_skipStripping, opt_doubleTap) {
48 /** @type {boolean} */
49 this.doubleTap = !!opt_doubleTap;
51 /** @type {cvox.PlatformFilter} */
54 if (opt_cvoxModifier == undefined) {
55 this.cvoxModifier = this.isCVoxModifierActive(originalEvent);
57 this.cvoxModifier = opt_cvoxModifier;
59 this.stickyMode = !!originalEvent['stickyMode'];
60 this.prefixKey = !!originalEvent['keyPrefix'];
61 this.skipStripping = !!opt_skipStripping;
63 if (this.stickyMode && this.prefixKey) {
64 throw 'Prefix key and sticky mode cannot both be enabled: ' + originalEvent;
67 var event = this.resolveChromeOSSpecialKeys_(originalEvent);
69 // TODO (rshearer): We should take the user out of sticky mode if they
70 // try to use the CVox modifier or prefix key.
73 * Stores the key codes and modifiers for the keys in the key sequence.
74 * TODO(rshearer): Consider making this structure an array of minimal
75 * keyEvent-like objects instead so we don't have to worry about what happens
76 * when ctrlKey.length is different from altKey.length.
78 * NOTE: If a modifier key is pressed by itself, we will store the keyCode
79 * *and* set the appropriate modKey to be true. This mirrors the way key
80 * events are created on Mac and Windows. For example, if the Meta key was
81 * pressed by itself, the keys object will have:
82 * {metaKey: [true], keyCode:[91]}
96 this.extractKey_(event);
100 // TODO(dtseng): This is incomplete; pull once we have appropriate libs.
102 * Maps a keypress keycode to a keydown or keyup keycode.
103 * @type {Object<number, number>}
105 cvox.KeySequence.KEY_PRESS_CODE = {
118 * A cache of all key sequences that have been set as double-tappable. We need
119 * this cache because repeated key down computations causes ChromeVox to become
120 * less responsive. This list is small so we currently use an array.
121 * @type {!Array<cvox.KeySequence>}
123 cvox.KeySequence.doubleTapCache = [];
127 * Adds an additional key onto the original sequence, for use when the user
128 * is entering two shortcut keys. This happens when the user presses a key,
129 * releases it, and then presses a second key. Those two keys together are
130 * considered part of the sequence.
131 * @param {Event|Object} additionalKeyEvent The additional key to be added to
132 * the original event. Should be an event or an event-shaped object.
133 * @return {boolean} Whether or not we were able to add a key. Returns false
134 * if there are already two keys attached to this event.
136 cvox.KeySequence.prototype.addKeyEvent = function(additionalKeyEvent) {
137 if (this.keys.keyCode.length > 1) {
140 this.extractKey_(additionalKeyEvent);
146 * Check for equality. Commands are matched based on the actual key codes
147 * involved and on whether or not they both require a ChromeVox modifier key.
149 * If sticky mode or a prefix is active on one of the commands but not on
150 * the other, then we try and match based on key code first.
151 * - If both commands have the same key code and neither of them have the
152 * ChromeVox modifier active then we have a match.
153 * - Next we try and match with the ChromeVox modifier. If both commands have
154 * the same key code, and one of them has the ChromeVox modifier and the other
155 * has sticky mode or an active prefix, then we also have a match.
156 * @param {!cvox.KeySequence} rhs The key sequence to compare against.
157 * @return {boolean} True if equal.
159 cvox.KeySequence.prototype.equals = function(rhs) {
160 // Check to make sure the same keys with the same modifiers were pressed.
161 if (!this.checkKeyEquality_(rhs)) {
165 if (this.doubleTap != rhs.doubleTap) {
169 // So now we know the actual keys are the same.
170 // If they both have the ChromeVox modifier, or they both don't have the
171 // ChromeVox modifier, then they are considered equal.
172 if (this.cvoxModifier === rhs.cvoxModifier) {
176 // So only one of them has the ChromeVox modifier. If the one that doesn't
177 // have the ChromeVox modifier has sticky mode or the prefix key then the
178 // keys are still considered equal.
179 var unmodified = this.cvoxModifier ? rhs : this;
180 return unmodified.stickyMode || unmodified.prefixKey;
185 * Utility method that extracts the key code and any modifiers from a given
186 * event and adds them to the object map.
187 * @param {Event|Object} keyEvent The keyEvent or event-shaped object to extract
191 cvox.KeySequence.prototype.extractKey_ = function(keyEvent) {
192 for (var prop in this.keys) {
193 if (prop == 'keyCode') {
195 // TODO (rshearer): This is temporary until we find a library that can
196 // convert between ASCII charcodes and keycodes.
197 if (keyEvent.type == 'keypress' && keyEvent[prop] >= 97 &&
198 keyEvent[prop] <= 122) {
199 // Alphabetic keypress. Convert to the upper case ASCII code.
200 keyCode = keyEvent[prop] - 32;
201 } else if (keyEvent.type == 'keypress') {
202 keyCode = cvox.KeySequence.KEY_PRESS_CODE[keyEvent[prop]];
204 this.keys[prop].push(keyCode || keyEvent[prop]);
206 if (this.isKeyModifierActive(keyEvent, prop)) {
207 this.keys[prop].push(true);
209 this.keys[prop].push(false);
213 if (this.cvoxModifier) {
214 this.rationalizeKeys_();
220 * Rationalizes the key codes and the ChromeVox modifier for this keySequence.
221 * This means we strip out the key codes and key modifiers stored for this
222 * KeySequence that are also present in the ChromeVox modifier. For example, if
223 * the ChromeVox modifier keys are Ctrl+Alt, and we've determined that the
224 * ChromeVox modifier is active (meaning the user has pressed Ctrl+Alt), we
225 * don't want this.keys.ctrlKey = true also because that implies that this
226 * KeySequence involves the ChromeVox modifier and the ctrl key being held down
227 * together, which doesn't make any sense.
230 cvox.KeySequence.prototype.rationalizeKeys_ = function() {
231 if (this.skipStripping) {
235 // TODO (rshearer): This is a hack. When the modifier key becomes customizable
236 // then we will not have to deal with strings here.
237 var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g);
239 var index = this.keys.keyCode.length - 1;
240 // For each modifier that is part of the CVox modifier, remove it from keys.
241 if (modifierKeyCombo.indexOf('Ctrl') != -1) {
242 this.keys.ctrlKey[index] = false;
244 if (modifierKeyCombo.indexOf('Alt') != -1) {
245 this.keys.altKey[index] = false;
247 if (modifierKeyCombo.indexOf('Shift') != -1) {
248 this.keys.shiftKey[index] = false;
250 var metaKeyName = this.getMetaKeyName_();
251 if (modifierKeyCombo.indexOf(metaKeyName) != -1) {
252 if (metaKeyName == 'Search') {
253 this.keys.searchKeyHeld[index] = false;
254 // TODO(dmazzoni): http://crbug.com/404763 Get rid of the code that
255 // tracks the search key and just use meta everywhere.
256 this.keys.metaKey[index] = false;
257 } else if (metaKeyName == 'Cmd' || metaKeyName == 'Win') {
258 this.keys.metaKey[index] = false;
265 * Get the user-facing name for the meta key (keyCode = 91), which varies
266 * depending on the platform.
267 * @return {string} The user-facing string name for the meta key.
270 cvox.KeySequence.prototype.getMetaKeyName_ = function() {
271 if (cvox.ChromeVox.isChromeOS) {
273 } else if (cvox.ChromeVox.isMac) {
282 * Utility method that checks for equality of the modifiers (like shift and alt)
283 * and the equality of key codes.
284 * @param {!cvox.KeySequence} rhs The key sequence to compare against.
285 * @return {boolean} True if the modifiers and key codes in the key sequence are
289 cvox.KeySequence.prototype.checkKeyEquality_ = function(rhs) {
290 for (var i in this.keys) {
291 for (var j = this.keys[i].length; j--;) {
292 if (this.keys[i][j] !== rhs.keys[i][j])
301 * Gets first key code
302 * @return {number} The first key code.
304 cvox.KeySequence.prototype.getFirstKeyCode = function() {
305 return this.keys.keyCode[0];
310 * Gets the number of keys in the sequence. Should be 1 or 2.
311 * @return {number} The number of keys in the sequence.
313 cvox.KeySequence.prototype.length = function() {
314 return this.keys.keyCode.length;
320 * Checks if the specified key code represents a modifier key, i.e. Ctrl, Alt,
321 * Shift, Search (on ChromeOS) or Meta.
323 * @param {number} keyCode key code.
324 * @return {boolean} true if it is a modifier keycode, false otherwise.
326 cvox.KeySequence.prototype.isModifierKey = function(keyCode) {
327 // Shift, Ctrl, Alt, Search/LWin
328 return keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 91 ||
334 * Determines whether the Cvox modifier key is active during the keyEvent.
335 * @param {Event|Object} keyEvent The keyEvent or event-shaped object to check.
336 * @return {boolean} Whether or not the modifier key was active during the
339 cvox.KeySequence.prototype.isCVoxModifierActive = function(keyEvent) {
340 // TODO (rshearer): Update this when the modifier key becomes customizable
341 var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g);
343 // For each modifier that is held down, remove it from the combo.
344 // If the combo string becomes empty, then the user has activated the combo.
345 if (this.isKeyModifierActive(keyEvent, 'ctrlKey')) {
346 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
347 return modifier != 'Ctrl';
350 if (this.isKeyModifierActive(keyEvent, 'altKey')) {
351 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
352 return modifier != 'Alt';
355 if (this.isKeyModifierActive(keyEvent, 'shiftKey')) {
356 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
357 return modifier != 'Shift';
360 if (this.isKeyModifierActive(keyEvent, 'metaKey') ||
361 this.isKeyModifierActive(keyEvent, 'searchKeyHeld')) {
362 var metaKeyName = this.getMetaKeyName_();
363 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
364 return modifier != metaKeyName;
367 return (modifierKeyCombo.length == 0);
372 * Determines whether a particular key modifier (for example, ctrl or alt) is
373 * active during the keyEvent.
374 * @param {Event|Object} keyEvent The keyEvent or Event-shaped object to check.
375 * @param {string} modifier The modifier to check.
376 * @return {boolean} Whether or not the modifier key was active during the
379 cvox.KeySequence.prototype.isKeyModifierActive = function(keyEvent, modifier) {
380 // We need to check the key event modifier and the keyCode because Linux will
381 // not set the keyEvent.modKey property if it is the modKey by itself.
382 // This bug filed as crbug.com/74044
385 return (keyEvent.ctrlKey || keyEvent.keyCode == 17);
388 return (keyEvent.altKey || (keyEvent.keyCode == 18));
391 return (keyEvent.shiftKey || (keyEvent.keyCode == 16));
394 return (keyEvent.metaKey || (keyEvent.keyCode == 91));
396 case 'searchKeyHeld':
397 return ((cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91) ||
398 keyEvent['searchKeyHeld']);
405 * Returns if any modifier is active in this sequence.
406 * @return {boolean} The result.
408 cvox.KeySequence.prototype.isAnyModifierActive = function() {
409 for (var modifierType in this.keys) {
410 for (var i = 0; i < this.length(); i++) {
411 if (this.keys[modifierType][i] && modifierType != 'keyCode') {
421 * Creates a KeySequence event from a generic object.
422 * @param {Object} sequenceObject The object.
423 * @return {cvox.KeySequence} The created KeySequence object.
425 cvox.KeySequence.deserialize = function(sequenceObject) {
426 var firstSequenceEvent = {};
428 firstSequenceEvent['stickyMode'] = (sequenceObject.stickyMode == undefined) ?
429 false : sequenceObject.stickyMode;
430 firstSequenceEvent['prefixKey'] = (sequenceObject.prefixKey == undefined) ?
431 false : sequenceObject.prefixKey;
434 var secondKeyPressed = sequenceObject.keys.keyCode.length > 1;
435 var secondSequenceEvent = {};
437 for (var keyPressed in sequenceObject.keys) {
438 firstSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][0];
439 if (secondKeyPressed) {
440 secondSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][1];
444 var keySeq = new cvox.KeySequence(firstSequenceEvent,
445 sequenceObject.cvoxModifier, true, sequenceObject.doubleTap);
446 if (secondKeyPressed) {
447 cvox.ChromeVox.sequenceSwitchKeyCodes.push(
448 new cvox.KeySequence(firstSequenceEvent, sequenceObject.cvoxModifier));
449 keySeq.addKeyEvent(secondSequenceEvent);
452 if (sequenceObject.doubleTap) {
453 cvox.KeySequence.doubleTapCache.push(keySeq);
461 * Creates a KeySequence event from a given string. The string should be in the
462 * standard key sequence format described in keyUtil.keySequenceToString and
463 * used in the key map JSON files.
464 * @param {string} keyStr The string representation of a key sequence.
465 * @return {!cvox.KeySequence} The created KeySequence object.
467 cvox.KeySequence.fromStr = function(keyStr) {
468 var sequenceEvent = {};
469 var secondSequenceEvent = {};
471 var secondKeyPressed;
472 if (keyStr.indexOf('>') == -1) {
473 secondKeyPressed = false;
475 secondKeyPressed = true;
478 var cvoxPressed = false;
479 sequenceEvent['stickyMode'] = false;
480 sequenceEvent['prefixKey'] = false;
482 var tokens = keyStr.split('+');
483 for (var i = 0; i < tokens.length; i++) {
484 var seqs = tokens[i].split('>');
485 for (var j = 0; j < seqs.length; j++) {
486 if (seqs[j].charAt(0) == '#') {
487 var keyCode = parseInt(seqs[j].substr(1), 10);
489 secondSequenceEvent['keyCode'] = keyCode;
491 sequenceEvent['keyCode'] = keyCode;
494 var keyName = seqs[j];
495 if (seqs[j].length == 1) {
496 // Key is A/B/C...1/2/3 and we don't need to worry about setting
499 secondSequenceEvent['keyCode'] = seqs[j].charCodeAt(0);
501 sequenceEvent['keyCode'] = seqs[j].charCodeAt(0);
504 // Key is a modifier key
506 cvox.KeySequence.setModifiersOnEvent_(keyName, secondSequenceEvent);
507 if (keyName == 'Cvox') {
511 cvox.KeySequence.setModifiersOnEvent_(keyName, sequenceEvent);
512 if (keyName == 'Cvox') {
519 var keySeq = new cvox.KeySequence(sequenceEvent, cvoxPressed);
520 if (secondKeyPressed) {
521 keySeq.addKeyEvent(secondSequenceEvent);
528 * Utility method for populating the modifiers on an event object that will be
529 * used to create a KeySequence.
530 * @param {string} keyName A particular modifier key name (such as 'Ctrl').
531 * @param {Object} seqEvent The event to populate.
534 cvox.KeySequence.setModifiersOnEvent_ = function(keyName, seqEvent) {
535 if (keyName == 'Ctrl') {
536 seqEvent['ctrlKey'] = true;
537 seqEvent['keyCode'] = 17;
538 } else if (keyName == 'Alt') {
539 seqEvent['altKey'] = true;
540 seqEvent['keyCode'] = 18;
541 } else if (keyName == 'Shift') {
542 seqEvent['shiftKey'] = true;
543 seqEvent['keyCode'] = 16;
544 } else if (keyName == 'Search') {
545 seqEvent['searchKeyHeld'] = true;
546 seqEvent['keyCode'] = 91;
547 } else if (keyName == 'Cmd') {
548 seqEvent['metaKey'] = true;
549 seqEvent['keyCode'] = 91;
550 } else if (keyName == 'Win') {
551 seqEvent['metaKey'] = true;
552 seqEvent['keyCode'] = 91;
553 } else if (keyName == 'Insert') {
554 seqEvent['keyCode'] = 45;
560 * Used to resolve special ChromeOS keys (see link for more detail).
561 * http://crbug.com/162268
562 * @param {Object} originalEvent The event.
563 * @return {Object} The resolved event.
566 cvox.KeySequence.prototype.resolveChromeOSSpecialKeys_ =
567 function(originalEvent) {
568 if (!this.cvoxModifier || this.stickyMode || this.prefixKey ||
569 !cvox.ChromeVox.isChromeOS) {
570 return originalEvent;
573 for (var key in originalEvent) {
574 evt[key] = originalEvent[key];
576 switch (evt['keyCode']) {
578 evt['keyCode'] = 38; // Up arrow.
580 case 34: // Page down.
581 evt['keyCode'] = 40; // Down arrow.
584 evt['keyCode'] = 39; // Right arrow.
587 evt['keyCode'] = 37; // Left arrow.
590 evt['keyCode'] = 190; // Period.
593 evt['keyCode'] = 8; // Backspace.
596 evt['keyCode'] = 49; // 1.
599 evt['keyCode'] = 50; // 2.
602 evt['keyCode'] = 51; // 3.
605 evt['keyCode'] = 52; // 4.
608 evt['keyCode'] = 53; // 5.
611 evt['keyCode'] = 54; // 6.
614 evt['keyCode'] = 55; // 7.
617 evt['keyCode'] = 56; // 8.
620 evt['keyCode'] = 57; // 9.
623 evt['keyCode'] = 48; // 0.
626 evt['keyCode'] = 189; // Hyphen.
629 evt['keyCode'] = 187; // Equals.