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 collection of JavaScript utilities used to simplify working
7 * with keyboard events.
11 goog.provide('cvox.KeyUtil');
13 goog.require('cvox.ChromeVox');
14 goog.require('cvox.KeySequence');
18 * Create the namespace
21 cvox.KeyUtil = function() {
25 * The time in ms at which the ChromeVox Sticky Mode key was pressed.
28 cvox.KeyUtil.modeKeyPressTime = 0;
31 * Indicates if sequencing is currently active for building a keyboard shortcut.
34 cvox.KeyUtil.sequencing = false;
37 * The previous KeySequence when sequencing is ON.
38 * @type {cvox.KeySequence}
40 cvox.KeyUtil.prevKeySequence = null;
44 * The sticky key sequence.
45 * @type {cvox.KeySequence}
47 cvox.KeyUtil.stickyKeySequence = null;
50 * Maximum number of key codes the sequence buffer may hold. This is the max
51 * length of a sequential keyboard shortcut, i.e. the number of key that can be
52 * pressed one after the other while modifier keys (Cros+Shift) are held down.
56 cvox.KeyUtil.maxSeqLength = 2;
60 * Convert a key event into a Key Sequence representation.
62 * @param {Event} keyEvent The keyEvent to convert.
63 * @return {cvox.KeySequence} A key sequence representation of the key event.
65 cvox.KeyUtil.keyEventToKeySequence = function(keyEvent) {
66 var util = cvox.KeyUtil;
67 if (util.prevKeySequence &&
68 (util.maxSeqLength == util.prevKeySequence.length())) {
69 // Reset the sequence buffer if max sequence length is reached.
70 util.sequencing = false;
71 util.prevKeySequence = null;
73 // Either we are in the middle of a key sequence (N > H), or the key prefix
74 // was pressed before (Ctrl+Z), or sticky mode is enabled
75 var keyIsPrefixed = util.sequencing || keyEvent['keyPrefix'] ||
76 keyEvent['stickyMode'];
78 // Create key sequence.
79 var keySequence = new cvox.KeySequence(keyEvent);
81 // Check if the Cvox key should be considered as pressed because the
82 // modifier key combination is active.
83 var keyWasCvox = keySequence.cvoxModifier;
85 if (keyIsPrefixed || keyWasCvox) {
86 if (!util.sequencing && util.isSequenceSwitchKeyCode(keySequence)) {
87 // If this is the beginning of a sequence.
88 util.sequencing = true;
89 util.prevKeySequence = keySequence;
91 } else if (util.sequencing) {
92 if (util.prevKeySequence.addKeyEvent(keyEvent)) {
93 keySequence = util.prevKeySequence;
94 util.prevKeySequence = null;
95 util.sequencing = false;
98 throw 'Think sequencing is enabled, yet util.prevKeySequence already' +
99 'has two key codes' + util.prevKeySequence;
103 util.sequencing = false;
106 // Repeated keys pressed.
107 var currTime = new Date().getTime();
108 if (cvox.KeyUtil.isDoubleTapKey(keySequence) &&
109 util.prevKeySequence &&
110 keySequence.equals(util.prevKeySequence)) {
111 var prevTime = util.modeKeyPressTime;
112 if (prevTime > 0 && currTime - prevTime < 300) { // Double tap
113 keySequence = util.prevKeySequence;
114 keySequence.doubleTap = true;
115 util.prevKeySequence = null;
116 util.sequencing = false;
117 // Resets the search key state tracked for ChromeOS because in OOBE,
118 // we never get a key up for the key down (keyCode 91).
119 if (cvox.ChromeVox.isChromeOS &&
120 keyEvent.keyCode == cvox.KeyUtil.getStickyKeyCode()) {
121 cvox.ChromeVox.searchKeyHeld = false;
125 // The user double tapped the sticky key but didn't do it within the
126 // required time. It's possible they will try again, so keep track of the
127 // time the sticky key was pressed and keep track of the corresponding
130 util.prevKeySequence = keySequence;
131 util.modeKeyPressTime = currTime;
136 * Returns the string representation of the specified key code.
138 * @param {number} keyCode key code.
139 * @return {string} A string representation of the key event.
141 cvox.KeyUtil.keyCodeToString = function(keyCode) {
151 if ((keyCode == 91) || (keyCode == 93)) {
152 if (cvox.ChromeVox.isChromeOS) {
154 } else if (cvox.ChromeVox.isMac) {
160 // TODO(rshearer): This is a hack to work around the special casing of the
161 // sticky mode string that used to happen in keyEventToString. We won't need
162 // it once we move away from strings completely.
166 if (keyCode >= 65 && keyCode <= 90) {
168 return String.fromCharCode(keyCode);
169 } else if (keyCode >= 48 && keyCode <= 57) {
171 return String.fromCharCode(keyCode);
174 return '#' + keyCode;
179 * Returns the keycode of a string representation of the specified modifier.
181 * @param {string} keyString Modifier key.
182 * @return {number} Key code.
184 cvox.KeyUtil.modStringToKeyCode = function(keyString) {
200 * Returns the key codes of a string respresentation of the ChromeVox modifiers.
202 * @return {Array.<number>} Array of key codes.
204 cvox.KeyUtil.cvoxModKeyCodes = function() {
205 var modKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g);
206 var modKeyCodes = modKeyCombo.map(function(keyString) {
207 return cvox.KeyUtil.modStringToKeyCode(keyString);
213 * Checks if the specified key code is a key used for switching into a sequence
214 * mode. Sequence switch keys are specified in
215 * cvox.KeyUtil.sequenceSwitchKeyCodes
217 * @param {!cvox.KeySequence} rhKeySeq The key sequence to check.
218 * @return {boolean} true if it is a sequence switch keycode, false otherwise.
220 cvox.KeyUtil.isSequenceSwitchKeyCode = function(rhKeySeq) {
221 for (var i = 0; i < cvox.ChromeVox.sequenceSwitchKeyCodes.length; i++) {
222 var lhKeySeq = cvox.ChromeVox.sequenceSwitchKeyCodes[i];
223 if (lhKeySeq.equals(rhKeySeq)) {
232 * Get readable string description of the specified keycode.
234 * @param {number} keyCode The key code.
235 * @return {string} Returns a string description.
237 cvox.KeyUtil.getReadableNameForKeyCode = function(keyCode) {
239 return 'Power button';
240 } else if (keyCode == 17) {
242 } else if (keyCode == 18) {
244 } else if (keyCode == 16) {
246 } else if (keyCode == 9) {
248 } else if ((keyCode == 91) || (keyCode == 93)) {
249 if (cvox.ChromeVox.isChromeOS) {
251 } else if (cvox.ChromeVox.isMac) {
256 } else if (keyCode == 8) {
258 } else if (keyCode == 32) {
260 } else if (keyCode == 35) {
262 } else if (keyCode == 36) {
264 } else if (keyCode == 37) {
266 } else if (keyCode == 38) {
268 } else if (keyCode == 39) {
269 return 'Right arrow';
270 } else if (keyCode == 40) {
272 } else if (keyCode == 45) {
274 } else if (keyCode == 13) {
276 } else if (keyCode == 27) {
278 } else if (keyCode == 112) {
279 return cvox.ChromeVox.isChromeOS ? 'Back' : 'F1';
280 } else if (keyCode == 113) {
281 return cvox.ChromeVox.isChromeOS ? 'Forward' : 'F2';
282 } else if (keyCode == 114) {
283 return cvox.ChromeVox.isChromeOS ? 'Refresh' : 'F3';
284 } else if (keyCode == 115) {
285 return cvox.ChromeVox.isChromeOS ? 'Toggle full screen' : 'F4';
286 } else if (keyCode == 116) {
288 } else if (keyCode == 117) {
290 } else if (keyCode == 118) {
292 } else if (keyCode == 119) {
294 } else if (keyCode == 120) {
296 } else if (keyCode == 121) {
298 } else if (keyCode == 122) {
300 } else if (keyCode == 123) {
302 } else if (keyCode == 186) {
304 } else if (keyCode == 187) {
306 } else if (keyCode == 188) {
308 } else if (keyCode == 189) {
310 } else if (keyCode == 190) {
312 } else if (keyCode == 191) {
313 return 'Forward slash';
314 } else if (keyCode == 192) {
315 return 'Grave accent';
316 } else if (keyCode == 219) {
317 return 'Open bracket';
318 } else if (keyCode == 220) {
320 } else if (keyCode == 221) {
321 return 'Close bracket';
322 } else if (keyCode == 222) {
323 return 'Single quote';
324 } else if (keyCode == 115) {
325 return 'Toggle full screen';
326 } else if (keyCode >= 48 && keyCode <= 90) {
327 return String.fromCharCode(keyCode);
332 * Get the platform specific sticky key keycode.
334 * @return {number} The platform specific sticky key keycode.
336 cvox.KeyUtil.getStickyKeyCode = function() {
337 // TODO (rshearer): This should not be hard-coded here.
338 var stickyKeyCode = 45; // Insert for Linux and Windows
339 if (cvox.ChromeVox.isChromeOS || cvox.ChromeVox.isMac) {
340 stickyKeyCode = 91; // GUI key (Search/Cmd) for ChromeOs and Mac
342 return stickyKeyCode;
347 * Get readable string description for an internal string representation of a
348 * key or a keyboard shortcut.
350 * @param {string} keyStr The internal string repsentation of a key or
351 * a keyboard shortcut.
352 * @return {?string} Readable string representation of the input.
354 cvox.KeyUtil.getReadableNameForStr = function(keyStr) {
355 // TODO (clchen): Refactor this function away since it is no longer used.
361 * Creates a string representation of a KeySequence.
362 * A KeySequence with a keyCode of 76 ('L') and the control and alt keys down
363 * would return the string 'Ctrl+Alt+L', for example. A key code that doesn't
364 * correspond to a letter or number will typically return a string with a
365 * pound and then its keyCode, like '#39' for Right Arrow. However,
366 * if the opt_readableKeyCode option is specified, the key code will return a
367 * readable string description like 'Right Arrow' instead of '#39'.
369 * The modifiers always come in this order:
376 * @param {cvox.KeySequence} keySequence The KeySequence object.
377 * @param {boolean=} opt_readableKeyCode Whether or not to return a readable
378 * string description instead of a string with a pound symbol and a keycode.
380 * @param {boolean=} opt_modifiers Restrict printout to only modifiers. Defaults
382 * @return {string} Readable string representation of the KeySequence object.
384 cvox.KeyUtil.keySequenceToString = function(
385 keySequence, opt_readableKeyCode, opt_modifiers) {
386 // TODO(rshearer): Move this method and the getReadableNameForKeyCode and the
387 // method to KeySequence after we refactor isModifierActive (when the modifie
388 // key becomes customizable and isn't stored as a string). We can't do it
389 // earlier because isModifierActive uses KeyUtil.getReadableNameForKeyCode,
390 // and I don't want KeySequence to depend on KeyUtil.
393 var numKeys = keySequence.length();
395 for (var index = 0; index < numKeys; index++) {
396 if (str != '' && !opt_modifiers) {
398 } else if (str != '') {
402 // This iterates through the sequence. Either we're on the first key
403 // pressed or the second
405 for (var keyPressed in keySequence.keys) {
406 // This iterates through the actual key, taking into account any
408 if (!keySequence.keys[keyPressed][index]) {
412 switch (keyPressed) {
414 // TODO(rshearer): This is a hack to work around the special casing
415 // of the Ctrl key that used to happen in keyEventToString. We won't
416 // need it once we move away from strings completely.
419 case 'searchKeyHeld':
420 var searchKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
421 modifier = searchKey;
427 modifier = 'AltGraph';
433 var metaKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
437 var keyCode = keySequence.keys[keyPressed][index];
438 // We make sure the keyCode isn't for a modifier key. If it is, then
439 // we've already added that into the string above.
440 if (!keySequence.isModifierKey(keyCode) && !opt_modifiers) {
441 if (opt_readableKeyCode) {
442 tempStr += cvox.KeyUtil.getReadableNameForKeyCode(keyCode);
444 tempStr += cvox.KeyUtil.keyCodeToString(keyCode);
448 if (str.indexOf(modifier) == -1) {
449 tempStr += modifier + '+';
455 if (str[str.length - 1] == '+') {
456 str = str.slice(0, -1);
460 if (keySequence.cvoxModifier || keySequence.prefixKey) {
466 } else if (keySequence.stickyMode) {
467 if (str[str.length - 1] == '>') {
468 str = str.slice(0, -1);
470 str = str + '+' + str;
476 * Looks up if the given key sequence is triggered via double tap.
477 * @param {cvox.KeySequence} key The key.
478 * @return {boolean} True if key is triggered via double tap.
480 cvox.KeyUtil.isDoubleTapKey = function(key) {
482 var originalState = key.doubleTap;
483 key.doubleTap = true;
484 for (var i = 0, keySeq; keySeq = cvox.KeySequence.doubleTapCache[i]; i++) {
485 if (keySeq.equals(key)) {
490 key.doubleTap = originalState;