Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / keyboard / resources / elements / kb-keyboard.js
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 });