Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / common / key_sequence.js
blob0a6f3d51c01ac88e0a711f3219f7b2f185f59373
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.
5 /**
6  * @fileoverview A JavaScript class that represents a sequence of keys entered
7  * by the user.
8  */
11 goog.provide('cvox.KeySequence');
13 goog.require('cvox.ChromeVox');
14 goog.require('cvox.PlatformFilter');
17 /**
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:
21  *
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.
27  *
28  * USER INPUT:
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
32  *   entered.
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.
44  * @constructor
45  */
46 cvox.KeySequence = function(
47     originalEvent, opt_cvoxModifier, opt_skipStripping, opt_doubleTap) {
48   /** @type {boolean} */
49   this.doubleTap = !!opt_doubleTap;
51   /** @type {cvox.PlatformFilter} */
52   this.platformFilter;
54   if (opt_cvoxModifier == undefined) {
55     this.cvoxModifier = this.isCVoxModifierActive(originalEvent);
56   } else {
57     this.cvoxModifier = opt_cvoxModifier;
58   }
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;
65   }
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.
72   /**
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.
77    *
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]}
83    *
84    * @type {Object}
85    */
86   this.keys = {
87     ctrlKey: [],
88     searchKeyHeld: [],
89     altKey: [],
90     altGraphKey: [],
91     shiftKey: [],
92     metaKey: [],
93     keyCode: []
94   };
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>}
104  */
105 cvox.KeySequence.KEY_PRESS_CODE = {
106   39: 222,
107   44: 188,
108   45: 189,
109   46: 190,
110   47: 191,
111   59: 186,
112   91: 219,
113   92: 220,
114   93: 221
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>}
122  */
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.
135  */
136 cvox.KeySequence.prototype.addKeyEvent = function(additionalKeyEvent) {
137   if (this.keys.keyCode.length > 1) {
138     return false;
139   }
140   this.extractKey_(additionalKeyEvent);
141   return true;
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.
158  */
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)) {
162     return false;
163   }
165   if (this.doubleTap != rhs.doubleTap) {
166     return false;
167   }
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) {
173     return true;
174   }
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
188  * from.
189  * @private
190  */
191 cvox.KeySequence.prototype.extractKey_ = function(keyEvent) {
192   for (var prop in this.keys) {
193     if (prop == 'keyCode') {
194       var 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]];
203       }
204       this.keys[prop].push(keyCode || keyEvent[prop]);
205     } else {
206       if (this.isKeyModifierActive(keyEvent, prop)) {
207         this.keys[prop].push(true);
208       } else {
209         this.keys[prop].push(false);
210       }
211     }
212   }
213   if (this.cvoxModifier) {
214     this.rationalizeKeys_();
215   }
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.
228  * @private
229  */
230 cvox.KeySequence.prototype.rationalizeKeys_ = function() {
231   if (this.skipStripping) {
232     return;
233   }
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;
243   }
244   if (modifierKeyCombo.indexOf('Alt') != -1) {
245     this.keys.altKey[index] = false;
246   }
247   if (modifierKeyCombo.indexOf('Shift') != -1) {
248     this.keys.shiftKey[index] = false;
249   }
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;
259     }
260   }
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.
268  * @private
269  */
270 cvox.KeySequence.prototype.getMetaKeyName_ = function() {
271   if (cvox.ChromeVox.isChromeOS) {
272     return 'Search';
273   } else if (cvox.ChromeVox.isMac) {
274     return 'Cmd';
275   } else {
276     return 'Win';
277   }
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
286  * the same.
287  * @private
288  */
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])
293         return false;
294     }
295   }
296   return true;
301  * Gets first key code
302  * @return {number} The first key code.
303  */
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.
312  */
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.
325  */
326 cvox.KeySequence.prototype.isModifierKey = function(keyCode) {
327   // Shift, Ctrl, Alt, Search/LWin
328   return keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 91 ||
329       keyCode == 93;
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
337  * keyEvent.
338  */
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';
348                             });
349   }
350   if (this.isKeyModifierActive(keyEvent, 'altKey')) {
351     modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
352                                                  return modifier != 'Alt';
353                                                });
354   }
355   if (this.isKeyModifierActive(keyEvent, 'shiftKey')) {
356     modifierKeyCombo = modifierKeyCombo.filter(function(modifier) {
357                                                  return modifier != 'Shift';
358                                                });
359   }
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;
365                                                });
366   }
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
377  * keyEvent.
378  */
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
383   switch (modifier) {
384     case 'ctrlKey':
385       return (keyEvent.ctrlKey || keyEvent.keyCode == 17);
386       break;
387     case 'altKey':
388       return (keyEvent.altKey || (keyEvent.keyCode == 18));
389       break;
390     case 'shiftKey':
391       return (keyEvent.shiftKey || (keyEvent.keyCode == 16));
392       break;
393     case 'metaKey':
394       return (keyEvent.metaKey || (keyEvent.keyCode == 91));
395       break;
396     case 'searchKeyHeld':
397       return ((cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91) ||
398           keyEvent['searchKeyHeld']);
399       break;
400   }
401   return false;
405  * Returns if any modifier is active in this sequence.
406  * @return {boolean} The result.
407  */
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') {
412         return true;
413       }
414     }
415   }
416   return false;
421  * Creates a KeySequence event from a generic object.
422  * @param {Object} sequenceObject The object.
423  * @return {cvox.KeySequence} The created KeySequence object.
424  */
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];
441     }
442   }
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);
450   }
452   if (sequenceObject.doubleTap) {
453     cvox.KeySequence.doubleTapCache.push(keySeq);
454   }
456   return 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.
466  */
467 cvox.KeySequence.fromStr = function(keyStr) {
468   var sequenceEvent = {};
469   var secondSequenceEvent = {};
471   var secondKeyPressed;
472   if (keyStr.indexOf('>') == -1) {
473     secondKeyPressed = false;
474   } else {
475     secondKeyPressed = true;
476   }
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);
488         if (j > 0) {
489           secondSequenceEvent['keyCode'] = keyCode;
490         } else {
491           sequenceEvent['keyCode'] = keyCode;
492         }
493       }
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
497         // modifiers.
498         if (j > 0) {
499           secondSequenceEvent['keyCode'] = seqs[j].charCodeAt(0);
500         } else {
501           sequenceEvent['keyCode'] = seqs[j].charCodeAt(0);
502         }
503       } else {
504         // Key is a modifier key
505         if (j > 0) {
506           cvox.KeySequence.setModifiersOnEvent_(keyName, secondSequenceEvent);
507           if (keyName == 'Cvox') {
508             cvoxPressed = true;
509           }
510         } else {
511           cvox.KeySequence.setModifiersOnEvent_(keyName, sequenceEvent);
512           if (keyName == 'Cvox') {
513             cvoxPressed = true;
514           }
515         }
516       }
517     }
518   }
519   var keySeq = new cvox.KeySequence(sequenceEvent, cvoxPressed);
520   if (secondKeyPressed) {
521     keySeq.addKeyEvent(secondSequenceEvent);
522   }
523   return keySeq;
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.
532  * @private
533  */
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;
555   }
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.
564  * @private
565  */
566 cvox.KeySequence.prototype.resolveChromeOSSpecialKeys_ =
567     function(originalEvent) {
568   if (!this.cvoxModifier || this.stickyMode || this.prefixKey ||
569       !cvox.ChromeVox.isChromeOS) {
570     return originalEvent;
571   }
572   var evt = {};
573   for (var key in originalEvent) {
574     evt[key] = originalEvent[key];
575   }
576   switch (evt['keyCode']) {
577     case 33:  // Page up.
578       evt['keyCode'] = 38;  // Up arrow.
579       break;
580     case 34:  // Page down.
581       evt['keyCode'] = 40;  // Down arrow.
582       break;
583     case 35:  // End.
584       evt['keyCode'] = 39;  // Right arrow.
585       break;
586     case 36:  // Home.
587       evt['keyCode'] = 37;  // Left arrow.
588       break;
589     case 45:  // Insert.
590       evt['keyCode'] = 190;  // Period.
591       break;
592     case 46:  // Delete.
593       evt['keyCode'] = 8;  // Backspace.
594       break;
595     case 112:  // F1.
596       evt['keyCode'] = 49;  // 1.
597       break;
598     case 113:  // F2.
599       evt['keyCode'] = 50;  // 2.
600       break;
601     case 114:  // F3.
602       evt['keyCode'] = 51;  // 3.
603       break;
604     case 115:  // F4.
605       evt['keyCode'] = 52;  // 4.
606       break;
607     case 116:  // F5.
608       evt['keyCode'] = 53;  // 5.
609       break;
610     case 117:  // F6.
611       evt['keyCode'] = 54;  // 6.
612       break;
613     case 118:  // F7.
614       evt['keyCode'] = 55;  // 7.
615       break;
616     case 119:  // F8.
617       evt['keyCode'] = 56;  // 8.
618       break;
619     case 120:  // F9.
620       evt['keyCode'] = 57;  // 9.
621       break;
622     case 121:  // F10.
623       evt['keyCode'] = 48;  // 0.
624       break;
625     case 122:  // F11
626       evt['keyCode'] = 189;  // Hyphen.
627       break;
628     case 123:  // F12
629       evt['keyCode'] = 187;  // Equals.
630       break;
631   }
632   return evt;