Update mojo sdk to rev 1dc8a9a5db73d3718d99917fadf31f5fb2ebad4f
[chromium-blink-merge.git] / third_party / google_input_tools / src / chrome / os / inputview / controller.js
blobdbf7a42da86a512125a0ab2994fba5e0f71271cb
1 // Copyright 2014 The ChromeOS IME Authors. All Rights Reserved.
2 // limitations under the License.
3 // See the License for the specific language governing permissions and
4 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5 // distributed under the License is distributed on an "AS-IS" BASIS,
6 // Unless required by applicable law or agreed to in writing, software
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // You may obtain a copy of the License at
11 // you may not use this file except in compliance with the License.
12 // Licensed under the Apache License, Version 2.0 (the "License");
14 goog.provide('i18n.input.chrome.inputview.Controller');
16 goog.require('goog.Disposable');
17 goog.require('goog.Timer');
18 goog.require('goog.array');
19 goog.require('goog.async.Delay');
20 goog.require('goog.dom.classlist');
21 goog.require('goog.events.Event');
22 goog.require('goog.events.EventHandler');
23 goog.require('goog.events.EventType');
24 goog.require('goog.events.KeyCodes');
25 goog.require('goog.i18n.bidi');
26 goog.require('goog.object');
27 goog.require('i18n.input.chrome.DataSource');
28 goog.require('i18n.input.chrome.Statistics');
29 goog.require('i18n.input.chrome.inputview.Adapter');
30 goog.require('i18n.input.chrome.inputview.CandidatesInfo');
31 goog.require('i18n.input.chrome.inputview.ConditionName');
32 goog.require('i18n.input.chrome.inputview.Css');
33 goog.require('i18n.input.chrome.inputview.FeatureName');
34 goog.require('i18n.input.chrome.inputview.KeyboardContainer');
35 goog.require('i18n.input.chrome.inputview.M17nModel');
36 goog.require('i18n.input.chrome.inputview.Model');
37 goog.require('i18n.input.chrome.inputview.PerfTracker');
38 goog.require('i18n.input.chrome.inputview.ReadyState');
39 goog.require('i18n.input.chrome.inputview.Settings');
40 goog.require('i18n.input.chrome.inputview.SizeSpec');
41 goog.require('i18n.input.chrome.inputview.SpecNodeName');
42 goog.require('i18n.input.chrome.inputview.StateType');
43 goog.require('i18n.input.chrome.inputview.SwipeDirection');
44 goog.require('i18n.input.chrome.inputview.elements.ElementType');
45 goog.require('i18n.input.chrome.inputview.elements.content.Candidate');
46 goog.require('i18n.input.chrome.inputview.elements.content.CandidateView');
47 goog.require('i18n.input.chrome.inputview.elements.content.ExpandedCandidateView');
48 goog.require('i18n.input.chrome.inputview.elements.content.MenuView');
49 goog.require('i18n.input.chrome.inputview.events.EventType');
50 goog.require('i18n.input.chrome.inputview.events.KeyCodes');
51 goog.require('i18n.input.chrome.inputview.handler.PointerHandler');
52 goog.require('i18n.input.chrome.inputview.util');
53 goog.require('i18n.input.chrome.message.ContextType');
54 goog.require('i18n.input.chrome.message.Name');
55 goog.require('i18n.input.chrome.message.Type');
56 goog.require('i18n.input.chrome.sounds.SoundController');
57 goog.require('i18n.input.lang.InputToolCode');
61 goog.scope(function() {
62 var CandidateType = i18n.input.chrome.inputview.elements.content.Candidate.Type;
63 var CandidateView = i18n.input.chrome.inputview.elements.content.CandidateView;
64 var ConditionName = i18n.input.chrome.inputview.ConditionName;
65 var ContextType = i18n.input.chrome.message.ContextType;
66 var Css = i18n.input.chrome.inputview.Css;
67 var ElementType = i18n.input.chrome.inputview.elements.ElementType;
68 var EventType = i18n.input.chrome.inputview.events.EventType;
69 var ExpandedCandidateView = i18n.input.chrome.inputview.elements.content.
70     ExpandedCandidateView;
71 var FeatureName = i18n.input.chrome.inputview.FeatureName;
72 var InputToolCode = i18n.input.lang.InputToolCode;
73 var KeyCodes = i18n.input.chrome.inputview.events.KeyCodes;
74 var MenuView = i18n.input.chrome.inputview.elements.content.MenuView;
75 var Name = i18n.input.chrome.message.Name;
76 var PerfTracker = i18n.input.chrome.inputview.PerfTracker;
77 var SizeSpec = i18n.input.chrome.inputview.SizeSpec;
78 var SpecNodeName = i18n.input.chrome.inputview.SpecNodeName;
79 var StateType = i18n.input.chrome.inputview.StateType;
80 var SoundController = i18n.input.chrome.sounds.SoundController;
81 var Type = i18n.input.chrome.message.Type;
82 var util = i18n.input.chrome.inputview.util;
86 /**
87  * The controller of the input view keyboard.
88  *
89  * @param {string} keyset The keyboard keyset.
90  * @param {string} languageCode The language code for this keyboard.
91  * @param {string} passwordLayout The layout for password box.
92  * @param {string} name The input tool name.
93  * @constructor
94  * @extends {goog.Disposable}
95  */
96 i18n.input.chrome.inputview.Controller = function(keyset, languageCode,
97     passwordLayout, name) {
98   /**
99    * The model.
100    *
101    * @type {!i18n.input.chrome.inputview.Model}
102    * @private
103    */
104   this.model_ = new i18n.input.chrome.inputview.Model();
106   /** @private {!i18n.input.chrome.inputview.PerfTracker} */
107   this.perfTracker_ = new i18n.input.chrome.inputview.PerfTracker(
108       PerfTracker.TickName.HTML_LOADED);
110   /**
111    * The layout map.
112    *
113    * @type {!Object.<string, !Object>}
114    * @private
115    */
116   this.layoutDataMap_ = {};
118   /**
119    * The element map.
120    *
121    * @private {!Object.<ElementType, !KeyCodes>}
122    */
123   this.elementTypeToKeyCode_ = goog.object.create(
124       ElementType.BOLD, KeyCodes.KEY_B,
125       ElementType.ITALICS, KeyCodes.KEY_I,
126       ElementType.UNDERLINE, KeyCodes.KEY_U,
127       ElementType.COPY, KeyCodes.KEY_C,
128       ElementType.PASTE, KeyCodes.KEY_V,
129       ElementType.CUT, KeyCodes.KEY_X,
130       ElementType.SELECT_ALL, KeyCodes.KEY_A,
131       ElementType.REDO, KeyCodes.KEY_Y,
132       ElementType.UNDO, KeyCodes.KEY_Z
133       );
135   /**
136    * The keyset data map.
137    *
138    * @type {!Object.<string, !Object>}
139    * @private
140    */
141   this.keysetDataMap_ = {};
143   /**
144    * The event handler.
145    *
146    * @type {!goog.events.EventHandler}
147    * @private
148    */
149   this.handler_ = new goog.events.EventHandler(this);
151   /**
152    * The m17n model.
153    *
154    * @type {!i18n.input.chrome.inputview.M17nModel}
155    * @private
156    */
157   this.m17nModel_ = new i18n.input.chrome.inputview.M17nModel();
159   /**
160    * The pointer handler.
161    *
162    * @type {!i18n.input.chrome.inputview.handler.PointerHandler}
163    * @private
164    */
165   this.pointerHandler_ = new i18n.input.chrome.inputview.handler.
166       PointerHandler();
168   /**
169    * The statistics object for recording metrics values.
170    *
171    * @type {!i18n.input.chrome.Statistics}
172    * @private
173    */
174   this.statistics_ = i18n.input.chrome.Statistics.getInstance();
176   /** @private {!i18n.input.chrome.inputview.ReadyState} */
177   this.readyState_ = new i18n.input.chrome.inputview.ReadyState();
179   /** @private {!i18n.input.chrome.inputview.Adapter} */
180   this.adapter_ = new i18n.input.chrome.inputview.Adapter(this.readyState_);
182   /** @private {!SoundController} */
183   this.soundController_ = new SoundController(false);
185   /** @private {!i18n.input.chrome.inputview.KeyboardContainer} */
186   this.container_ = new i18n.input.chrome.inputview.KeyboardContainer(
187       this.adapter_, this.soundController_);
188   this.container_.render();
190   /**
191    * The context type and keyset mapping group by input method id.
192    * key: input method id.
193    * value: Object
194    *    key: context type string.
195    *    value: keyset string.
196    *
197    * @private {!Object.<string, !Object.<string, string>>}
198    */
199   this.contextTypeToKeysetMap_ = {};
202   /**
203    * The previous raw keyset code before switched to hwt or emoji layout.
204    *  key: context type string.
205    *  value: keyset string.
206    *
207    * @private {!Object.<string, string>}
208    */
209   this.contextTypeToLastKeysetMap_ = {};
211   /**
212    * The stats map for input view closing.
213    *
214    * @type {!Object.<string, !Array.<number>>}
215    * @private
216    */
217   this.statsForClosing_ = {};
219   /**
220    * The last height sent to window.resizeTo to avoid multiple equivalent calls.
221    *
222    * @private {number}
223    */
224   this.lastResizeHeight_ = -1;
226   /**
227    * The activate (show) time stamp for statistics.
228    *
229    * @type {Date}
230    * @private
231    */
232   this.showTimeStamp_ = new Date();
234   this.initialize(keyset, languageCode, passwordLayout, name);
235   /**
236    * The suggestions.
237    * Note: sets a default empty result to avoid null check.
238    *
239    * @private {!i18n.input.chrome.inputview.CandidatesInfo}
240    */
241   this.candidatesInfo_ = i18n.input.chrome.inputview.CandidatesInfo.getEmpty();
243   this.registerEventHandler_();
245 goog.inherits(i18n.input.chrome.inputview.Controller,
246     goog.Disposable);
247 var Controller = i18n.input.chrome.inputview.Controller;
251  * @define {boolean} Flag to disable handwriting.
252  */
253 Controller.DISABLE_HWT = false;
257  * A flag to indicate whether the shift is for auto capital.
259  * @private {boolean}
260  */
261 Controller.prototype.shiftForAutoCapital_ = false;
265  * @define {boolean} Flag to indicate whether it is debugging.
266  */
267 Controller.DEV = false;
271  * The handwriting view code, use the code can switch handwriting panel view.
273  * @const {string}
274  * @private
275  */
276 Controller.HANDWRITING_VIEW_CODE_ = 'hwt';
280  * The emoji view code, use the code can switch to emoji.
282  * @const {string}
283  * @private
284  */
285 Controller.EMOJI_VIEW_CODE_ = 'emoji';
289  * The limitation for backspace repeat time to avoid unexpected
290  * problem that backspace is held all the time.
292  * @private {number}
293  */
294 Controller.BACKSPACE_REPEAT_LIMIT_ = 255;
298  * The repeated times of the backspace.
300  * @private {number}
301  */
302 Controller.prototype.backspaceRepeated_ = 0;
306  * The handwriting input tool code suffix.
308  * @const {string}
309  * @private
310  */
311 Controller.HANDWRITING_CODE_SUFFIX_ = '-t-i0-handwrit';
315  * True if the settings is loaded.
317  * @type {boolean}
318  */
319 Controller.prototype.isSettingReady = false;
323  * True if the keyboard is set up.
324  * Note: This flag is only used for automation testing.
326  * @type {boolean}
327  */
328 Controller.prototype.isKeyboardReady = false;
332  * The auto repeat timer for backspace hold.
334  * @type {goog.async.Delay}
335  * @private
336  */
337 Controller.prototype.backspaceAutoRepeat_;
341  * The initial keyset determined by inputview url and/or settings.
343  * @type {string}
344  * @private
345  */
346 Controller.prototype.initialKeyset_ = '';
350  * The current raw keyset code.
352  * @type {string}
353  * @private
354  */
355 Controller.prototype.currentKeyset_ = '';
359  * The current input method id.
361  * @private {string}
362  */
363 Controller.prototype.currentInputmethod_ = '';
367  * The operations on candidates.
369  * @enum {number}
370  */
371 Controller.CandidatesOperation = {
372   NONE: 0,
373   EXPAND: 1,
374   SHRINK: 2
379  * A temporary list to track keysets have customized in material design.
381  * @private {!Array.<string>}
382  */
383 Controller.MATERIAL_KEYSETS_ = [
384   'emoji',
385   'hwt'
390  * The active language code.
392  * @type {string}
393  * @private
394  */
395 Controller.prototype.lang_;
399  * The password keyset.
401  * @private {string}
402  */
403 Controller.prototype.passwordKeyset_ = '';
407  * The soft key map, because key configuration is loaded before layout,
408  * controller needs this varaible to save it and hook into keyboard view.
410  * @type {!Array.<!i18n.input.chrome.inputview.elements.content.SoftKey>}
411  * @private
412  */
413 Controller.prototype.softKeyList_;
417  * The mapping from soft key id to soft key view id.
419  * @type {!Object.<string, string>}
420  * @private
421  */
422 Controller.prototype.mapping_;
426  * The input tool name.
428  * @type {string}
429  * @private
430  */
431 Controller.prototype.title_;
435  * Registers event handlers.
436  * @private
437  */
438 Controller.prototype.registerEventHandler_ = function() {
439   this.handler_.
440       listen(this.model_,
441           EventType.LAYOUT_LOADED,
442           this.onLayoutLoaded_).
443       listen(this.model_,
444           EventType.CONFIG_LOADED,
445           this.onConfigLoaded_).
446       listen(this.m17nModel_,
447           EventType.CONFIG_LOADED,
448           this.onConfigLoaded_).
449       listen(this.pointerHandler_, [
450             EventType.LONG_PRESS,
451             EventType.CLICK,
452             EventType.DOUBLE_CLICK,
453             EventType.DOUBLE_CLICK_END,
454             EventType.POINTER_UP,
455             EventType.POINTER_DOWN,
456             EventType.POINTER_OVER,
457             EventType.POINTER_OUT,
458             EventType.SWIPE
459           ], this.onPointerEvent_).
460       listen(this.pointerHandler_,
461           EventType.DRAG,
462           this.onDragEvent_).
463       listen(window, goog.events.EventType.RESIZE, this.resize).
464       listen(this.adapter_,
465           EventType.SURROUNDING_TEXT_CHANGED, this.onSurroundingTextChanged_).
466       listen(this.adapter_,
467           i18n.input.chrome.DataSource.EventType.CANDIDATES_BACK,
468           this.onCandidatesBack_).
469       listen(this.adapter_, EventType.URL_CHANGED, this.onURLChanged_).
470       listen(this.adapter_, EventType.CONTEXT_FOCUS, this.onContextFocus_).
471       listen(this.adapter_, EventType.CONTEXT_BLUR, this.onContextBlur_).
472       listen(this.adapter_, EventType.VISIBILITY_CHANGE,
473           this.onVisibilityChange_).
474       listen(this.adapter_, EventType.SETTINGS_READY, this.onSettingsReady_).
475       listen(this.adapter_, Type.UPDATE_SETTINGS, this.onUpdateSettings_).
476       listen(this.adapter_, Type.FRONT_TOGGLE_LANGUAGE_STATE,
477           this.onUpdateToggleLanguateState_).
478       listen(this.adapter_, Type.VOICE_STATE_CHANGE, this.onVoiceStateChange_).
479       listen(this.adapter_, EventType.REFRESH, this.onRefresh_);
484  * Handler for voice module state change.
486  * @param {!i18n.input.chrome.message.Event} e .
487  * @private
488  */
489 Controller.prototype.onVoiceStateChange_ = function(e) {
490   if (!e.msg[Name.VOICE_STATE]) {
491     this.container_.candidateView.switchToIcon(
492         CandidateView.IconType.VOICE, true);
493     this.container_.voiceView.stop();
494   }
499  * Handles the refresh event from adapter.
501  * @private
502  */
503 Controller.prototype.onRefresh_ = function() {
504   window.location.reload();
509  * Sets the default keyset for context types.
511  * @param {string} newKeyset .
512  * @private
513  */
514 Controller.prototype.setDefaultKeyset_ = function(newKeyset) {
515   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
516   for (var context in keysetMap) {
517     if (context != ContextType.DEFAULT &&
518         keysetMap[context] == keysetMap[ContextType.DEFAULT]) {
519       keysetMap[context] = newKeyset;
520     }
521   }
522   keysetMap[ContextType.DEFAULT] = this.initialKeyset_ = newKeyset;
527  * Callback for updating settings.
529  * @param {!i18n.input.chrome.message.Event} e .
530  * @private
531  */
532 Controller.prototype.onUpdateSettings_ = function(e) {
533   var settings = this.model_.settings;
534   if (goog.isDef(e.msg['autoSpace'])) {
535     settings.autoSpace = e.msg['autoSpace'];
536   }
537   if (goog.isDef(e.msg['autoCapital'])) {
538     settings.autoCapital = e.msg['autoCapital'];
539   }
540   if (goog.isDef(e.msg['candidatesNavigation'])) {
541     settings.candidatesNavigation = e.msg['candidatesNavigation'];
542     this.container_.candidateView.setNavigation(settings.candidatesNavigation);
543   }
544   if (goog.isDef(e.msg[Name.KEYSET])) {
545     this.setDefaultKeyset_(e.msg[Name.KEYSET]);
546   }
547   if (goog.isDef(e.msg['enableLongPress'])) {
548     settings.enableLongPress = e.msg['enableLongPress'];
549   }
550   if (goog.isDef(e.msg['doubleSpacePeriod'])) {
551     settings.doubleSpacePeriod = e.msg['doubleSpacePeriod'];
552   }
553   if (goog.isDef(e.msg['soundOnKeypress'])) {
554     settings.soundOnKeypress = e.msg['soundOnKeypress'];
555     this.soundController_.setEnabled(settings.soundOnKeypress);
556   }
557   this.perfTracker_.tick(PerfTracker.TickName.BACKGROUND_SETTINGS_FETCHED);
558   this.model_.stateManager.contextType = this.adapter_.contextType;
559   this.maybeCreateViews_();
564  * Callback for url changed.
566  * @private
567  */
568 Controller.prototype.onURLChanged_ = function() {
569   this.container_.candidateView.setToolbarVisible(this.shouldShowToolBar_());
574  * Callback for setting ready.
576  * @private
577  */
578 Controller.prototype.onSettingsReady_ = function() {
579   if (this.isSettingReady) {
580     return;
581   }
583   this.isSettingReady = true;
584   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
585   var newKeyset = '';
586   if (this.adapter_.isA11yMode) {
587     newKeyset = util.getConfigName(keysetMap[ContextType.DEFAULT]);
588   } else {
589     newKeyset = /** @type {string} */ (this.model_.settings.
590         getPreference(util.getConfigName(keysetMap[ContextType.DEFAULT])));
591   }
592   if (!this.adapter_.features.isEnabled(FeatureName.EXPERIMENTAL) &&
593       keysetMap[ContextType.DEFAULT] ==
594       'zhuyin.compact.qwerty') {
595     newKeyset = 'zhuyin';
596   }
597   if (newKeyset) {
598     this.setDefaultKeyset_(newKeyset);
599   }
600   this.container_.selectView.setVisible(
601       this.adapter_.features.isEnabled(FeatureName.GESTURE_SELECTION));
602   // Loads resources in case the default keyset is changed.
603   this.loadAllResources_();
604   this.maybeCreateViews_();
609  * Gets the data for spatial module.
611  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} key .
612  * @param {number} x The x-offset of the touch point.
613  * @param {number} y The y-offset of the touch point.
614  * @return {!Object} .
615  * @private
616  */
617 Controller.prototype.getSpatialData_ = function(key, x, y) {
618   var items = [];
619   items.push([this.getKeyContent_(key), key.estimator.estimateInLogSpace(x, y)
620       ]);
621   for (var i = 0; i < key.nearbyKeys.length; i++) {
622     var nearByKey = key.nearbyKeys[i];
623     var content = this.getKeyContent_(nearByKey);
624     if (content && util.REGEX_LANGUAGE_MODEL_CHARACTERS.test(content)) {
625       items.push([content, nearByKey.estimator.estimateInLogSpace(x, y)]);
626     }
627   }
628   goog.array.sort(items, function(item1, item2) {
629     return item1[1] - item2[1];
630   });
631   var sources = items.map(function(item) {
632     return item[0].toLowerCase();
633   });
634   var possibilities = items.map(function(item) {
635     return item[1];
636   });
637   return {
638     'sources': sources,
639     'possibilities': possibilities
640   };
645  * Gets the key content.
647  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} key .
648  * @return {string} .
649  * @private
650  */
651 Controller.prototype.getKeyContent_ = function(key) {
652   if (key.type == i18n.input.chrome.inputview.elements.ElementType.
653       CHARACTER_KEY) {
654     key = /** @type {!i18n.input.chrome.inputview.elements.content.
655         CharacterKey} */ (key);
656     return key.getActiveCharacter();
657   }
658   if (key.type == i18n.input.chrome.inputview.elements.ElementType.
659       COMPACT_KEY) {
660     key = /** @type {!i18n.input.chrome.inputview.elements.content.
661         FunctionalKey} */ (key);
662     return key.text;
663   }
664   return '';
669  * Callback for pointer event.
671  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
672  * @private
673  */
674 Controller.prototype.onPointerEvent_ = function(e) {
675   if (e.type == EventType.LONG_PRESS) {
676     if (this.adapter_.isChromeVoxOn || !this.model_.settings.enableLongPress) {
677       return;
678     }
679     var keyset = this.keysetDataMap_[this.currentKeyset_];
680     var layout = keyset && keyset[SpecNodeName.LAYOUT];
681     var data = layout && this.layoutDataMap_[layout];
682     if (data && data[SpecNodeName.DISABLE_LONGPRESS]) {
683       return;
684     }
685   }
687   // POINTER_UP event may be dispatched without a view. This is the case when
688   // user selected an accent character which is displayed outside of the
689   // keyboard window bounds. For other cases, we expect a view associated with a
690   // pointer up event.
691   if (e.type == EventType.POINTER_UP && !e.view) {
692     if (this.container_.altDataView.isVisible() &&
693         e.identifier == this.container_.altDataView.identifier) {
694       var altDataView = this.container_.altDataView;
695       var ch = altDataView.getHighlightedCharacter();
696       if (ch) {
697         this.adapter_.sendKeyDownAndUpEvent(ch, altDataView.triggeredBy.id,
698             altDataView.triggeredBy.keyCode,
699             {'sources': [ch.toLowerCase()], 'possibilities': [1]});
700       }
701       altDataView.hide();
702       this.clearUnstickyState_();
703     }
704     return;
705   }
707   if (e.type == EventType.POINTER_UP) {
708     this.stopBackspaceAutoRepeat_();
709   }
711   if (e.view) {
712     this.handlePointerAction_(e.view, e);
713   }
718  * Handles the drag events. Generally, this will forward the event details to
719  * the components that handle drawing, decoding, etc.
721  * @param {!i18n.input.chrome.inputview.events.DragEvent} e .
722  * @private
723  */
724 Controller.prototype.onDragEvent_ = function(e) {
725   if (this.adapter_.isGestureTypingEnabled() && e.type == EventType.DRAG) {
726     this.container_.gestureCanvasView.addPoint(e);
727     return;
728   }
733  * Handles the swipe action.
735  * @param {!i18n.input.chrome.inputview.elements.Element} view The view, for
736  *     swipe event, the view would be the soft key which starts the swipe.
737  * @param {!i18n.input.chrome.inputview.events.SwipeEvent} e The swipe event.
738  * @private
739  */
740 Controller.prototype.handleSwipeAction_ = function(view, e) {
741   var direction = e.direction;
742   if (this.container_.altDataView.isVisible()) {
743     this.container_.altDataView.highlightItem(e.x, e.y, e.identifier);
744     return;
745   }
746   if (view.type == ElementType.BACKSPACE_KEY) {
747     if (this.container_.swipeView.isVisible() ||
748         this.container_.swipeView.isArmed()) {
749       this.stopBackspaceAutoRepeat_();
750       return;
751     }
752   }
754   if (view.type == ElementType.CHARACTER_KEY) {
755     view = /** @type {!i18n.input.chrome.inputview.elements.content.
756         CharacterKey} */ (view);
757     if (direction & i18n.input.chrome.inputview.SwipeDirection.UP ||
758         direction & i18n.input.chrome.inputview.SwipeDirection.DOWN) {
759       var ch = view.getCharacterByGesture(!!(direction &
760           i18n.input.chrome.inputview.SwipeDirection.UP));
761       if (ch) {
762         view.flickerredCharacter = ch;
763       }
764     }
765   }
767   if (view.type == ElementType.COMPACT_KEY) {
768     view = /** @type {!i18n.input.chrome.inputview.elements.content.
769         CompactKey} */ (view);
770     if ((direction & i18n.input.chrome.inputview.SwipeDirection.UP) &&
771         view.hintText) {
772       view.flickerredCharacter = view.hintText;
773     }
774   }
779  * Execute a command.
781  * @param {!i18n.input.chrome.inputview.elements.content.MenuView.Command}
782  *     command The command that about to be executed.
783  * @param {string=} opt_arg The optional command argument.
784  * @private
785  */
786 Controller.prototype.executeCommand_ = function(command, opt_arg) {
787   var CommandEnum = MenuView.Command;
788   switch (command) {
789     case CommandEnum.SWITCH_IME:
790       var inputMethodId = opt_arg;
791       if (inputMethodId) {
792         this.adapter_.switchToInputMethod(inputMethodId);
793       }
794       break;
796     case CommandEnum.SWITCH_KEYSET:
797       var keyset = opt_arg;
798       if (keyset) {
799         this.recordStatsForClosing_(
800             'InputMethod.VirtualKeyboard.LayoutSwitch', 1, 25, 25);
801         this.switchToKeyset(keyset);
802       }
803       break;
804     case CommandEnum.OPEN_EMOJI:
805       this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
806           this.currentKeyset_;
807       this.switchToKeyset(Controller.EMOJI_VIEW_CODE_);
808       break;
810     case CommandEnum.OPEN_HANDWRITING:
811       this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
812           this.currentKeyset_;
813       // TODO: remember handwriting keyset.
814       this.switchToKeyset(Controller.HANDWRITING_VIEW_CODE_);
815       break;
817     case CommandEnum.OPEN_SETTING:
818       if (window.inputview) {
819         inputview.openSettings();
820       }
821       break;
822   }
827  * Handles the pointer action.
829  * @param {!i18n.input.chrome.inputview.elements.Element} view The view.
830  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
831  * @private
832  */
833 Controller.prototype.handlePointerAction_ = function(view, e) {
834   if (this.adapter_.isGestureTypingEnabled() &&
835       e.type == EventType.POINTER_DOWN) {
836     this.container_.gestureCanvasView.startStroke(e);
837   }
839   if (this.adapter_.isGestureTypingEnabled() &&
840       e.type == EventType.POINTER_UP) {
841     this.container_.gestureCanvasView.endStroke(e);
842   }
844   // Do not trigger other actives when gesturing.
845   if (this.adapter_.isGestureTypingEnabled() &&
846       this.container_.gestureCanvasView.isGesturing) {
847     return;
848   }
850   // Listen for DOUBLE_CLICK as well to capture secondary taps on the spacebar.
851   if (e.type == EventType.POINTER_UP || e.type == EventType.DOUBLE_CLICK) {
852     this.recordStatsForClosing_(
853         'InputMethod.VirtualKeyboard.TapCount', 1, 4095, 4096);
854   }
856   if (e.type == EventType.SWIPE) {
857     e =  /** @type {!i18n.input.chrome.inputview.events.SwipeEvent} */ (e);
858     this.handleSwipeAction_(view, e);
859   }
860   switch (view.type) {
861     case ElementType.KEYBOARD_CONTAINER_VIEW:
862       if (e.type == EventType.POINTER_DOWN) {
863         var tabbableKeysets = [
864           Controller.HANDWRITING_VIEW_CODE_,
865           Controller.EMOJI_VIEW_CODE_];
866         if (goog.array.contains(tabbableKeysets, this.currentKeyset_)) {
867           this.resetAll();
868           this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
869         }
870       }
871       return;
872     case ElementType.BACK_BUTTON:
873     case ElementType.BACK_TO_KEYBOARD:
874       if (e.type == EventType.POINTER_OUT || e.type == EventType.POINTER_UP) {
875         view.setHighlighted(false);
876       } else if (e.type == EventType.POINTER_DOWN ||
877           e.type == EventType.POINTER_OVER) {
878         view.setHighlighted(true);
879       }
880       if (e.type == EventType.POINTER_UP) {
881         this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
882         this.clearCandidates_();
883         this.soundController_.onKeyUp(view.type);
884       }
885       return;
886     case ElementType.EXPAND_CANDIDATES:
887       if (e.type == EventType.POINTER_UP) {
888         this.showCandidates_(this.candidatesInfo_.source,
889             this.candidatesInfo_.candidates,
890             Controller.CandidatesOperation.EXPAND);
891         this.soundController_.onKeyUp(view.type);
892       }
893       return;
894     case ElementType.SHRINK_CANDIDATES:
895       if (e.type == EventType.POINTER_UP) {
896         this.showCandidates_(this.candidatesInfo_.source,
897             this.candidatesInfo_.candidates,
898             Controller.CandidatesOperation.SHRINK);
899         this.soundController_.onKeyUp(view.type);
900       }
901       return;
902     case ElementType.CANDIDATE:
903       view = /** @type {!i18n.input.chrome.inputview.elements.content.
904           Candidate} */ (view);
905       if (e.type == EventType.POINTER_UP) {
906         if (view.candidateType == CandidateType.CANDIDATE) {
907           this.adapter_.selectCandidate(view.candidate);
908         } else if (view.candidateType == CandidateType.NUMBER) {
909           this.adapter_.commitText(view.candidate[Name.CANDIDATE]);
910         }
911         this.container_.cleanStroke();
912         this.soundController_.onKeyUp(ElementType.CANDIDATE);
913       }
914       if (e.type == EventType.POINTER_OUT || e.type == EventType.POINTER_UP) {
915         view.setHighlighted(false);
916       } else if (e.type == EventType.POINTER_DOWN ||
917           e.type == EventType.POINTER_OVER) {
918         view.setHighlighted(true);
919       }
920       return;
922     case ElementType.ALTDATA_VIEW:
923       view = /** @type {!i18n.input.chrome.inputview.elements.content.
924           AltDataView} */ (view);
925       if (e.type == EventType.POINTER_UP && e.identifier == view.identifier) {
926         var ch = view.getHighlightedCharacter();
927         if (ch) {
928           this.adapter_.sendKeyDownAndUpEvent(ch, view.triggeredBy.id,
929               view.triggeredBy.keyCode,
930               {'sources': [ch.toLowerCase()], 'possibilities': [1]});
931         }
932         view.hide();
933         this.clearUnstickyState_();
934         this.soundController_.onKeyUp(view.type);
935       }
936       return;
938     case ElementType.MENU_ITEM:
939       view = /** @type {!i18n.input.chrome.inputview.elements.content.
940           MenuItem} */ (view);
941       if (e.type == EventType.POINTER_UP) {
942         this.executeCommand_.apply(this, view.getCommand());
943         this.container_.menuView.hide();
944         this.soundController_.onKeyUp(view.type);
945         this.resetAll();
946       }
947       view.setHighlighted(e.type == EventType.POINTER_DOWN ||
948           e.type == EventType.POINTER_OVER);
949       // TODO: Add chrome vox support.
950       return;
952     case ElementType.MENU_VIEW:
953       view = /** @type {!i18n.input.chrome.inputview.elements.content.
954           MenuView} */ (view);
956       if (e.type == EventType.CLICK &&
957           e.target == view.getCoverElement()) {
958         view.hide();
959       }
960       return;
962     case ElementType.EMOJI_KEY:
963       if (e.type == EventType.CLICK) {
964         if (!this.container_.currentKeysetView.isDragging && view.text != '') {
965           this.adapter_.commitText(view.text);
966           this.soundController_.onKeyUp(view.type);
967         }
968       }
969       return;
971     case ElementType.HWT_PRIVACY_GOT_IT:
972       // Broadcasts the handwriting privacy confirmed message to let canvas
973       // view handle it.
974       this.adapter_.dispatchEvent(new goog.events.Event(
975           Type.HWT_PRIVACY_GOT_IT));
976       return;
978     case ElementType.VOICE_PRIVACY_GOT_IT:
979       // Broadcasts the voice privacy confirmed message to let voice
980       // view handle it.
981       this.adapter_.dispatchEvent(new goog.events.Event(
982           Type.VOICE_PRIVACY_GOT_IT));
983       return;
985     case ElementType.VOICE_VIEW:
986       if (e.type == EventType.POINTER_UP) {
987         this.adapter_.sendVoiceViewStateChange(false);
988         this.container_.candidateView.switchToIcon(
989             CandidateView.IconType.VOICE, true);
990         this.container_.voiceView.stop();
991       }
992       return;
993     case ElementType.SWIPE_VIEW:
994       this.stopBackspaceAutoRepeat_();
995       if (e.type == EventType.POINTER_UP ||
996           e.type == EventType.POINTER_OUT) {
997         this.clearUnstickyState_();
998       }
999       return;
1000     case ElementType.CUT:
1001     case ElementType.COPY:
1002     case ElementType.PASTE:
1003     case ElementType.BOLD:
1004     case ElementType.ITALICS:
1005     case ElementType.UNDERLINE:
1006     case ElementType.REDO:
1007     case ElementType.UNDO:
1008     case ElementType.SELECT_ALL:
1009       view.setHighlighted(e.type == EventType.POINTER_DOWN ||
1010           e.type == EventType.POINTER_OVER);
1011       if (e.type == EventType.POINTER_UP) {
1012         this.adapter_.sendKeyDownAndUpEvent(
1013             '', this.elementTypeToKeyCode_[view.type], undefined, undefined, {
1014               ctrl: true,
1015               alt: false,
1016               shift: false
1017             });
1018       }
1019       return;
1020     case ElementType.SOFT_KEY_VIEW:
1021       // Delegates the events on the soft key view to its soft key.
1022       view = /** @type {!i18n.input.chrome.inputview.elements.layout.
1023           SoftKeyView} */ (view);
1024       if (!view.softKey) {
1025         return;
1026       }
1027       view = view.softKey;
1028   }
1030   if (view.type != ElementType.MODIFIER_KEY &&
1031       !this.container_.altDataView.isVisible() &&
1032       !this.container_.menuView.isVisible() &&
1033       !this.container_.swipeView.isVisible()) {
1034     // The highlight of the modifier key is depending on the state instead
1035     // of the key down or up.
1036     if (e.type == EventType.POINTER_OVER || e.type == EventType.POINTER_DOWN ||
1037         e.type == EventType.DOUBLE_CLICK) {
1038       view.setHighlighted(true);
1039     } else if (e.type == EventType.POINTER_OUT ||
1040         e.type == EventType.POINTER_UP ||
1041         e.type == EventType.DOUBLE_CLICK_END) {
1042       view.setHighlighted(false);
1043     }
1044   }
1045   view = /** @type {!i18n.input.chrome.inputview.elements.content.
1046       SoftKey} */ (view);
1047   this.handlePointerEventForSoftKey_(view, e);
1048   this.updateContextModifierState_();
1053  * Handles softkey of the pointer action.
1055  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} softKey .
1056  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
1057  * @private
1058  */
1059 Controller.prototype.handlePointerEventForSoftKey_ = function(softKey, e) {
1060   var key;
1061   switch (softKey.type) {
1062     case ElementType.VOICE_BTN:
1063       if (e.type == EventType.POINTER_UP) {
1064         this.container_.candidateView.switchToIcon(
1065             CandidateView.IconType.VOICE, false);
1066         this.container_.voiceView.start();
1067       }
1068       break;
1069     case ElementType.CANDIDATES_PAGE_UP:
1070       if (e.type == EventType.POINTER_UP) {
1071         this.container_.expandedCandidateView.pageUp();
1072       }
1073       break;
1074     case ElementType.CANDIDATES_PAGE_DOWN:
1075       if (e.type == EventType.POINTER_UP) {
1076         this.container_.expandedCandidateView.pageDown();
1077       }
1078       break;
1079     case ElementType.CHARACTER_KEY:
1080       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1081           CharacterKey} */ (softKey);
1082       if (e.type == EventType.LONG_PRESS) {
1083         this.container_.altDataView.show(
1084             key, goog.i18n.bidi.isRtlLanguage(this.languageCode_),
1085             e.identifier);
1086       } else if (e.type == EventType.POINTER_UP) {
1087         this.model_.stateManager.triggerChording();
1088         var ch = key.getActiveCharacter();
1089         if (ch) {
1090           this.adapter_.sendKeyDownAndUpEvent(ch, key.id, key.keyCode,
1091               this.getSpatialData_(key, e.x, e.y));
1092         }
1093         this.clearUnstickyState_();
1094         key.flickerredCharacter = '';
1095       }
1096       break;
1098     case ElementType.MODIFIER_KEY:
1099       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1100           ModifierKey} */(softKey);
1101       var isStateEnabled = this.model_.stateManager.hasState(key.toState);
1102       var isChording = this.model_.stateManager.isChording(key.toState);
1103       if (e.type == EventType.POINTER_DOWN) {
1104         this.changeState_(key.toState, !isStateEnabled, true, false);
1105         this.model_.stateManager.setKeyDown(key.toState, true);
1106       } else if (e.type == EventType.POINTER_UP || e.type == EventType.
1107           POINTER_OUT) {
1108         if (isChording) {
1109           this.changeState_(key.toState, false, false);
1110         } else if (key.toState == StateType.CAPSLOCK) {
1111           this.changeState_(key.toState, isStateEnabled, true, true);
1112         } else if (this.model_.stateManager.isKeyDown(key.toState)) {
1113           this.changeState_(key.toState, isStateEnabled, false);
1114         }
1115         this.model_.stateManager.setKeyDown(key.toState, false);
1116       } else if (e.type == EventType.DOUBLE_CLICK) {
1117         this.changeState_(key.toState, isStateEnabled, true, true);
1118       } else if (e.type == EventType.LONG_PRESS) {
1119         if (!isChording) {
1120           this.changeState_(key.toState, true, true, true);
1121           this.model_.stateManager.setKeyDown(key.toState, false);
1122         }
1123       }
1124       break;
1126     case ElementType.BACKSPACE_KEY:
1127       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1128           FunctionalKey} */(softKey);
1129       if (e.type == EventType.POINTER_DOWN) {
1130         this.backspaceTick_();
1131       } else if (e.type == EventType.POINTER_UP || e.type == EventType.
1132           POINTER_OUT) {
1133         if (!this.container_.swipeView.isVisible()) {
1134           this.stopBackspaceAutoRepeat_();
1135         }
1136       }
1137       break;
1139     case ElementType.TAB_KEY:
1140       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1141           FunctionalKey} */ (softKey);
1142       if (e.type == EventType.POINTER_DOWN) {
1143         this.adapter_.sendKeyDownEvent('\u0009', KeyCodes.TAB);
1144       } else if (e.type == EventType.POINTER_UP) {
1145         this.adapter_.sendKeyUpEvent('\u0009', KeyCodes.TAB);
1146       }
1147       break;
1149     case ElementType.ENTER_KEY:
1150       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1151           FunctionalKey} */ (softKey);
1152       if (e.type == EventType.POINTER_UP) {
1153         this.adapter_.sendKeyDownAndUpEvent('\u000D', KeyCodes.ENTER);
1154       }
1155       break;
1157     case ElementType.ARROW_UP:
1158       if (e.type == EventType.POINTER_DOWN) {
1159         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_UP);
1160       } else if (e.type == EventType.POINTER_UP) {
1161         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_UP);
1162       }
1163       break;
1165     case ElementType.ARROW_DOWN:
1166       if (e.type == EventType.POINTER_DOWN) {
1167         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_DOWN);
1168       } else if (e.type == EventType.POINTER_UP) {
1169         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_DOWN);
1170       }
1171       break;
1173     case ElementType.ARROW_LEFT:
1174       if (e.type == EventType.POINTER_DOWN) {
1175         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_LEFT);
1176       } else if (e.type == EventType.POINTER_UP) {
1177         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_LEFT);
1178       }
1179       break;
1181     case ElementType.ARROW_RIGHT:
1182       if (e.type == EventType.POINTER_DOWN) {
1183         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_RIGHT);
1184       } else if (e.type == EventType.POINTER_UP) {
1185         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_RIGHT);
1186       }
1187       break;
1188     case ElementType.EN_SWITCHER:
1189       if (e.type == EventType.POINTER_UP) {
1190         key = /** @type {!i18n.input.chrome.inputview.elements.content.
1191             EnSwitcherKey} */ (softKey);
1192         this.adapter_.toggleLanguageState(this.model_.stateManager.isEnMode);
1193         this.model_.stateManager.isEnMode = !this.model_.stateManager.isEnMode;
1194         key.update();
1195       }
1196       break;
1197     case ElementType.SPACE_KEY:
1198       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1199           SpaceKey} */ (softKey);
1200       var doubleSpacePeriod = this.model_.settings.doubleSpacePeriod &&
1201           this.currentKeyset_ != Controller.HANDWRITING_VIEW_CODE_ &&
1202           this.currentKeyset_ != Controller.EMOJI_VIEW_CODE_;
1203       if (e.type == EventType.POINTER_UP || (!doubleSpacePeriod && e.type ==
1204           EventType.DOUBLE_CLICK_END)) {
1205         this.adapter_.sendKeyDownAndUpEvent(key.getCharacter(),
1206             KeyCodes.SPACE);
1207         this.clearUnstickyState_();
1208       } else if (e.type == EventType.DOUBLE_CLICK && doubleSpacePeriod) {
1209         this.adapter_.doubleClickOnSpaceKey();
1210       }
1211       break;
1213     case ElementType.SWITCHER_KEY:
1214       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1215           SwitcherKey} */ (softKey);
1216       if (e.type == EventType.POINTER_UP) {
1217         this.recordStatsForClosing_(
1218             'InputMethod.VirtualKeyboard.LayoutSwitch', 1, 25, 25);
1219         if (this.isSubKeyset_(key.toKeyset, this.currentKeyset_)) {
1220           this.model_.stateManager.reset();
1221           this.container_.update();
1222           this.updateContextModifierState_();
1223           this.container_.menuView.hide();
1224         } else {
1225           this.resetAll();
1226         }
1227         // Switch to the specific keyboard.
1228         this.switchToKeyset(key.toKeyset);
1229         if (key.record) {
1230           this.model_.settings.savePreference(
1231               util.getConfigName(key.toKeyset),
1232               key.toKeyset);
1233         }
1234       }
1235       break;
1237     case ElementType.COMPACT_KEY:
1238       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1239           CompactKey} */(softKey);
1240       if (e.type == EventType.LONG_PRESS) {
1241         this.container_.altDataView.show(
1242             key, goog.i18n.bidi.isRtlLanguage(this.languageCode_),
1243             e.identifier);
1244       } else if (e.type == EventType.POINTER_UP) {
1245         this.model_.stateManager.triggerChording();
1246         var ch = key.getActiveCharacter();
1247         if (ch.length == 1) {
1248           this.adapter_.sendKeyDownAndUpEvent(key.getActiveCharacter(), '', 0,
1249               this.getSpatialData_(key, e.x, e.y));
1250         } else if (ch.length > 1) {
1251           // Some compact keys contains more than 1 characters, such as '.com',
1252           // 'http://', etc. Those keys should trigger a direct commit text
1253           // instead of key events.
1254           this.adapter_.commitText(ch);
1255         }
1256         this.clearUnstickyState_();
1257         key.flickerredCharacter = '';
1258       }
1259       break;
1261     case ElementType.HIDE_KEYBOARD_KEY:
1262       var defaultKeyset = this.getActiveKeyset_();
1263       if (e.type == EventType.POINTER_UP) {
1264         this.adapter_.hideKeyboard();
1265         if (this.currentKeyset_ != defaultKeyset) {
1266           this.switchToKeyset(defaultKeyset);
1267         }
1268       }
1269       break;
1271     case ElementType.MENU_KEY:
1272       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1273           MenuKey} */ (softKey);
1274       if (e.type == EventType.POINTER_DOWN) {
1275         var isCompact = this.currentKeyset_.indexOf('compact') != -1;
1276         // Gets the default full keyboard instead of default keyset because
1277         // the default keyset can be a compact keyset which would cause problem
1278         // in MenuView.show().
1279         var defaultFullKeyset = this.initialKeyset_.split(/\./)[0];
1280         var enableCompact = !this.adapter_.isA11yMode && goog.array.contains(
1281             util.KEYSETS_HAVE_COMPACT, defaultFullKeyset);
1282         if (defaultFullKeyset == 'zhuyin' &&
1283             !this.adapter_.features.isEnabled(FeatureName.EXPERIMENTAL) ||
1284             this.languageCode_ == 'ko') {
1285           // Hides 'switch to compact' for zhuyin when not in experimental env.
1286           enableCompact = false;
1287         }
1288         var hasHwt = !this.adapter_.isPasswordBox() &&
1289             !Controller.DISABLE_HWT && goog.object.contains(
1290             InputToolCode, this.getHwtInputToolCode_());
1291         var hasEmoji = !this.adapter_.isPasswordBox();
1292         var enableSettings = this.shouldEnableSettings() &&
1293             !!window.inputview && !!inputview.openSettings;
1294         this.adapter_.getInputMethods(function(inputMethods) {
1295           this.container_.menuView.show(key, defaultFullKeyset, isCompact,
1296               enableCompact, this.currentInputMethod_, inputMethods, hasHwt,
1297               enableSettings, hasEmoji, this.adapter_.isA11yMode);
1298         }.bind(this));
1299       }
1300       break;
1302     case ElementType.GLOBE_KEY:
1303       if (e.type == EventType.POINTER_UP) {
1304         this.adapter_.clearModifierStates();
1305         this.adapter_.setModifierState(
1306             i18n.input.chrome.inputview.StateType.ALT, true);
1307         this.adapter_.sendKeyDownAndUpEvent(
1308             KeyCodes.SHIFT, KeyCodes.SHIFT_LEFT, goog.events.KeyCodes.SHIFT);
1309         this.adapter_.setModifierState(
1310             i18n.input.chrome.inputview.StateType.ALT, false);
1311       }
1312       break;
1313     case ElementType.IME_SWITCH:
1314       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1315           FunctionalKey} */ (softKey);
1316       this.adapter_.sendKeyDownAndUpEvent('', key.id);
1317       break;
1318   }
1319   // Play key sound on pointer up or double click.
1320   if (e.type == EventType.POINTER_UP || e.type == EventType.DOUBLE_CLICK)
1321     this.soundController_.onKeyUp(softKey.type);
1326  * Clears unsticky state.
1328  * @private
1329  */
1330 Controller.prototype.clearUnstickyState_ = function() {
1331   if (this.model_.stateManager.hasUnStickyState()) {
1332     for (var key in StateType) {
1333       var value = StateType[key];
1334       if (this.model_.stateManager.hasState(value) &&
1335           !this.model_.stateManager.isSticky(value)) {
1336         this.changeState_(value, false, false);
1337       }
1338     }
1339   }
1340   this.container_.update();
1345  * Stops the auto-repeat for backspace.
1347  * @private
1348  */
1349 Controller.prototype.stopBackspaceAutoRepeat_ = function() {
1350   if (this.backspaceAutoRepeat_) {
1351     goog.dispose(this.backspaceAutoRepeat_);
1352     this.backspaceAutoRepeat_ = null;
1353     this.adapter_.sendKeyUpEvent('\u0008', KeyCodes.BACKSPACE);
1354     this.backspaceRepeated_ = 0;
1355   }
1360  * The tick for the backspace key.
1362  * @private
1363  */
1364 Controller.prototype.backspaceTick_ = function() {
1365   if (this.backspaceRepeated_ >= Controller.BACKSPACE_REPEAT_LIMIT_) {
1366     this.stopBackspaceAutoRepeat_();
1367     return;
1368   }
1369   this.backspaceRepeated_++;
1370   this.backspaceDown_();
1371   this.soundController_.onKeyRepeat(ElementType.BACKSPACE_KEY);
1373   if (this.backspaceAutoRepeat_) {
1374     this.backspaceAutoRepeat_.start(75);
1375   } else {
1376     this.backspaceAutoRepeat_ = new goog.async.Delay(
1377         goog.bind(this.backspaceTick_, this), 300);
1378     this.backspaceAutoRepeat_.start();
1379   }
1384  * Callback for VISIBILITY_CHANGE.
1386  * @private
1387  */
1388 Controller.prototype.onVisibilityChange_ = function() {
1389   if (!this.adapter_.isVisible) {
1390     for (var name in this.statsForClosing_) {
1391       var stat = this.statsForClosing_[name];
1392       this.statistics_.recordValue(name, stat[0], stat[1], stat[2]);
1393     }
1394     this.statistics_.recordValue('InputMethod.VirtualKeyboard.Duration',
1395         Math.floor((new Date() - this.showTimeStamp_) / 1000), 4096, 50);
1396     this.statsForClosing_ = {};
1397     this.showTimeStamp_ = new Date();
1398     this.resetAll();
1399   }
1404  * Resets the whole keyboard include clearing candidates,
1405  * reset modifier state, etc.
1406  */
1407 Controller.prototype.resetAll = function() {
1408   this.clearCandidates_();
1409   this.container_.cleanStroke();
1410   this.model_.stateManager.reset();
1411   this.container_.update();
1412   this.updateContextModifierState_();
1413   this.resize();
1414   this.container_.expandedCandidateView.close();
1415   this.container_.menuView.hide();
1416   this.container_.swipeView.reset();
1417   this.container_.altDataView.hide();
1422  * Returns whether the toolbar should be shown.
1424  * @return {boolean}
1425  * @private
1426  */
1427 Controller.prototype.shouldShowToolBar_ = function() {
1428   return this.adapter_.features.isEnabled(FeatureName.OPTIMIZED_LAYOUTS) &&
1429       this.adapter_.isGoogleDocument() &&
1430       this.adapter_.contextType == ContextType.DEFAULT;
1435  * Callback when the context is changed.
1437  * @private
1438  */
1439 Controller.prototype.onContextFocus_ = function() {
1440   this.resetAll();
1441   this.model_.stateManager.contextType = this.adapter_.contextType;
1442   this.switchToKeyset(this.getActiveKeyset_());
1447  * Callback when surrounding text is changed.
1449  * @param {!i18n.input.chrome.inputview.events.SurroundingTextChangedEvent} e .
1450  * @private
1451  */
1452 Controller.prototype.onSurroundingTextChanged_ = function(e) {
1453   if (!this.model_.settings.autoCapital || !e.text) {
1454     return;
1455   }
1457   var isShiftEnabled = this.model_.stateManager.hasState(StateType.SHIFT);
1458   var needAutoCap = this.model_.settings.autoCapital &&
1459       util.needAutoCap(e.text);
1460   if (needAutoCap && !isShiftEnabled) {
1461     this.changeState_(StateType.SHIFT, true, false);
1462     this.shiftForAutoCapital_ = true;
1463   } else if (!needAutoCap && this.shiftForAutoCapital_) {
1464     this.changeState_(StateType.SHIFT, false, false);
1465   }
1470  * Callback for Context blurs.
1472  * @private
1473  */
1474 Controller.prototype.onContextBlur_ = function() {
1475   this.container_.cleanStroke();
1476   this.container_.menuView.hide();
1481  * Backspace key is down.
1483  * @private
1484  */
1485 Controller.prototype.backspaceDown_ = function() {
1486   if (this.container_.hasStrokesOnCanvas()) {
1487     this.clearCandidates_();
1488     this.container_.cleanStroke();
1489   } else {
1490     this.adapter_.sendKeyDownEvent('\u0008', KeyCodes.BACKSPACE);
1491   }
1492   this.recordStatsForClosing_(
1493       'InputMethod.VirtualKeyboard.BackspaceCount', 1, 4095, 4096);
1494   this.statistics_.recordEnum('InputMethod.VirtualKeyboard.BackspaceOnLayout',
1495       this.statistics_.getLayoutType(this.currentKeyset_,
1496           this.adapter_.isA11yMode),
1497       i18n.input.chrome.Statistics.LayoutTypes.MAX);
1502  * Callback for state change.
1504  * @param {StateType} stateType The state type.
1505  * @param {boolean} enable True to enable the state.
1506  * @param {boolean} isSticky True to make the state sticky.
1507  * @param {boolean=} opt_isFinalSticky .
1508  * @private
1509  */
1510 Controller.prototype.changeState_ = function(stateType, enable, isSticky,
1511     opt_isFinalSticky) {
1512   if (stateType == StateType.ALTGR) {
1513     var code = KeyCodes.ALT_RIGHT;
1514     if (enable) {
1515       this.adapter_.sendKeyDownEvent('', code);
1516     } else {
1517       this.adapter_.sendKeyUpEvent('', code);
1518     }
1519   }
1520   if (stateType == StateType.SHIFT) {
1521     this.shiftForAutoCapital_ = false;
1522   }
1523   var isEnabledBefore = this.model_.stateManager.hasState(stateType);
1524   var isStickyBefore = this.model_.stateManager.isSticky(stateType);
1525   this.model_.stateManager.setState(stateType, enable);
1526   this.model_.stateManager.setSticky(stateType, isSticky);
1527   var isFinalSticky = goog.isDef(opt_isFinalSticky) ? opt_isFinalSticky :
1528       false;
1529   var isFinalStikcyBefore = this.model_.stateManager.isFinalSticky(stateType);
1530   this.model_.stateManager.setFinalSticky(stateType, isFinalSticky);
1531   if (isEnabledBefore != enable || isStickyBefore != isSticky ||
1532       isFinalStikcyBefore != isFinalSticky) {
1533     this.container_.update();
1534   }
1539  * Updates the modifier state for context.
1541  * @private
1542  */
1543 Controller.prototype.updateContextModifierState_ = function() {
1544   var stateManager = this.model_.stateManager;
1545   this.adapter_.setModifierState(StateType.ALT,
1546       stateManager.hasState(StateType.ALT));
1547   this.adapter_.setModifierState(StateType.CTRL,
1548       stateManager.hasState(StateType.CTRL));
1549   this.adapter_.setModifierState(StateType.CAPSLOCK,
1550       stateManager.hasState(StateType.CAPSLOCK));
1551   if (!this.shiftForAutoCapital_) {
1552     // If shift key is automatically on because of feature - autoCapital,
1553     // Don't set modifier state to adapter.
1554     this.adapter_.setModifierState(StateType.SHIFT,
1555         stateManager.hasState(StateType.SHIFT));
1556   }
1561  * Callback for AUTO-COMPLETE event.
1563  * @param {!i18n.input.chrome.DataSource.CandidatesBackEvent} e .
1564  * @private
1565  */
1566 Controller.prototype.onCandidatesBack_ = function(e) {
1567   this.candidatesInfo_ = new i18n.input.chrome.inputview.CandidatesInfo(
1568       e.source, e.candidates);
1569   this.showCandidates_(e.source, e.candidates, Controller.CandidatesOperation.
1570       NONE);
1575  * Shows the candidates to the candidate view.
1577  * @param {string} source The source text.
1578  * @param {!Array.<!Object>} candidates The candidate text list.
1579  * @param {Controller.CandidatesOperation} operation .
1580  * @private
1581  */
1582 Controller.prototype.showCandidates_ = function(source, candidates,
1583     operation) {
1584   var state = !!source ? ExpandedCandidateView.State.COMPLETION_CORRECTION :
1585       ExpandedCandidateView.State.PREDICTION;
1586   var expandView = this.container_.expandedCandidateView;
1587   var expand = false;
1588   if (operation == Controller.CandidatesOperation.NONE) {
1589     expand = expandView.state == state;
1590   } else {
1591     expand = operation == Controller.CandidatesOperation.EXPAND;
1592   }
1594   if (candidates.length == 0) {
1595     this.clearCandidates_();
1596     expandView.state = ExpandedCandidateView.State.NONE;
1597     return;
1598   }
1600   // The compact pinyin needs full candidates instead of three candidates.
1601   var isThreeCandidates = this.currentKeyset_.indexOf('compact') != -1 &&
1602       this.currentKeyset_.indexOf('pinyin-zh-CN') == -1;
1603   if (isThreeCandidates) {
1604     if (candidates.length > 1) {
1605       // Swap the first candidate and the second candidate.
1606       var tmp = candidates[0];
1607       candidates[0] = candidates[1];
1608       candidates[1] = tmp;
1609     }
1610   }
1611   var isHwt = Controller.HANDWRITING_VIEW_CODE_ == this.currentKeyset_;
1612   this.container_.candidateView.showCandidates(candidates, isThreeCandidates,
1613       this.model_.settings.candidatesNavigation && !isHwt);
1615   // Only sum of candidate is greater than top line count. Need to update
1616   // expand view.
1617   if (expand && this.container_.candidateView.candidateCount <
1618       candidates.length) {
1619     expandView.state = state;
1620     this.container_.currentKeysetView.setVisible(false);
1621     expandView.showCandidates(candidates,
1622         this.container_.candidateView.candidateCount);
1623     this.container_.candidateView.switchToIcon(CandidateView.IconType.
1624         SHRINK_CANDIDATES, true);
1625   } else {
1626     expandView.state = ExpandedCandidateView.State.NONE;
1627     expandView.setVisible(false);
1628     this.container_.candidateView.switchToIcon(CandidateView.IconType.
1629         SHRINK_CANDIDATES, false);
1630     this.container_.currentKeysetView.setVisible(true);
1631   }
1636  * Clears candidates.
1638  * @private
1639  */
1640 Controller.prototype.clearCandidates_ = function() {
1641   this.candidatesInfo_ = i18n.input.chrome.inputview.CandidatesInfo.getEmpty();
1642   this.container_.candidateView.clearCandidates();
1643   this.container_.expandedCandidateView.close();
1644   this.container_.expandedCandidateView.state = ExpandedCandidateView.State.
1645       NONE;
1646   if (this.container_.currentKeysetView) {
1647     this.container_.currentKeysetView.setVisible(true);
1648   }
1650   if (this.currentKeyset_ == Controller.HANDWRITING_VIEW_CODE_ ||
1651       this.currentKeyset_ == Controller.EMOJI_VIEW_CODE_) {
1652     if (!this.adapter_.isQPInputView) {
1653       this.container_.candidateView.switchToIcon(
1654           CandidateView.IconType.BACK, true);
1655     } else {
1656       this.container_.candidateView.switchToIcon(
1657           CandidateView.IconType.VOICE, false);
1658       this.container_.candidateView.switchToIcon(
1659           CandidateView.IconType.EXPAND_CANDIDATES, false);
1660     }
1661   } else {
1662     this.container_.candidateView.switchToIcon(CandidateView.IconType.VOICE,
1663         this.adapter_.isVoiceInputEnabled);
1664   }
1669  * Callback when the layout is loaded.
1671  * @param {!i18n.input.chrome.inputview.events.LayoutLoadedEvent} e The event.
1672  * @private
1673  */
1674 Controller.prototype.onLayoutLoaded_ = function(e) {
1675   var layoutID = e.data['layoutID'];
1676   this.layoutDataMap_[layoutID] = e.data;
1677   this.perfTracker_.tick(PerfTracker.TickName.LAYOUT_LOADED);
1678   this.maybeCreateViews_();
1683  * Creates a keyset view.
1685  * @param {string} keyset The non-raw keyset.
1686  * @private
1687  */
1688 Controller.prototype.createView_ = function(keyset) {
1689   if (this.isDisposed()) {
1690     return;
1691   }
1692   var keysetData = this.keysetDataMap_[keyset];
1693   var layoutId = keysetData[SpecNodeName.LAYOUT];
1694   var layoutData = this.layoutDataMap_[layoutId];
1695   if (this.container_.keysetViewMap[keyset] || !layoutData) {
1696     return;
1697   }
1698   var conditions = {};
1699   conditions[ConditionName.SHOW_ALTGR] =
1700       keysetData[SpecNodeName.HAS_ALTGR_KEY];
1702   conditions[ConditionName.SHOW_MENU] =
1703       keysetData[SpecNodeName.SHOW_MENU_KEY];
1704   // In symbol and more keysets, we want to show a symbol key in the globe
1705   // SoftKeyView. So this view should alway visible in the two keysets.
1706   // Currently, SHOW_MENU_KEY is false for the two keysets, so we use
1707   // !keysetData[SpecNodeName.SHOW_MENU_KEY] here.
1708   conditions[ConditionName.SHOW_GLOBE_OR_SYMBOL] =
1709       !keysetData[SpecNodeName.SHOW_MENU_KEY] ||
1710       this.adapter_.showGlobeKey;
1711   conditions[ConditionName.SHOW_EN_SWITCHER_KEY] = false;
1713   this.container_.addKeysetView(keysetData, layoutData, keyset,
1714       this.languageCode_, this.model_, this.title_, conditions);
1715   this.perfTracker_.tick(PerfTracker.TickName.KEYBOARD_CREATED);
1720  * Creates the whole view.
1722  * @private
1723  */
1724 Controller.prototype.maybeCreateViews_ = function() {
1725   if (!this.isSettingReady) {
1726     return;
1727   }
1729   // Emoji is temp keyset which is delay loaded. So active keyset can be 'us'
1730   // while current keyset is 'emoji'. To make sure delay load can work
1731   // correctly, here need to create/switch to 'emoji' instead of 'us'.
1732   var activeKeyset = (this.currentKeyset_ == Controller.EMOJI_VIEW_CODE_) ?
1733       this.currentKeyset_ : this.getActiveKeyset_();
1734   var remappedActiveKeyset = this.getRemappedKeyset_(activeKeyset);
1735   var created = false;
1736   if (this.keysetDataMap_[remappedActiveKeyset]) {
1737     this.createView_(remappedActiveKeyset);
1738     this.switchToKeyset(activeKeyset);
1739     created = true;
1740   }
1741   // Async creating the non-active keysets to reduce the latency of showing the
1742   // active keyset.
1743   var keyLen = Object.keys(this.keysetDataMap_).length;
1744   if (created && keyLen > 1 || !created && keyLen > 0) {
1745     goog.Timer.callOnce((function() {
1746       for (var keyset in this.keysetDataMap_) {
1747         this.createView_(keyset);
1748       }
1749     }).bind(this));
1750   }
1755  * Switch to a specific keyboard.
1757  * @param {string} keyset The keyset name.
1758  */
1759 Controller.prototype.switchToKeyset = function(keyset) {
1760   if (!this.isSettingReady || this.adapter_.isSwitching()) {
1761     return;
1762   }
1764   var contextType = this.adapter_.contextType;
1765   var ret = this.container_.switchToKeyset(this.getRemappedKeyset_(keyset),
1766       this.title_, this.adapter_.isPasswordBox(), this.adapter_.isA11yMode,
1767       keyset, this.contextTypeToLastKeysetMap_[contextType] ||
1768       this.getActiveKeyset_(), this.languageCode_);
1770   if (ret) {
1771     if (!this.isSubKeyset_(this.currentKeyset_, keyset) &&
1772         keyset != Controller.EMOJI_VIEW_CODE_) {
1773       // If it is the sub keyset switching, or emoji, don't record it.
1774       // Update the keyset of current context type.
1775       this.contextTypeToKeysetMap_[this.currentInputMethod_][contextType] =
1776           keyset;
1777     }
1778     this.updateLanguageState_(this.currentKeyset_, keyset);
1779     this.currentKeyset_ = keyset;
1780     this.resize(Controller.DEV);
1781     this.statistics_.recordLayout(keyset, this.adapter_.isA11yMode);
1782     this.perfTracker_.tick(PerfTracker.TickName.KEYBOARD_SHOWN);
1783     this.perfTracker_.stop();
1784   } else {
1785     // Sets the current keyset for delay switching.
1786     this.currentKeyset_ = keyset;
1787     if (keyset != Controller.EMOJI_VIEW_CODE_) {  // Emoji is temp keyset.
1788       this.contextTypeToKeysetMap_[this.currentInputMethod_][contextType] =
1789           keyset;
1790     }
1791     if (this.adapter_.isQPInputView &&
1792         goog.array.contains(Controller.MATERIAL_KEYSETS_, keyset)) {
1793       this.loadResource_('m-' + keyset);
1794     } else {
1795       this.loadResource_(keyset);
1796     }
1797   }
1802  * Callback when the configuration is loaded.
1804  * @param {!i18n.input.chrome.inputview.events.ConfigLoadedEvent} e The event.
1805  * @private
1806  */
1807 Controller.prototype.onConfigLoaded_ = function(e) {
1808   if (this.isDisposed()) {
1809     return;
1810   }
1811   var data = e.data;
1812   var keyboardCode = data[i18n.input.chrome.inputview.SpecNodeName.ID];
1813   this.keysetDataMap_[keyboardCode] = data;
1814   this.perfTracker_.tick(PerfTracker.TickName.KEYSET_LOADED);
1815   var context = data[i18n.input.chrome.inputview.SpecNodeName.ON_CONTEXT];
1816   if (context && !this.adapter_.isA11yMode) {
1817     var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
1818     if (!keySetMap) {
1819       keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
1820     }
1821     keySetMap[context] = keyboardCode;
1822   }
1824   var layoutId = data[i18n.input.chrome.inputview.SpecNodeName.LAYOUT];
1825   if (this.adapter_.isQPInputView) {
1826     layoutId = 'm-' + layoutId;
1827     data[i18n.input.chrome.inputview.SpecNodeName.LAYOUT] = layoutId;
1828   }
1829   var layoutData = this.layoutDataMap_[layoutId];
1830   if (layoutData) {
1831     this.maybeCreateViews_();
1832   } else {
1833     this.model_.loadLayout(layoutId);
1834   }
1839  * Resizes the whole UI.
1841  * @param {boolean=} opt_ignoreWindowResize .
1842  */
1843 Controller.prototype.resize = function(opt_ignoreWindowResize) {
1844   var height;
1845   var widthPercent;
1846   var candidateViewHeight;
1847   var isLandScape = screen.width > screen.height;
1848   if (isLandScape) {
1849     goog.dom.classlist.addRemove(this.container_.getElement(),
1850         Css.PORTRAIT, Css.LANDSCAPE);
1851   } else {
1852     goog.dom.classlist.addRemove(this.container_.getElement(),
1853         Css.LANDSCAPE, Css.PORTRAIT);
1854   }
1855   var isWideScreen = (Math.min(screen.width, screen.height) / Math.max(
1856       screen.width, screen.height)) < 0.6;
1857   this.model_.stateManager.covariance.update(isWideScreen, isLandScape,
1858       this.adapter_.isA11yMode);
1859   if (this.adapter_.isA11yMode) {
1860     height = SizeSpec.A11Y_HEIGHT;
1861     widthPercent = screen.width > screen.height ? SizeSpec.A11Y_WIDTH_PERCENT.
1862         LANDSCAPE : SizeSpec.A11Y_WIDTH_PERCENT.PORTRAIT;
1863     candidateViewHeight = SizeSpec.A11Y_CANDIDATE_VIEW_HEIGHT;
1864   } else {
1865     var keyset = this.keysetDataMap_[this.currentKeyset_];
1866     var layout = keyset && keyset[SpecNodeName.LAYOUT];
1867     var data = layout && this.layoutDataMap_[layout];
1868     var spec = data && data[SpecNodeName.WIDTH_PERCENT] ||
1869         SizeSpec.NON_A11Y_WIDTH_PERCENT;
1870     height = SizeSpec.NON_A11Y_HEIGHT;
1871     if (isLandScape) {
1872       if (isWideScreen) {
1873         widthPercent = spec['LANDSCAPE_WIDE_SCREEN'];
1874       } else {
1875         widthPercent = spec['LANDSCAPE'];
1876       }
1877     } else {
1878       widthPercent = spec['PORTRAIT'];
1879     }
1880     candidateViewHeight = SizeSpec.NON_A11Y_CANDIDATE_VIEW_HEIGHT;
1881   }
1883   if (window.innerHeight != height && !opt_ignoreWindowResize) {
1884     if (this.lastResizeHeight_ != height) {
1885       this.lastResizeHeight_ = height;
1886       window.resizeTo(screen.width, height);
1887     }
1888     return;
1889   }
1891   this.container_.setContainerSize(screen.width, height, widthPercent,
1892       candidateViewHeight);
1893   this.container_.candidateView.setToolbarVisible(this.shouldShowToolBar_());
1894   if (this.container_.currentKeysetView) {
1895     this.isKeyboardReady = true;
1896   }
1901  * Loads the resources, for currentKeyset, passwdKeyset, handwriting,
1902  * emoji, etc.
1904  * @private
1905  */
1906 Controller.prototype.loadAllResources_ = function() {
1907   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
1908   goog.array.forEach([keysetMap[ContextType.DEFAULT],
1909     keysetMap[ContextType.PASSWORD]], function(keyset) {
1910     this.loadResource_(keyset);
1911   }, this);
1916  * Gets the remapped keyset.
1918  * @param {string} keyset .
1919  * @return {string} The remapped keyset.
1920  * @private
1921  */
1922 Controller.prototype.getRemappedKeyset_ = function(keyset) {
1923   if (goog.array.contains(util.KEYSETS_USE_US, keyset)) {
1924     return 'us-ltr';
1925   }
1926   var match = keyset.match(/^(.*)-rtl$/);
1927   if (match && goog.array.contains(util.KEYSETS_USE_US, match[1])) {
1928     return 'us-rtl';
1929   }
1930   return keyset;
1935  * Loads a single resource.
1937  * @param {string} keyset .
1938  * loaded.
1939  * @private
1940  */
1941 Controller.prototype.loadResource_ = function(keyset) {
1942   var remapped = this.getRemappedKeyset_(keyset);
1943   if (!this.keysetDataMap_[remapped]) {
1944     if (/^m17n:/.test(remapped)) {
1945       this.m17nModel_.loadConfig(remapped);
1946     } else {
1947       this.model_.loadConfig(remapped);
1948     }
1949     return;
1950   }
1952   var layoutId = this.keysetDataMap_[remapped][SpecNodeName.LAYOUT];
1953   if (!this.layoutDataMap_[layoutId]) {
1954     this.model_.loadLayout(layoutId);
1955     return;
1956   }
1961  * Sets the keyboard.
1963  * @param {string} keyset The keyboard keyset.
1964  * @param {string} languageCode The language code for this keyboard.
1965  * @param {string} passwordLayout The layout for password box.
1966  * @param {string} title The title for this keyboard.
1967  */
1968 Controller.prototype.initialize = function(keyset, languageCode, passwordLayout,
1969     title) {
1970   this.perfTracker_.restart();
1971   this.adapter_.getCurrentInputMethod(function(currentInputMethod) {
1972     // TODO: remove this hack as soon as the manifest is fixed in chromium.
1973     if (languageCode == 'ko') {
1974       if (currentInputMethod.indexOf('hangul_2set') > 0) {
1975         keyset = 'm17n:ko_2set';
1976       }
1977     }
1978     this.languageCode_ = languageCode;
1979     this.currentInputMethod_ = currentInputMethod;
1980     var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
1981     if (!keySetMap) {
1982       keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
1983     }
1984     keySetMap[ContextType.PASSWORD] = passwordLayout;
1985     keySetMap[ContextType.DEFAULT] = keyset;
1987     this.initialKeyset_ = keyset;
1988     this.title_ = title;
1989     this.isSettingReady = false;
1990     this.model_.settings = new i18n.input.chrome.inputview.Settings();
1991     this.model_.stateManager.isEnMode = false;
1992     this.adapter_.initialize(languageCode ? languageCode.split('-')[0] : '');
1993     this.loadAllResources_();
1994     this.switchToKeyset(this.getActiveKeyset_());
1996     // Set language attribute and font of body.
1997     document.body.setAttribute('lang', this.languageCode_);
1998     goog.dom.classlist.add(document.body, Css.FONT);
1999   }.bind(this));
2003 /** @override */
2004 Controller.prototype.disposeInternal = function() {
2005   goog.dispose(this.container_);
2006   goog.dispose(this.adapter_);
2007   goog.dispose(this.handler_);
2008   goog.dispose(this.soundController_);
2010   goog.base(this, 'disposeInternal');
2015  * Gets the handwriting Input Tool code of current language code.
2017  * @return {string} The handwriting Input Tool code.
2018  * @private
2019  */
2020 Controller.prototype.getHwtInputToolCode_ = function() {
2021   return this.languageCode_.split(/_|-/)[0] +
2022       Controller.HANDWRITING_CODE_SUFFIX_;
2027  * True to enable settings link.
2029  * @return {boolean} .
2030  */
2031 Controller.prototype.shouldEnableSettings = function() {
2032   return !this.adapter_.screen || this.adapter_.screen == 'normal';
2037  * Gets the active keyset, if there is a keyset to switch, return it.
2038  * otherwise if it's a password box, return the password keyset,
2039  * otherwise return the current keyset.
2041  * @return {string} .
2042  * @private
2043  */
2044 Controller.prototype.getActiveKeyset_ = function() {
2045   var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
2046   return keySetMap[this.adapter_.contextType] || this.initialKeyset_;
2051  * True if keysetB is the sub keyset of keysetA.
2053  * @param {string} keysetA .
2054  * @param {string} keysetB .
2055  * @return {boolean} .
2056  * @private
2057  */
2058 Controller.prototype.isSubKeyset_ = function(keysetA, keysetB) {
2059   var segmentsA = keysetA.split('.');
2060   var segmentsB = keysetB.split('.');
2061   return segmentsA.length >= 2 && segmentsB.length >= 2 &&
2062       segmentsA[0] == segmentsB[0] && segmentsA[1] == segmentsB[1];
2067  * Updates the compact pinyin to set the inputcode for english and pinyin.
2069  * @param {string} fromRawKeyset .
2070  * @param {string} toRawKeyset .
2071  * @private
2072  */
2073 Controller.prototype.updateLanguageState_ =
2074     function(fromRawKeyset, toRawKeyset) {
2075   var toggle = false;
2076   var toggleState = false;
2077   if (fromRawKeyset != toRawKeyset) {
2078     // Deal with the switch logic to/from English within the compact layout.
2079     if (fromRawKeyset.indexOf('en.compact') *
2080         toRawKeyset.indexOf('en.compact') < 0) { // Switches between non-en/en.
2081       toggle = true;
2082       toggleState = toRawKeyset.indexOf('en.compact') == -1;
2083     } else if (fromRawKeyset.indexOf(toRawKeyset) == 0 &&
2084         fromRawKeyset.indexOf('.compact') > 0 &&
2085         goog.array.contains(util.KEYSETS_HAVE_EN_SWTICHER, toRawKeyset) ||
2086         fromRawKeyset && toRawKeyset.indexOf(fromRawKeyset) == 0 &&
2087         toRawKeyset.indexOf('.compact') > 0) {
2088       // Switch between full/compact layouts, reset the default button and
2089       // language.
2090       toggle = true;
2091       toggleState = true;
2092     }
2093   }
2094   if (toggle) {
2095     this.adapter_.toggleLanguageState(toggleState);
2096     this.model_.stateManager.isEnMode = !toggleState;
2097     this.container_.currentKeysetView.update();
2098   }
2103  * Records the stats which will be reported when input view is closing.
2105  * @param {string} name The metrics name.
2106  * @param {number} count The count value for histogram.
2107  * @param {number} max .
2108  * @param {number} bucketCount .
2109  * @private
2110  */
2111 Controller.prototype.recordStatsForClosing_ = function(
2112     name, count, max, bucketCount) {
2113   if (!this.statsForClosing_[name]) {
2114     this.statsForClosing_[name] = [count, max, bucketCount];
2115   } else {
2116     this.statsForClosing_[name][0] += count;
2117     this.statsForClosing_[name][1] = max;
2118     this.statsForClosing_[name][2] = bucketCount;
2119   }
2124  * Handles language state changing event.
2126  * @param {!i18n.input.chrome.message.Event} e .
2127  * @private
2128  */
2129 Controller.prototype.onUpdateToggleLanguateState_ = function(e) {
2130   if (this.adapter_.isA11yMode || this.currentKeyset_.indexOf('.compact') < 0) {
2131     // e.msg value means whether is Chinese mode now.
2132     if (this.model_.stateManager.isEnMode == e.msg) {
2133       this.model_.stateManager.isEnMode = !e.msg;
2134       this.container_.currentKeysetView.update();
2135     }
2136   } else {
2137     var pos = this.currentKeyset_.indexOf('en.compact');
2138     var toKeyset;
2139     if (pos > 0) { // Means en mode
2140       if (e.msg) { // Needs switch cn mode
2141         toKeyset = this.currentKeyset_.replace('en.compact', 'compact');
2142       }
2143     } else {
2144       if (!e.msg) { // Needs switch en mode
2145         toKeyset = this.currentKeyset_.replace('compact', 'en.compact');
2146       }
2147     }
2148     if (toKeyset) {
2149       this.resetAll();
2150       this.switchToKeyset(toKeyset);
2151     }
2152   }
2154 });  // goog.scope