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 } else if (metaKeyName == 'Cmd' || metaKeyName == 'Win') { 251 this.keys.metaKey[index] = false; 252 } 253 } 254}; 255 256 257/** 258 * Get the user-facing name for the meta key (keyCode = 91), which varies 259 * depending on the platform. 260 * @return {string} The user-facing string name for the meta key. 261 * @private 262 */ 263cvox.KeySequence.prototype.getMetaKeyName_ = function() { 264 if (cvox.ChromeVox.isChromeOS) { 265 return 'Search'; 266 } else if (cvox.ChromeVox.isMac) { 267 return 'Cmd'; 268 } else { 269 return 'Win'; 270 } 271}; 272 273 274/** 275 * Utility method that checks for equality of the modifiers (like shift and alt) 276 * and the equality of key codes. 277 * @param {!cvox.KeySequence} rhs The key sequence to compare against. 278 * @return {boolean} True if the modifiers and key codes in the key sequence are 279 * the same. 280 * @private 281 */ 282cvox.KeySequence.prototype.checkKeyEquality_ = function(rhs) { 283 for (var i in this.keys) { 284 for (var j = this.keys[i].length; j--;) { 285 if (this.keys[i][j] !== rhs.keys[i][j]) 286 return false; 287 } 288 } 289 return true; 290}; 291 292 293/** 294 * Gets first key code 295 * @return {number} The first key code. 296 */ 297cvox.KeySequence.prototype.getFirstKeyCode = function() { 298 return this.keys.keyCode[0]; 299}; 300 301 302/** 303 * Gets the number of keys in the sequence. Should be 1 or 2. 304 * @return {number} The number of keys in the sequence. 305 */ 306cvox.KeySequence.prototype.length = function() { 307 return this.keys.keyCode.length; 308}; 309 310 311 312/** 313 * Checks if the specified key code represents a modifier key, i.e. Ctrl, Alt, 314 * Shift, Search (on ChromeOS) or Meta. 315 * 316 * @param {number} keyCode key code. 317 * @return {boolean} true if it is a modifier keycode, false otherwise. 318 */ 319cvox.KeySequence.prototype.isModifierKey = function(keyCode) { 320 // Shift, Ctrl, Alt, Search/LWin 321 return keyCode == 16 || keyCode == 17 || keyCode == 18 || keyCode == 91 || 322 keyCode == 93; 323}; 324 325 326/** 327 * Determines whether the Cvox modifier key is active during the keyEvent. 328 * @param {Event|Object} keyEvent The keyEvent or event-shaped object to check. 329 * @return {boolean} Whether or not the modifier key was active during the 330 * keyEvent. 331 */ 332cvox.KeySequence.prototype.isCVoxModifierActive = function(keyEvent) { 333 // TODO (rshearer): Update this when the modifier key becomes customizable 334 var modifierKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g); 335 336 // For each modifier that is held down, remove it from the combo. 337 // If the combo string becomes empty, then the user has activated the combo. 338 if (this.isKeyModifierActive(keyEvent, 'ctrlKey')) { 339 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 340 return modifier != 'Ctrl'; 341 }); 342 } 343 if (this.isKeyModifierActive(keyEvent, 'altKey')) { 344 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 345 return modifier != 'Alt'; 346 }); 347 } 348 if (this.isKeyModifierActive(keyEvent, 'shiftKey')) { 349 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 350 return modifier != 'Shift'; 351 }); 352 } 353 if (this.isKeyModifierActive(keyEvent, 'metaKey') || 354 this.isKeyModifierActive(keyEvent, 'searchKeyHeld')) { 355 var metaKeyName = this.getMetaKeyName_(); 356 modifierKeyCombo = modifierKeyCombo.filter(function(modifier) { 357 return modifier != metaKeyName; 358 }); 359 } 360 return (modifierKeyCombo.length == 0); 361}; 362 363 364/** 365 * Determines whether a particular key modifier (for example, ctrl or alt) is 366 * active during the keyEvent. 367 * @param {Event|Object} keyEvent The keyEvent or Event-shaped object to check. 368 * @param {string} modifier The modifier to check. 369 * @return {boolean} Whether or not the modifier key was active during the 370 * keyEvent. 371 */ 372cvox.KeySequence.prototype.isKeyModifierActive = function(keyEvent, modifier) { 373 // We need to check the key event modifier and the keyCode because Linux will 374 // not set the keyEvent.modKey property if it is the modKey by itself. 375 // This bug filed as crbug.com/74044 376 switch (modifier) { 377 case 'ctrlKey': 378 return (keyEvent.ctrlKey || keyEvent.keyCode == 17); 379 break; 380 case 'altKey': 381 return (keyEvent.altKey || (keyEvent.keyCode == 18)); 382 break; 383 case 'shiftKey': 384 return (keyEvent.shiftKey || (keyEvent.keyCode == 16)); 385 break; 386 case 'metaKey': 387 return (keyEvent.metaKey || 388 (!cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91)); 389 break; 390 case 'searchKeyHeld': 391 return ((cvox.ChromeVox.isChromeOS && keyEvent.keyCode == 91) || 392 keyEvent['searchKeyHeld']); 393 break; 394 } 395 return false; 396}; 397 398/** 399 * Returns if any modifier is active in this sequence. 400 * @return {boolean} The result. 401 */ 402cvox.KeySequence.prototype.isAnyModifierActive = function() { 403 for (var modifierType in this.keys) { 404 for (var i = 0; i < this.length(); i++) { 405 if (this.keys[modifierType][i] && modifierType != 'keyCode') { 406 return true; 407 } 408 } 409 } 410 return false; 411}; 412 413 414/** 415 * Creates a KeySequence event from a generic object. 416 * @param {Object} sequenceObject The object. 417 * @return {cvox.KeySequence} The created KeySequence object. 418 */ 419cvox.KeySequence.deserialize = function(sequenceObject) { 420 var firstSequenceEvent = {}; 421 422 firstSequenceEvent['stickyMode'] = (sequenceObject.stickyMode == undefined) ? 423 false : sequenceObject.stickyMode; 424 firstSequenceEvent['prefixKey'] = (sequenceObject.prefixKey == undefined) ? 425 false : sequenceObject.prefixKey; 426 427 428 var secondKeyPressed = sequenceObject.keys.keyCode.length > 1; 429 var secondSequenceEvent = {}; 430 431 for (var keyPressed in sequenceObject.keys) { 432 firstSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][0]; 433 if (secondKeyPressed) { 434 secondSequenceEvent[keyPressed] = sequenceObject.keys[keyPressed][1]; 435 } 436 } 437 438 var keySeq = new cvox.KeySequence(firstSequenceEvent, 439 sequenceObject.cvoxModifier, true, sequenceObject.doubleTap); 440 if (secondKeyPressed) { 441 cvox.ChromeVox.sequenceSwitchKeyCodes.push( 442 new cvox.KeySequence(firstSequenceEvent, sequenceObject.cvoxModifier)); 443 keySeq.addKeyEvent(secondSequenceEvent); 444 } 445 446 if (sequenceObject.doubleTap) { 447 cvox.KeySequence.doubleTapCache.push(keySeq); 448 } 449 450 return keySeq; 451}; 452 453 454/** 455 * Creates a KeySequence event from a given string. The string should be in the 456 * standard key sequence format described in keyUtil.keySequenceToString and 457 * used in the key map JSON files. 458 * @param {string} keyStr The string representation of a key sequence. 459 * @return {!cvox.KeySequence} The created KeySequence object. 460 */ 461cvox.KeySequence.fromStr = function(keyStr) { 462 var sequenceEvent = {}; 463 var secondSequenceEvent = {}; 464 465 var secondKeyPressed; 466 if (keyStr.indexOf('>') == -1) { 467 secondKeyPressed = false; 468 } else { 469 secondKeyPressed = true; 470 } 471 472 var cvoxPressed = false; 473 sequenceEvent['stickyMode'] = false; 474 sequenceEvent['prefixKey'] = false; 475 476 var tokens = keyStr.split('+'); 477 for (var i = 0; i < tokens.length; i++) { 478 var seqs = tokens[i].split('>'); 479 for (var j = 0; j < seqs.length; j++) { 480 if (seqs[j].charAt(0) == '#') { 481 var keyCode = parseInt(seqs[j].substr(1), 10); 482 if (j > 0) { 483 secondSequenceEvent['keyCode'] = keyCode; 484 } else { 485 sequenceEvent['keyCode'] = keyCode; 486 } 487 } 488 var keyName = seqs[j]; 489 if (seqs[j].length == 1) { 490 // Key is A/B/C...1/2/3 and we don't need to worry about setting 491 // modifiers. 492 if (j > 0) { 493 secondSequenceEvent['keyCode'] = seqs[j].charCodeAt(0); 494 } else { 495 sequenceEvent['keyCode'] = seqs[j].charCodeAt(0); 496 } 497 } else { 498 // Key is a modifier key 499 if (j > 0) { 500 cvox.KeySequence.setModifiersOnEvent_(keyName, secondSequenceEvent); 501 if (keyName == 'Cvox') { 502 cvoxPressed = true; 503 } 504 } else { 505 cvox.KeySequence.setModifiersOnEvent_(keyName, sequenceEvent); 506 if (keyName == 'Cvox') { 507 cvoxPressed = true; 508 } 509 } 510 } 511 } 512 } 513 var keySeq = new cvox.KeySequence(sequenceEvent, cvoxPressed); 514 if (secondKeyPressed) { 515 keySeq.addKeyEvent(secondSequenceEvent); 516 } 517 return keySeq; 518}; 519 520 521/** 522 * Utility method for populating the modifiers on an event object that will be 523 * used to create a KeySequence. 524 * @param {string} keyName A particular modifier key name (such as 'Ctrl'). 525 * @param {Object} seqEvent The event to populate. 526 * @private 527 */ 528cvox.KeySequence.setModifiersOnEvent_ = function(keyName, seqEvent) { 529 if (keyName == 'Ctrl') { 530 seqEvent['ctrlKey'] = true; 531 seqEvent['keyCode'] = 17; 532 } else if (keyName == 'Alt') { 533 seqEvent['altKey'] = true; 534 seqEvent['keyCode'] = 18; 535 } else if (keyName == 'Shift') { 536 seqEvent['shiftKey'] = true; 537 seqEvent['keyCode'] = 16; 538 } else if (keyName == 'Search') { 539 seqEvent['searchKeyHeld'] = true; 540 seqEvent['keyCode'] = 91; 541 } else if (keyName == 'Cmd') { 542 seqEvent['metaKey'] = true; 543 seqEvent['keyCode'] = 91; 544 } else if (keyName == 'Win') { 545 seqEvent['metaKey'] = true; 546 seqEvent['keyCode'] = 91; 547 } else if (keyName == 'Insert') { 548 seqEvent['keyCode'] = 45; 549 } 550}; 551 552 553/** 554 * Used to resolve special ChromeOS keys (see link for more detail). 555 * http://crbug.com/162268 556 * @param {Object} originalEvent The event. 557 * @return {Object} The resolved event. 558 * @private 559 */ 560cvox.KeySequence.prototype.resolveChromeOSSpecialKeys_ = 561 function(originalEvent) { 562 if (!this.cvoxModifier || this.stickyMode || this.prefixKey || 563 !cvox.ChromeVox.isChromeOS) { 564 return originalEvent; 565 } 566 var evt = {}; 567 for (var key in originalEvent) { 568 evt[key] = originalEvent[key]; 569 } 570 switch (evt['keyCode']) { 571 case 33: // Page up. 572 evt['keyCode'] = 38; // Up arrow. 573 break; 574 case 34: // Page down. 575 evt['keyCode'] = 40; // Down arrow. 576 break; 577 case 35: // End. 578 evt['keyCode'] = 39; // Right arrow. 579 break; 580 case 36: // Home. 581 evt['keyCode'] = 37; // Left arrow. 582 break; 583 case 45: // Insert. 584 evt['keyCode'] = 190; // Period. 585 break; 586 case 46: // Delete. 587 evt['keyCode'] = 8; // Backspace. 588 break; 589 case 112: // F1. 590 evt['keyCode'] = 49; // 1. 591 break; 592 case 113: // F2. 593 evt['keyCode'] = 50; // 2. 594 break; 595 case 114: // F3. 596 evt['keyCode'] = 51; // 3. 597 break; 598 case 115: // F4. 599 evt['keyCode'] = 52; // 4. 600 break; 601 case 116: // F5. 602 evt['keyCode'] = 53; // 5. 603 break; 604 case 117: // F6. 605 evt['keyCode'] = 54; // 6. 606 break; 607 case 118: // F7. 608 evt['keyCode'] = 55; // 7. 609 break; 610 case 119: // F8. 611 evt['keyCode'] = 56; // 8. 612 break; 613 case 120: // F9. 614 evt['keyCode'] = 57; // 9. 615 break; 616 case 121: // F10. 617 evt['keyCode'] = 48; // 0. 618 break; 619 case 122: // F11 620 evt['keyCode'] = 189; // Hyphen. 621 break; 622 case 123: // F12 623 evt['keyCode'] = 187; // Equals. 624 break; 625 } 626 return evt; 627}; 628