ChildAccountService[Java] delegates everything to native side.
[chromium-blink-merge.git] / third_party / google_input_tools / src / chrome / os / inputview / controller.js
blob9c4d6ce0d9257ed2e216e19a5b2636c7ff44e3ac
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 events = i18n.input.chrome.inputview.events;
83 var util = i18n.input.chrome.inputview.util;
87 /**
88  * The controller of the input view keyboard.
89  *
90  * @param {string} keyset The keyboard keyset.
91  * @param {string} languageCode The language code for this keyboard.
92  * @param {string} passwordLayout The layout for password box.
93  * @param {string} name The input tool name.
94  * @constructor
95  * @extends {goog.Disposable}
96  */
97 i18n.input.chrome.inputview.Controller = function(keyset, languageCode,
98     passwordLayout, name) {
99   /**
100    * The model.
101    *
102    * @type {!i18n.input.chrome.inputview.Model}
103    * @private
104    */
105   this.model_ = new i18n.input.chrome.inputview.Model();
107   /** @private {!i18n.input.chrome.inputview.PerfTracker} */
108   this.perfTracker_ = new i18n.input.chrome.inputview.PerfTracker(
109       PerfTracker.TickName.HTML_LOADED);
111   /**
112    * The layout map.
113    *
114    * @type {!Object.<string, !Object>}
115    * @private
116    */
117   this.layoutDataMap_ = {};
119   /**
120    * The element map.
121    *
122    * @private {!Object.<ElementType, !KeyCodes>}
123    */
124   this.elementTypeToKeyCode_ = goog.object.create(
125       ElementType.BOLD, KeyCodes.KEY_B,
126       ElementType.ITALICS, KeyCodes.KEY_I,
127       ElementType.UNDERLINE, KeyCodes.KEY_U,
128       ElementType.COPY, KeyCodes.KEY_C,
129       ElementType.PASTE, KeyCodes.KEY_V,
130       ElementType.CUT, KeyCodes.KEY_X,
131       ElementType.SELECT_ALL, KeyCodes.KEY_A,
132       ElementType.REDO, KeyCodes.KEY_Y,
133       ElementType.UNDO, KeyCodes.KEY_Z
134       );
136   /**
137    * The keyset data map.
138    *
139    * @type {!Object.<string, !Object>}
140    * @private
141    */
142   this.keysetDataMap_ = {};
144   /**
145    * The event handler.
146    *
147    * @type {!goog.events.EventHandler}
148    * @private
149    */
150   this.handler_ = new goog.events.EventHandler(this);
152   /**
153    * The m17n model.
154    *
155    * @type {!i18n.input.chrome.inputview.M17nModel}
156    * @private
157    */
158   this.m17nModel_ = new i18n.input.chrome.inputview.M17nModel();
160   /**
161    * The pointer handler.
162    *
163    * @type {!i18n.input.chrome.inputview.handler.PointerHandler}
164    * @private
165    */
166   this.pointerHandler_ = new i18n.input.chrome.inputview.handler.
167       PointerHandler();
169   /**
170    * The statistics object for recording metrics values.
171    *
172    * @type {!i18n.input.chrome.Statistics}
173    * @private
174    */
175   this.statistics_ = i18n.input.chrome.Statistics.getInstance();
177   /** @private {!i18n.input.chrome.inputview.ReadyState} */
178   this.readyState_ = new i18n.input.chrome.inputview.ReadyState();
180   /** @private {!i18n.input.chrome.inputview.Adapter} */
181   this.adapter_ = new i18n.input.chrome.inputview.Adapter(this.readyState_);
183   /** @private {!SoundController} */
184   this.soundController_ = new SoundController(false);
186   /**
187    * Whether or not to commit the next gesture result.
188    *
189    * @private {boolean}
190    */
191   this.commitNextGestureResult_ = false;
193   /** @private {!i18n.input.chrome.inputview.KeyboardContainer} */
194   this.container_ = new i18n.input.chrome.inputview.KeyboardContainer(
195       this.adapter_, this.soundController_);
197   /**
198    * The context type and keyset mapping group by input method id.
199    * key: input method id.
200    * value: Object
201    *    key: context type string.
202    *    value: keyset string.
203    *
204    * @private {!Object.<string, !Object.<string, string>>}
205    */
206   this.contextTypeToKeysetMap_ = {};
209   /**
210    * The previous raw keyset code before switched to hwt or emoji layout.
211    *  key: context type string.
212    *  value: keyset string.
213    *
214    * @private {!Object.<string, string>}
215    */
216   this.contextTypeToLastKeysetMap_ = {};
218   /**
219    * The stats map for input view closing.
220    *
221    * @type {!Object.<string, !Array.<number>>}
222    * @private
223    */
224   this.statsForClosing_ = {};
226   /**
227    * The time the last point was accepted.
228    *
229    * @private {number}
230    */
231   this.lastPointTime_ = 0;
233   /**
234    * The last height sent to window.resizeTo to avoid multiple equivalent calls.
235    *
236    * @private {number}
237    */
238   this.lastResizeHeight_ = -1;
240   /**
241    * The activate (show) time stamp for statistics.
242    *
243    * @type {Date}
244    * @private
245    */
246   this.showTimeStamp_ = new Date();
248   this.initialize(keyset, languageCode, passwordLayout, name);
249   /**
250    * The suggestions.
251    * Note: sets a default empty result to avoid null check.
252    *
253    * @private {!i18n.input.chrome.inputview.CandidatesInfo}
254    */
255   this.candidatesInfo_ = i18n.input.chrome.inputview.CandidatesInfo.getEmpty();
257   this.registerEventHandler_();
259 goog.inherits(i18n.input.chrome.inputview.Controller,
260     goog.Disposable);
261 var Controller = i18n.input.chrome.inputview.Controller;
265  * @define {boolean} Flag to disable handwriting.
266  */
267 Controller.DISABLE_HWT = false;
271  * A flag to indicate whether the shift is for auto capital.
273  * @private {boolean}
274  */
275 Controller.prototype.shiftForAutoCapital_ = false;
279  * @define {boolean} Flag to indicate whether it is debugging.
280  */
281 Controller.DEV = false;
285  * The handwriting view code, use the code can switch handwriting panel view.
287  * @const {string}
288  * @private
289  */
290 Controller.HANDWRITING_VIEW_CODE_ = 'hwt';
294  * The emoji view code, use the code can switch to emoji.
296  * @const {string}
297  * @private
298  */
299 Controller.EMOJI_VIEW_CODE_ = 'emoji';
303  * The limitation for backspace repeat time to avoid unexpected
304  * problem that backspace is held all the time.
306  * @private {number}
307  */
308 Controller.BACKSPACE_REPEAT_LIMIT_ = 255;
312  * The repeated times of the backspace.
314  * @private {number}
315  */
316 Controller.prototype.backspaceRepeated_ = 0;
320  * The handwriting input tool code suffix.
322  * @const {string}
323  * @private
324  */
325 Controller.HANDWRITING_CODE_SUFFIX_ = '-t-i0-handwrit';
329  * The US English compact layout prefix.
331  * @const {string}
332  * @private
333  */
334 Controller.US_COMPACT_PREFIX_ = 'us.compact';
338  * Time threshold between samples sent to the back end.
340  * @const {number}
341  */
342 Controller.SUBSAMPLING_TIME_THRESHOLD = 100;
346  * True if the settings is loaded.
348  * @type {boolean}
349  */
350 Controller.prototype.isSettingReady = false;
354  * True if the keyboard is set up.
355  * Note: This flag is only used for automation testing.
357  * @type {boolean}
358  */
359 Controller.prototype.isKeyboardReady = false;
363  * True if the current keyset is the US english compact layout.
365  * @type {boolean}
366  * @private
367  */
368 Controller.prototype.isKeysetUSCompact_ = false;
372  * The auto repeat timer for backspace hold.
374  * @type {goog.async.Delay}
375  * @private
376  */
377 Controller.prototype.backspaceAutoRepeat_;
381  * The initial keyset determined by inputview url and/or settings.
383  * @type {string}
384  * @private
385  */
386 Controller.prototype.initialKeyset_ = '';
390  * The current raw keyset code.
392  * @type {string}
393  * @private
394  */
395 Controller.prototype.currentKeyset_ = '';
399  * The current input method id.
401  * @private {string}
402  */
403 Controller.prototype.currentInputmethod_ = '';
407  * The operations on candidates.
409  * @enum {number}
410  */
411 Controller.CandidatesOperation = {
412   NONE: 0,
413   EXPAND: 1,
414   SHRINK: 2
419  * A temporary list to track keysets have customized in material design.
421  * @private {!Array.<string>}
422  */
423 Controller.MATERIAL_KEYSETS_ = [
424   'emoji',
425   'hwt'
430  * The active language code.
432  * @type {string}
433  * @private
434  */
435 Controller.prototype.lang_;
439  * The password keyset.
441  * @private {string}
442  */
443 Controller.prototype.passwordKeyset_ = '';
447  * The soft key map, because key configuration is loaded before layout,
448  * controller needs this varaible to save it and hook into keyboard view.
450  * @type {!Array.<!i18n.input.chrome.inputview.elements.content.SoftKey>}
451  * @private
452  */
453 Controller.prototype.softKeyList_;
457  * The mapping from soft key id to soft key view id.
459  * @type {!Object.<string, string>}
460  * @private
461  */
462 Controller.prototype.mapping_;
466  * The input tool name.
468  * @type {string}
469  * @private
470  */
471 Controller.prototype.title_;
475  * Whether to return to the standard character keyset after space is touched.
477  * @type {boolean}
478  * @private
479  */
480 Controller.prototype.returnToLetterKeysetOnSpace_ = false;
484  * A cache of the previous gesture results.
486  * @private {!Array.<string>}
487  */
488 Controller.prototype.gestureResultsCache_;
492  * Registers event handlers.
493  * @private
494  */
495 Controller.prototype.registerEventHandler_ = function() {
496   this.handler_.
497       listen(this.model_,
498           EventType.LAYOUT_LOADED,
499           this.onLayoutLoaded_).
500       listen(this.model_,
501           EventType.CONFIG_LOADED,
502           this.onConfigLoaded_).
503       listen(this.m17nModel_,
504           EventType.CONFIG_LOADED,
505           this.onConfigLoaded_).
506       listen(this.pointerHandler_, [
507             EventType.LONG_PRESS,
508             EventType.CLICK,
509             EventType.DOUBLE_CLICK,
510             EventType.DOUBLE_CLICK_END,
511             EventType.POINTER_UP,
512             EventType.POINTER_DOWN,
513             EventType.POINTER_OVER,
514             EventType.POINTER_OUT,
515             EventType.SWIPE
516           ], this.onPointerEvent_).
517       listen(this.pointerHandler_,
518           EventType.DRAG,
519           this.onDragEvent_).
520       listen(window, goog.events.EventType.RESIZE, this.resize).
521       listen(this.adapter_,
522           EventType.SURROUNDING_TEXT_CHANGED, this.onSurroundingTextChanged_).
523       listen(this.adapter_,
524           i18n.input.chrome.DataSource.EventType.CANDIDATES_BACK,
525           this.onCandidatesBack_).
526       listen(this.adapter_,
527           i18n.input.chrome.DataSource.EventType.GESTURES_BACK,
528           this.onGesturesBack_).
529       listen(this.adapter_, EventType.URL_CHANGED, this.onURLChanged_).
530       listen(this.adapter_, EventType.CONTEXT_FOCUS, this.onContextFocus_).
531       listen(this.adapter_, EventType.CONTEXT_BLUR, this.onContextBlur_).
532       listen(this.adapter_, EventType.VISIBILITY_CHANGE,
533           this.onVisibilityChange_).
534       listen(this.adapter_, EventType.SETTINGS_READY, this.onSettingsReady_).
535       listen(this.adapter_, EventType.UPDATE_SETTINGS, this.onUpdateSettings_).
536       listen(this.adapter_, EventType.FRONT_TOGGLE_LANGUAGE_STATE,
537           this.onUpdateToggleLanguageState_).
538       listen(this.adapter_, EventType.VOICE_STATE_CHANGE,
539           this.onVoiceStateChange_).
540       listen(this.adapter_, EventType.REFRESH, this.onRefresh_);
545  * Handler for voice module state change.
547  * @param {!events.MessageEvent} e .
548  * @private
549  */
550 Controller.prototype.onVoiceStateChange_ = function(e) {
551   if (!e.msg[Name.VOICE_STATE]) {
552     this.container_.candidateView.switchToIcon(
553         CandidateView.IconType.VOICE, true);
554     this.container_.voiceView.stop();
555   }
560  * Handles the refresh event from adapter.
562  * @private
563  */
564 Controller.prototype.onRefresh_ = function() {
565   window.location.reload();
570  * Sets the default keyset for context types.
572  * @param {string} newKeyset .
573  * @private
574  */
575 Controller.prototype.setDefaultKeyset_ = function(newKeyset) {
576   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
577   for (var context in keysetMap) {
578     if (context != ContextType.DEFAULT &&
579         keysetMap[context] == keysetMap[ContextType.DEFAULT]) {
580       keysetMap[context] = newKeyset;
581     }
582   }
583   keysetMap[ContextType.DEFAULT] = this.initialKeyset_ = newKeyset;
588  * Callback for updating settings.
590  * @param {!events.MessageEvent} e .
591  * @private
592  */
593 Controller.prototype.onUpdateSettings_ = function(e) {
594   var settings = this.model_.settings;
595   if (goog.isDef(e.msg['autoSpace'])) {
596     settings.autoSpace = e.msg['autoSpace'];
597   }
598   if (goog.isDef(e.msg['autoCapital'])) {
599     settings.autoCapital = e.msg['autoCapital'];
600   }
601   if (goog.isDef(e.msg['candidatesNavigation'])) {
602     settings.candidatesNavigation = e.msg['candidatesNavigation'];
603     this.container_.candidateView.setNavigation(settings.candidatesNavigation);
604   }
605   if (goog.isDef(e.msg[Name.KEYSET])) {
606     this.setDefaultKeyset_(e.msg[Name.KEYSET]);
607   }
608   if (goog.isDef(e.msg['enableLongPress'])) {
609     settings.enableLongPress = e.msg['enableLongPress'];
610   }
611   if (goog.isDef(e.msg['doubleSpacePeriod'])) {
612     settings.doubleSpacePeriod = e.msg['doubleSpacePeriod'];
613   }
614   if (goog.isDef(e.msg['soundOnKeypress'])) {
615     settings.soundOnKeypress = e.msg['soundOnKeypress'];
616     this.soundController_.setEnabled(settings.soundOnKeypress);
617   }
618   if (goog.isDef(e.msg['gestureEditing'])) {
619     settings.gestureEditing = e.msg['gestureEditing'];
620     var enabled = settings.gestureEditing && !this.adapter_.isA11yMode;
621     this.container_.swipeView.enabled = enabled;
622     this.container_.selectView.setSettingsEnabled(enabled);
623   }
624   if (goog.isDef(e.msg['gestureTyping'])) {
625     settings.gestureTyping = e.msg['gestureTyping'];
626   } else {
627     settings.gestureTyping = false;
628   }
629   this.perfTracker_.tick(PerfTracker.TickName.BACKGROUND_SETTINGS_FETCHED);
630   this.model_.stateManager.contextType = this.adapter_.contextType;
631   this.maybeCreateViews_();
636  * Callback for url changed.
638  * @private
639  */
640 Controller.prototype.onURLChanged_ = function() {
641   this.container_.candidateView.setToolbarVisible(this.shouldShowToolBar_());
646  * Callback for setting ready.
648  * @private
649  */
650 Controller.prototype.onSettingsReady_ = function() {
651   if (this.isSettingReady) {
652     return;
653   }
655   this.isSettingReady = true;
656   // Don't render container twice.
657   if (!this.container_.isInDocument()) {
658     this.container_.render();
659   }
660   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
661   var newKeyset = '';
662   if (this.adapter_.isA11yMode) {
663     newKeyset = util.getConfigName(keysetMap[ContextType.DEFAULT]);
664   } else {
665     newKeyset = /** @type {string} */ (this.model_.settings.
666         getPreference(util.getConfigName(keysetMap[ContextType.DEFAULT])));
667   }
668   if (!this.adapter_.features.isEnabled(FeatureName.EXPERIMENTAL) &&
669       keysetMap[ContextType.DEFAULT] ==
670       'zhuyin.compact.qwerty') {
671     newKeyset = 'zhuyin';
672   }
673   if (newKeyset) {
674     this.setDefaultKeyset_(newKeyset);
675   }
676   this.container_.selectView.setSettingsEnabled(
677       this.model_.settings.gestureEditing && !this.adapter_.isA11yMode);
678   // Loads resources in case the default keyset is changed.
679   this.loadAllResources_();
680   this.maybeCreateViews_();
685  * Returns whether or not the gesture typing feature is enabled.
687  * @return {boolean}
688  * @private
689  */
690 Controller.prototype.gestureTypingEnabled_ = function() {
691   return this.isKeysetUSCompact_ && this.model_.settings.gestureTyping &&
692       !this.adapter_.isA11yMode && !this.adapter_.isChromeVoxOn;
697  * Returns the time threshold for subsampling, in ms.
699  * @return {number}
700  * @private
701  */
702 Controller.prototype.subsamplingThreshold_ = function() {
703   return Controller.SUBSAMPLING_TIME_THRESHOLD;
708  * Gets the data for spatial module.
710  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} key .
711  * @param {number} x The x-offset of the touch point.
712  * @param {number} y The y-offset of the touch point.
713  * @return {!Object} .
714  * @private
715  */
716 Controller.prototype.getSpatialData_ = function(key, x, y) {
717   var items = [];
718   items.push([this.getKeyContent_(key), key.estimator.estimateInLogSpace(x, y)
719       ]);
720   for (var i = 0; i < key.nearbyKeys.length; i++) {
721     var nearByKey = key.nearbyKeys[i];
722     var content = this.getKeyContent_(nearByKey);
723     if (content && util.REGEX_LANGUAGE_MODEL_CHARACTERS.test(content)) {
724       items.push([content, nearByKey.estimator.estimateInLogSpace(x, y)]);
725     }
726   }
727   goog.array.sort(items, function(item1, item2) {
728     return item1[1] - item2[1];
729   });
730   var sources = items.map(function(item) {
731     return item[0].toLowerCase();
732   });
733   var possibilities = items.map(function(item) {
734     return item[1];
735   });
736   return {
737     'sources': sources,
738     'possibilities': possibilities
739   };
744  * Gets the key content.
746  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} key .
747  * @return {string} .
748  * @private
749  */
750 Controller.prototype.getKeyContent_ = function(key) {
751   if (key.type == i18n.input.chrome.inputview.elements.ElementType.
752       CHARACTER_KEY) {
753     key = /** @type {!i18n.input.chrome.inputview.elements.content.
754         CharacterKey} */ (key);
755     return key.getActiveCharacter();
756   }
757   if (key.type == i18n.input.chrome.inputview.elements.ElementType.
758       COMPACT_KEY) {
759     key = /** @type {!i18n.input.chrome.inputview.elements.content.
760         FunctionalKey} */ (key);
761     return key.text;
762   }
763   return '';
768  * Callback for pointer event.
770  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
771  * @private
772  */
773 Controller.prototype.onPointerEvent_ = function(e) {
774   if (e.type == EventType.LONG_PRESS) {
775     if (this.adapter_.isChromeVoxOn || !this.model_.settings.enableLongPress) {
776       return;
777     }
778     var keyset = this.keysetDataMap_[this.currentKeyset_];
779     var layout = keyset && keyset[SpecNodeName.LAYOUT];
780     var data = layout && this.layoutDataMap_[layout];
781     if (data && data[SpecNodeName.DISABLE_LONGPRESS]) {
782       return;
783     }
784   }
786   // POINTER_UP event may be dispatched without a view. This is the case when
787   // user selected an accent character which is displayed outside of the
788   // keyboard window bounds. For other cases, we expect a view associated with a
789   // pointer up event.
790   if (e.type == EventType.POINTER_UP && !e.view) {
791     if (this.container_.altDataView.isVisible() &&
792         e.identifier == this.container_.altDataView.identifier) {
793       var altDataView = this.container_.altDataView;
794       var ch = altDataView.getHighlightedCharacter();
795       if (ch) {
796         this.adapter_.sendKeyDownAndUpEvent(ch, altDataView.triggeredBy.id,
797             altDataView.triggeredBy.keyCode,
798             {'sources': [ch.toLowerCase()], 'possibilities': [1]});
799       }
800       altDataView.hide();
801       this.clearUnstickyState_();
802     }
803     return;
804   }
806   if (e.type == EventType.POINTER_UP) {
807     this.stopBackspaceAutoRepeat_();
808   }
810   if (e.view) {
811     this.handlePointerAction_(e.view, e);
812   }
817  * Sends the last stroke from the gesture canvas view to the gesture decoder, if
818  * the last point was added past a time threshold.
820  * @param {boolean=} opt_force Whether or not to force send the gesture event.
821  * @private
822  */
823 Controller.prototype.maybeSendLastStroke_ = function(opt_force) {
824   // Subsample by returning early if the previous point was added too recently.
825   var currentTime = Date.now();
826   if (!opt_force && currentTime - this.lastPointTime_ <
827       this.subsamplingThreshold_()) {
828     return;
829   } else {
830     this.lastPointTime_ = currentTime;
831   }
832   var lastStroke = this.container_.gestureCanvasView.getLastStroke();
833   if (lastStroke) {
834     // This call will set up the necessary callbacks the decoder will use to
835     // communicate back to this class.
836     this.adapter_.sendGestureEvent(lastStroke.points);
837   }
842  * Handles the drag events. Generally, this will forward the event details to
843  * the components that handle drawing, decoding, etc.
845  * @param {!i18n.input.chrome.inputview.events.DragEvent} e .
846  * @private
847  */
848 Controller.prototype.onDragEvent_ = function(e) {
849   if (this.gestureTypingEnabled_() && e.type == EventType.DRAG &&
850       !this.container_.swipeView.isVisible()) {
851     this.container_.gestureCanvasView.addPoint(e);
852     if (e.view && this.container_.gestureCanvasView.isGesturing) {
853       // Ensure the last touched key is not highlighted.
854       e.view.setHighlighted(false);
855       this.maybeSendLastStroke_();
856     }
857     return;
858   }
863  * Handles the swipe action.
865  * @param {!i18n.input.chrome.inputview.elements.Element} view The view, for
866  *     swipe event, the view would be the soft key which starts the swipe.
867  * @param {!i18n.input.chrome.inputview.events.SwipeEvent} e The swipe event.
868  * @private
869  */
870 Controller.prototype.handleSwipeAction_ = function(view, e) {
871   var direction = e.direction;
872   if (this.container_.altDataView.isVisible()) {
873     this.container_.altDataView.highlightItem(e.x, e.y, e.identifier);
874     return;
875   }
876   if (view.type == ElementType.BACKSPACE_KEY) {
877     if (this.container_.swipeView.isVisible() ||
878         this.container_.swipeView.isArmed()) {
879       this.stopBackspaceAutoRepeat_();
880       return;
881     }
882   }
884   if (view.type == ElementType.CHARACTER_KEY) {
885     view = /** @type {!i18n.input.chrome.inputview.elements.content.
886         CharacterKey} */ (view);
887     if (direction & i18n.input.chrome.inputview.SwipeDirection.UP ||
888         direction & i18n.input.chrome.inputview.SwipeDirection.DOWN) {
889       var ch = view.getCharacterByGesture(!!(direction &
890           i18n.input.chrome.inputview.SwipeDirection.UP));
891       if (ch) {
892         view.flickerredCharacter = ch;
893       }
894     }
895   }
897   if (view.type == ElementType.COMPACT_KEY) {
898     view = /** @type {!i18n.input.chrome.inputview.elements.content.
899         CompactKey} */ (view);
900     if ((direction & i18n.input.chrome.inputview.SwipeDirection.UP) &&
901         view.hintText) {
902       view.flickerredCharacter = view.hintText;
903     }
904   }
909  * Execute a command.
911  * @param {!i18n.input.chrome.inputview.elements.content.MenuView.Command}
912  *     command The command that about to be executed.
913  * @param {string=} opt_arg The optional command argument.
914  * @private
915  */
916 Controller.prototype.executeCommand_ = function(command, opt_arg) {
917   var CommandEnum = MenuView.Command;
918   switch (command) {
919     case CommandEnum.SWITCH_IME:
920       var inputMethodId = opt_arg;
921       if (inputMethodId) {
922         this.adapter_.switchToInputMethod(inputMethodId);
923       }
924       break;
926     case CommandEnum.SWITCH_KEYSET:
927       var keyset = opt_arg;
928       if (keyset) {
929         this.recordStatsForClosing_(
930             'InputMethod.VirtualKeyboard.LayoutSwitch', 1, 25, 25);
931         this.switchToKeyset(keyset);
932       }
933       break;
934     case CommandEnum.OPEN_EMOJI:
935       this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
936           this.currentKeyset_;
937       this.switchToKeyset(Controller.EMOJI_VIEW_CODE_);
938       break;
940     case CommandEnum.OPEN_HANDWRITING:
941       this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
942           this.currentKeyset_;
943       // TODO: remember handwriting keyset.
944       this.switchToKeyset(Controller.HANDWRITING_VIEW_CODE_);
945       break;
947     case CommandEnum.OPEN_SETTING:
948       if (window.inputview) {
949         inputview.openSettings();
950       }
951       break;
953     case CommandEnum.FLOATING:
954       if (inputview.setMode) {
955         inputview.setMode('FLOATING');
956         this.adapter_.isFloating = true;
957         this.container_.candidateView.setFloatingVKButtonsVisible(true);
958         this.resize();
959         setTimeout(function() {
960           var x = Math.floor((screen.width - window.innerWidth) / 2);
961           window.moveTo(x, window.screenY);
962         }, 0);
963       }
964       break;
966     case CommandEnum.DOCKING:
967       if (inputview.setMode) {
968         inputview.setMode('FULL_WIDTH');
969         this.adapter_.isFloating = false;
970         this.container_.candidateView.setFloatingVKButtonsVisible(false);
971       }
972       break;
974   }
979  * Handles the pointer action.
981  * @param {!i18n.input.chrome.inputview.elements.Element} view The view.
982  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
983  * @private
984  */
985 Controller.prototype.handlePointerAction_ = function(view, e) {
986   if (this.gestureTypingEnabled_() && !this.container_.swipeView.isVisible()) {
987     if (e.type == EventType.POINTER_DOWN) {
988       this.container_.gestureCanvasView.startStroke(e);
989       view.setHighlighted(false);
990     }
992     // Determine if the gestureCanvasView was handling a gesture before calling
993     // endStroke, as it ends the current gesture.
994     var wasGesturing = this.container_.gestureCanvasView.isGesturing;
995     if (e.type == EventType.POINTER_UP && wasGesturing) {
996       this.container_.gestureCanvasView.endStroke(e);
997       this.maybeSendLastStroke_(true);
998       this.commitNextGestureResult_ = true;
999     }
1001     // Do not trigger other activities when gesturing.
1002     if (wasGesturing) {
1003       if (e.type == EventType.POINTER_OVER) {
1004         view.setHighlighted(false);
1005       }
1006       return;
1007     }
1008   }
1010   // Listen for DOUBLE_CLICK as well to capture secondary taps on the spacebar.
1011   if (e.type == EventType.POINTER_UP || e.type == EventType.DOUBLE_CLICK) {
1012     this.recordStatsForClosing_(
1013         'InputMethod.VirtualKeyboard.TapCount', 1, 4095, 4096);
1014   }
1016   if (e.type == EventType.SWIPE) {
1017     e =  /** @type {!i18n.input.chrome.inputview.events.SwipeEvent} */ (e);
1018     this.handleSwipeAction_(view, e);
1019   }
1020   switch (view.type) {
1021     case ElementType.KEYBOARD_CONTAINER_VIEW:
1022       if (e.type == EventType.POINTER_DOWN) {
1023         var tabbableKeysets = [
1024           Controller.HANDWRITING_VIEW_CODE_,
1025           Controller.EMOJI_VIEW_CODE_];
1026         if (goog.array.contains(tabbableKeysets, this.currentKeyset_)) {
1027           this.resetAll();
1028           this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
1029         }
1030       }
1031       return;
1032     case ElementType.BACK_BUTTON:
1033     case ElementType.BACK_TO_KEYBOARD:
1034       if (e.type == EventType.POINTER_OUT || e.type == EventType.POINTER_UP) {
1035         view.setHighlighted(false);
1036       } else if (e.type == EventType.POINTER_DOWN ||
1037           e.type == EventType.POINTER_OVER) {
1038         view.setHighlighted(true);
1039       }
1040       if (e.type == EventType.POINTER_UP) {
1041         this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
1042         this.clearCandidates_();
1043         this.soundController_.onKeyUp(view.type);
1044       }
1045       return;
1046     case ElementType.EXPAND_CANDIDATES:
1047       if (e.type == EventType.POINTER_UP) {
1048         this.showCandidates_(this.candidatesInfo_.source,
1049             this.candidatesInfo_.candidates,
1050             Controller.CandidatesOperation.EXPAND);
1051         this.soundController_.onKeyUp(view.type);
1052       }
1053       return;
1054     case ElementType.SHRINK_CANDIDATES:
1055       if (e.type == EventType.POINTER_UP) {
1056         this.showCandidates_(this.candidatesInfo_.source,
1057             this.candidatesInfo_.candidates,
1058             Controller.CandidatesOperation.SHRINK);
1059         this.soundController_.onKeyUp(view.type);
1060       }
1061       return;
1062     case ElementType.CANDIDATE:
1063       view = /** @type {!i18n.input.chrome.inputview.elements.content.
1064           Candidate} */ (view);
1065       if (view.candidateType == CandidateType.TOOLTIP)
1066         return;
1067       if (e.type == EventType.POINTER_UP) {
1068         if (view.candidateType == CandidateType.CANDIDATE) {
1069           this.adapter_.selectCandidate(view.candidate);
1070         } else if (view.candidateType == CandidateType.NUMBER) {
1071           this.adapter_.sendKeyDownAndUpEvent(
1072               view.candidate[Name.CANDIDATE], '');
1073         }
1074         this.container_.cleanStroke();
1075         this.soundController_.onKeyUp(ElementType.CANDIDATE);
1076       }
1077       if (e.type == EventType.POINTER_OUT || e.type == EventType.POINTER_UP) {
1078         view.setHighlighted(false);
1079       } else if (e.type == EventType.POINTER_DOWN ||
1080           e.type == EventType.POINTER_OVER) {
1081         view.setHighlighted(true);
1082       }
1083       return;
1085     case ElementType.ALTDATA_VIEW:
1086       view = /** @type {!i18n.input.chrome.inputview.elements.content.
1087           AltDataView} */ (view);
1088       if (e.type == EventType.POINTER_UP && e.identifier == view.identifier) {
1089         var ch = view.getHighlightedCharacter();
1090         if (ch) {
1091           this.adapter_.sendKeyDownAndUpEvent(ch, view.triggeredBy.id,
1092               view.triggeredBy.keyCode,
1093               {'sources': [ch.toLowerCase()], 'possibilities': [1]});
1094         }
1095         view.hide();
1096         this.clearUnstickyState_();
1097         this.soundController_.onKeyUp(view.type);
1098       }
1099       return;
1101     case ElementType.MENU_ITEM:
1102       view = /** @type {!i18n.input.chrome.inputview.elements.content.
1103           MenuItem} */ (view);
1104       if (e.type == EventType.POINTER_UP) {
1105         this.executeCommand_.apply(this, view.getCommand());
1106         this.container_.menuView.hide();
1107         this.soundController_.onKeyUp(view.type);
1108         this.resetAll();
1109       }
1110       view.setHighlighted(e.type == EventType.POINTER_DOWN ||
1111           e.type == EventType.POINTER_OVER);
1112       // TODO: Add chrome vox support.
1113       return;
1115     case ElementType.MENU_VIEW:
1116       view = /** @type {!i18n.input.chrome.inputview.elements.content.
1117           MenuView} */ (view);
1119       if (e.type == EventType.CLICK &&
1120           e.target == view.getCoverElement()) {
1121         view.hide();
1122       }
1123       return;
1125     case ElementType.EMOJI_KEY:
1126       if (e.type == EventType.CLICK) {
1127         if (!this.container_.currentKeysetView.isDragging && view.text != '') {
1128           this.adapter_.commitText(view.text);
1129           this.soundController_.onKeyUp(view.type);
1130         }
1131       }
1132       return;
1134     case ElementType.HWT_PRIVACY_GOT_IT:
1135       // Broadcasts the handwriting privacy confirmed message to let canvas
1136       // view handle it.
1137       if (e.type == EventType.POINTER_UP) {
1138         this.adapter_.dispatchEvent(new goog.events.Event(
1139             Type.HWT_PRIVACY_GOT_IT));
1140       }
1141       return;
1143     case ElementType.VOICE_PRIVACY_GOT_IT:
1144       // Broadcasts the voice privacy confirmed message to let voice
1145       // view handle it.
1146       if (e.type == EventType.POINTER_UP) {
1147         this.adapter_.dispatchEvent(new goog.events.Event(
1148             Type.VOICE_PRIVACY_GOT_IT));
1149       }
1150       return;
1152     case ElementType.VOICE_VIEW:
1153       if (e.type == EventType.POINTER_UP) {
1154         this.adapter_.sendVoiceViewStateChange(false);
1155         this.container_.candidateView.switchToIcon(
1156             CandidateView.IconType.VOICE, true);
1157         this.container_.voiceView.stop();
1158       }
1159       return;
1160     case ElementType.SWIPE_VIEW:
1161       this.stopBackspaceAutoRepeat_();
1162       if (e.type == EventType.POINTER_UP ||
1163           e.type == EventType.POINTER_OUT) {
1164         this.clearUnstickyState_();
1165       }
1166       return;
1167     case ElementType.DRAG:
1168       if (e.type == EventType.POINTER_DOWN && this.container_.floatingView) {
1169         this.container_.floatingView.show();
1170       }
1171       return;
1172     case ElementType.FLOATING_VIEW:
1173       if (e.type == EventType.POINTER_UP && this.container_.floatingView) {
1174         this.container_.floatingView.hide();
1175       }
1176       return;
1177     case ElementType.RESIZE:
1178       if (e.type == EventType.POINTER_UP) {
1179         goog.dom.classlist.toggle(this.container_.getElement(), Css.SMALL);
1180         this.resize();
1181       }
1182       return;
1183     case ElementType.CUT:
1184     case ElementType.COPY:
1185     case ElementType.PASTE:
1186     case ElementType.BOLD:
1187     case ElementType.ITALICS:
1188     case ElementType.UNDERLINE:
1189     case ElementType.REDO:
1190     case ElementType.UNDO:
1191     case ElementType.SELECT_ALL:
1192       view.setHighlighted(e.type == EventType.POINTER_DOWN ||
1193           e.type == EventType.POINTER_OVER);
1194       if (e.type == EventType.POINTER_UP) {
1195         this.adapter_.sendKeyDownAndUpEvent(
1196             '', this.elementTypeToKeyCode_[view.type], undefined, undefined, {
1197               ctrl: true,
1198               alt: false,
1199               shift: false
1200             });
1201       }
1202       return;
1203     case ElementType.SOFT_KEY_VIEW:
1204       // Delegates the events on the soft key view to its soft key.
1205       view = /** @type {!i18n.input.chrome.inputview.elements.layout.
1206           SoftKeyView} */ (view);
1207       if (!view.softKey) {
1208         return;
1209       }
1210       view = view.softKey;
1211   }
1213   if (view.type != ElementType.MODIFIER_KEY &&
1214       !this.container_.altDataView.isVisible() &&
1215       !this.container_.menuView.isVisible() &&
1216       !this.container_.swipeView.isVisible()) {
1217     // The highlight of the modifier key is depending on the state instead
1218     // of the key down or up.
1219     if (e.type == EventType.POINTER_OVER || e.type == EventType.POINTER_DOWN ||
1220         e.type == EventType.DOUBLE_CLICK) {
1221       view.setHighlighted(true);
1222     } else if (e.type == EventType.POINTER_OUT ||
1223         e.type == EventType.POINTER_UP ||
1224         e.type == EventType.DOUBLE_CLICK_END) {
1225       view.setHighlighted(false);
1226     }
1227   }
1228   view = /** @type {!i18n.input.chrome.inputview.elements.content.
1229       SoftKey} */ (view);
1230   this.handlePointerEventForSoftKey_(view, e);
1231   this.updateContextModifierState_();
1236  * Handles softkey of the pointer action.
1238  * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} softKey .
1239  * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
1240  * @private
1241  */
1242 Controller.prototype.handlePointerEventForSoftKey_ = function(softKey, e) {
1243   var key;
1244   switch (softKey.type) {
1245     case ElementType.VOICE_BTN:
1246       if (e.type == EventType.POINTER_UP) {
1247         this.container_.candidateView.switchToIcon(
1248             CandidateView.IconType.VOICE, false);
1249         this.container_.voiceView.start();
1250       }
1251       break;
1252     case ElementType.CANDIDATES_PAGE_UP:
1253       if (e.type == EventType.POINTER_UP) {
1254         this.container_.expandedCandidateView.pageUp();
1255       }
1256       break;
1257     case ElementType.CANDIDATES_PAGE_DOWN:
1258       if (e.type == EventType.POINTER_UP) {
1259         this.container_.expandedCandidateView.pageDown();
1260       }
1261       break;
1262     case ElementType.CHARACTER_KEY:
1263       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1264           CharacterKey} */ (softKey);
1265       if (e.type == EventType.LONG_PRESS) {
1266         this.container_.altDataView.show(
1267             key, goog.i18n.bidi.isRtlLanguage(this.languageCode_),
1268             e.identifier);
1269       } else if (e.type == EventType.POINTER_UP) {
1270         this.model_.stateManager.triggerChording();
1271         var ch = key.getActiveCharacter();
1272         if (ch) {
1273           this.adapter_.sendKeyDownAndUpEvent(ch, key.id, key.keyCode,
1274               this.getSpatialData_(key, e.x, e.y));
1275         }
1276         this.clearUnstickyState_();
1277         key.flickerredCharacter = '';
1278       }
1279       break;
1281     case ElementType.MODIFIER_KEY:
1282       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1283           ModifierKey} */(softKey);
1284       var isStateEnabled = this.model_.stateManager.hasState(key.toState);
1285       var isChording = this.model_.stateManager.isChording(key.toState);
1286       if (e.type == EventType.POINTER_DOWN) {
1287         this.changeState_(key.toState, !isStateEnabled, true, false);
1288         this.model_.stateManager.setKeyDown(key.toState, true);
1289       } else if (e.type == EventType.POINTER_UP || e.type == EventType.
1290           POINTER_OUT) {
1291         if (isChording) {
1292           this.changeState_(key.toState, false, false);
1293         } else if (key.toState == StateType.CAPSLOCK) {
1294           this.changeState_(key.toState, isStateEnabled, true, true);
1295         } else if (this.model_.stateManager.isKeyDown(key.toState)) {
1296           this.changeState_(key.toState, isStateEnabled, false);
1297         }
1298         this.model_.stateManager.setKeyDown(key.toState, false);
1299       } else if (e.type == EventType.DOUBLE_CLICK) {
1300         this.changeState_(key.toState, isStateEnabled, true, true);
1301       } else if (e.type == EventType.LONG_PRESS) {
1302         if (!isChording) {
1303           this.changeState_(key.toState, true, true, true);
1304           this.model_.stateManager.setKeyDown(key.toState, false);
1305         }
1306       }
1307       break;
1309     case ElementType.BACKSPACE_KEY:
1310       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1311           FunctionalKey} */(softKey);
1312       if (e.type == EventType.POINTER_DOWN) {
1313         this.backspaceTick_();
1314       } else if (e.type == EventType.POINTER_UP || e.type == EventType.
1315           POINTER_OUT) {
1316         if (!this.container_.swipeView.isVisible()) {
1317           this.stopBackspaceAutoRepeat_();
1318         }
1319       }
1320       this.returnToLetterKeysetOnSpace_ = false;
1321       break;
1323     case ElementType.TAB_KEY:
1324       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1325           FunctionalKey} */ (softKey);
1326       if (e.type == EventType.POINTER_DOWN) {
1327         this.adapter_.sendKeyDownEvent('\u0009', KeyCodes.TAB);
1328       } else if (e.type == EventType.POINTER_UP) {
1329         this.adapter_.sendKeyUpEvent('\u0009', KeyCodes.TAB);
1330       }
1331       this.returnToLetterKeysetOnSpace_ = false;
1332       break;
1334     case ElementType.ENTER_KEY:
1335       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1336           FunctionalKey} */ (softKey);
1337       if (e.type == EventType.POINTER_UP) {
1338         this.adapter_.sendKeyDownAndUpEvent('\u000D', KeyCodes.ENTER);
1339       }
1340       break;
1342     case ElementType.ARROW_UP:
1343       if (e.type == EventType.POINTER_DOWN) {
1344         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_UP);
1345       } else if (e.type == EventType.POINTER_UP) {
1346         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_UP);
1347       }
1348       break;
1350     case ElementType.ARROW_DOWN:
1351       if (e.type == EventType.POINTER_DOWN) {
1352         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_DOWN);
1353       } else if (e.type == EventType.POINTER_UP) {
1354         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_DOWN);
1355       }
1356       break;
1358     case ElementType.ARROW_LEFT:
1359       if (e.type == EventType.POINTER_DOWN) {
1360         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_LEFT);
1361       } else if (e.type == EventType.POINTER_UP) {
1362         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_LEFT);
1363       }
1364       break;
1366     case ElementType.ARROW_RIGHT:
1367       if (e.type == EventType.POINTER_DOWN) {
1368         this.adapter_.sendKeyDownEvent('', KeyCodes.ARROW_RIGHT);
1369       } else if (e.type == EventType.POINTER_UP) {
1370         this.adapter_.sendKeyUpEvent('', KeyCodes.ARROW_RIGHT);
1371       }
1372       break;
1373     case ElementType.EN_SWITCHER:
1374       if (e.type == EventType.POINTER_UP) {
1375         key = /** @type {!i18n.input.chrome.inputview.elements.content.
1376             EnSwitcherKey} */ (softKey);
1377         this.adapter_.toggleLanguageState(this.model_.stateManager.isEnMode);
1378         this.model_.stateManager.isEnMode = !this.model_.stateManager.isEnMode;
1379         key.update();
1380       }
1381       break;
1382     case ElementType.SPACE_KEY:
1383       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1384           SpaceKey} */ (softKey);
1385       var doubleSpacePeriod = this.model_.settings.doubleSpacePeriod &&
1386           this.currentKeyset_ != Controller.HANDWRITING_VIEW_CODE_ &&
1387           this.currentKeyset_ != Controller.EMOJI_VIEW_CODE_;
1388       if (e.type == EventType.POINTER_UP || (!doubleSpacePeriod && e.type ==
1389           EventType.DOUBLE_CLICK_END)) {
1390         this.adapter_.sendKeyDownAndUpEvent(key.getCharacter(),
1391             KeyCodes.SPACE);
1392         this.clearUnstickyState_();
1393       } else if (e.type == EventType.DOUBLE_CLICK && doubleSpacePeriod) {
1394         this.adapter_.doubleClickOnSpaceKey();
1395       }
1396       if (this.returnToLetterKeysetOnSpace_) {
1397         // Return to the letter keyset.
1398         this.switchToKeyset(key.toKeyset);
1399         this.returnToLetterKeysetOnSpace_ = false;
1400       }
1401       break;
1403     case ElementType.SWITCHER_KEY:
1404       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1405           SwitcherKey} */ (softKey);
1406       if (e.type == EventType.POINTER_UP) {
1407         this.recordStatsForClosing_(
1408             'InputMethod.VirtualKeyboard.LayoutSwitch', 1, 25, 25);
1409         if (this.isSubKeyset_(key.toKeyset, this.currentKeyset_)) {
1410           this.model_.stateManager.reset();
1411           this.container_.update();
1412           this.updateContextModifierState_();
1413           this.container_.menuView.hide();
1414         } else {
1415           this.resetAll();
1416         }
1417         // Switch to the specific keyboard.
1418         this.switchToKeyset(key.toKeyset);
1419         if (key.record) {
1420           this.model_.settings.savePreference(
1421               util.getConfigName(key.toKeyset),
1422               key.toKeyset);
1423         }
1424       }
1425       this.returnToLetterKeysetOnSpace_ = false;
1426       break;
1428     case ElementType.COMPACT_KEY:
1429       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1430           CompactKey} */(softKey);
1431       if (e.type == EventType.LONG_PRESS) {
1432         this.container_.altDataView.show(
1433             key, goog.i18n.bidi.isRtlLanguage(this.languageCode_),
1434             e.identifier);
1435       } else if (e.type == EventType.POINTER_UP) {
1436         this.model_.stateManager.triggerChording();
1437         var ch = key.getActiveCharacter();
1438         if (ch.length == 1) {
1439           this.adapter_.sendKeyDownAndUpEvent(key.getActiveCharacter(), '', 0,
1440               this.getSpatialData_(key, e.x, e.y));
1441         } else if (ch.length > 1) {
1442           // Some compact keys contains more than 1 characters, such as '.com',
1443           // 'http://', etc. Those keys should trigger a direct commit text
1444           // instead of key events.
1445           this.adapter_.commitText(ch);
1446         }
1447         this.clearUnstickyState_();
1448         key.flickerredCharacter = '';
1449         if (this.currentKeyset_.indexOf('symbol') != -1) {
1450           // If this is the symbol keyset, a space as the next input should
1451           // switch us to the standard keyset.
1452           this.returnToLetterKeysetOnSpace_ = true;
1453         }
1454       }
1455       break;
1457     case ElementType.HIDE_KEYBOARD_KEY:
1458       var defaultKeyset = this.getActiveKeyset_();
1459       if (e.type == EventType.POINTER_UP) {
1460         this.adapter_.hideKeyboard();
1461         if (this.currentKeyset_ != defaultKeyset) {
1462           this.switchToKeyset(defaultKeyset);
1463         }
1464       }
1465       break;
1467     case ElementType.MENU_KEY:
1468       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1469           MenuKey} */ (softKey);
1470       if (e.type == EventType.POINTER_DOWN) {
1471         var isCompact = this.currentKeyset_.indexOf('compact') != -1;
1472         // Gets the default full keyboard instead of default keyset because
1473         // the default keyset can be a compact keyset which would cause problem
1474         // in MenuView.show().
1475         var defaultFullKeyset = this.initialKeyset_.split(/\./)[0];
1476         var enableCompact = !this.adapter_.isA11yMode && goog.array.contains(
1477             util.KEYSETS_HAVE_COMPACT, defaultFullKeyset);
1478         if (defaultFullKeyset == 'zhuyin' &&
1479             !this.adapter_.features.isEnabled(FeatureName.EXPERIMENTAL) ||
1480             this.languageCode_ == 'ko') {
1481           // Hides 'switch to compact' for zhuyin when not in experimental env.
1482           enableCompact = false;
1483         }
1484         var hasHwt = !this.adapter_.isPasswordBox() &&
1485             !Controller.DISABLE_HWT && goog.object.contains(
1486             InputToolCode, this.getHwtInputToolCode_());
1487         var hasEmoji = !this.adapter_.isPasswordBox();
1488         var enableSettings = this.shouldEnableSettings() &&
1489             !!window.inputview && !!inputview.openSettings;
1490         var enableFVK = this.adapter_.isFloatingVirtualKeyboardEnabled();
1491         this.adapter_.getInputMethods(function(inputMethods) {
1492           this.container_.menuView.show(key, defaultFullKeyset, isCompact,
1493               enableCompact, this.currentInputMethod_, inputMethods, hasHwt,
1494               enableSettings, hasEmoji, this.adapter_.isA11yMode, enableFVK,
1495               this.adapter_.isFloating);
1496         }.bind(this));
1497       }
1498       break;
1500     case ElementType.GLOBE_KEY:
1501       if (e.type == EventType.POINTER_UP) {
1502         this.adapter_.clearModifierStates();
1503         this.adapter_.setModifierState(
1504             i18n.input.chrome.inputview.StateType.ALT, true);
1505         this.adapter_.sendKeyDownAndUpEvent(
1506             KeyCodes.SHIFT, KeyCodes.SHIFT_LEFT, goog.events.KeyCodes.SHIFT);
1507         this.adapter_.setModifierState(
1508             i18n.input.chrome.inputview.StateType.ALT, false);
1509       }
1510       break;
1511     case ElementType.IME_SWITCH:
1512       key = /** @type {!i18n.input.chrome.inputview.elements.content.
1513           FunctionalKey} */ (softKey);
1514       this.adapter_.sendKeyDownAndUpEvent('', key.id);
1515       break;
1516   }
1517   // Play key sound on pointer up or double click.
1518   if (e.type == EventType.POINTER_UP || e.type == EventType.DOUBLE_CLICK)
1519     this.soundController_.onKeyUp(softKey.type);
1524  * Clears unsticky state.
1526  * @private
1527  */
1528 Controller.prototype.clearUnstickyState_ = function() {
1529   if (this.model_.stateManager.hasUnStickyState()) {
1530     for (var key in StateType) {
1531       var value = StateType[key];
1532       if (this.model_.stateManager.hasState(value) &&
1533           !this.model_.stateManager.isSticky(value)) {
1534         this.changeState_(value, false, false);
1535       }
1536     }
1537   }
1538   this.container_.update();
1543  * Stops the auto-repeat for backspace.
1545  * @private
1546  */
1547 Controller.prototype.stopBackspaceAutoRepeat_ = function() {
1548   if (this.backspaceAutoRepeat_) {
1549     goog.dispose(this.backspaceAutoRepeat_);
1550     this.backspaceAutoRepeat_ = null;
1551     this.adapter_.sendKeyUpEvent('\u0008', KeyCodes.BACKSPACE);
1552     this.backspaceRepeated_ = 0;
1553   }
1558  * The tick for the backspace key.
1560  * @private
1561  */
1562 Controller.prototype.backspaceTick_ = function() {
1563   if (this.backspaceRepeated_ >= Controller.BACKSPACE_REPEAT_LIMIT_) {
1564     this.stopBackspaceAutoRepeat_();
1565     return;
1566   }
1567   this.backspaceRepeated_++;
1568   this.backspaceDown_();
1569   this.soundController_.onKeyRepeat(ElementType.BACKSPACE_KEY);
1571   if (this.backspaceAutoRepeat_) {
1572     this.backspaceAutoRepeat_.start(75);
1573   } else {
1574     this.backspaceAutoRepeat_ = new goog.async.Delay(
1575         goog.bind(this.backspaceTick_, this), 300);
1576     this.backspaceAutoRepeat_.start();
1577   }
1582  * Callback for VISIBILITY_CHANGE.
1584  * @private
1585  */
1586 Controller.prototype.onVisibilityChange_ = function() {
1587   if (!this.adapter_.isVisible) {
1588     for (var name in this.statsForClosing_) {
1589       var stat = this.statsForClosing_[name];
1590       this.statistics_.recordValue(name, stat[0], stat[1], stat[2]);
1591     }
1592     this.statistics_.recordValue('InputMethod.VirtualKeyboard.Duration',
1593         Math.floor((new Date() - this.showTimeStamp_) / 1000), 4096, 50);
1594     this.statsForClosing_ = {};
1595     this.showTimeStamp_ = new Date();
1596     this.resetAll();
1597   }
1602  * Resets the whole keyboard include clearing candidates,
1603  * reset modifier state, etc.
1604  */
1605 Controller.prototype.resetAll = function() {
1606   this.clearCandidates_();
1607   this.container_.cleanStroke();
1608   this.model_.stateManager.reset();
1609   this.container_.update();
1610   this.updateContextModifierState_();
1611   this.resize();
1612   this.container_.expandedCandidateView.close();
1613   this.container_.menuView.hide();
1614   this.container_.swipeView.reset();
1615   this.container_.altDataView.hide();
1616   this.container_.gesturePreviewView.hide();
1617   if (this.container_.floatingView) {
1618     this.container_.floatingView.hide();
1619   }
1624  * Returns whether the toolbar should be shown.
1626  * @return {boolean}
1627  * @private
1628  */
1629 Controller.prototype.shouldShowToolBar_ = function() {
1630   return this.adapter_.features.isEnabled(FeatureName.OPTIMIZED_LAYOUTS) &&
1631       this.adapter_.isGoogleDocument() &&
1632       this.adapter_.contextType == ContextType.DEFAULT;
1637  * Callback when the context is changed.
1639  * @private
1640  */
1641 Controller.prototype.onContextFocus_ = function() {
1642   this.resetAll();
1643   this.model_.stateManager.contextType = this.adapter_.contextType;
1644   this.switchToKeyset(this.getActiveKeyset_());
1649  * Callback when surrounding text is changed.
1651  * @param {!i18n.input.chrome.inputview.events.SurroundingTextChangedEvent} e .
1652  * @private
1653  */
1654 Controller.prototype.onSurroundingTextChanged_ = function(e) {
1655   if (!this.model_.settings.autoCapital || !e.text) {
1656     return;
1657   }
1659   var isShiftEnabled = this.model_.stateManager.hasState(StateType.SHIFT);
1660   var needAutoCap = this.model_.settings.autoCapital &&
1661       util.needAutoCap(e.text);
1662   if (needAutoCap && !isShiftEnabled) {
1663     this.changeState_(StateType.SHIFT, true, false);
1664     this.shiftForAutoCapital_ = true;
1665   } else if (!needAutoCap && this.shiftForAutoCapital_) {
1666     this.changeState_(StateType.SHIFT, false, false);
1667   }
1672  * Callback for Context blurs.
1674  * @private
1675  */
1676 Controller.prototype.onContextBlur_ = function() {
1677   this.container_.cleanStroke();
1678   this.container_.menuView.hide();
1683  * Backspace key is down.
1685  * @private
1686  */
1687 Controller.prototype.backspaceDown_ = function() {
1688   if (this.container_.hasStrokesOnCanvas()) {
1689     this.clearCandidates_();
1690     this.container_.cleanStroke();
1691   } else {
1692     this.adapter_.sendKeyDownEvent('\u0008', KeyCodes.BACKSPACE);
1693   }
1694   this.recordStatsForClosing_(
1695       'InputMethod.VirtualKeyboard.BackspaceCount', 1, 4095, 4096);
1696   this.statistics_.recordEnum('InputMethod.VirtualKeyboard.BackspaceOnLayout',
1697       this.statistics_.getLayoutType(this.currentKeyset_,
1698           this.adapter_.isA11yMode),
1699       i18n.input.chrome.Statistics.LayoutTypes.MAX);
1704  * Callback for state change.
1706  * @param {StateType} stateType The state type.
1707  * @param {boolean} enable True to enable the state.
1708  * @param {boolean} isSticky True to make the state sticky.
1709  * @param {boolean=} opt_isFinalSticky .
1710  * @private
1711  */
1712 Controller.prototype.changeState_ = function(stateType, enable, isSticky,
1713     opt_isFinalSticky) {
1714   if (stateType == StateType.ALTGR) {
1715     var code = KeyCodes.ALT_RIGHT;
1716     if (enable) {
1717       this.adapter_.sendKeyDownEvent('', code);
1718     } else {
1719       this.adapter_.sendKeyUpEvent('', code);
1720     }
1721   }
1722   if (stateType == StateType.SHIFT) {
1723     this.shiftForAutoCapital_ = false;
1724   }
1725   var isEnabledBefore = this.model_.stateManager.hasState(stateType);
1726   var isStickyBefore = this.model_.stateManager.isSticky(stateType);
1727   this.model_.stateManager.setState(stateType, enable);
1728   this.model_.stateManager.setSticky(stateType, isSticky);
1729   var isFinalSticky = goog.isDef(opt_isFinalSticky) ? opt_isFinalSticky :
1730       false;
1731   var isFinalStikcyBefore = this.model_.stateManager.isFinalSticky(stateType);
1732   this.model_.stateManager.setFinalSticky(stateType, isFinalSticky);
1733   if (isEnabledBefore != enable || isStickyBefore != isSticky ||
1734       isFinalStikcyBefore != isFinalSticky) {
1735     this.container_.update();
1736   }
1741  * Updates the modifier state for context.
1743  * @private
1744  */
1745 Controller.prototype.updateContextModifierState_ = function() {
1746   var stateManager = this.model_.stateManager;
1747   this.adapter_.setModifierState(StateType.ALT,
1748       stateManager.hasState(StateType.ALT));
1749   this.adapter_.setModifierState(StateType.CTRL,
1750       stateManager.hasState(StateType.CTRL));
1751   this.adapter_.setModifierState(StateType.CAPSLOCK,
1752       stateManager.hasState(StateType.CAPSLOCK));
1753   if (!this.shiftForAutoCapital_) {
1754     // If shift key is automatically on because of feature - autoCapital,
1755     // Don't set modifier state to adapter.
1756     this.adapter_.setModifierState(StateType.SHIFT,
1757         stateManager.hasState(StateType.SHIFT));
1758   }
1763  * Callback for AUTO-COMPLETE event.
1765  * @param {!i18n.input.chrome.DataSource.CandidatesBackEvent} e .
1766  * @private
1767  */
1768 Controller.prototype.onCandidatesBack_ = function(e) {
1769   this.candidatesInfo_ = new i18n.input.chrome.inputview.CandidatesInfo(
1770       e.source, e.candidates);
1771   this.showCandidates_(e.source, e.candidates, Controller.CandidatesOperation.
1772       NONE);
1777  * Callback for gestures results event.
1779  * @param {!i18n.input.chrome.DataSource.GesturesBackEvent} e .
1780  * @private
1781  */
1782 Controller.prototype.onGesturesBack_ = function(e) {
1783   if (!this.commitNextGestureResult_ &&
1784       goog.array.equals(e.results, this.gestureResultsCache_)) {
1785     // If gesture results have not updated, do not transmit to the UI.
1786     return;
1787   } else {
1788     this.gestureResultsCache_ = e.results;
1789   }
1790   var bestResult = e.results[0];
1791   if (this.container_.gesturePreviewView) {
1792     this.container_.gesturePreviewView.show(bestResult);
1793   }
1794   // TODO: Resolve a race where multiple decoder results return after this flag
1795   // is set to true.
1796   if (this.commitNextGestureResult_) {
1797     // Commit the best result.
1798     this.adapter_.commitGestureResult(bestResult);
1799     this.commitNextGestureResult_ = false;
1800     this.gestureResultsCache_ = [];
1801   }
1802   this.showGestureCandidates_(e.results.slice(1));
1807  * Shows the gesture results as candidates.
1809  * @param {!Array<string>} results The gesture results to show.
1810  * @private
1811  */
1812 Controller.prototype.showGestureCandidates_ = function(results) {
1813   // Convert the results to the candidate format.
1814   var candidates = [];
1815   for (var i = 0; i < results.length; ++i) {
1816     var candidate = {};
1817     candidate[Name.CANDIDATE] = results[i];
1818     candidate[Name.CANDIDATE_ID] = i;
1819     candidate[Name.IS_EMOJI] = false;
1820     candidate[Name.MATCHED_LENGTHS] = 0;
1821     candidates.push(candidate);
1822   }
1823   // The source is empty as this is a gesture and not a series of key presses.
1824   this.showCandidates_('', candidates, Controller.CandidatesOperation.NONE);
1829  * Shows the candidates to the candidate view.
1831  * @param {string} source The source text.
1832  * @param {!Array.<!Object>} candidates The candidate text list.
1833  * @param {Controller.CandidatesOperation} operation .
1834  * @private
1835  */
1836 Controller.prototype.showCandidates_ = function(source, candidates,
1837     operation) {
1838   var state = !!source ? ExpandedCandidateView.State.COMPLETION_CORRECTION :
1839       ExpandedCandidateView.State.PREDICTION;
1840   var expandView = this.container_.expandedCandidateView;
1841   var expand = false;
1842   if (operation == Controller.CandidatesOperation.NONE) {
1843     expand = expandView.state == state;
1844   } else {
1845     expand = operation == Controller.CandidatesOperation.EXPAND;
1846   }
1848   if (candidates.length == 0) {
1849     this.clearCandidates_();
1850     expandView.state = ExpandedCandidateView.State.NONE;
1851     return;
1852   }
1854   // The compact pinyin needs full candidates instead of three candidates.
1855   var isThreeCandidates = this.currentKeyset_.indexOf('compact') != -1 &&
1856       this.currentKeyset_.indexOf('pinyin-zh-CN') == -1;
1857   if (isThreeCandidates) {
1858     if (candidates.length > 1) {
1859       // Swap the first candidate and the second candidate.
1860       var tmp = candidates[0];
1861       candidates[0] = candidates[1];
1862       candidates[1] = tmp;
1863     }
1864   }
1865   var isHwt = Controller.HANDWRITING_VIEW_CODE_ == this.currentKeyset_;
1866   this.container_.candidateView.showCandidates(candidates, isThreeCandidates,
1867       this.model_.settings.candidatesNavigation && !isHwt);
1869   // Only sum of candidate is greater than top line count. Need to update
1870   // expand view.
1871   if (expand && this.container_.candidateView.candidateCount <
1872       candidates.length) {
1873     expandView.state = state;
1874     this.container_.currentKeysetView.setVisible(false);
1875     expandView.showCandidates(candidates,
1876         this.container_.candidateView.candidateCount);
1877     this.container_.candidateView.switchToIcon(CandidateView.IconType.
1878         SHRINK_CANDIDATES, true);
1879   } else {
1880     expandView.state = ExpandedCandidateView.State.NONE;
1881     expandView.setVisible(false);
1882     this.container_.candidateView.switchToIcon(CandidateView.IconType.
1883         SHRINK_CANDIDATES, false);
1884     this.container_.currentKeysetView.setVisible(true);
1885   }
1890  * Clears candidates.
1892  * @private
1893  */
1894 Controller.prototype.clearCandidates_ = function() {
1895   this.candidatesInfo_ = i18n.input.chrome.inputview.CandidatesInfo.getEmpty();
1896   this.container_.candidateView.clearCandidates();
1897   this.container_.expandedCandidateView.close();
1898   this.container_.expandedCandidateView.state = ExpandedCandidateView.State.
1899       NONE;
1900   if (this.container_.currentKeysetView) {
1901     this.container_.currentKeysetView.setVisible(true);
1902   }
1904   if (this.currentKeyset_ == Controller.HANDWRITING_VIEW_CODE_ ||
1905       this.currentKeyset_ == Controller.EMOJI_VIEW_CODE_) {
1906     this.container_.candidateView.switchToIcon(
1907         CandidateView.IconType.VOICE, false);
1908     this.container_.candidateView.switchToIcon(
1909         CandidateView.IconType.EXPAND_CANDIDATES, false);
1910   } else {
1911     this.container_.candidateView.switchToIcon(CandidateView.IconType.VOICE,
1912         this.adapter_.isVoiceInputEnabled);
1913   }
1918  * Callback when the layout is loaded.
1920  * @param {!i18n.input.chrome.inputview.events.LayoutLoadedEvent} e The event.
1921  * @private
1922  */
1923 Controller.prototype.onLayoutLoaded_ = function(e) {
1924   var layoutID = e.data['layoutID'];
1925   this.layoutDataMap_[layoutID] = e.data;
1926   this.perfTracker_.tick(PerfTracker.TickName.LAYOUT_LOADED);
1927   this.maybeCreateViews_();
1932  * Creates a keyset view.
1934  * @param {string} keyset The non-raw keyset.
1935  * @private
1936  */
1937 Controller.prototype.createView_ = function(keyset) {
1938   if (this.isDisposed()) {
1939     return;
1940   }
1941   var keysetData = this.keysetDataMap_[keyset];
1942   var layoutId = keysetData[SpecNodeName.LAYOUT];
1943   var layoutData = this.layoutDataMap_[layoutId];
1944   if (this.container_.keysetViewMap[keyset] || !layoutData) {
1945     return;
1946   }
1947   var conditions = {};
1948   conditions[ConditionName.SHOW_ALTGR] =
1949       keysetData[SpecNodeName.HAS_ALTGR_KEY];
1951   conditions[ConditionName.SHOW_MENU] =
1952       keysetData[SpecNodeName.SHOW_MENU_KEY];
1953   // In symbol and more keysets, we want to show a symbol key in the globe
1954   // SoftKeyView. So this view should alway visible in the two keysets.
1955   // Currently, SHOW_MENU_KEY is false for the two keysets, so we use
1956   // !keysetData[SpecNodeName.SHOW_MENU_KEY] here.
1957   conditions[ConditionName.SHOW_GLOBE_OR_SYMBOL] =
1958       !keysetData[SpecNodeName.SHOW_MENU_KEY] ||
1959       this.adapter_.showGlobeKey;
1960   conditions[ConditionName.SHOW_EN_SWITCHER_KEY] = false;
1962   this.container_.addKeysetView(keysetData, layoutData, keyset,
1963       this.languageCode_, this.model_, this.title_, conditions);
1964   this.perfTracker_.tick(PerfTracker.TickName.KEYBOARD_CREATED);
1969  * Creates the whole view.
1971  * @private
1972  */
1973 Controller.prototype.maybeCreateViews_ = function() {
1974   if (!this.isSettingReady) {
1975     return;
1976   }
1978   // Emoji is temp keyset which is delay loaded. So active keyset can be 'us'
1979   // while current keyset is 'emoji'. To make sure delay load can work
1980   // correctly, here need to create/switch to 'emoji' instead of 'us'.
1981   var activeKeyset = (this.currentKeyset_ == Controller.EMOJI_VIEW_CODE_) ?
1982       this.currentKeyset_ : this.getActiveKeyset_();
1983   var remappedActiveKeyset = this.getRemappedKeyset_(activeKeyset);
1984   var created = false;
1985   if (this.keysetDataMap_[remappedActiveKeyset]) {
1986     this.createView_(remappedActiveKeyset);
1987     this.switchToKeyset(activeKeyset);
1988     created = true;
1989   }
1990   // Async creating the non-active keysets to reduce the latency of showing the
1991   // active keyset.
1992   var keyLen = Object.keys(this.keysetDataMap_).length;
1993   if (created && keyLen > 1 || !created && keyLen > 0) {
1994     goog.Timer.callOnce((function() {
1995       for (var keyset in this.keysetDataMap_) {
1996         this.createView_(keyset);
1997       }
1998     }).bind(this));
1999   }
2004  * Switch to a specific keyboard.
2006  * @param {string} keyset The keyset name.
2007  */
2008 Controller.prototype.switchToKeyset = function(keyset) {
2009   if (!this.isSettingReady || this.adapter_.isSwitching()) {
2010     return;
2011   }
2013   var contextType = this.adapter_.contextType;
2014   var ret = this.container_.switchToKeyset(this.getRemappedKeyset_(keyset),
2015       this.title_, this.adapter_.isPasswordBox(), this.adapter_.isA11yMode,
2016       keyset, this.contextTypeToLastKeysetMap_[contextType] ||
2017       this.getActiveKeyset_(), this.languageCode_);
2019   if (ret) {
2020     if (!this.isSubKeyset_(this.currentKeyset_, keyset) &&
2021         keyset != Controller.EMOJI_VIEW_CODE_) {
2022       // If it is the sub keyset switching, or emoji, don't record it.
2023       // Update the keyset of current context type.
2024       this.contextTypeToKeysetMap_[this.currentInputMethod_][contextType] =
2025           keyset;
2026     }
2027     this.updateLanguageState_(this.currentKeyset_, keyset);
2028     this.currentKeyset_ = keyset;
2029     this.resize(Controller.DEV);
2030     this.statistics_.recordLayout(keyset, this.adapter_.isA11yMode);
2031     this.perfTracker_.tick(PerfTracker.TickName.KEYBOARD_SHOWN);
2032     this.perfTracker_.stop();
2033   } else {
2034     // Sets the current keyset for delay switching.
2035     this.currentKeyset_ = keyset;
2036     if (keyset != Controller.EMOJI_VIEW_CODE_) {  // Emoji is temp keyset.
2037       this.contextTypeToKeysetMap_[this.currentInputMethod_][contextType] =
2038           keyset;
2039     }
2040     this.loadResource_(keyset);
2041   }
2043   // TODO: The 'us' part of this code is a workaround an issue where other xkb
2044   // languages seem to be sharing options between each other.
2045   this.isKeysetUSCompact_ =
2046       this.currentKeyset_.indexOf(Controller.US_COMPACT_PREFIX_) >= 0;
2047   // If we're switching to a new keyset, we don't want spacebar to trigger
2048   // another keyset switch.
2049   this.returnToLetterKeysetOnSpace_ = false;
2054  * Callback when the configuration is loaded.
2056  * @param {!i18n.input.chrome.inputview.events.ConfigLoadedEvent} e The event.
2057  * @private
2058  */
2059 Controller.prototype.onConfigLoaded_ = function(e) {
2060   if (this.isDisposed()) {
2061     return;
2062   }
2063   var data = e.data;
2064   var keyboardCode = data[i18n.input.chrome.inputview.SpecNodeName.ID];
2065   this.keysetDataMap_[keyboardCode] = data;
2066   this.perfTracker_.tick(PerfTracker.TickName.KEYSET_LOADED);
2067   var context = data[i18n.input.chrome.inputview.SpecNodeName.ON_CONTEXT];
2068   if (context && !this.adapter_.isA11yMode) {
2069     var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
2070     if (!keySetMap) {
2071       keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
2072     }
2073     keySetMap[context] = keyboardCode;
2074   }
2076   var layoutId = data[i18n.input.chrome.inputview.SpecNodeName.LAYOUT];
2077   data[i18n.input.chrome.inputview.SpecNodeName.LAYOUT] = layoutId;
2078   var layoutData = this.layoutDataMap_[layoutId];
2079   if (layoutData) {
2080     this.maybeCreateViews_();
2081   } else {
2082     this.model_.loadLayout(layoutId);
2083   }
2088  * Resizes the whole UI.
2090  * @param {boolean=} opt_preventResizeTo True if prevent calling
2091  *     window.resizeTo. Used in tests and local UI debug.
2092  */
2093 Controller.prototype.resize = function(opt_preventResizeTo) {
2094   var height;
2095   var width;
2096   var widthPercent;
2097   var candidateViewHeight;
2098   var isLandScape = screen.width > screen.height;
2099   if (isLandScape) {
2100     goog.dom.classlist.addRemove(this.container_.getElement(),
2101         Css.PORTRAIT, Css.LANDSCAPE);
2102   } else {
2103     goog.dom.classlist.addRemove(this.container_.getElement(),
2104         Css.LANDSCAPE, Css.PORTRAIT);
2105   }
2106   var isWideScreen = (Math.min(screen.width, screen.height) / Math.max(
2107       screen.width, screen.height)) < 0.6;
2108   this.model_.stateManager.covariance.update(isWideScreen, isLandScape,
2109       this.adapter_.isA11yMode);
2110   if (this.adapter_.isA11yMode) {
2111     height = SizeSpec.A11Y_HEIGHT;
2112     widthPercent = screen.width > screen.height ? SizeSpec.A11Y_WIDTH_PERCENT.
2113         LANDSCAPE : SizeSpec.A11Y_WIDTH_PERCENT.PORTRAIT;
2114     candidateViewHeight = SizeSpec.A11Y_CANDIDATE_VIEW_HEIGHT;
2115   } else {
2116     var keyset = this.keysetDataMap_[this.currentKeyset_];
2117     var layout = keyset && keyset[SpecNodeName.LAYOUT];
2118     var data = layout && this.layoutDataMap_[layout];
2119     var spec = data && data[SpecNodeName.WIDTH_PERCENT] ||
2120         SizeSpec.NON_A11Y_WIDTH_PERCENT;
2121     height = SizeSpec.NON_A11Y_HEIGHT;
2122     if (isLandScape) {
2123       if (isWideScreen) {
2124         widthPercent = spec['LANDSCAPE_WIDE_SCREEN'];
2125       } else {
2126         widthPercent = spec['LANDSCAPE'];
2127       }
2128     } else {
2129       widthPercent = spec['PORTRAIT'];
2130     }
2131     candidateViewHeight = SizeSpec.NON_A11Y_CANDIDATE_VIEW_HEIGHT;
2132   }
2133   width = this.adapter_.isFloating ?
2134       Math.floor(screen.width * widthPercent) : screen.width;
2135   widthPercent = this.adapter_.isFloating ? 1.0 : widthPercent;
2136   if (goog.dom.classlist.contains(this.container_.getElement(), Css.SMALL)) {
2137     height = SizeSpec.SMALL_SIZE_HEIGHT;
2138     width = SizeSpec.SMALL_SIZE_WIDTH;
2139     candidateViewHeight = SizeSpec.SMALL_SIZE_CANDIDATE_VIEW_HEIGHT;
2140   }
2142   if ((window.innerHeight != height || window.innerWidth != width) &&
2143       !opt_preventResizeTo) {
2144     if (this.lastResizeHeight_ != height || window.innerWidth != width) {
2145       this.lastResizeHeight_ = height;
2146       window.resizeTo(width, height);
2147     }
2148     return;
2149   }
2151   this.container_.setContainerSize(width, height, widthPercent,
2152       candidateViewHeight);
2153   this.container_.candidateView.setToolbarVisible(this.shouldShowToolBar_());
2154   if (this.container_.currentKeysetView) {
2155     this.isKeyboardReady = true;
2156   }
2158   // Transmit the new layout to the decoder.
2159   if (this.gestureTypingEnabled_()) {
2160     this.adapter_.sendKeyboardLayout(
2161         this.container_.currentKeysetView.getKeyboardLayoutForGesture());
2162   }
2167  * Loads the resources, for currentKeyset, passwdKeyset, handwriting,
2168  * emoji, etc.
2170  * @private
2171  */
2172 Controller.prototype.loadAllResources_ = function() {
2173   var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
2174   goog.array.forEach([keysetMap[ContextType.DEFAULT],
2175     keysetMap[ContextType.PASSWORD]], function(keyset) {
2176     this.loadResource_(keyset);
2177   }, this);
2182  * Gets the remapped keyset.
2184  * @param {string} keyset .
2185  * @return {string} The remapped keyset.
2186  * @private
2187  */
2188 Controller.prototype.getRemappedKeyset_ = function(keyset) {
2189   if (goog.array.contains(util.KEYSETS_USE_US, keyset)) {
2190     return 'us-ltr';
2191   }
2192   var match = keyset.match(/^(.*)-rtl$/);
2193   if (match && goog.array.contains(util.KEYSETS_USE_US, match[1])) {
2194     return 'us-rtl';
2195   }
2196   return keyset;
2201  * Loads a single resource.
2203  * @param {string} keyset .
2204  * loaded.
2205  * @private
2206  */
2207 Controller.prototype.loadResource_ = function(keyset) {
2208   var remapped = this.getRemappedKeyset_(keyset);
2209   if (!this.keysetDataMap_[remapped]) {
2210     if (/^m17n:/.test(remapped)) {
2211       this.m17nModel_.loadConfig(remapped);
2212     } else {
2213       this.model_.loadConfig(remapped);
2214     }
2215     return;
2216   }
2218   var layoutId = this.keysetDataMap_[remapped][SpecNodeName.LAYOUT];
2219   if (!this.layoutDataMap_[layoutId]) {
2220     this.model_.loadLayout(layoutId);
2221     return;
2222   }
2227  * Sets the keyboard.
2229  * @param {string} keyset The keyboard keyset.
2230  * @param {string} languageCode The language code for this keyboard.
2231  * @param {string} passwordLayout The layout for password box.
2232  * @param {string} title The title for this keyboard.
2233  */
2234 Controller.prototype.initialize = function(keyset, languageCode, passwordLayout,
2235     title) {
2236   this.perfTracker_.restart();
2237   this.adapter_.getCurrentInputMethod(function(currentInputMethod) {
2238     // TODO: remove this hack as soon as the manifest is fixed in chromium.
2239     if (languageCode == 'ko') {
2240       if (currentInputMethod.indexOf('hangul_2set') > 0) {
2241         keyset = 'm17n:ko_2set';
2242       }
2243     }
2244     this.languageCode_ = languageCode;
2245     // If can't get the current input method, set the original keyset as
2246     // current input method.
2247     this.currentInputMethod_ = currentInputMethod || keyset;
2248     var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
2249     if (!keySetMap) {
2250       keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
2251     }
2252     keySetMap[ContextType.PASSWORD] = passwordLayout;
2253     keySetMap[ContextType.DEFAULT] = keyset;
2255     this.initialKeyset_ = keyset;
2256     this.title_ = title;
2257     this.isSettingReady = false;
2258     this.model_.settings = new i18n.input.chrome.inputview.Settings();
2259     this.model_.stateManager.isEnMode = false;
2260     this.adapter_.initialize();
2261     this.loadAllResources_();
2262     this.switchToKeyset(this.getActiveKeyset_());
2264     // Set language attribute and font of body.
2265     document.body.setAttribute('lang', this.languageCode_);
2266     goog.dom.classlist.add(document.body, Css.FONT);
2267   }.bind(this));
2271 /** @override */
2272 Controller.prototype.disposeInternal = function() {
2273   goog.dispose(this.container_);
2274   goog.dispose(this.adapter_);
2275   goog.dispose(this.handler_);
2276   goog.dispose(this.soundController_);
2278   goog.base(this, 'disposeInternal');
2283  * Gets the handwriting Input Tool code of current language code.
2285  * @return {string} The handwriting Input Tool code.
2286  * @private
2287  */
2288 Controller.prototype.getHwtInputToolCode_ = function() {
2289   return this.languageCode_.split(/_|-/)[0] +
2290       Controller.HANDWRITING_CODE_SUFFIX_;
2295  * True to enable settings link.
2297  * @return {boolean} .
2298  */
2299 Controller.prototype.shouldEnableSettings = function() {
2300   return !this.adapter_.screen || this.adapter_.screen == 'normal';
2305  * Gets the active keyset, if there is a keyset to switch, return it.
2306  * otherwise if it's a password box, return the password keyset,
2307  * otherwise return the current keyset.
2309  * @return {string} .
2310  * @private
2311  */
2312 Controller.prototype.getActiveKeyset_ = function() {
2313   var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
2314   return keySetMap[this.adapter_.contextType] || this.initialKeyset_;
2319  * True if keysetB is the sub keyset of keysetA.
2321  * @param {string} keysetA .
2322  * @param {string} keysetB .
2323  * @return {boolean} .
2324  * @private
2325  */
2326 Controller.prototype.isSubKeyset_ = function(keysetA, keysetB) {
2327   var segmentsA = keysetA.split('.');
2328   var segmentsB = keysetB.split('.');
2329   return segmentsA.length >= 2 && segmentsB.length >= 2 &&
2330       segmentsA[0] == segmentsB[0] && segmentsA[1] == segmentsB[1];
2335  * Updates the compact pinyin to set the inputcode for english and pinyin.
2337  * @param {string} fromRawKeyset .
2338  * @param {string} toRawKeyset .
2339  * @private
2340  */
2341 Controller.prototype.updateLanguageState_ =
2342     function(fromRawKeyset, toRawKeyset) {
2343   var toggle = false;
2344   var toggleState = false;
2345   if (fromRawKeyset != toRawKeyset) {
2346     // Deal with the switch logic to/from English within the compact layout.
2347     if (fromRawKeyset.indexOf('en.compact') *
2348         toRawKeyset.indexOf('en.compact') < 0) { // Switches between non-en/en.
2349       toggle = true;
2350       toggleState = toRawKeyset.indexOf('en.compact') == -1;
2351     } else if (fromRawKeyset.indexOf(toRawKeyset) == 0 &&
2352         fromRawKeyset.indexOf('.compact') > 0 &&
2353         goog.array.contains(util.KEYSETS_HAVE_EN_SWTICHER, toRawKeyset) ||
2354         fromRawKeyset && toRawKeyset.indexOf(fromRawKeyset) == 0 &&
2355         toRawKeyset.indexOf('.compact') > 0) {
2356       // Switch between full/compact layouts, reset the default button and
2357       // language.
2358       toggle = true;
2359       toggleState = true;
2360     }
2361   }
2362   if (toggle) {
2363     this.adapter_.toggleLanguageState(toggleState);
2364     this.model_.stateManager.isEnMode = !toggleState;
2365     this.container_.currentKeysetView.update();
2366   }
2371  * Records the stats which will be reported when input view is closing.
2373  * @param {string} name The metrics name.
2374  * @param {number} count The count value for histogram.
2375  * @param {number} max .
2376  * @param {number} bucketCount .
2377  * @private
2378  */
2379 Controller.prototype.recordStatsForClosing_ = function(
2380     name, count, max, bucketCount) {
2381   if (!this.statsForClosing_[name]) {
2382     this.statsForClosing_[name] = [count, max, bucketCount];
2383   } else {
2384     this.statsForClosing_[name][0] += count;
2385     this.statsForClosing_[name][1] = max;
2386     this.statsForClosing_[name][2] = bucketCount;
2387   }
2392  * Handles language state changing event.
2394  * @param {!events.MessageEvent} e .
2395  * @private
2396  */
2397 Controller.prototype.onUpdateToggleLanguageState_ = function(e) {
2398   if (this.adapter_.isA11yMode || this.currentKeyset_.indexOf('.compact') < 0) {
2399     // e.msg value means whether is Chinese mode now.
2400     if (this.model_.stateManager.isEnMode == e.msg) {
2401       this.model_.stateManager.isEnMode = !e.msg;
2402       this.container_.currentKeysetView.update();
2403     }
2404   } else {
2405     var pos = this.currentKeyset_.indexOf('en.compact');
2406     var toKeyset;
2407     if (pos > 0) { // Means en mode
2408       if (e.msg) { // Needs switch cn mode
2409         toKeyset = this.currentKeyset_.replace('en.compact', 'compact');
2410       }
2411     } else {
2412       if (!e.msg) { // Needs switch en mode
2413         toKeyset = this.currentKeyset_.replace('compact', 'en.compact');
2414       }
2415     }
2416     if (toKeyset) {
2417       this.resetAll();
2418       this.switchToKeyset(toKeyset);
2419     }
2420   }
2422 });  // goog.scope