key_sequence.js revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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 JavaScript class that represents a sequence of keys entered 7 * by the user. 8 */ 9 10 11goog.provide('cvox.KeySequence'); 12 13goog.require('cvox.ChromeVox'); 14 15 16/** 17 * A class to represent a sequence of keys entered by a user or affiliated with 18 * a ChromeVox command. 19 * This class can represent the data from both types of key sequences: 20 * 21 * COMMAND KEYS SPECIFIED IN A KEYMAP: 22 * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc. Can 23 * specify one or both. 24 * - Modifiers (like ctrl, alt, meta, etc) 25 * - Whether or not the ChromeVox modifier key is required with the command. 26 * 27 * USER INPUT: 28 * - Two discrete keys (at most): [Down arrow], [A, A] or [O, W] etc. 29 * - Modifiers (like ctlr, alt, meta, etc) 30 * - Whether or not the ChromeVox modifier key was active when the keys were 31 * entered. 32 * - Whether or not a prefix key was entered before the discrete keys. 33 * - Whether sticky mode was active. 34 * @param {Event|Object} originalEvent The original key event entered by a user. 35 * The originalEvent may or may not have parameters stickyMode and keyPrefix 36 * specified. We will also accept an event-shaped object. 37 * @param {boolean=} opt_cvoxModifier Whether or not the ChromeVox modifier key 38 * is active. If not specified, we will try to determine whether the modifier 39 * was active by looking at the originalEvent. 40 * @param {boolean=} opt_skipStripping Skips stripping of ChromeVox modifiers 41 * from key events when the cvox modifiers are set. Defaults to false. 42 * @param {boolean=} opt_doubleTap Whether this is triggered via double tap. 43 * @constructor 44 */ 45cvox.KeySequence = function( 46 originalEvent, opt_cvoxModifier, opt_skipStripping, opt_doubleTap) { 47 /** @type {boolean} */ 48 this.doubleTap = !!opt_doubleTap; 49 50 if (opt_cvoxModifier == undefined) { 51 this.cvoxModifier = this.isCVoxModifierActive(originalEvent); 52 } else { 53 this.cvoxModifier = opt_cvoxModifier; 54 } 55 this.stickyMode = !!originalEvent['stickyMode']; 56 this.prefixKey = !!originalEvent['keyPrefix']; 57 this.skipStripping = !!opt_skipStripping; 58 59 if (this.stickyMode && this.prefixKey) { 60 throw 'Prefix key and sticky mode cannot both be enabled: ' + originalEvent; 61 } 62 63 var event = this.resolveChromeOSSpecialKeys_(originalEvent); 64 65 // TODO (rshearer): We should take the user out of sticky mode if they 66 // try to use the CVox modifier or prefix key. 67 68 /** 69 * Stores the key codes and modifiers for the keys in the key sequence. 70 * TODO(rshearer): Consider making this structure an array of minimal 71 * keyEvent-like objects instead so we don't have to worry about what happens 72 * when ctrlKey.length is different from altKey.length. 73 * 74 * NOTE: If a modifier key is pressed by itself, we will store the keyCode 75 * *and* set the appropriate modKey to be true. This mirrors the way key 76 * events are created on Mac and Windows. For example, if the Meta key was 77 * pressed by itself, the keys object will have: 78 * {metaKey: [true], keyCode:[91]} 79 * 80 * @type {Object} 81 */ 82 this.keys = { 83 ctrlKey: [], 84 searchKeyHeld: [], 85 altKey: [], 86 altGraphKey: [], 87 shiftKey: [], 88 metaKey: [], 89 keyCode: [] 90 }; 91 92 this.extractKey_(event); 93}; 94 95 96// TODO(dtseng): This is incomplete; pull once we have appropriate libs. 97/** 98 * Maps a keypress keycode to a keydown or keyup keycode. 99 * @type {Object.<number, number>} 100 */ 101cvox.KeySequence.KEY_PRESS_CODE = { 102 39: 222, 103 44: 188, 104 45: 189, 105 46: 190, 106 47: 191, 107 59: 186, 108 91: 219, 109 92: 220, 110 93: 221 111}; 112 113/** 114 * A cache of all key sequences that have been set as double-tappable. We need 115 * this cache because repeated key down computations causes ChromeVox to become 116 * less responsive. This list is small so we currently use an array. 117 * @type {!Array.<cvox.KeySequence>} 118 */ 119cvox.KeySequence.doubleTapCache = []; 120 121 122/** 123 * Adds an additional key onto the original sequence, for use when the user 124 * is entering two shortcut keys. This happens when the user presses a key, 125 * releases it, and then presses a second key. Those two keys together are 126 * considered part of the sequence. 127 * @param {Event|Object} additionalKeyEvent The additional key to be added to 128 * the original event. Should be an event or an event-shaped object. 129 * @return {boolean} Whether or not we were able to add a key. Returns false 130 * if there are already two keys attached to this event. 131 */ 132cvox.KeySequence.prototype.addKeyEvent = function(additionalKeyEvent) { 133 if (this.keys.keyCode.length > 1) { 134 return false; 135 } 136 this.extractKey_(additionalKeyEvent); 137 return true; 138}; 139 140 141/** 142 * Check for equality. Commands are matched based on the actual key codes 143 * involved and on whether or not they both require a ChromeVox modifier key. 144 * 145 * If sticky mode or a prefix is active on one of the commands but not on 146 * the other, then we try and match based on key code first. 147 * - If both commands have the same key code and neither of them have the 148 * ChromeVox modifier active then we have a match. 149 * - Next we try and match with the ChromeVox modifier. If both commands have 150 * the same key code, and one of them has the ChromeVox modifier and the other 151 * has sticky mode or an active prefix, then we also have a match. 152 * @param {!cvox.KeySequence} rhs The key sequence to compare against. 153 * @return {boolean} True if equal. 154 */ 155cvox.KeySequence.prototype.equals = function(rhs) { 156 // Check to make sure the same keys with the same modifiers were pressed. 157 if (!this.checkKeyEquality_(rhs)) { 158 return false; 159 } 160 161 if (this.doubleTap != rhs.doubleTap) { 162 return false; 163 } 164 165 // So now we know the actual keys are the same. 166 // If they both have the ChromeVox modifier, or they both don't have the 167 // ChromeVox modifier, then they are considered equal. 168 if (this.cvoxModifier === rhs.cvoxModifier) { 169 return true; 170 } 171 172 // So only one of them has the ChromeVox modifier. If the one that doesn't 173 // have the ChromeVox modifier has sticky mode or the prefix key then the 174 // keys are still considered equal. 175 var unmodified = this.cvoxModifier ? rhs : this; 176 return unmodified.stickyMode || unmodified.prefixKey; 177}; 178 179 180/** 181 * Utility method that extracts the key code and any modifiers from a given 182 * event and adds them to the object map. 183 * @param {Event|Object} keyEvent The keyEvent or event-shaped object to extract 184 * from. 185 * @private 186 */ 187cvox.KeySequence.prototype.extractKey_ = function(keyEvent) { 188 for (var prop in this.keys) { 189 if (prop == 'keyCode') { 190 var keyCode; 191 // TODO (rshearer): This is temporary until we find a library that can 192 // convert between ASCII charcodes and keycodes. 193 if (keyEvent.type == 'keypress' && keyEvent[prop] >= 97 && 194 keyEvent[prop] <= 122) { 195 // Alphabetic keypress. Convert to the upper case ASCII code. 196 keyCode = keyEvent[prop] - 32; 197 } else if (keyEvent.type == 'keypress') { 198 keyCode = cvox.KeySequence.KEY_PRESS_CODE[keyEvent[prop]]; 199 } 200 this.keys[prop].push(keyCode || keyEvent[prop]); 201 } else { 202 if (this.isKeyModifierActive(keyEvent, prop)) { 203 this.keys[prop].push(true); 204 } else { 205 this.keys[prop].push(false); 206 } 207 } 208 } 209 if (this.cvoxModifier) { 210 this.rationalizeKeys_(); 211 } 212}; 213 214 215/** 216 * Rationalizes the key codes and the ChromeVox modifier for this keySequence. 217 * This means we strip out the key codes and key modifiers stored for this 218 * KeySequence that are also present in the ChromeVox modifier. For example, if 219 * the ChromeVox modifier keys are Ctrl+Alt, and we've determined that the 220 * ChromeVox modifier is active (meaning the user has pressed Ctrl+Alt), we 221 * don't want this.keys.ctrlKey = true also because that implies that this 222 * KeySequence involves the ChromeVox modifier and the ctrl key being held down 223 * together, which doesn't make any sense. 224 * @private 225 */ 226cvox.KeySequence.prototype.rationalizeKeys_ = function() { 227 if (this.skipStripping) { 228 return; 229 } 230 231 // TODO (rshearer): This is a hack. When the modifier key becomes customizable 232 // then we will not have to deal with strings here. 233 var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g); 234 235 var index = this.keys.keyCode.length - 1; 236 // For each modifier that is part of the CVox modifier, remove it from keys. 237 if (modifierKeyCombo.indexOf('Ctrl') != -1) { 238 this.keys.ctrlKey[index] = false; 239 } 240 if (modifierKeyCombo.indexOf('Alt') != -1) { 241 this.keys.altKey[index] = false; 242 } 243 if (modifierKeyCombo.indexOf('Shift') != -1) { 244 this.keys.shiftKey[index] = false; 245 } 246 var metaKeyName = this.getMetaKeyName_(); 247 if (modifierKeyCombo.indexOf(metaKeyName) != -1) { 248 if (metaKeyName == 'Search') { 249 this.keys.searchKeyHeld[index] = false; 250 // TODO(dmazzoni): http://crbug.com/404763 Get rid of the code that 251 // tracks the search key and just use meta everywhere. 252 this.keys.metaKey[index] = false; 253 } else if (metaKeyName == 'Cmd' || metaKeyName == 'Win') { 254 this.keys.metaKey[index] = false; 255 } 256 } 257}; 258 259 260/** 261 * Get the user-facing name for the meta key (keyCode = 91), which varies 262 * depending on the platform. 263 * @return {string} The user-facing string name for the meta key. 264 * @private 265 */ 266cvox.KeySequence.prototype.getMetaKeyName_ = function() { 267 if (cvox.ChromeVox.isChromeOS) { 268 return 'Search'; 269 } else if (cvox.ChromeVox.isMac) { 270 return 'Cmd'; 271 } else { 272 return 'Win'; 273 } 274}; 275 276 277/** 278 * Utility method that checks for equality of the modifiers (like shift and alt) 279 * and the equality of key codes. 280 * @param {!cvox.KeySequence} rhs The key sequence to compare against. 281 * @return {boolean} True if the modifiers and key codes in the key sequence are 282 * the same. 283 * @private 284 */ 285cvox.KeySequence.prototype.checkKeyEquality_ = function(rhs) { 286 for (var i in this.keys) { 287 for (var j = this.keys[i].length; j--;) { 288 if (this.keys[i][j] !== rhs.keys[i][j]) 289 return false; 290 } 291 } 292 return true; 293}; 294 295 296/** 297 * Gets first key code 298 * @return {number} The first key code. 299 */ 300cvox.KeySequence.prototype.getFirstKeyCode = function() { 301 return this.keys.keyCode[0]; 302}; 303 304 305/** 306 * Gets the number of keys in the sequence. Should be 1 or 2. 307 * @return {number} The number of keys in the sequence. 308 */ 309cvox.KeySequence.prototype.length = function() { 310 return this.keys.keyCode.length; 311}; 312 313 314 315/** 316 * Checks if the specified key code represents a modifier key, i.e. Ctrl, Alt, 317 * Shift, Search (on ChromeOS) or Meta. 318 * 319 * @param {number} keyCode key code. 320 * @return {boolean} true if it is a modifier keycode, false otherwise. 321 */ 322cvox.KeySequence.prototype.isModifierKey = function(keyCode) { 323 // Shift, Ctrl, Alt, Search/LWin 324 return keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 91 || 325 keyCode == 93; 326}; 327 328 329/** 330 * Determines whether the Cvox modifier key is active during the keyEvent. 331 * @param {Event|Object} keyEvent The keyEvent or event-shaped object to check. 332 * @return {boolean} Whether or not the modifier key was active during the 333 * keyEvent. 334 */ 335cvox.KeySequence.prototype.isCVoxModifierActive = function(keyEvent) { 336 // TODO (rshearer): Update this when the modifier key becomes customizable 337 var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g); 338 339 // For each modifier that is held down, remove it from the combo. 340 // If the combo string becomes empty, then the user has activated the combo. 341 if (this.isKeyModifierActive(keyEvent, 'ctrlKey')) { 342 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 343 return modifier != 'Ctrl'; 344 }); 345 } 346 if (this.isKeyModifierActive(keyEvent, 'altKey')) { 347 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 348 return modifier != 'Alt'; 349 }); 350 } 351 if (this.isKeyModifierActive(keyEvent, 'shiftKey')) { 352 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 353 return modifier != 'Shift'; 354 }); 355 } 356 if (this.isKeyModifierActive(keyEvent, 'metaKey') || 357 this.isKeyModifierActive(keyEvent, 'searchKeyHeld')) { 358 var metaKeyName = this.getMetaKeyName_(); 359 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 360 return modifier != metaKeyName; 361 }); 362 } 363 return (modifierKeyCombo.length == 0); 364}; 365 366 367/** 368 * Determines whether a particular key modifier (for example, ctrl or alt) is 369 * active during the keyEvent. 370 * @param {Event|Object} keyEvent The keyEvent or Event-shaped object to check. 371 * @param {string} modifier The modifier to check. 372 * @return {boolean} Whether or not the modifier key was active during the 373 * keyEvent. 374 */ 375cvox.KeySequence.prototype.isKeyModifierActive = function(keyEvent, modifier) { 376 // We need to check the key event modifier and the keyCode because Linux will 377 // not set the keyEvent.modKey property if it is the modKey by itself. 378 // This bug filed as crbug.com/74044 379 switch (modifier) { 380 case 'ctrlKey': 381 return (keyEvent.ctrlKey || keyEvent.keyCode == 17); 382 break; 383 case 'altKey': 384 return (keyEvent.altKey || (keyEvent.keyCode == 18)); 385 break; 386 case 'shiftKey': 387 return (keyEvent.shiftKey || (keyEvent.keyCode == 16)); 388 break; 389 case 'metaKey': 390 return (keyEvent.metaKey || (keyEvent.keyCode == 91)); 391 break; 392 case 'searchKeyHeld': 393 return ((cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91) || 394 keyEvent['searchKeyHeld']); 395 break; 396 } 397 return false; 398}; 399 400/** 401 * Returns if any modifier is active in this sequence. 402 * @return {boolean} The result. 403 */ 404cvox.KeySequence.prototype.isAnyModifierActive = function() { 405 for (var modifierType in this.keys) { 406 for (var i = 0; i < this.length(); i++) { 407 if (this.keys[modifierType][i] && modifierType != 'keyCode') { 408 return true; 409 } 410 } 411 } 412 return false; 413}; 414 415 416/** 417 * Creates a KeySequence event from a generic object. 418 * @param {Object} sequenceObject The object. 419 * @return {cvox.KeySequence} The created KeySequence object. 420 */ 421cvox.KeySequence.deserialize = function(sequenceObject) { 422 var firstSequenceEvent = {}; 423 424 firstSequenceEvent['stickyMode'] = (sequenceObject.stickyMode == undefined) ? 425 false : sequenceObject.stickyMode; 426 firstSequenceEvent['prefixKey'] = (sequenceObject.prefixKey == undefined) ? 427 false : sequenceObject.prefixKey; 428 429 430 var secondKeyPressed = sequenceObject.keys.keyCode.length > 1; 431 var secondSequenceEvent = {}; 432 433 for (var keyPressed in sequenceObject.keys) { 434 firstSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][0]; 435 if (secondKeyPressed) { 436 secondSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][1]; 437 } 438 } 439 440 var keySeq = new cvox.KeySequence(firstSequenceEvent, 441 sequenceObject.cvoxModifier, true, sequenceObject.doubleTap); 442 if (secondKeyPressed) { 443 cvox.ChromeVox.sequenceSwitchKeyCodes.push( 444 new cvox.KeySequence(firstSequenceEvent, sequenceObject.cvoxModifier)); 445 keySeq.addKeyEvent(secondSequenceEvent); 446 } 447 448 if (sequenceObject.doubleTap) { 449 cvox.KeySequence.doubleTapCache.push(keySeq); 450 } 451 452 return keySeq; 453}; 454 455 456/** 457 * Creates a KeySequence event from a given string. The string should be in the 458 * standard key sequence format described in keyUtil.keySequenceToString and 459 * used in the key map JSON files. 460 * @param {string} keyStr The string representation of a key sequence. 461 * @return {!cvox.KeySequence} The created KeySequence object. 462 */ 463cvox.KeySequence.fromStr = function(keyStr) { 464 var sequenceEvent = {}; 465 var secondSequenceEvent = {}; 466 467 var secondKeyPressed; 468 if (keyStr.indexOf('>') == -1) { 469 secondKeyPressed = false; 470 } else { 471 secondKeyPressed = true; 472 } 473 474 var cvoxPressed = false; 475 sequenceEvent['stickyMode'] = false; 476 sequenceEvent['prefixKey'] = false; 477 478 var tokens = keyStr.split('+'); 479 for (var i = 0; i < tokens.length; i++) { 480 var seqs = tokens[i].split('>'); 481 for (var j = 0; j < seqs.length; j++) { 482 if (seqs[j].charAt(0) == '#') { 483 var keyCode = parseInt(seqs[j].substr(1), 10); 484 if (j > 0) { 485 secondSequenceEvent['keyCode'] = keyCode; 486 } else { 487 sequenceEvent['keyCode'] = keyCode; 488 } 489 } 490 var keyName = seqs[j]; 491 if (seqs[j].length == 1) { 492 // Key is A/B/C...1/2/3 and we don't need to worry about setting 493 // modifiers. 494 if (j > 0) { 495 secondSequenceEvent['keyCode'] = seqs[j].charCodeAt(0); 496 } else { 497 sequenceEvent['keyCode'] = seqs[j].charCodeAt(0); 498 } 499 } else { 500 // Key is a modifier key 501 if (j > 0) { 502 cvox.KeySequence.setModifiersOnEvent_(keyName, secondSequenceEvent); 503 if (keyName == 'Cvox') { 504 cvoxPressed = true; 505 } 506 } else { 507 cvox.KeySequence.setModifiersOnEvent_(keyName, sequenceEvent); 508 if (keyName == 'Cvox') { 509 cvoxPressed = true; 510 } 511 } 512 } 513 } 514 } 515 var keySeq = new cvox.KeySequence(sequenceEvent, cvoxPressed); 516 if (secondKeyPressed) { 517 keySeq.addKeyEvent(secondSequenceEvent); 518 } 519 return keySeq; 520}; 521 522 523/** 524 * Utility method for populating the modifiers on an event object that will be 525 * used to create a KeySequence. 526 * @param {string} keyName A particular modifier key name (such as 'Ctrl'). 527 * @param {Object} seqEvent The event to populate. 528 * @private 529 */ 530cvox.KeySequence.setModifiersOnEvent_ = function(keyName, seqEvent) { 531 if (keyName == 'Ctrl') { 532 seqEvent['ctrlKey'] = true; 533 seqEvent['keyCode'] = 17; 534 } else if (keyName == 'Alt') { 535 seqEvent['altKey'] = true; 536 seqEvent['keyCode'] = 18; 537 } else if (keyName == 'Shift') { 538 seqEvent['shiftKey'] = true; 539 seqEvent['keyCode'] = 16; 540 } else if (keyName == 'Search') { 541 seqEvent['searchKeyHeld'] = true; 542 seqEvent['keyCode'] = 91; 543 } else if (keyName == 'Cmd') { 544 seqEvent['metaKey'] = true; 545 seqEvent['keyCode'] = 91; 546 } else if (keyName == 'Win') { 547 seqEvent['metaKey'] = true; 548 seqEvent['keyCode'] = 91; 549 } else if (keyName == 'Insert') { 550 seqEvent['keyCode'] = 45; 551 } 552}; 553 554 555/** 556 * Used to resolve special ChromeOS keys (see link for more detail). 557 * http://crbug.com/162268 558 * @param {Object} originalEvent The event. 559 * @return {Object} The resolved event. 560 * @private 561 */ 562cvox.KeySequence.prototype.resolveChromeOSSpecialKeys_ = 563 function(originalEvent) { 564 if (!this.cvoxModifier || this.stickyMode || this.prefixKey || 565 !cvox.ChromeVox.isChromeOS) { 566 return originalEvent; 567 } 568 var evt = {}; 569 for (var key in originalEvent) { 570 evt[key] = originalEvent[key]; 571 } 572 switch (evt['keyCode']) { 573 case 33: // Page up. 574 evt['keyCode'] = 38; // Up arrow. 575 break; 576 case 34: // Page down. 577 evt['keyCode'] = 40; // Down arrow. 578 break; 579 case 35: // End. 580 evt['keyCode'] = 39; // Right arrow. 581 break; 582 case 36: // Home. 583 evt['keyCode'] = 37; // Left arrow. 584 break; 585 case 45: // Insert. 586 evt['keyCode'] = 190; // Period. 587 break; 588 case 46: // Delete. 589 evt['keyCode'] = 8; // Backspace. 590 break; 591 case 112: // F1. 592 evt['keyCode'] = 49; // 1. 593 break; 594 case 113: // F2. 595 evt['keyCode'] = 50; // 2. 596 break; 597 case 114: // F3. 598 evt['keyCode'] = 51; // 3. 599 break; 600 case 115: // F4. 601 evt['keyCode'] = 52; // 4. 602 break; 603 case 116: // F5. 604 evt['keyCode'] = 53; // 5. 605 break; 606 case 117: // F6. 607 evt['keyCode'] = 54; // 6. 608 break; 609 case 118: // F7. 610 evt['keyCode'] = 55; // 7. 611 break; 612 case 119: // F8. 613 evt['keyCode'] = 56; // 8. 614 break; 615 case 120: // F9. 616 evt['keyCode'] = 57; // 9. 617 break; 618 case 121: // F10. 619 evt['keyCode'] = 48; // 0. 620 break; 621 case 122: // F11 622 evt['keyCode'] = 189; // Hyphen. 623 break; 624 case 123: // F12 625 evt['keyCode'] = 187; // Equals. 626 break; 627 } 628 return evt; 629}; 630