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/** 347 * Get readable string description for an internal string representation of a 348 * key or a keyboard shortcut. 349 * 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. 353 */ 354cvox.KeyUtil.getReadableNameForStr = function(keyStr) { 355 // TODO (clchen): Refactor this function away since it is no longer used. 356 return null; 357}; 358 359 360/** 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'. 368 * 369 * The modifiers always come in this order: 370 * 371 * Ctrl 372 * Alt 373 * Shift 374 * Meta 375 * 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. 379 * Default is false. 380 * @param {boolean=} opt_modifiers Restrict printout to only modifiers. Defaults 381 * to false. 382 * @return {string} Readable string representation of the KeySequence object. 383 */ 384cvox.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. 391 var str = ''; 392 393 var numKeys = keySequence.length(); 394 395 for (var index = 0; index < numKeys; index++) { 396 if (str != '' && !opt_modifiers) { 397 str += '>'; 398 } else if (str != '') { 399 str += '+'; 400 } 401 402 // This iterates through the sequence. Either we're on the first key 403 // pressed or the second 404 var tempStr = ''; 405 for (var keyPressed in keySequence.keys) { 406 // This iterates through the actual key, taking into account any 407 // modifiers. 408 if (!keySequence.keys[keyPressed][index]) { 409 continue; 410 } 411 var modifier = ''; 412 switch (keyPressed) { 413 case 'ctrlKey': 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. 417 modifier = 'Ctrl'; 418 break; 419 case 'searchKeyHeld': 420 var searchKey = cvox.KeyUtil.getReadableNameForKeyCode(91); 421 modifier = searchKey; 422 break; 423 case 'altKey': 424 modifier = 'Alt'; 425 break; 426 case 'altGraphKey': 427 modifier = 'AltGraph'; 428 break; 429 case 'shiftKey': 430 modifier = 'Shift'; 431 break; 432 case 'metaKey': 433 var metaKey = cvox.KeyUtil.getReadableNameForKeyCode(91); 434 modifier = metaKey; 435 break; 436 case 'keyCode': 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); 443 } else { 444 tempStr += cvox.KeyUtil.keyCodeToString(keyCode); 445 } 446 } 447 } 448 if (str.indexOf(modifier) == -1) { 449 tempStr += modifier + '+'; 450 } 451 } 452 str += tempStr; 453 454 // Strip trailing +. 455 if (str[str.length - 1] == '+') { 456 str = str.slice(0, -1); 457 } 458 } 459 460 if (keySequence.cvoxModifier || keySequence.prefixKey) { 461 if (str != '') { 462 str = 'Cvox+' + str; 463 } else { 464 str = 'Cvox'; 465 } 466 } else if (keySequence.stickyMode) { 467 if (str[str.length - 1] == '>') { 468 str = str.slice(0, -1); 469 } 470 str = str + '+' + str; 471 } 472 return str; 473}; 474 475/** 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. 479 */ 480cvox.KeyUtil.isDoubleTapKey = function(key) { 481 var isSet = false; 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)) { 486 isSet = true; 487 break; 488 } 489 } 490 key.doubleTap = originalState; 491 return isSet; 492}; 493