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