Home | History | Annotate | Download | only in common
      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 
     11 goog.provide('cvox.KeyUtil');
     12 
     13 goog.require('cvox.ChromeVox');
     14 goog.require('cvox.KeySequence');
     15 
     16 
     17 /**
     18  * Create the namespace
     19  * @constructor
     20  */
     21 cvox.KeyUtil = function() {
     22 };
     23 
     24 /**
     25  * The time in ms at which the ChromeVox Sticky Mode key was pressed.
     26  * @type {number}
     27  */
     28 cvox.KeyUtil.modeKeyPressTime = 0;
     29 
     30 /**
     31  * Indicates if sequencing is currently active for building a keyboard shortcut.
     32  * @type {boolean}
     33  */
     34 cvox.KeyUtil.sequencing = false;
     35 
     36 /**
     37  * The previous KeySequence when sequencing is ON.
     38  * @type {cvox.KeySequence}
     39  */
     40 cvox.KeyUtil.prevKeySequence = null;
     41 
     42 
     43 /**
     44  * The sticky key sequence.
     45  * @type {cvox.KeySequence}
     46  */
     47 cvox.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  */
     56 cvox.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  */
     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;
     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  */
    141 cvox.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  */
    184 cvox.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  */
    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);
    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  */
    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)) {
    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  */
    237 cvox.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  */
    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
    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  */
    351 cvox.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  */
    371 cvox.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  */
    401 cvox.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  */
    497 cvox.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