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
8 // http://www.apache.org/licenses/LICENSE-2.0
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;
87 * The controller of the input view keyboard.
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.
94 * @extends {goog.Disposable}
96 i18n.input.chrome.inputview.Controller = function(keyset, languageCode,
97 passwordLayout, name) {
101 * @type {!i18n.input.chrome.inputview.Model}
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);
113 * @type {!Object.<string, !Object>}
116 this.layoutDataMap_ = {};
121 * @private {!Object.<ElementType, !KeyCodes>}
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
136 * The keyset data map.
138 * @type {!Object.<string, !Object>}
141 this.keysetDataMap_ = {};
146 * @type {!goog.events.EventHandler}
149 this.handler_ = new goog.events.EventHandler(this);
154 * @type {!i18n.input.chrome.inputview.M17nModel}
157 this.m17nModel_ = new i18n.input.chrome.inputview.M17nModel();
160 * The pointer handler.
162 * @type {!i18n.input.chrome.inputview.handler.PointerHandler}
165 this.pointerHandler_ = new i18n.input.chrome.inputview.handler.
169 * The statistics object for recording metrics values.
171 * @type {!i18n.input.chrome.Statistics}
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();
191 * The context type and keyset mapping group by input method id.
192 * key: input method id.
194 * key: context type string.
195 * value: keyset string.
197 * @private {!Object.<string, !Object.<string, string>>}
199 this.contextTypeToKeysetMap_ = {};
203 * The previous raw keyset code before switched to hwt or emoji layout.
204 * key: context type string.
205 * value: keyset string.
207 * @private {!Object.<string, string>}
209 this.contextTypeToLastKeysetMap_ = {};
212 * The stats map for input view closing.
214 * @type {!Object.<string, !Array.<number>>}
217 this.statsForClosing_ = {};
220 * The last height sent to window.resizeTo to avoid multiple equivalent calls.
224 this.lastResizeHeight_ = -1;
227 * The activate (show) time stamp for statistics.
232 this.showTimeStamp_ = new Date();
234 this.initialize(keyset, languageCode, passwordLayout, name);
237 * Note: sets a default empty result to avoid null check.
239 * @private {!i18n.input.chrome.inputview.CandidatesInfo}
241 this.candidatesInfo_ = i18n.input.chrome.inputview.CandidatesInfo.getEmpty();
243 this.registerEventHandler_();
245 goog.inherits(i18n.input.chrome.inputview.Controller,
247 var Controller = i18n.input.chrome.inputview.Controller;
251 * @define {boolean} Flag to disable handwriting.
253 Controller.DISABLE_HWT = false;
257 * A flag to indicate whether the shift is for auto capital.
261 Controller.prototype.shiftForAutoCapital_ = false;
265 * @define {boolean} Flag to indicate whether it is debugging.
267 Controller.DEV = false;
271 * The handwriting view code, use the code can switch handwriting panel view.
276 Controller.HANDWRITING_VIEW_CODE_ = 'hwt';
280 * The emoji view code, use the code can switch to emoji.
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.
294 Controller.BACKSPACE_REPEAT_LIMIT_ = 255;
298 * The repeated times of the backspace.
302 Controller.prototype.backspaceRepeated_ = 0;
306 * The handwriting input tool code suffix.
311 Controller.HANDWRITING_CODE_SUFFIX_ = '-t-i0-handwrit';
315 * True if the settings is loaded.
319 Controller.prototype.isSettingReady = false;
323 * True if the keyboard is set up.
324 * Note: This flag is only used for automation testing.
328 Controller.prototype.isKeyboardReady = false;
332 * The auto repeat timer for backspace hold.
334 * @type {goog.async.Delay}
337 Controller.prototype.backspaceAutoRepeat_;
341 * The initial keyset determined by inputview url and/or settings.
346 Controller.prototype.initialKeyset_ = '';
350 * The current raw keyset code.
355 Controller.prototype.currentKeyset_ = '';
359 * The current input method id.
363 Controller.prototype.currentInputmethod_ = '';
367 * The operations on candidates.
371 Controller.CandidatesOperation = {
379 * A temporary list to track keysets have customized in material design.
381 * @private {!Array.<string>}
383 Controller.MATERIAL_KEYSETS_ = [
390 * The active language code.
395 Controller.prototype.lang_;
399 * The password keyset.
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>}
413 Controller.prototype.softKeyList_;
417 * The mapping from soft key id to soft key view id.
419 * @type {!Object.<string, string>}
422 Controller.prototype.mapping_;
426 * The input tool name.
431 Controller.prototype.title_;
435 * Registers event handlers.
438 Controller.prototype.registerEventHandler_ = function() {
441 EventType.LAYOUT_LOADED,
442 this.onLayoutLoaded_).
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,
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,
459 ], this.onPointerEvent_).
460 listen(this.pointerHandler_,
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 .
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();
499 * Handles the refresh event from adapter.
503 Controller.prototype.onRefresh_ = function() {
504 window.location.reload();
509 * Sets the default keyset for context types.
511 * @param {string} newKeyset .
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;
522 keysetMap[ContextType.DEFAULT] = this.initialKeyset_ = newKeyset;
527 * Callback for updating settings.
529 * @param {!i18n.input.chrome.message.Event} e .
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'];
537 if (goog.isDef(e.msg['autoCapital'])) {
538 settings.autoCapital = e.msg['autoCapital'];
540 if (goog.isDef(e.msg['candidatesNavigation'])) {
541 settings.candidatesNavigation = e.msg['candidatesNavigation'];
542 this.container_.candidateView.setNavigation(settings.candidatesNavigation);
544 if (goog.isDef(e.msg[Name.KEYSET])) {
545 this.setDefaultKeyset_(e.msg[Name.KEYSET]);
547 if (goog.isDef(e.msg['enableLongPress'])) {
548 settings.enableLongPress = e.msg['enableLongPress'];
550 if (goog.isDef(e.msg['doubleSpacePeriod'])) {
551 settings.doubleSpacePeriod = e.msg['doubleSpacePeriod'];
553 if (goog.isDef(e.msg['soundOnKeypress'])) {
554 settings.soundOnKeypress = e.msg['soundOnKeypress'];
555 this.soundController_.setEnabled(settings.soundOnKeypress);
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.
568 Controller.prototype.onURLChanged_ = function() {
569 this.container_.candidateView.setToolbarVisible(this.shouldShowToolBar_());
574 * Callback for setting ready.
578 Controller.prototype.onSettingsReady_ = function() {
579 if (this.isSettingReady) {
583 this.isSettingReady = true;
584 var keysetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
586 if (this.adapter_.isA11yMode) {
587 newKeyset = util.getConfigName(keysetMap[ContextType.DEFAULT]);
589 newKeyset = /** @type {string} */ (this.model_.settings.
590 getPreference(util.getConfigName(keysetMap[ContextType.DEFAULT])));
592 if (!this.adapter_.features.isEnabled(FeatureName.EXPERIMENTAL) &&
593 keysetMap[ContextType.DEFAULT] ==
594 'zhuyin.compact.qwerty') {
595 newKeyset = 'zhuyin';
598 this.setDefaultKeyset_(newKeyset);
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} .
617 Controller.prototype.getSpatialData_ = function(key, x, y) {
619 items.push([this.getKeyContent_(key), key.estimator.estimateInLogSpace(x, y)
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)]);
628 goog.array.sort(items, function(item1, item2) {
629 return item1[1] - item2[1];
631 var sources = items.map(function(item) {
632 return item[0].toLowerCase();
634 var possibilities = items.map(function(item) {
639 'possibilities': possibilities
645 * Gets the key content.
647 * @param {!i18n.input.chrome.inputview.elements.content.SoftKey} key .
651 Controller.prototype.getKeyContent_ = function(key) {
652 if (key.type == i18n.input.chrome.inputview.elements.ElementType.
654 key = /** @type {!i18n.input.chrome.inputview.elements.content.
655 CharacterKey} */ (key);
656 return key.getActiveCharacter();
658 if (key.type == i18n.input.chrome.inputview.elements.ElementType.
660 key = /** @type {!i18n.input.chrome.inputview.elements.content.
661 FunctionalKey} */ (key);
669 * Callback for pointer event.
671 * @param {!i18n.input.chrome.inputview.events.PointerEvent} e .
674 Controller.prototype.onPointerEvent_ = function(e) {
675 if (e.type == EventType.LONG_PRESS) {
676 if (this.adapter_.isChromeVoxOn || !this.model_.settings.enableLongPress) {
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]) {
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
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();
697 this.adapter_.sendKeyDownAndUpEvent(ch, altDataView.triggeredBy.id,
698 altDataView.triggeredBy.keyCode,
699 {'sources': [ch.toLowerCase()], 'possibilities': [1]});
702 this.clearUnstickyState_();
707 if (e.type == EventType.POINTER_UP) {
708 this.stopBackspaceAutoRepeat_();
712 this.handlePointerAction_(e.view, e);
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 .
724 Controller.prototype.onDragEvent_ = function(e) {
725 if (this.adapter_.isGestureTypingEnabled() && e.type == EventType.DRAG) {
726 this.container_.gestureCanvasView.addPoint(e);
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.
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);
746 if (view.type == ElementType.BACKSPACE_KEY) {
747 if (this.container_.swipeView.isVisible() ||
748 this.container_.swipeView.isArmed()) {
749 this.stopBackspaceAutoRepeat_();
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));
762 view.flickerredCharacter = ch;
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) &&
772 view.flickerredCharacter = view.hintText;
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.
786 Controller.prototype.executeCommand_ = function(command, opt_arg) {
787 var CommandEnum = MenuView.Command;
789 case CommandEnum.SWITCH_IME:
790 var inputMethodId = opt_arg;
792 this.adapter_.switchToInputMethod(inputMethodId);
796 case CommandEnum.SWITCH_KEYSET:
797 var keyset = opt_arg;
799 this.recordStatsForClosing_(
800 'InputMethod.VirtualKeyboard.LayoutSwitch', 1, 25, 25);
801 this.switchToKeyset(keyset);
804 case CommandEnum.OPEN_EMOJI:
805 this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
807 this.switchToKeyset(Controller.EMOJI_VIEW_CODE_);
810 case CommandEnum.OPEN_HANDWRITING:
811 this.contextTypeToLastKeysetMap_[this.adapter_.contextType] =
813 // TODO: remember handwriting keyset.
814 this.switchToKeyset(Controller.HANDWRITING_VIEW_CODE_);
817 case CommandEnum.OPEN_SETTING:
818 if (window.inputview) {
819 inputview.openSettings();
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 .
833 Controller.prototype.handlePointerAction_ = function(view, e) {
834 if (this.adapter_.isGestureTypingEnabled() &&
835 e.type == EventType.POINTER_DOWN) {
836 this.container_.gestureCanvasView.startStroke(e);
839 if (this.adapter_.isGestureTypingEnabled() &&
840 e.type == EventType.POINTER_UP) {
841 this.container_.gestureCanvasView.endStroke(e);
844 // Do not trigger other actives when gesturing.
845 if (this.adapter_.isGestureTypingEnabled() &&
846 this.container_.gestureCanvasView.isGesturing) {
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);
856 if (e.type == EventType.SWIPE) {
857 e = /** @type {!i18n.input.chrome.inputview.events.SwipeEvent} */ (e);
858 this.handleSwipeAction_(view, e);
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_)) {
868 this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
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);
880 if (e.type == EventType.POINTER_UP) {
881 this.switchToKeyset(this.container_.currentKeysetView.fromKeyset);
882 this.clearCandidates_();
883 this.soundController_.onKeyUp(view.type);
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);
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);
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]);
911 this.container_.cleanStroke();
912 this.soundController_.onKeyUp(ElementType.CANDIDATE);
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);
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();
928 this.adapter_.sendKeyDownAndUpEvent(ch, view.triggeredBy.id,
929 view.triggeredBy.keyCode,
930 {'sources': [ch.toLowerCase()], 'possibilities': [1]});
933 this.clearUnstickyState_();
934 this.soundController_.onKeyUp(view.type);
938 case ElementType.MENU_ITEM:
939 view = /** @type {!i18n.input.chrome.inputview.elements.content.
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);
947 view.setHighlighted(e.type == EventType.POINTER_DOWN ||
948 e.type == EventType.POINTER_OVER);
949 // TODO: Add chrome vox support.
952 case ElementType.MENU_VIEW:
953 view = /** @type {!i18n.input.chrome.inputview.elements.content.
956 if (e.type == EventType.CLICK &&
957 e.target == view.getCoverElement()) {
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);
971 case ElementType.HWT_PRIVACY_GOT_IT:
972 // Broadcasts the handwriting privacy confirmed message to let canvas
974 this.adapter_.dispatchEvent(new goog.events.Event(
975 Type.HWT_PRIVACY_GOT_IT));
978 case ElementType.VOICE_PRIVACY_GOT_IT:
979 // Broadcasts the voice privacy confirmed message to let voice
981 this.adapter_.dispatchEvent(new goog.events.Event(
982 Type.VOICE_PRIVACY_GOT_IT));
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();
993 case ElementType.SWIPE_VIEW:
994 this.stopBackspaceAutoRepeat_();
995 if (e.type == EventType.POINTER_UP ||
996 e.type == EventType.POINTER_OUT) {
997 this.clearUnstickyState_();
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, {
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) {
1027 view = view.softKey;
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);
1045 view = /** @type {!i18n.input.chrome.inputview.elements.content.
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 .
1059 Controller.prototype.handlePointerEventForSoftKey_ = function(softKey, e) {
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();
1069 case ElementType.CANDIDATES_PAGE_UP:
1070 if (e.type == EventType.POINTER_UP) {
1071 this.container_.expandedCandidateView.pageUp();
1074 case ElementType.CANDIDATES_PAGE_DOWN:
1075 if (e.type == EventType.POINTER_UP) {
1076 this.container_.expandedCandidateView.pageDown();
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_),
1086 } else if (e.type == EventType.POINTER_UP) {
1087 this.model_.stateManager.triggerChording();
1088 var ch = key.getActiveCharacter();
1090 this.adapter_.sendKeyDownAndUpEvent(ch, key.id, key.keyCode,
1091 this.getSpatialData_(key, e.x, e.y));
1093 this.clearUnstickyState_();
1094 key.flickerredCharacter = '';
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.
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);
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) {
1120 this.changeState_(key.toState, true, true, true);
1121 this.model_.stateManager.setKeyDown(key.toState, false);
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.
1133 if (!this.container_.swipeView.isVisible()) {
1134 this.stopBackspaceAutoRepeat_();
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);
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);
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);
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);
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);
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);
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;
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(),
1207 this.clearUnstickyState_();
1208 } else if (e.type == EventType.DOUBLE_CLICK && doubleSpacePeriod) {
1209 this.adapter_.doubleClickOnSpaceKey();
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();
1227 // Switch to the specific keyboard.
1228 this.switchToKeyset(key.toKeyset);
1230 this.model_.settings.savePreference(
1231 util.getConfigName(key.toKeyset),
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_),
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);
1256 this.clearUnstickyState_();
1257 key.flickerredCharacter = '';
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);
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;
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);
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);
1313 case ElementType.IME_SWITCH:
1314 key = /** @type {!i18n.input.chrome.inputview.elements.content.
1315 FunctionalKey} */ (softKey);
1316 this.adapter_.sendKeyDownAndUpEvent('', key.id);
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.
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);
1340 this.container_.update();
1345 * Stops the auto-repeat for backspace.
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;
1360 * The tick for the backspace key.
1364 Controller.prototype.backspaceTick_ = function() {
1365 if (this.backspaceRepeated_ >= Controller.BACKSPACE_REPEAT_LIMIT_) {
1366 this.stopBackspaceAutoRepeat_();
1369 this.backspaceRepeated_++;
1370 this.backspaceDown_();
1371 this.soundController_.onKeyRepeat(ElementType.BACKSPACE_KEY);
1373 if (this.backspaceAutoRepeat_) {
1374 this.backspaceAutoRepeat_.start(75);
1376 this.backspaceAutoRepeat_ = new goog.async.Delay(
1377 goog.bind(this.backspaceTick_, this), 300);
1378 this.backspaceAutoRepeat_.start();
1384 * Callback for VISIBILITY_CHANGE.
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]);
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();
1404 * Resets the whole keyboard include clearing candidates,
1405 * reset modifier state, etc.
1407 Controller.prototype.resetAll = function() {
1408 this.clearCandidates_();
1409 this.container_.cleanStroke();
1410 this.model_.stateManager.reset();
1411 this.container_.update();
1412 this.updateContextModifierState_();
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.
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.
1439 Controller.prototype.onContextFocus_ = function() {
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 .
1452 Controller.prototype.onSurroundingTextChanged_ = function(e) {
1453 if (!this.model_.settings.autoCapital || !e.text) {
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);
1470 * Callback for Context blurs.
1474 Controller.prototype.onContextBlur_ = function() {
1475 this.container_.cleanStroke();
1476 this.container_.menuView.hide();
1481 * Backspace key is down.
1485 Controller.prototype.backspaceDown_ = function() {
1486 if (this.container_.hasStrokesOnCanvas()) {
1487 this.clearCandidates_();
1488 this.container_.cleanStroke();
1490 this.adapter_.sendKeyDownEvent('\u0008', KeyCodes.BACKSPACE);
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 .
1510 Controller.prototype.changeState_ = function(stateType, enable, isSticky,
1511 opt_isFinalSticky) {
1512 if (stateType == StateType.ALTGR) {
1513 var code = KeyCodes.ALT_RIGHT;
1515 this.adapter_.sendKeyDownEvent('', code);
1517 this.adapter_.sendKeyUpEvent('', code);
1520 if (stateType == StateType.SHIFT) {
1521 this.shiftForAutoCapital_ = false;
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 :
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();
1539 * Updates the modifier state for context.
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));
1561 * Callback for AUTO-COMPLETE event.
1563 * @param {!i18n.input.chrome.DataSource.CandidatesBackEvent} e .
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.
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 .
1582 Controller.prototype.showCandidates_ = function(source, candidates,
1584 var state = !!source ? ExpandedCandidateView.State.COMPLETION_CORRECTION :
1585 ExpandedCandidateView.State.PREDICTION;
1586 var expandView = this.container_.expandedCandidateView;
1588 if (operation == Controller.CandidatesOperation.NONE) {
1589 expand = expandView.state == state;
1591 expand = operation == Controller.CandidatesOperation.EXPAND;
1594 if (candidates.length == 0) {
1595 this.clearCandidates_();
1596 expandView.state = ExpandedCandidateView.State.NONE;
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;
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
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);
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);
1636 * Clears candidates.
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.
1646 if (this.container_.currentKeysetView) {
1647 this.container_.currentKeysetView.setVisible(true);
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);
1656 this.container_.candidateView.switchToIcon(
1657 CandidateView.IconType.VOICE, false);
1658 this.container_.candidateView.switchToIcon(
1659 CandidateView.IconType.EXPAND_CANDIDATES, false);
1662 this.container_.candidateView.switchToIcon(CandidateView.IconType.VOICE,
1663 this.adapter_.isVoiceInputEnabled);
1669 * Callback when the layout is loaded.
1671 * @param {!i18n.input.chrome.inputview.events.LayoutLoadedEvent} e The event.
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.
1688 Controller.prototype.createView_ = function(keyset) {
1689 if (this.isDisposed()) {
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) {
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.
1724 Controller.prototype.maybeCreateViews_ = function() {
1725 if (!this.isSettingReady) {
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);
1741 // Async creating the non-active keysets to reduce the latency of showing the
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);
1755 * Switch to a specific keyboard.
1757 * @param {string} keyset The keyset name.
1759 Controller.prototype.switchToKeyset = function(keyset) {
1760 if (!this.isSettingReady || this.adapter_.isSwitching()) {
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_);
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] =
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();
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] =
1791 if (this.adapter_.isQPInputView &&
1792 goog.array.contains(Controller.MATERIAL_KEYSETS_, keyset)) {
1793 this.loadResource_('m-' + keyset);
1795 this.loadResource_(keyset);
1802 * Callback when the configuration is loaded.
1804 * @param {!i18n.input.chrome.inputview.events.ConfigLoadedEvent} e The event.
1807 Controller.prototype.onConfigLoaded_ = function(e) {
1808 if (this.isDisposed()) {
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_];
1819 keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
1821 keySetMap[context] = keyboardCode;
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;
1829 var layoutData = this.layoutDataMap_[layoutId];
1831 this.maybeCreateViews_();
1833 this.model_.loadLayout(layoutId);
1839 * Resizes the whole UI.
1841 * @param {boolean=} opt_ignoreWindowResize .
1843 Controller.prototype.resize = function(opt_ignoreWindowResize) {
1846 var candidateViewHeight;
1847 var isLandScape = screen.width > screen.height;
1849 goog.dom.classlist.addRemove(this.container_.getElement(),
1850 Css.PORTRAIT, Css.LANDSCAPE);
1852 goog.dom.classlist.addRemove(this.container_.getElement(),
1853 Css.LANDSCAPE, Css.PORTRAIT);
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;
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;
1873 widthPercent = spec['LANDSCAPE_WIDE_SCREEN'];
1875 widthPercent = spec['LANDSCAPE'];
1878 widthPercent = spec['PORTRAIT'];
1880 candidateViewHeight = SizeSpec.NON_A11Y_CANDIDATE_VIEW_HEIGHT;
1883 if (window.innerHeight != height && !opt_ignoreWindowResize) {
1884 if (this.lastResizeHeight_ != height) {
1885 this.lastResizeHeight_ = height;
1886 window.resizeTo(screen.width, height);
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;
1901 * Loads the resources, for currentKeyset, passwdKeyset, handwriting,
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);
1916 * Gets the remapped keyset.
1918 * @param {string} keyset .
1919 * @return {string} The remapped keyset.
1922 Controller.prototype.getRemappedKeyset_ = function(keyset) {
1923 if (goog.array.contains(util.KEYSETS_USE_US, keyset)) {
1926 var match = keyset.match(/^(.*)-rtl$/);
1927 if (match && goog.array.contains(util.KEYSETS_USE_US, match[1])) {
1935 * Loads a single resource.
1937 * @param {string} keyset .
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);
1947 this.model_.loadConfig(remapped);
1952 var layoutId = this.keysetDataMap_[remapped][SpecNodeName.LAYOUT];
1953 if (!this.layoutDataMap_[layoutId]) {
1954 this.model_.loadLayout(layoutId);
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.
1968 Controller.prototype.initialize = function(keyset, languageCode, passwordLayout,
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';
1978 this.languageCode_ = languageCode;
1979 this.currentInputMethod_ = currentInputMethod;
1980 var keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_];
1982 keySetMap = this.contextTypeToKeysetMap_[this.currentInputMethod_] = {};
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);
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.
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} .
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} .
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} .
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 .
2073 Controller.prototype.updateLanguageState_ =
2074 function(fromRawKeyset, toRawKeyset) {
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.
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
2095 this.adapter_.toggleLanguageState(toggleState);
2096 this.model_.stateManager.isEnMode = !toggleState;
2097 this.container_.currentKeysetView.update();
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 .
2111 Controller.prototype.recordStatsForClosing_ = function(
2112 name, count, max, bucketCount) {
2113 if (!this.statsForClosing_[name]) {
2114 this.statsForClosing_[name] = [count, max, bucketCount];
2116 this.statsForClosing_[name][0] += count;
2117 this.statsForClosing_[name][1] = max;
2118 this.statsForClosing_[name][2] = bucketCount;
2124 * Handles language state changing event.
2126 * @param {!i18n.input.chrome.message.Event} e .
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();
2137 var pos = this.currentKeyset_.indexOf('en.compact');
2139 if (pos > 0) { // Means en mode
2140 if (e.msg) { // Needs switch cn mode
2141 toKeyset = this.currentKeyset_.replace('en.compact', 'compact');
2144 if (!e.msg) { // Needs switch en mode
2145 toKeyset = this.currentKeyset_.replace('compact', 'en.compact');
2150 this.switchToKeyset(toKeyset);