key_util.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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.
4
5/**
6 * @fileoverview A collection of JavaScript utilities used to simplify working
7 * with keyboard events.
8 */
9
10
11goog.provide('cvox.KeyUtil');
12
13goog.require('cvox.ChromeVox');
14goog.require('cvox.KeySequence');
15
16
17/**
18 * Create the namespace
19 * @constructor
20 */
21cvox.KeyUtil = function() {
22};
23
24/**
25 * The time in ms at which the ChromeVox Sticky Mode key was pressed.
26 * @type {number}
27 */
28cvox.KeyUtil.modeKeyPressTime = 0;
29
30/**
31 * Indicates if sequencing is currently active for building a keyboard shortcut.
32 * @type {boolean}
33 */
34cvox.KeyUtil.sequencing = false;
35
36/**
37 * The previous KeySequence when sequencing is ON.
38 * @type {cvox.KeySequence}
39 */
40cvox.KeyUtil.prevKeySequence = null;
41
42
43/**
44 * The sticky key sequence.
45 * @type {cvox.KeySequence}
46 */
47cvox.KeyUtil.stickyKeySequence = null;
48
49/**
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.
53 * @const
54 * @type {number}
55 */
56cvox.KeyUtil.maxSeqLength = 2;
57
58
59/**
60 * Convert a key event into a Key Sequence representation.
61 *
62 * @param {Event} keyEvent The keyEvent to convert.
63 * @return {cvox.KeySequence} A key sequence representation of the key event.
64 */
65cvox.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;
72  }
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'];
77
78  // Create key sequence.
79  var keySequence = new cvox.KeySequence(keyEvent);
80
81  // Check if the Cvox key should be considered as pressed because the
82  // modifier key combination is active.
83  var keyWasCvox = keySequence.cvoxModifier;
84
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;
90      return keySequence;
91    } else if (util.sequencing) {
92      if (util.prevKeySequence.addKeyEvent(keyEvent)) {
93        keySequence = util.prevKeySequence;
94        util.prevKeySequence = null;
95        util.sequencing = false;
96        return keySequence;
97      } else {
98        throw 'Think sequencing is enabled, yet util.prevKeySequence already' +
99            'has two key codes' + util.prevKeySequence;
100      }
101    }
102  } else {
103    util.sequencing = false;
104  }
105
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;
122      }
123      return keySequence;
124    }
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
128    // key sequence.
129  }
130  util.prevKeySequence = keySequence;
131  util.modeKeyPressTime = currTime;
132  return keySequence;
133};
134
135/**
136 * Returns the string representation of the specified key code.
137 *
138 * @param {number} keyCode key code.
139 * @return {string} A string representation of the key event.
140 */
141cvox.KeyUtil.keyCodeToString = function(keyCode) {
142  if (keyCode == 17) {
143    return 'Ctrl';
144  }
145  if (keyCode == 18) {
146    return 'Alt';
147  }
148  if (keyCode == 16) {
149    return 'Shift';
150  }
151  if ((keyCode == 91) || (keyCode == 93)) {
152    if (cvox.ChromeVox.isChromeOS) {
153      return 'Search';
154    } else if (cvox.ChromeVox.isMac) {
155      return 'Cmd';
156    } else {
157      return 'Win';
158    }
159  }
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.
163  if (keyCode == 45) {
164    return 'Insert';
165  }
166  if (keyCode >= 65 && keyCode <= 90) {
167    // A - Z
168    return String.fromCharCode(keyCode);
169  } else if (keyCode >= 48 && keyCode <= 57) {
170    // 0 - 9
171    return String.fromCharCode(keyCode);
172  } else {
173    // Anything else
174    return '#' + keyCode;
175  }
176};
177
178/**
179 * Returns the keycode of a string representation of the specified modifier.
180 *
181 * @param {string} keyString Modifier key.
182 * @return {number} Key code.
183 */
184cvox.KeyUtil.modStringToKeyCode = function(keyString) {
185  switch (keyString) {
186  case 'Ctrl':
187    return 17;
188  case 'Alt':
189    return 18;
190  case 'Shift':
191    return 16;
192  case 'Cmd':
193  case 'Win':
194    return 91;
195  }
196  return -1;
197};
198
199/**
200 * Returns the key codes of a string respresentation of the ChromeVox modifiers.
201 *
202 * @return {Array.<number>} Array of key codes.
203 */
204cvox.KeyUtil.cvoxModKeyCodes = function() {
205  var modKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g);
206  var modKeyCodes = modKeyCombo.map(function(keyString) {
207    return cvox.KeyUtil.modStringToKeyCode(keyString);
208  });
209  return modKeyCodes;
210};
211
212/**
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
216 *
217 * @param {!cvox.KeySequence} rhKeySeq The key sequence to check.
218 * @return {boolean} true if it is a sequence switch keycode, false otherwise.
219 */
220cvox.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)) {
224      return true;
225    }
226  }
227  return false;
228};
229
230
231/**
232 * Get readable string description of the specified keycode.
233 *
234 * @param {number} keyCode The key code.
235 * @return {string} Returns a string description.
236 */
237cvox.KeyUtil.getReadableNameForKeyCode = function(keyCode) {
238  if (keyCode == 0) {
239    return 'Power button';
240  } else if (keyCode == 17) {
241    return 'Control';
242  } else if (keyCode == 18) {
243    return 'Alt';
244  } else if (keyCode == 16) {
245    return 'Shift';
246  } else if (keyCode == 9) {
247    return 'Tab';
248  } else if ((keyCode == 91) || (keyCode == 93)) {
249    if (cvox.ChromeVox.isChromeOS) {
250      return 'Search';
251    } else if (cvox.ChromeVox.isMac) {
252      return 'Cmd';
253    } else {
254      return 'Win';
255    }
256  } else if (keyCode == 8) {
257    return 'Backspace';
258  } else if (keyCode == 32) {
259    return 'Space';
260  } else if (keyCode == 35) {
261    return'end';
262  } else if (keyCode == 36) {
263    return 'home';
264  } else if (keyCode == 37) {
265    return 'Left arrow';
266  } else if (keyCode == 38) {
267    return 'Up arrow';
268  } else if (keyCode == 39) {
269    return 'Right arrow';
270  } else if (keyCode == 40) {
271    return 'Down arrow';
272  } else if (keyCode == 45) {
273    return 'Insert';
274  } else if (keyCode == 13) {
275    return 'Enter';
276  } else if (keyCode == 27) {
277    return 'Escape';
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) {
287    return 'F5';
288  } else if (keyCode == 117) {
289    return 'F6';
290  } else if (keyCode == 118) {
291    return 'F7';
292  } else if (keyCode == 119) {
293    return 'F8';
294  } else if (keyCode == 120) {
295    return 'F9';
296  } else if (keyCode == 121) {
297    return 'F10';
298  } else if (keyCode == 122) {
299    return 'F11';
300  } else if (keyCode == 123) {
301    return 'F12';
302  } else if (keyCode == 186) {
303    return 'Semicolon';
304  } else if (keyCode == 187) {
305    return 'Equal sign';
306  } else if (keyCode == 188) {
307    return 'Comma';
308  } else if (keyCode == 189) {
309    return 'Dash';
310  } else if (keyCode == 190) {
311    return 'Period';
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) {
319    return 'Back slash';
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);
328  }
329};
330
331/**
332 * Get the platform specific sticky key keycode.
333 *
334 * @return {number} The platform specific sticky key keycode.
335 */
336cvox.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
341  }
342  return stickyKeyCode;
343};
344
345/**
346 * Get the platform specific sticky key KeySequence. Creates the KeySequence
347 * object if it doesn't already exist.
348 *
349 * @return {cvox.KeySequence} The platform specific sticky key KeySequence.
350 */
351cvox.KeyUtil.getStickyKeySequence = function() {
352  if (cvox.KeyUtil.stickyKeySequence == null) {
353    var stickyKeyCode = cvox.KeyUtil.getStickyKeyCode();
354    var stickyKeyObj = {keyCode: stickyKeyCode, stickyMode: true};
355    var stickyKeySequence = new cvox.KeySequence(stickyKeyObj);
356    stickyKeySequence.addKeyEvent(stickyKeyObj);
357    cvox.KeyUtil.stickyKeySequence = stickyKeySequence;
358  }
359  return cvox.KeyUtil.stickyKeySequence;
360};
361
362
363/**
364 * Get readable string description for an internal string representation of a
365 * key or a keyboard shortcut.
366 *
367 * @param {string} keyStr The internal string repsentation of a key or
368 *     a keyboard shortcut.
369 * @return {?string} Readable string representation of the input.
370 */
371cvox.KeyUtil.getReadableNameForStr = function(keyStr) {
372  // TODO (clchen): Refactor this function away since it is no longer used.
373  return null;
374};
375
376
377/**
378 * Creates a string representation of a KeySequence.
379 * A KeySequence  with a keyCode of 76 ('L') and the control and alt keys down
380 * would return the string 'Ctrl+Alt+L', for example. A key code that doesn't
381 * correspond to a letter or number will typically return a string with a
382 * pound and then its keyCode, like '#39' for Right Arrow. However,
383 * if the opt_readableKeyCode option is specified, the key code will return a
384 * readable string description like 'Right Arrow' instead of '#39'.
385 *
386 * The modifiers always come in this order:
387 *
388 *   Ctrl
389 *   Alt
390 *   Shift
391 *   Meta
392 *
393 * @param {cvox.KeySequence} keySequence The KeySequence object.
394 * @param {boolean=} opt_readableKeyCode Whether or not to return a readable
395 * string description instead of a string with a pound symbol and a keycode.
396 * Default is false.
397 * @param {boolean=} opt_modifiers Restrict printout to only modifiers. Defaults
398 * to false.
399 * @return {string} Readable string representation of the KeySequence object.
400 */
401cvox.KeyUtil.keySequenceToString = function(
402    keySequence, opt_readableKeyCode, opt_modifiers) {
403  // TODO(rshearer): Move this method and the getReadableNameForKeyCode and the
404  // method to KeySequence after we refactor isModifierActive (when the modifie
405  // key becomes customizable and isn't stored as a string). We can't do it
406  // earlier because isModifierActive uses KeyUtil.getReadableNameForKeyCode,
407  // and I don't want KeySequence to depend on KeyUtil.
408  var str = '';
409
410  var numKeys = keySequence.length();
411
412  for (var index = 0; index < numKeys; index++) {
413    if (str != '' && !opt_modifiers) {
414      str += '>';
415    } else if (str != '') {
416      str += '+';
417    }
418
419    // This iterates through the sequence. Either we're on the first key
420    // pressed or the second
421    var tempStr = '';
422    for (var keyPressed in keySequence.keys) {
423      // This iterates through the actual key, taking into account any
424      // modifiers.
425      if (!keySequence.keys[keyPressed][index]) {
426        continue;
427      }
428      var modifier = '';
429      switch (keyPressed) {
430        case 'ctrlKey':
431        // TODO(rshearer): This is a hack to work around the special casing
432        // of the Ctrl key that used to happen in keyEventToString. We won't
433        // need it once we move away from strings completely.
434        modifier = 'Ctrl';
435        break;
436      case 'searchKeyHeld':
437        var searchKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
438        modifier = searchKey;
439        break;
440      case 'altKey':
441        modifier = 'Alt';
442        break;
443      case 'altGraphKey':
444        modifier = 'AltGraph';
445        break;
446      case 'shiftKey':
447        modifier = 'Shift';
448        break;
449      case 'metaKey':
450        var metaKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
451        modifier = metaKey;
452        break;
453      case 'keyCode':
454        var keyCode = keySequence.keys[keyPressed][index];
455        // We make sure the keyCode isn't for a modifier key. If it is, then
456        // we've already added that into the string above.
457        if (!keySequence.isModifierKey(keyCode) && !opt_modifiers) {
458          if (opt_readableKeyCode) {
459            tempStr += cvox.KeyUtil.getReadableNameForKeyCode(keyCode);
460          } else {
461            tempStr += cvox.KeyUtil.keyCodeToString(keyCode);
462          }
463        }
464      }
465      if (str.indexOf(modifier) == -1) {
466          tempStr += modifier + '+';
467      }
468    }
469    str += tempStr;
470
471    // Strip trailing +.
472    if (str[str.length - 1] == '+') {
473      str = str.slice(0, -1);
474    }
475  }
476
477  if (keySequence.cvoxModifier || keySequence.prefixKey) {
478    if (str != '') {
479      str = 'Cvox+' + str;
480    } else {
481      str = 'Cvox';
482    }
483  } else if (keySequence.stickyMode) {
484    if (str[str.length - 1] == '>') {
485      str = str.slice(0, -1);
486    }
487    str = str + '+' + str;
488  }
489  return str;
490};
491
492/**
493 * Looks up if the given key sequence is triggered via double tap.
494 * @param {cvox.KeySequence} key The key.
495 * @return {boolean} True if key is triggered via double tap.
496 */
497cvox.KeyUtil.isDoubleTapKey = function(key) {
498  var isSet = false;
499  var originalState = key.doubleTap;
500  key.doubleTap = true;
501  for (var i = 0, keySeq; keySeq = cvox.KeySequence.doubleTapCache[i]; i++) {
502    if (keySeq.equals(key)) {
503      isSet = true;
504      break;
505    }
506  }
507  key.doubleTap = originalState;
508  return isSet;
509};
510