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 * The repeat delay in milliseconds before a key starts repeating. Use the 7 * same rate as Chromebook. 8 * (See chrome/browser/chromeos/language_preferences.cc) 9 * @const 10 * @type {number} 11 */ 12var REPEAT_DELAY_MSEC = 500; 13 14/** 15 * The repeat interval or number of milliseconds between subsequent 16 * keypresses. Use the same rate as Chromebook. 17 * @const 18 * @type {number} 19 */ 20var REPEAT_INTERVAL_MSEC = 50; 21 22/** 23 * The double click/tap interval. 24 * @const 25 * @type {number} 26 */ 27var DBL_INTERVAL_MSEC = 300; 28 29/** 30 * The index of the name of the keyset when searching for all keysets. 31 * @const 32 * @type {number} 33 */ 34var REGEX_KEYSET_INDEX = 1; 35 36/** 37 * The integer number of matches when searching for keysets. 38 * @const 39 * @type {number} 40 */ 41var REGEX_MATCH_COUNT = 2; 42 43/** 44 * The boolean to decide if keyboard should transit to upper case keyset 45 * when spacebar is pressed. If a closing punctuation is followed by a 46 * spacebar, keyboard should automatically transit to upper case. 47 * @type {boolean} 48 */ 49var enterUpperOnSpace = false; 50 51/** 52 * A structure to track the currently repeating key on the keyboard. 53 */ 54var repeatKey = { 55 56 /** 57 * The timer for the delay before repeating behaviour begins. 58 * @type {number|undefined} 59 */ 60 timer: undefined, 61 62 /** 63 * The interval timer for issuing keypresses of a repeating key. 64 * @type {number|undefined} 65 */ 66 interval: undefined, 67 68 /** 69 * The key which is currently repeating. 70 * @type {BaseKey|undefined} 71 */ 72 key: undefined, 73 74 /** 75 * Cancel the repeat timers of the currently active key. 76 */ 77 cancel: function() { 78 clearTimeout(this.timer); 79 clearInterval(this.interval); 80 this.timer = undefined; 81 this.interval = undefined; 82 this.key = undefined; 83 } 84}; 85 86/** 87 * The minimum movement interval needed to trigger cursor move on 88 * horizontal and vertical way. 89 * @const 90 * @type {number} 91 */ 92var MIN_SWIPE_DIST_X = 50; 93var MIN_SWIPE_DIST_Y = 20; 94 95/** 96 * The maximum swipe distance that will trigger hintText of a key 97 * to be typed. 98 * @const 99 * @type {number} 100 */ 101var MAX_SWIPE_FLICK_DIST = 60; 102 103/** 104 * The boolean to decide if it is swipe in process or finished. 105 * @type {boolean} 106 */ 107var swipeInProgress = false; 108 109// Flag values for ctrl, alt and shift as defined by EventFlags 110// in "event_constants.h". 111// @enum {number} 112var Modifier = { 113 NONE: 0, 114 ALT: 8, 115 CONTROL: 4, 116 SHIFT: 2 117}; 118 119/** 120 * A structure to track the current swipe status. 121 */ 122var swipeTracker = { 123 /** 124 * The latest PointerMove event in the swipe. 125 * @type {Object} 126 */ 127 currentEvent: undefined, 128 129 /** 130 * Whether or not a swipe changes direction. 131 * @type {false} 132 */ 133 isComplex: false, 134 135 /** 136 * The count of horizontal and vertical movement. 137 * @type {number} 138 */ 139 offset_x : 0, 140 offset_y : 0, 141 142 /** 143 * Last touch coordinate. 144 * @type {number} 145 */ 146 pre_x : 0, 147 pre_y : 0, 148 149 /** 150 * The PointerMove event which triggered the swipe. 151 * @type {Object} 152 */ 153 startEvent: undefined, 154 155 /** 156 * The flag of current modifier key. 157 * @type {number} 158 */ 159 swipeFlags : 0, 160 161 /** 162 * Current swipe direction. 163 * @type {number} 164 */ 165 swipeDirection : 0, 166 167 /** 168 * The number of times we've swiped within a single swipe. 169 * @type {number} 170 */ 171 swipeIndex: 0, 172 173 /** 174 * Returns the combined direction of the x and y offsets. 175 * @return {number} The latest direction. 176 */ 177 getOffsetDirection: function() { 178 // TODO (rsadam): Use angles to figure out the direction. 179 var direction = 0; 180 // Checks for horizontal swipe. 181 if (Math.abs(this.offset_x) > MIN_SWIPE_DIST_X) { 182 if (this.offset_x > 0) { 183 direction |= SwipeDirection.RIGHT; 184 } else { 185 direction |= SwipeDirection.LEFT; 186 } 187 } 188 // Checks for vertical swipe. 189 if (Math.abs(this.offset_y) > MIN_SWIPE_DIST_Y) { 190 if (this.offset_y < 0) { 191 direction |= SwipeDirection.UP; 192 } else { 193 direction |= SwipeDirection.DOWN; 194 } 195 } 196 return direction; 197 }, 198 199 /** 200 * Populates the swipe update details. 201 * @param {boolean} endSwipe Whether this is the final event for this 202 * swipe. 203 * @return {Object} The current state of the swipeTracker. 204 */ 205 populateDetails: function(endSwipe) { 206 var detail = {}; 207 detail.direction = this.swipeDirection; 208 detail.index = this.swipeIndex; 209 detail.status = this.swipeStatus; 210 detail.endSwipe = endSwipe; 211 detail.startEvent = this.startEvent; 212 detail.currentEvent = this.currentEvent; 213 detail.isComplex = this.isComplex; 214 return detail; 215 }, 216 217 /** 218 * Reset all the values when swipe finished. 219 */ 220 resetAll: function() { 221 this.offset_x = 0; 222 this.offset_y = 0; 223 this.pre_x = 0; 224 this.pre_y = 0; 225 this.swipeFlags = 0; 226 this.swipeDirection = 0; 227 this.swipeIndex = 0; 228 this.startEvent = undefined; 229 this.currentEvent = undefined; 230 this.isComplex = false; 231 }, 232 233 /** 234 * Updates the swipe path with the current event. 235 * @param {Object} event The PointerEvent that triggered this update. 236 * @return {boolean} Whether or not to notify swipe observers. 237 */ 238 update: function(event) { 239 if(!event.isPrimary) 240 return false; 241 // Update priors. 242 this.offset_x += event.screenX - this.pre_x; 243 this.offset_y += event.screenY - this.pre_y; 244 this.pre_x = event.screenX; 245 this.pre_y = event.screenY; 246 247 // Check if movement crosses minimum thresholds in each direction. 248 var direction = this.getOffsetDirection(); 249 if (direction == 0) 250 return false; 251 // If swipeIndex is zero the current event is triggering the swipe. 252 if (this.swipeIndex == 0) { 253 this.startEvent = event; 254 } else if (direction != this.swipeDirection) { 255 // Toggle the isComplex flag. 256 this.isComplex = true; 257 } 258 // Update the swipe tracker. 259 this.swipeDirection = direction; 260 this.offset_x = 0; 261 this.offset_y = 0; 262 this.currentEvent = event; 263 this.swipeIndex++; 264 return true; 265 }, 266 267}; 268 269Polymer('kb-keyboard', { 270 alt: null, 271 config: null, 272 control: null, 273 dblDetail_: null, 274 dblTimer_: null, 275 inputType: null, 276 lastPressedKey: null, 277 shift: null, 278 sounds: {}, 279 stale: true, 280 swipeHandler: null, 281 voiceInput_: null, 282 //TODO(rsadam@): Add a control to let users change this. 283 volume: DEFAULT_VOLUME, 284 285 /** 286 * The default input type to keyboard layout map. The key must be one of 287 * the input box type values. 288 * @type {object} 289 */ 290 inputTypeToLayoutMap: { 291 number: "numeric", 292 text: "qwerty", 293 password: "qwerty" 294 }, 295 296 /** 297 * Caches the specified sound on the keyboard. 298 * @param {string} soundId The name of the .wav file in the "sounds" 299 directory. 300 */ 301 addSound: function(soundId) { 302 // Check if already loaded. 303 if (soundId == Sound.NONE || this.sounds[soundId]) 304 return; 305 var pool = []; 306 for (var i = 0; i < SOUND_POOL_SIZE; i++) { 307 var audio = document.createElement('audio'); 308 audio.preload = "auto"; 309 audio.id = soundId; 310 audio.src = "../sounds/" + soundId + ".wav"; 311 audio.volume = this.volume; 312 pool.push(audio); 313 } 314 this.sounds[soundId] = pool; 315 }, 316 317 /** 318 * Changes the current keyset. 319 * @param {Object} detail The detail of the event that called this 320 * function. 321 */ 322 changeKeyset: function(detail) { 323 if (detail.relegateToShift && this.shift) { 324 this.keyset = this.shift.textKeyset; 325 this.activeKeyset.nextKeyset = undefined; 326 return true; 327 } 328 var toKeyset = detail.toKeyset; 329 if (toKeyset) { 330 this.keyset = toKeyset; 331 this.activeKeyset.nextKeyset = detail.nextKeyset; 332 return true; 333 } 334 return false; 335 }, 336 337 keysetChanged: function() { 338 var keyset = this.activeKeyset; 339 // Show the keyset if it has been initialized. 340 if (keyset) 341 keyset.show(); 342 }, 343 344 configChanged: function() { 345 this.layout = this.config.layout; 346 }, 347 348 ready: function() { 349 this.voiceInput_ = new VoiceInput(this); 350 this.swipeHandler = this.move.bind(this); 351 var self = this; 352 getKeyboardConfig(function(config) { 353 self.config = config; 354 }); 355 }, 356 357 /** 358 * Registers a callback for state change events. 359 * @param{!Function} callback Callback function to register. 360 */ 361 addKeysetChangedObserver: function(callback) { 362 this.addEventListener('stateChange', callback); 363 }, 364 365 /** 366 * Called when the type of focused input box changes. If a keyboard layout 367 * is defined for the current input type, that layout will be loaded. 368 * Otherwise, the keyboard layout for 'text' type will be loaded. 369 */ 370 inputTypeChanged: function() { 371 // Disable layout switching at accessbility mode. 372 if (this.config && this.config.a11ymode) 373 return; 374 375 // TODO(bshe): Toggle visibility of some keys in a keyboard layout 376 // according to the input type. 377 var layout = this.inputTypeToLayoutMap[this.inputType]; 378 if (!layout) 379 layout = this.inputTypeToLayoutMap.text; 380 this.layout = layout; 381 }, 382 383 /** 384 * When double click/tap event is enabled, the second key-down and key-up 385 * events on the same key should be skipped. Return true when the event 386 * with |detail| should be skipped. 387 * @param {Object} detail The detail of key-up or key-down event. 388 */ 389 skipEvent: function(detail) { 390 if (this.dblDetail_) { 391 if (this.dblDetail_.char != detail.char) { 392 // The second key down is not on the same key. Double click/tap 393 // should be ignored. 394 this.dblDetail_ = null; 395 clearTimeout(this.dblTimer_); 396 } else if (this.dblDetail_.clickCount == 1) { 397 return true; 398 } 399 } 400 return false; 401 }, 402 403 /** 404 * Handles a swipe update. 405 * param {Object} detail The swipe update details. 406 */ 407 onSwipeUpdate: function(detail) { 408 var direction = detail.direction; 409 if (!direction) 410 console.error("Swipe direction cannot be: " + direction); 411 // Triggers swipe editting if it's a purely horizontal swipe. 412 if (!(direction & (SwipeDirection.UP | SwipeDirection.DOWN))) { 413 // Nothing to do if the swipe has ended. 414 if (detail.endSwipe) 415 return; 416 var modifiers = 0; 417 // TODO (rsadam): This doesn't take into account index shifts caused 418 // by vertical swipes. 419 if (detail.index % 2 != 0) { 420 modifiers |= Modifier.SHIFT; 421 modifiers |= Modifier.CONTROL; 422 } 423 MoveCursor(direction, modifiers); 424 return; 425 } 426 // Triggers swipe hintText if it's a purely vertical swipe. 427 if (this.activeKeyset.flick && 428 !(direction & (SwipeDirection.LEFT | SwipeDirection.RIGHT))) { 429 // Check if event is relevant to us. 430 if ((!detail.endSwipe) || (detail.isComplex)) 431 return; 432 // Too long a swipe. 433 var distance = Math.abs(detail.startEvent.screenY - 434 detail.currentEvent.screenY); 435 if (distance > MAX_SWIPE_FLICK_DIST) 436 return; 437 var triggerKey = detail.startEvent.target; 438 if (triggerKey && triggerKey.onFlick) 439 triggerKey.onFlick(detail); 440 } 441 }, 442 443 /** 444 * This function is bound to swipeHandler. Updates the current swipe 445 * status so that PointerEvents can be converted to Swipe events. 446 * @param {PointerEvent} event. 447 */ 448 move: function(event) { 449 if (!swipeTracker.update(event)) 450 return; 451 // Conversion was successful, swipe is now in progress. 452 swipeInProgress = true; 453 if (this.lastPressedKey) { 454 this.lastPressedKey.classList.remove('active'); 455 this.lastPressedKey = null; 456 } 457 this.onSwipeUpdate(swipeTracker.populateDetails(false)); 458 }, 459 460 /** 461 * Handles key-down event that is sent by kb-key-base. 462 * @param {CustomEvent} event The key-down event dispatched by 463 * kb-key-base. 464 * @param {Object} detail The detail of pressed kb-key. 465 */ 466 keyDown: function(event, detail) { 467 if (this.skipEvent(detail)) 468 return; 469 470 if (this.lastPressedKey) { 471 this.lastPressedKey.classList.remove('active'); 472 this.lastPressedKey.autoRelease(); 473 } 474 this.lastPressedKey = event.target; 475 this.lastPressedKey.classList.add('active'); 476 repeatKey.cancel(); 477 this.playSound(detail.sound); 478 479 var char = detail.char; 480 switch(char) { 481 case 'Shift': 482 this.classList.remove('caps-locked'); 483 break; 484 case 'Alt': 485 case 'Ctrl': 486 var modifier = char.toLowerCase() + "-active"; 487 // Removes modifier if already active. 488 if (this.classList.contains(modifier)) 489 this.classList.remove(modifier); 490 break; 491 case 'Invalid': 492 // Not all Invalid keys are transition keys. Reset control keys if 493 // we pressed a transition key. 494 if (event.target.toKeyset || detail.relegateToShift) 495 this.onNonControlKeyTyped(); 496 break; 497 default: 498 // Notify shift key. 499 if (this.shift) 500 this.shift.onNonControlKeyDown(); 501 if (this.ctrl) 502 this.ctrl.onNonControlKeyDown(); 503 if (this.alt) 504 this.alt.onNonControlKeyDown(); 505 break; 506 } 507 if(this.changeKeyset(detail)) 508 return; 509 if (detail.repeat) { 510 this.keyTyped(detail); 511 this.onNonControlKeyTyped(); 512 repeatKey.key = this.lastPressedKey; 513 var self = this; 514 repeatKey.timer = setTimeout(function() { 515 repeatKey.timer = undefined; 516 repeatKey.interval = setInterval(function() { 517 self.playSound(detail.sound); 518 self.keyTyped(detail); 519 }, REPEAT_INTERVAL_MSEC); 520 }, Math.max(0, REPEAT_DELAY_MSEC - REPEAT_INTERVAL_MSEC)); 521 } 522 }, 523 524 /** 525 * Handles key-out event that is sent by kb-shift-key. 526 * @param {CustomEvent} event The key-out event dispatched by 527 * kb-shift-key. 528 * @param {Object} detail The detail of pressed kb-shift-key. 529 */ 530 keyOut: function(event, detail) { 531 this.changeKeyset(detail); 532 }, 533 534 /** 535 * Enable/start double click/tap event recognition. 536 * @param {CustomEvent} event The enable-dbl event dispatched by 537 * kb-shift-key. 538 * @param {Object} detail The detail of pressed kb-shift-key. 539 */ 540 enableDbl: function(event, detail) { 541 if (!this.dblDetail_) { 542 this.dblDetail_ = detail; 543 this.dblDetail_.clickCount = 0; 544 var self = this; 545 this.dblTimer_ = setTimeout(function() { 546 self.dblDetail_.callback = null; 547 self.dblDetail_ = null; 548 }, DBL_INTERVAL_MSEC); 549 } 550 }, 551 552 /** 553 * Enable the selection while swipe. 554 * @param {CustomEvent} event The enable-dbl event dispatched by 555 * kb-shift-key. 556 */ 557 enableSel: function(event) { 558 // TODO(rsadam): Disabled for now. May come back if we revert swipe 559 // selection to not do word selection. 560 }, 561 562 /** 563 * Handles pointerdown event. This is used for swipe selection process. 564 * to get the start pre_x and pre_y. And also add a pointermove handler 565 * to start handling the swipe selection event. 566 * @param {PointerEvent} event The pointerup event that received by 567 * kb-keyboard. 568 */ 569 down: function(event) { 570 var layout = getKeysetLayout(this.activeKeysetId); 571 var key = layout.findClosestKey(event.clientX, event.clientY); 572 if (key) 573 key.down(event); 574 if (event.isPrimary) { 575 swipeTracker.pre_x = event.screenX; 576 swipeTracker.pre_y = event.screenY; 577 this.addEventListener("pointermove", this.swipeHandler, false); 578 } 579 }, 580 581 /** 582 * Handles pointerup event. This is used for double tap/click events. 583 * @param {PointerEvent} event The pointerup event that bubbled to 584 * kb-keyboard. 585 */ 586 up: function(event) { 587 var layout = getKeysetLayout(this.activeKeysetId); 588 var key = layout.findClosestKey(event.clientX, event.clientY); 589 if (key) 590 key.up(event); 591 // When touch typing, it is very possible that finger moves slightly out 592 // of the key area before releases. The key should not be dropped in 593 // this case. 594 // TODO(rsadam@) Change behaviour such that the key drops and the second 595 // key gets pressed. 596 if (this.lastPressedKey && 597 this.lastPressedKey.pointerId == event.pointerId) { 598 this.lastPressedKey.autoRelease(); 599 } 600 601 if (this.dblDetail_) { 602 this.dblDetail_.clickCount++; 603 if (this.dblDetail_.clickCount == 2) { 604 this.dblDetail_.callback(); 605 this.changeKeyset(this.dblDetail_); 606 clearTimeout(this.dblTimer_); 607 608 this.classList.add('caps-locked'); 609 610 this.dblDetail_ = null; 611 } 612 } 613 614 // TODO(zyaozhujun): There are some edge cases to deal with later. 615 // (for instance, what if a second finger trigger a down and up 616 // event sequence while swiping). 617 // When pointer up from the screen, a swipe selection session finished, 618 // all the data should be reset to prepare for the next session. 619 if (event.isPrimary && swipeInProgress) { 620 swipeInProgress = false; 621 this.onSwipeUpdate(swipeTracker.populateDetails(true)) 622 swipeTracker.resetAll(); 623 } 624 this.removeEventListener('pointermove', this.swipeHandler, false); 625 }, 626 627 /** 628 * Handles PointerOut event. This is used for when a swipe gesture goes 629 * outside of the keyboard window. 630 * @param {Object} event The pointerout event that bubbled to the 631 * kb-keyboard. 632 */ 633 out: function(event) { 634 repeatKey.cancel(); 635 // Ignore if triggered from one of the keys. 636 if (this.compareDocumentPosition(event.relatedTarget) & 637 Node.DOCUMENT_POSITION_CONTAINED_BY) 638 return; 639 if (swipeInProgress) 640 this.onSwipeUpdate(swipeTracker.populateDetails(true)) 641 // Touched outside of the keyboard area, so disables swipe. 642 swipeInProgress = false; 643 swipeTracker.resetAll(); 644 this.removeEventListener('pointermove', this.swipeHandler, false); 645 }, 646 647 /** 648 * Handles a TypeKey event. This is used for when we programmatically 649 * want to type a specific key. 650 * @param {CustomEvent} event The TypeKey event that bubbled to the 651 * kb-keyboard. 652 */ 653 type: function(event) { 654 this.keyTyped(event.detail); 655 }, 656 657 /** 658 * Handles key-up event that is sent by kb-key-base. 659 * @param {CustomEvent} event The key-up event dispatched by kb-key-base. 660 * @param {Object} detail The detail of pressed kb-key. 661 */ 662 keyUp: function(event, detail) { 663 if (this.skipEvent(detail)) 664 return; 665 if (swipeInProgress) 666 return; 667 if (detail.activeModifier) { 668 var modifier = detail.activeModifier.toLowerCase() + "-active"; 669 this.classList.add(modifier); 670 } 671 // Adds the current keyboard modifiers to the detail. 672 if (this.ctrl) 673 detail.controlModifier = this.ctrl.isActive(); 674 if (this.alt) 675 detail.altModifier = this.alt.isActive(); 676 if (this.lastPressedKey) 677 this.lastPressedKey.classList.remove('active'); 678 // Keyset transition key. This is needed to transition from upper 679 // to lower case when we are not in caps mode, as well as when 680 // we're ending chording. 681 this.changeKeyset(detail); 682 683 if (this.lastPressedKey && 684 this.lastPressedKey.charValue != event.target.charValue) { 685 return; 686 } 687 if (repeatKey.key == event.target) { 688 repeatKey.cancel(); 689 this.lastPressedKey = null; 690 return; 691 } 692 var toLayoutId = detail.toLayout; 693 // Layout transition key. 694 if (toLayoutId) 695 this.layout = toLayoutId; 696 var char = detail.char; 697 this.lastPressedKey = null; 698 // Characters that should not be typed. 699 switch(char) { 700 case 'Invalid': 701 case 'Shift': 702 case 'Ctrl': 703 case 'Alt': 704 enterUpperOnSpace = false; 705 swipeTracker.swipeFlags = 0; 706 return; 707 case 'Microphone': 708 this.voiceInput_.onDown(); 709 return; 710 default: 711 break; 712 } 713 // Tries to type the character. Resorts to insertText if that fails. 714 if(!this.keyTyped(detail)) 715 insertText(char); 716 // Post-typing logic. 717 switch(char) { 718 case '\n': 719 case ' ': 720 if(enterUpperOnSpace) { 721 enterUpperOnSpace = false; 722 if (this.shift) { 723 var shiftDetail = this.shift.onSpaceAfterPunctuation(); 724 // Check if transition defined. 725 this.changeKeyset(shiftDetail); 726 } else { 727 console.error('Capitalization on space after punctuation \ 728 enabled, but cannot find target keyset.'); 729 } 730 // Immediately return to maintain shift-state. Space is a 731 // non-control key and would otherwise trigger a reset of the 732 // shift key, causing a transition to lower case. 733 return; 734 } 735 break; 736 case '.': 737 case '?': 738 case '!': 739 enterUpperOnSpace = this.shouldUpperOnSpace(); 740 break; 741 default: 742 enterUpperOnSpace = false; 743 break; 744 } 745 // Reset control keys. 746 this.onNonControlKeyTyped(); 747 }, 748 749 /* 750 * Handles key-longpress event that is sent by kb-key-base. 751 * @param {CustomEvent} event The key-longpress event dispatched by 752 * kb-key-base. 753 * @param {Object} detail The detail of pressed key. 754 */ 755 keyLongpress: function(event, detail) { 756 // If the gesture is long press, remove the pointermove listener. 757 this.removeEventListener('pointermove', this.swipeHandler, false); 758 // Keyset transtion key. 759 if (this.changeKeyset(detail)) { 760 // Locks the keyset before removing active to prevent flicker. 761 this.classList.add('caps-locked'); 762 // Makes last pressed key inactive if transit to a new keyset on long 763 // press. 764 if (this.lastPressedKey) 765 this.lastPressedKey.classList.remove('active'); 766 } 767 }, 768 769 /** 770 * Plays the specified sound. 771 * @param {Sound} sound The id of the audio tag. 772 */ 773 playSound: function(sound) { 774 if (!SOUND_ENABLED || !sound || sound == Sound.NONE) 775 return; 776 var pool = this.sounds[sound]; 777 if (!pool) { 778 console.error("Cannot find audio tag: " + sound); 779 return; 780 } 781 // Search the sound pool for a free resource. 782 for (var i = 0; i < pool.length; i++) { 783 if (pool[i].paused) { 784 pool[i].play(); 785 return; 786 } 787 } 788 }, 789 790 /** 791 * Whether we should transit to upper case when seeing a space after 792 * punctuation. 793 * @return {boolean} 794 */ 795 shouldUpperOnSpace: function() { 796 // TODO(rsadam): Add other input types in which we should not 797 // transition to upper after a space. 798 return this.inputTypeValue != 'password'; 799 }, 800 801 /** 802 * Handler for the 'set-layout' event. 803 * @param {!Event} event The triggering event. 804 * @param {{layout: string}} details Details of the event, which contains 805 * the name of the layout to activate. 806 */ 807 setLayout: function(event, details) { 808 this.layout = details.layout; 809 }, 810 811 /** 812 * Handles a change in the keyboard layout. Auto-selects the default 813 * keyset for the new layout. 814 */ 815 layoutChanged: function() { 816 this.stale = true; 817 if (!this.selectDefaultKeyset()) { 818 console.error('No default keyset found for layout: ' + this.layout); 819 return; 820 } 821 this.activeKeyset.show(); 822 }, 823 824 /** 825 * Notifies the modifier keys that a non-control key was typed. This 826 * lets them reset sticky behaviour. A non-control key is defined as 827 * any key that is not Control, Alt, or Shift. 828 */ 829 onNonControlKeyTyped: function() { 830 if (this.shift) 831 this.shift.onNonControlKeyTyped(); 832 if (this.ctrl) 833 this.ctrl.onNonControlKeyTyped(); 834 if (this.alt) 835 this.alt.onNonControlKeyTyped(); 836 this.classList.remove('ctrl-active'); 837 this.classList.remove('alt-active'); 838 }, 839 840 /** 841 * Callback function for when volume is changed. 842 */ 843 volumeChanged: function() { 844 var toChange = Object.keys(this.sounds); 845 for (var i = 0; i < toChange.length; i++) { 846 var pool = this.sounds[toChange[i]]; 847 for (var j = 0; j < pool.length; j++) { 848 pool[j].volume = this.volume; 849 } 850 } 851 }, 852 853 /** 854 * Id for the active keyset. 855 * @type {string} 856 */ 857 get activeKeysetId() { 858 return this.layout + '-' + this.keyset; 859 }, 860 861 /** 862 * The active keyset DOM object. 863 * @type {kb-keyset} 864 */ 865 get activeKeyset() { 866 return this.querySelector('#' + this.activeKeysetId); 867 }, 868 869 /** 870 * The current input type. 871 * @type {string} 872 */ 873 get inputTypeValue() { 874 return this.inputType; 875 }, 876 877 /** 878 * Changes the input type if it's different from the current 879 * type, else resets the keyset to the default keyset. 880 * @type {string} 881 */ 882 set inputTypeValue(value) { 883 if (value == this.inputType) 884 this.selectDefaultKeyset(); 885 else 886 this.inputType = value; 887 }, 888 889 /** 890 * The keyboard is ready for input once the target keyset appears 891 * in the distributed nodes for the keyboard. 892 * @return {boolean} Indicates if the keyboard is ready for input. 893 */ 894 isReady: function() { 895 var keyset = this.activeKeyset; 896 if (!keyset) 897 return false; 898 var nodes = this.$.content.getDistributedNodes(); 899 for (var i = 0; i < nodes.length; i++) { 900 if (nodes[i].id && nodes[i].id == keyset.id) 901 return true; 902 } 903 return false; 904 }, 905 906 /** 907 * Generates fabricated key events to simulate typing on a 908 * physical keyboard. 909 * @param {Object} detail Attributes of the key being typed. 910 * @return {boolean} Whether the key type succeeded. 911 */ 912 keyTyped: function(detail) { 913 var builder = this.$.keyCodeMetadata; 914 if (this.ctrl) 915 detail.controlModifier = this.ctrl.isActive(); 916 if (this.alt) 917 detail.altModifier = this.alt.isActive(); 918 var downEvent = builder.createVirtualKeyEvent(detail, "keydown"); 919 if (downEvent) { 920 sendKeyEvent(downEvent); 921 sendKeyEvent(builder.createVirtualKeyEvent(detail, "keyup")); 922 return true; 923 } 924 return false; 925 }, 926 927 /** 928 * Selects the default keyset for a layout. 929 * @return {boolean} True if successful. This method can fail if the 930 * keysets corresponding to the layout have not been injected. 931 */ 932 selectDefaultKeyset: function() { 933 var keysets = this.querySelectorAll('kb-keyset'); 934 // Full name of the keyset is of the form 'layout-keyset'. 935 var regex = new RegExp('^' + this.layout + '-(.+)'); 936 var keysetsLoaded = false; 937 for (var i = 0; i < keysets.length; i++) { 938 var matches = keysets[i].id.match(regex); 939 if (matches && matches.length == REGEX_MATCH_COUNT) { 940 keysetsLoaded = true; 941 // Without both tests for a default keyset, it is possible to get 942 // into a state where multiple layouts are displayed. A 943 // reproducable test case is do the following set of keyset 944 // transitions: qwerty -> system -> dvorak -> qwerty. 945 // TODO(kevers): Investigate why this is the case. 946 if (keysets[i].isDefault || 947 keysets[i].getAttribute('isDefault') == 'true') { 948 this.keyset = matches[REGEX_KEYSET_INDEX]; 949 this.classList.remove('caps-locked'); 950 this.classList.remove('alt-active'); 951 this.classList.remove('ctrl-active'); 952 // Caches shift key. 953 this.shift = this.querySelector('kb-shift-key'); 954 if (this.shift) 955 this.shift.reset(); 956 // Caches control key. 957 this.ctrl = this.querySelector('kb-modifier-key[char=Ctrl]'); 958 if (this.ctrl) 959 this.ctrl.reset(); 960 // Caches alt key. 961 this.alt = this.querySelector('kb-modifier-key[char=Alt]'); 962 if (this.alt) 963 this.alt.reset(); 964 this.fire('stateChange', { 965 state: 'keysetLoaded', 966 value: this.keyset, 967 }); 968 keyboardLoaded(); 969 return true; 970 } 971 } 972 } 973 if (keysetsLoaded) 974 console.error('No default keyset found for ' + this.layout); 975 return false; 976 } 977}); 978