Home | History | Annotate | Download | only in elements
      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  */
     12 var 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  */
     20 var REPEAT_INTERVAL_MSEC = 50;
     21 
     22 /**
     23  * The double click/tap interval.
     24  * @const
     25  * @type {number}
     26  */
     27 var 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  */
     34 var REGEX_KEYSET_INDEX = 1;
     35 
     36 /**
     37  * The integer number of matches when searching for keysets.
     38  * @const
     39  * @type {number}
     40  */
     41 var 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  */
     49 var enterUpperOnSpace = false;
     50 
     51 /**
     52  * A structure to track the currently repeating key on the keyboard.
     53  */
     54 var 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  */
     92 var MIN_SWIPE_DIST_X = 50;
     93 var 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  */
    101 var MAX_SWIPE_FLICK_DIST = 60;
    102 
    103 /**
    104  * The boolean to decide if it is swipe in process or finished.
    105  * @type {boolean}
    106  */
    107 var swipeInProgress = false;
    108 
    109 // Flag values for ctrl, alt and shift as defined by EventFlags
    110 // in "event_constants.h".
    111 // @enum {number}
    112 var 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  */
    122 var 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 
    269 Polymer('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