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.Adapter');
16 goog.require('goog.events.Event');
17 goog.require('goog.events.EventHandler');
18 goog.require('goog.events.EventTarget');
19 goog.require('goog.events.EventType');
20 goog.require('goog.object');
21 goog.require('i18n.input.chrome.DataSource');
22 goog.require('i18n.input.chrome.inputview.FeatureName');
23 goog.require('i18n.input.chrome.inputview.FeatureTracker');
24 goog.require('i18n.input.chrome.inputview.ReadyState');
25 goog.require('i18n.input.chrome.inputview.StateType');
26 goog.require('i18n.input.chrome.inputview.events.EventType');
27 goog.require('i18n.input.chrome.inputview.events.MessageEvent');
28 goog.require('i18n.input.chrome.inputview.events.SurroundingTextChangedEvent');
29 goog.require('i18n.input.chrome.message');
30 goog.require('i18n.input.chrome.message.ContextType');
31 goog.require('i18n.input.chrome.message.Name');
32 goog.require('i18n.input.chrome.message.Type');
34 goog.scope(function() {
35 var CandidatesBackEvent = i18n.input.chrome.DataSource.CandidatesBackEvent;
36 var ContextType = i18n.input.chrome.message.ContextType;
37 var FeatureTracker = i18n.input.chrome.inputview.FeatureTracker;
38 var FeatureName = i18n.input.chrome.inputview.FeatureName;
39 var GesturesBackEvent = i18n.input.chrome.DataSource.GesturesBackEvent;
40 var Name = i18n.input.chrome.message.Name;
41 var State = i18n.input.chrome.inputview.ReadyState.State;
42 var Type = i18n.input.chrome.message.Type;
43 var events = i18n.input.chrome.inputview.events;
48 * The adapter for interview.
50 * @param {!i18n.input.chrome.inputview.ReadyState} readyState .
51 * @extends {goog.events.EventTarget}
54 i18n.input.chrome.inputview.Adapter = function(readyState) {
58 * Whether the keyboard is visible.
62 this.isVisible = !document.webkitHidden;
65 * The modifier state map.
67 * @type {!Object.<i18n.input.chrome.inputview.StateType, boolean>}
70 this.modifierState_ = {};
74 * Tracker for which FeatureName are enabled.
76 * @type {!FeatureTracker};
78 this.features = new FeatureTracker();
82 * The system ready state.
84 * @private {!i18n.input.chrome.inputview.ReadyState}
86 this.readyState_ = readyState;
88 chrome.runtime.onMessage.addListener(this.onMessage_.bind(this));
90 /** @private {!goog.events.EventHandler} */
91 this.handler_ = new goog.events.EventHandler(this);
93 listen(document, 'webkitvisibilitychange', this.onVisibilityChange_).
94 // When screen rotate, will trigger resize event.
95 listen(window, goog.events.EventType.RESIZE, this.onVisibilityChange_);
97 // Notifies the initial visibility change message to background.
98 this.onVisibilityChange_();
100 goog.inherits(i18n.input.chrome.inputview.Adapter,
101 goog.events.EventTarget);
102 var Adapter = i18n.input.chrome.inputview.Adapter;
106 * URL prefixes of common Google sites.
110 Adapter.GoogleSites = {
111 // TODO: Add support for spreadsheets.
112 DOCS: 'https://docs.google.com/document/d'
116 /** @type {boolean} */
117 Adapter.prototype.isA11yMode = false;
120 /** @type {boolean} */
121 Adapter.prototype.isVoiceInputEnabled = true;
124 /** @type {boolean} */
125 Adapter.prototype.showGlobeKey = false;
128 /** @type {string} */
129 Adapter.prototype.contextType = ContextType.DEFAULT;
132 /** @type {string} */
133 Adapter.prototype.screen = '';
136 /** @type {boolean} */
137 Adapter.prototype.isChromeVoxOn = false;
140 /** @type {string} */
141 Adapter.prototype.textBeforeCursor = '';
144 /** @type {boolean} */
145 Adapter.prototype.isFloating = false;
149 * Whether the background controller is on switching.
153 Adapter.prototype.isBgControllerSwitching_ = false;
157 * Callback for updating settings.
159 * @param {!Object} message .
162 Adapter.prototype.onUpdateSettings_ = function(message) {
163 this.screen = message[Name.SCREEN];
164 this.queryCurrentSite();
165 this.contextType = /** @type {string} */ (message[Name.CONTEXT_TYPE]);
166 // Resets the flag, since when inputview receive the update setting response,
167 // it means the background switching is done.
168 this.isBgControllerSwitching_ = false;
170 new events.MessageEvent(events.EventType.UPDATE_SETTINGS, message));
175 * Sets the modifier states.
177 * @param {i18n.input.chrome.inputview.StateType} stateType .
178 * @param {boolean} enable True to enable the state, false otherwise.
180 Adapter.prototype.setModifierState = function(stateType, enable) {
181 this.modifierState_[stateType] = enable;
186 * Clears the modifier states.
188 Adapter.prototype.clearModifierStates = function() {
189 this.modifierState_ = {};
194 * Simulates to send 'keydown' and 'keyup' event.
196 * @param {string} key
197 * @param {string} code
198 * @param {number=} opt_keyCode The key code.
199 * @param {!Object=} opt_spatialData .
200 * @param {!Object.<{ctrl: boolean, shift: boolean}>=} opt_modifiers .
202 Adapter.prototype.sendKeyDownAndUpEvent = function(key, code, opt_keyCode,
203 opt_spatialData, opt_modifiers) {
205 this.generateKeyboardEvent_(
206 goog.events.EventType.KEYDOWN,
212 this.generateKeyboardEvent_(
213 goog.events.EventType.KEYUP,
224 * Simulates to send 'keydown' event.
226 * @param {string} key
227 * @param {string} code
228 * @param {number=} opt_keyCode The key code.
229 * @param {!Object=} opt_spatialData .
231 Adapter.prototype.sendKeyDownEvent = function(key, code, opt_keyCode,
233 this.sendKeyEvent_([this.generateKeyboardEvent_(
234 goog.events.EventType.KEYDOWN, key, code, opt_keyCode,
240 * Sets whether gesture editing is in progress.
242 * @param {boolean} inProgress
243 * @param {?boolean=} opt_isSwipe Whether it was triggered by a swipe.
245 Adapter.prototype.setGestureEditingInProgress = function(inProgress,
247 chrome.runtime.sendMessage(
249 Name.TYPE, Type.SET_GESTURE_EDITING,
250 Name.IN_PROGRESS, inProgress,
251 Name.IS_SWIPE, opt_isSwipe));
256 * Sends a gesture typing event to the backend for decoding.
258 * @param {!Array.<!i18n.input.chrome.inputview.elements.content.
259 * GestureCanvasView.Point>} gestureData The gesture data
262 Adapter.prototype.sendGestureEvent = function(gestureData) {
263 chrome.runtime.sendMessage(
264 goog.object.create(Name.TYPE, Type.SEND_GESTURE_EVENT, Name.GESTURE_DATA,
270 * Simulates to send 'keyup' event.
272 * @param {string} key
273 * @param {string} code
274 * @param {number=} opt_keyCode The key code.
275 * @param {!Object=} opt_spatialData .
277 Adapter.prototype.sendKeyUpEvent = function(key, code, opt_keyCode,
279 this.sendKeyEvent_([this.generateKeyboardEvent_(
280 goog.events.EventType.KEYUP, key, code, opt_keyCode, opt_spatialData)]);
285 * Use {@code chrome.input.ime.sendKeyEvents} to simulate key events.
287 * @param {!Array.<!Object.<string, string|boolean>>} keyData .
290 Adapter.prototype.sendKeyEvent_ = function(keyData) {
291 chrome.runtime.sendMessage(
292 goog.object.create(Name.TYPE, Type.SEND_KEY_EVENT, Name.KEY_DATA,
298 * Sends an updated keyboard layout to the backend gesture typing decoder.
300 * @param {?Object} keyboardLayout The keyboard layout object to send.
302 Adapter.prototype.sendKeyboardLayout = function(keyboardLayout) {
303 chrome.runtime.sendMessage(
304 goog.object.create(Name.TYPE, Type.SEND_KEYBOARD_LAYOUT,
305 Name.KEYBOARD_LAYOUT, keyboardLayout));
310 * Generates a {@code ChromeKeyboardEvent} by given values.
312 * @param {string} type .
313 * @param {string} key The key.
314 * @param {string} code The code.
315 * @param {number=} opt_keyCode The key code.
316 * @param {!Object=} opt_spatialData .
317 * @param {!Object.<{ctrl: boolean, shift: boolean}>=} opt_modifiers .
318 * @return {!Object.<string, string|boolean>}
321 Adapter.prototype.generateKeyboardEvent_ = function(
322 type, key, code, opt_keyCode, opt_spatialData, opt_modifiers) {
323 var StateType = i18n.input.chrome.inputview.StateType;
324 var ctrl = !!this.modifierState_[StateType.CTRL];
325 var alt = !!this.modifierState_[StateType.ALT];
326 var shift = !!this.modifierState_[StateType.SHIFT];
329 if (opt_modifiers.ctrl)
330 ctrl = opt_modifiers.ctrl;
331 if (opt_modifiers.shift)
332 shift = opt_modifiers.shift;
342 'keyCode': opt_keyCode || 0,
343 'spatialData': opt_spatialData
346 result['altKey'] = alt;
347 result['ctrlKey'] = ctrl;
348 result['shiftKey'] = shift;
349 result['capsLock'] = !!this.modifierState_[StateType.CAPSLOCK];
356 * Callback when surrounding text is changed.
358 * @param {string} text .
359 * @param {number} anchor .
360 * @param {number} focus .
363 Adapter.prototype.onSurroundingTextChanged_ = function(text, anchor, focus) {
364 this.textBeforeCursor = text;
365 this.dispatchEvent(new i18n.input.chrome.inputview.events.
366 SurroundingTextChangedEvent(this.textBeforeCursor, anchor, focus));
375 Adapter.prototype.getContext = function() {
376 var matches = this.textBeforeCursor.match(/([a-zA-Z'-Ḁ-ỹÀ-ȳ]+)\s+$/);
377 var text = matches ? matches[1] : '';
383 * Sends request for handwriting.
385 * @param {!Object} payload .
387 Adapter.prototype.sendHwtRequest = function(payload) {
388 chrome.runtime.sendMessage(goog.object.create(
389 Name.TYPE, Type.HWT_REQUEST, Name.MSG, payload
395 * True if it is a password box.
397 * @return {boolean} .
399 Adapter.prototype.isPasswordBox = function() {
400 return this.contextType == 'password';
405 * Whether the floating virtual keyboard feature is enabled.
409 Adapter.prototype.isFloatingVirtualKeyboardEnabled = function() {
410 // This feature depends on setMode API. The api is a private API and may not
411 // be available all the time.
412 if (!inputview || !inputview.setMode) {
415 return this.features.isEnabled(FeatureName.FLOATING_VIRTUAL_KEYBOARD);
420 * Callback when blurs in the context.
424 Adapter.prototype.onContextBlur_ = function() {
425 this.contextType = '';
426 this.dispatchEvent(new goog.events.Event(i18n.input.chrome.inputview.events.
427 EventType.CONTEXT_BLUR));
432 * Asynchronously queries the current site.
434 Adapter.prototype.queryCurrentSite = function() {
436 var criteria = {'active': true, 'lastFocusedWindow': true};
437 if (chrome && chrome.tabs) {
438 chrome.tabs.query(criteria, function(tabs) {
439 tabs[0] && adapter.setCurrentSite_(tabs[0].url);
446 * Sets the current context URL.
448 * @param {string} url .
451 Adapter.prototype.setCurrentSite_ = function(url) {
452 if (url != this.currentSite_) {
453 this.currentSite_ = url;
454 this.dispatchEvent(new goog.events.Event(
455 i18n.input.chrome.inputview.events.EventType.URL_CHANGED));
461 * Returns whether the current context is Google Documents.
463 * @return {boolean} .
465 Adapter.prototype.isGoogleDocument = function() {
466 return this.currentSite_ &&
467 this.currentSite_.lastIndexOf(Adapter.GoogleSites.DOCS) === 0;
472 * Callback when focus on a context.
474 * @param {!Object<string, *>} message .
477 Adapter.prototype.onContextFocus_ = function(message) {
478 // URL might have changed.
479 this.queryCurrentSite();
481 this.contextType = /** @type {string} */ (message[Name.CONTEXT_TYPE]);
482 this.dispatchEvent(new goog.events.Event(
483 i18n.input.chrome.inputview.events.EventType.CONTEXT_FOCUS));
488 * Initializes the communication to background page.
492 Adapter.prototype.initBackground_ = function() {
493 chrome.runtime.getBackgroundPage((function() {
494 this.isBgControllerSwitching_ = true;
495 chrome.runtime.sendMessage(goog.object.create(
496 Name.TYPE, Type.CONNECT,
497 Name.VISIBILITY, this.isVisible));
503 * Loads the keyboard settings.
505 Adapter.prototype.initialize = function() {
506 if (chrome.accessibilityFeatures &&
507 chrome.accessibilityFeatures.spokenFeedback) {
508 chrome.accessibilityFeatures.spokenFeedback.get({}, (function(details) {
509 this.isChromeVoxOn = details['value'];
511 chrome.accessibilityFeatures.spokenFeedback.onChange.addListener((function(
513 if (!this.isChromeVoxOn && details['value']) {
514 this.dispatchEvent(new goog.events.Event(
515 i18n.input.chrome.inputview.events.EventType.REFRESH));
517 this.isChromeVoxOn = details['value'];
521 this.initBackground_();
523 if (window.inputview) {
524 inputview.getKeyboardConfig((function(config) {
525 this.isA11yMode = !!config['a11ymode'];
526 this.features.initialize(config);
527 this.isVoiceInputEnabled =
528 this.features.isEnabled(FeatureName.VOICE_INPUT);
529 this.readyState_.markStateReady(State.KEYBOARD_CONFIG_READY);
530 this.maybeDispatchSettingsReadyEvent_();
532 inputview.getInputMethods((function(inputMethods) {
533 // Only show globe key to switching between IMEs when there are more
535 this.showGlobeKey = inputMethods.length > 1;
536 this.readyState_.markStateReady(State.IME_LIST_READY);
537 this.maybeDispatchSettingsReadyEvent_();
540 this.readyState_.markStateReady(State.IME_LIST_READY);
541 this.readyState_.markStateReady(State.KEYBOARD_CONFIG_READY);
544 this.maybeDispatchSettingsReadyEvent_();
549 * Dispatch event SETTINGS_READY if all required bits are flipped.
553 Adapter.prototype.maybeDispatchSettingsReadyEvent_ = function() {
555 State.KEYBOARD_CONFIG_READY,
556 State.IME_LIST_READY];
558 for (var i = 0; i < states.length; i++) {
559 ready = ready && this.readyState_.isReady(states[i]);
562 window.setTimeout((function() {
563 this.dispatchEvent(new goog.events.Event(
564 i18n.input.chrome.inputview.events.EventType.SETTINGS_READY));
571 * Gets the currently activated input method.
573 * @param {function(string)} callback .
575 Adapter.prototype.getCurrentInputMethod = function(callback) {
576 if (window.inputview && inputview.getCurrentInputMethod) {
577 inputview.getCurrentInputMethod(callback);
585 * Gets the list of all activated input methods.
587 * @param {function(Array.<Object>)} callback .
589 Adapter.prototype.getInputMethods = function(callback) {
590 if (window.inputview && inputview.getInputMethods) {
591 inputview.getInputMethods(callback);
593 // Provides a dummy IME item to enable IME switcher UI.
595 {'indicator': 'DU', 'id': 'DU', 'name': 'Dummy IME', 'command': 1}]);
601 * Switches to the input method with id equals |inputMethodId|.
603 * @param {!string} inputMethodId .
605 Adapter.prototype.switchToInputMethod = function(inputMethodId) {
606 if (window.inputview && inputview.switchToInputMethod) {
607 inputview.switchToInputMethod(inputMethodId);
613 * Callback for visibility change on the input view window.
617 Adapter.prototype.onVisibilityChange_ = function() {
618 this.isVisible = !document.webkitHidden;
619 this.dispatchEvent(new goog.events.Event(i18n.input.chrome.inputview.
620 events.EventType.VISIBILITY_CHANGE));
621 chrome.runtime.sendMessage(goog.object.create(
622 Name.TYPE, Type.VISIBILITY_CHANGE,
623 Name.VISIBILITY, !document.webkitHidden));
628 * Sends request for completion.
630 * @param {string} query .
631 * @param {!Object=} opt_spatialData .
633 Adapter.prototype.sendCompletionRequest = function(query, opt_spatialData) {
634 var spatialData = {};
635 if (opt_spatialData) {
636 spatialData[Name.SOURCES] = opt_spatialData.sources;
637 spatialData[Name.POSSIBILITIES] = opt_spatialData.possibilities;
639 chrome.runtime.sendMessage(goog.object.create(Name.TYPE,
640 Type.COMPLETION, Name.TEXT, query, Name.SPATIAL_DATA, spatialData));
645 * Selects the candidate.
647 * @param {!Object} candidate .
649 Adapter.prototype.selectCandidate = function(candidate) {
650 chrome.runtime.sendMessage(goog.object.create(
651 Name.TYPE, Type.SELECT_CANDIDATE, Name.CANDIDATE, candidate));
658 * @param {string} text .
660 Adapter.prototype.commitText = function(text) {
661 chrome.runtime.sendMessage(goog.object.create(
662 Name.TYPE, Type.COMMIT_TEXT, Name.TEXT, text));
667 * Commits the gesture result.
669 * @param {string} text .
671 Adapter.prototype.commitGestureResult = function(text) {
672 chrome.runtime.sendMessage(goog.object.create(
673 Name.TYPE, Type.CONFIRM_GESTURE_RESULT, Name.TEXT, text));
680 * @param {string} language .
682 Adapter.prototype.setLanguage = function(language) {
683 chrome.runtime.sendMessage(goog.object.create(
684 Name.TYPE, Type.SET_LANGUAGE, Name.LANGUAGE, language));
689 * Callbck when completion is back.
691 * @param {!Object} message .
694 Adapter.prototype.onCandidatesBack_ = function(message) {
695 var source = message['source'] || '';
696 var candidates = message['candidates'] || [];
697 this.dispatchEvent(new CandidatesBackEvent(source, candidates));
702 * Callbck when completion is back.
704 * @param {!Object} message .
707 Adapter.prototype.onGesturesBack_ = function(message) {
708 var results = message[Name.GESTURE_RESULTS];
709 this.dispatchEvent(new GesturesBackEvent(results));
714 * Hides the keyboard.
716 Adapter.prototype.hideKeyboard = function() {
717 chrome.input.ime.hideInputView();
722 * Sends DOUBLE_CLICK_ON_SPACE_KEY message.
724 Adapter.prototype.doubleClickOnSpaceKey = function() {
725 chrome.runtime.sendMessage(
728 Type.DOUBLE_CLICK_ON_SPACE_KEY));
733 * Sends message to the background when do internal inputtool switch.
735 * @param {boolean} inputToolValue The value of the language flag.
737 Adapter.prototype.toggleLanguageState = function(inputToolValue) {
738 chrome.runtime.sendMessage(
741 Type.TOGGLE_LANGUAGE_STATE,
748 * Processes incoming message from option page or inputview window.
750 * @param {*} request Message from option page or inputview window.
751 * @param {*} sender Information about the script
752 * context that sent the message.
753 * @param {function(*): void} sendResponse Function to call to send a response.
754 * @return {boolean|undefined} {@code true} to keep the message channel open in
755 * order to send a response asynchronously.
758 Adapter.prototype.onMessage_ = function(request, sender, sendResponse) {
759 var type = request[Name.TYPE];
760 var msg = request[Name.MSG];
761 if (!i18n.input.chrome.message.isFromBackground(type)) {
765 case Type.CANDIDATES_BACK:
766 this.onCandidatesBack_(msg);
768 case Type.CONTEXT_FOCUS:
769 this.onContextFocus_(msg);
771 case Type.CONTEXT_BLUR:
772 this.onContextBlur_();
774 case Type.GESTURES_BACK:
775 this.onGesturesBack_(msg);
777 case Type.SURROUNDING_TEXT_CHANGED:
778 this.onSurroundingTextChanged_(request[Name.TEXT],
779 request[Name.ANCHOR],
780 request[Name.FOCUS]);
782 case Type.UPDATE_SETTINGS:
783 this.onUpdateSettings_(msg);
785 case Type.VOICE_STATE_CHANGE:
787 new events.MessageEvent(events.EventType.VOICE_STATE_CHANGE,
790 case Type.HWT_NETWORK_ERROR:
792 new events.MessageEvent(events.EventType.HWT_NETWORK_ERROR,
795 case Type.FRONT_TOGGLE_LANGUAGE_STATE:
797 new events.MessageEvent(events.EventType.FRONT_TOGGLE_LANGUAGE_STATE,
805 * Sends the voice state to background.
807 * @param {boolean} state .
809 Adapter.prototype.sendVoiceViewStateChange = function(state) {
810 chrome.runtime.sendMessage(goog.object.create(
811 Name.TYPE, Type.VOICE_VIEW_STATE_CHANGE, Name.MSG, state));
816 Adapter.prototype.disposeInternal = function() {
817 goog.dispose(this.handler_);
819 goog.base(this, 'disposeInternal');
824 * Return the background IME switching state.
828 Adapter.prototype.isSwitching = function() {
829 return this.isBgControllerSwitching_;
836 * @param {string} keyset The keyset.
837 * @param {string} languageCode The language code.
839 Adapter.prototype.setController = function(keyset, languageCode) {
840 chrome.runtime.sendMessage(
845 {'rawkeyset': keyset, 'languageCode': languageCode}));
850 * Unset the inputtool
852 Adapter.prototype.unsetController = function() {
853 chrome.runtime.sendMessage(
856 Type.UNSET_CONTROLLER));