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.