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.GlobalFlags');
25 goog
.require('i18n.input.chrome.inputview.ReadyState');
26 goog
.require('i18n.input.chrome.inputview.StateType');
27 goog
.require('i18n.input.chrome.inputview.events.EventType');
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.Event');
32 goog
.require('i18n.input.chrome.message.Name');
33 goog
.require('i18n.input.chrome.message.Type');
35 goog
.scope(function() {
36 var CandidatesBackEvent
= i18n
.input
.chrome
.DataSource
.CandidatesBackEvent
;
37 var ContextType
= i18n
.input
.chrome
.message
.ContextType
;
38 var FeatureTracker
= i18n
.input
.chrome
.inputview
.FeatureTracker
;
39 var FeatureName
= i18n
.input
.chrome
.inputview
.FeatureName
;
40 var Name
= i18n
.input
.chrome
.message
.Name
;
41 var Type
= i18n
.input
.chrome
.message
.Type
;
46 * The adapter for interview.
48 * @param {!i18n.input.chrome.inputview.ReadyState} readyState .
49 * @extends {goog.events.EventTarget}
52 i18n
.input
.chrome
.inputview
.Adapter = function(readyState
) {
56 * Whether the keyboard is visible.
60 this.isVisible
= !document
.webkitHidden
;
63 * The modifier state map.
65 * @type {!Object.<i18n.input.chrome.inputview.StateType, boolean>}
68 this.modifierState_
= {};
72 * Tracker for which FeatureName are enabled.
74 * @type {!FeatureTracker};
76 this.features
= new FeatureTracker();
80 * The system ready state.
82 * @private {!i18n.input.chrome.inputview.ReadyState}
84 this.readyState_
= readyState
;
86 chrome
.runtime
.onMessage
.addListener(this.onMessage_
.bind(this));
88 /** @private {!goog.events.EventHandler} */
89 this.handler_
= new goog
.events
.EventHandler(this);
91 listen(document
, 'webkitvisibilitychange', this.onVisibilityChange_
).
92 // When screen rotate, will trigger resize event.
93 listen(window
, goog
.events
.EventType
.RESIZE
, this.onVisibilityChange_
);
95 // Notifies the initial visibility change message to background.
96 this.onVisibilityChange_();
98 goog
.inherits(i18n
.input
.chrome
.inputview
.Adapter
,
99 goog
.events
.EventTarget
);
100 var Adapter
= i18n
.input
.chrome
.inputview
.Adapter
;
104 * URL prefixes of common Google sites.
108 Adapter
.GoogleSites
= {
109 // TODO: Add support for spreadsheets.
110 DOCS
: 'https://docs.google.com/document/d'
114 /** @type {boolean} */
115 Adapter
.prototype.isA11yMode
= false;
118 /** @type {boolean} */
119 Adapter
.prototype.isVoiceInputEnabled
= true;
122 /** @type {boolean} */
123 Adapter
.prototype.showGlobeKey
= false;
126 /** @type {string} */
127 Adapter
.prototype.contextType
= ContextType
.DEFAULT
;
130 /** @type {string} */
131 Adapter
.prototype.screen
= '';
134 /** @type {boolean} */
135 Adapter
.prototype.isChromeVoxOn
= false;
138 /** @type {string} */
139 Adapter
.prototype.textBeforeCursor
= '';
142 /** @type {boolean} */
143 Adapter
.prototype.isQPInputView
= true;
147 * Whether the background controller is on switching.
151 Adapter
.prototype.isBgControllerSwitching_
= false;
155 * Callback for updating settings.
157 * @param {!Object} message .
160 Adapter
.prototype.onUpdateSettings_ = function(message
) {
161 this.screen
= message
[Name
.SCREEN
];
162 this.queryCurrentSite();
163 this.contextType
= /** @type {string} */ (message
[Name
.CONTEXT_TYPE
]);
164 // Resets the flag, since when inputview receive the update setting response,
165 // it means the background switching is done.
166 this.isBgControllerSwitching_
= false;
167 this.dispatchEvent(new i18n
.input
.chrome
.message
.Event(Type
.UPDATE_SETTINGS
,
173 * Sets the modifier states.
175 * @param {i18n.input.chrome.inputview.StateType} stateType .
176 * @param {boolean} enable True to enable the state, false otherwise.
178 Adapter
.prototype.setModifierState = function(stateType
, enable
) {
179 this.modifierState_
[stateType
] = enable
;
184 * Clears the modifier states.
186 Adapter
.prototype.clearModifierStates = function() {
187 this.modifierState_
= {};
192 * Simulates to send 'keydown' and 'keyup' event.
194 * @param {string} key
195 * @param {string} code
196 * @param {number=} opt_keyCode The key code.
197 * @param {!Object=} opt_spatialData .
198 * @param {!Object.<{ctrl: boolean, shift: boolean}>=} opt_modifiers .
200 Adapter
.prototype.sendKeyDownAndUpEvent = function(key
, code
, opt_keyCode
,
201 opt_spatialData
, opt_modifiers
) {
203 this.generateKeyboardEvent_(
204 goog
.events
.EventType
.KEYDOWN
,
210 this.generateKeyboardEvent_(
211 goog
.events
.EventType
.KEYUP
,
222 * Simulates to send 'keydown' event.
224 * @param {string} key
225 * @param {string} code
226 * @param {number=} opt_keyCode The key code.
227 * @param {!Object=} opt_spatialData .
229 Adapter
.prototype.sendKeyDownEvent = function(key
, code
, opt_keyCode
,
231 this.sendKeyEvent_([this.generateKeyboardEvent_(
232 goog
.events
.EventType
.KEYDOWN
, key
, code
, opt_keyCode
,
238 * Simulates to send 'keyup' event.
240 * @param {string} key
241 * @param {string} code
242 * @param {number=} opt_keyCode The key code.
243 * @param {!Object=} opt_spatialData .
245 Adapter
.prototype.sendKeyUpEvent = function(key
, code
, opt_keyCode
,
247 this.sendKeyEvent_([this.generateKeyboardEvent_(
248 goog
.events
.EventType
.KEYUP
, key
, code
, opt_keyCode
, opt_spatialData
)]);
253 * Use {@code chrome.input.ime.sendKeyEvents} to simulate key events.
255 * @param {!Array.<!Object.<string, string|boolean>>} keyData .
258 Adapter
.prototype.sendKeyEvent_ = function(keyData
) {
259 chrome
.runtime
.sendMessage(
260 goog
.object
.create(Name
.TYPE
, Type
.SEND_KEY_EVENT
, Name
.KEY_DATA
,
266 * Generates a {@code ChromeKeyboardEvent} by given values.
268 * @param {string} type .
269 * @param {string} key The key.
270 * @param {string} code The code.
271 * @param {number=} opt_keyCode The key code.
272 * @param {!Object=} opt_spatialData .
273 * @param {!Object.<{ctrl: boolean, shift: boolean}>=} opt_modifiers .
274 * @return {!Object.<string, string|boolean>}
277 Adapter
.prototype.generateKeyboardEvent_ = function(
278 type
, key
, code
, opt_keyCode
, opt_spatialData
, opt_modifiers
) {
279 var StateType
= i18n
.input
.chrome
.inputview
.StateType
;
280 var ctrl
= !!this.modifierState_
[StateType
.CTRL
];
281 var alt
= !!this.modifierState_
[StateType
.ALT
];
282 var shift
= !!this.modifierState_
[StateType
.SHIFT
];
285 if (opt_modifiers
.ctrl
)
286 ctrl
= opt_modifiers
.ctrl
;
287 if (opt_modifiers
.shift
)
288 shift
= opt_modifiers
.shift
;
298 'keyCode': opt_keyCode
|| 0,
299 'spatialData': opt_spatialData
302 result
['altKey'] = alt
;
303 result
['ctrlKey'] = ctrl
;
304 result
['shiftKey'] = shift
;
305 result
['capsLock'] = !!this.modifierState_
[StateType
.CAPSLOCK
];
312 * Callback when surrounding text is changed.
314 * @param {string} text .
315 * @param {number} anchor .
316 * @param {number} focus .
319 Adapter
.prototype.onSurroundingTextChanged_ = function(text
, anchor
, focus
) {
320 this.textBeforeCursor
= text
;
321 this.dispatchEvent(new i18n
.input
.chrome
.inputview
.events
.
322 SurroundingTextChangedEvent(this.textBeforeCursor
, anchor
, focus
));
331 Adapter
.prototype.getContext = function() {
332 var matches
= this.textBeforeCursor
.match(/([a-zA-Z'-Ḁ-ỹÀ-ȳ]+)\s+$/);
333 var text
= matches
? matches
[1] : '';
339 * Sends request for handwriting.
341 * @param {!Object} payload .
343 Adapter
.prototype.sendHwtRequest = function(payload
) {
344 chrome
.runtime
.sendMessage(goog
.object
.create(
345 Name
.TYPE
, Type
.HWT_REQUEST
, Name
.MSG
, payload
351 * True if it is a password box.
353 * @return {boolean} .
355 Adapter
.prototype.isPasswordBox = function() {
356 return this.contextType
== 'password';
361 * True to enable gesture deletion.
365 Adapter
.prototype.isGestureDeletionEnabled = function() {
366 // TODO: Omni bar sends wrong anchor/focus when autocompleting
367 // URLs. Re-enable when that is fixed.
368 if (this.contextType
== ContextType
.URL
) {
371 return this.features
.isEnabled(FeatureName
.GESTURE_DELETION
);
376 * True to enable gesture typing.
380 Adapter
.prototype.isGestureTypingEnabled = function() {
381 return this.features
.isEnabled(FeatureName
.GESTURE_TYPING
);
386 * Callback when blurs in the context.
390 Adapter
.prototype.onContextBlur_ = function() {
391 this.contextType
= '';
392 this.dispatchEvent(new goog
.events
.Event(i18n
.input
.chrome
.inputview
.events
.
393 EventType
.CONTEXT_BLUR
));
398 * Asynchronously queries the current site.
400 Adapter
.prototype.queryCurrentSite = function() {
402 var criteria
= {'active': true, 'lastFocusedWindow': true};
403 if (chrome
&& chrome
.tabs
) {
404 chrome
.tabs
.query(criteria
, function(tabs
) {
405 tabs
[0] && adapter
.setCurrentSite_(tabs
[0].url
);
412 * Sets the current context URL.
414 * @param {string} url .
417 Adapter
.prototype.setCurrentSite_ = function(url
) {
418 if (url
!= this.currentSite_
) {
419 this.currentSite_
= url
;
420 this.dispatchEvent(new goog
.events
.Event(
421 i18n
.input
.chrome
.inputview
.events
.EventType
.URL_CHANGED
));
427 * Returns whether the current context is Google Documents.
429 * @return {boolean} .
431 Adapter
.prototype.isGoogleDocument = function() {
432 return this.currentSite_
&&
433 this.currentSite_
.lastIndexOf(Adapter
.GoogleSites
.DOCS
) === 0;
438 * Callback when focus on a context.
440 * @param {!Object<string, *>} message .
443 Adapter
.prototype.onContextFocus_ = function(message
) {
444 // URL might have changed.
445 this.queryCurrentSite();
447 this.contextType
= /** @type {string} */ (message
[Name
.CONTEXT_TYPE
]);
448 this.dispatchEvent(new goog
.events
.Event(
449 i18n
.input
.chrome
.inputview
.events
.EventType
.CONTEXT_FOCUS
));
454 * Initializes the communication to background page.
458 Adapter
.prototype.initBackground_ = function() {
459 chrome
.runtime
.getBackgroundPage((function() {
460 this.isBgControllerSwitching_
= true;
461 chrome
.runtime
.sendMessage(goog
.object
.create(
462 Name
.TYPE
, Type
.CONNECT
,
463 Name
.VISIBILITY
, this.isVisible
));
469 * Loads the keyboard settings.
471 * @param {string} languageCode The language code.
473 Adapter
.prototype.initialize = function(languageCode
) {
474 if (chrome
.accessibilityFeatures
&&
475 chrome
.accessibilityFeatures
.spokenFeedback
) {
476 chrome
.accessibilityFeatures
.spokenFeedback
.get({}, (function(details
) {
477 this.isChromeVoxOn
= details
['value'];
479 chrome
.accessibilityFeatures
.spokenFeedback
.onChange
.addListener((function(
481 if (!this.isChromeVoxOn
&& details
['value']) {
482 this.dispatchEvent(new goog
.events
.Event(
483 i18n
.input
.chrome
.inputview
.events
.EventType
.REFRESH
));
485 this.isChromeVoxOn
= details
['value'];
489 this.initBackground_();
491 var StateType
= i18n
.input
.chrome
.inputview
.ReadyState
.StateType
;
492 if (window
.inputview
) {
493 inputview
.getKeyboardConfig((function(config
) {
494 this.isA11yMode
= !!config
['a11ymode'];
495 this.features
.initialize(config
);
496 this.readyState_
.markStateReady(StateType
.KEYBOARD_CONFIG_READY
);
497 this.maybeDispatchSettingsReadyEvent_();
499 inputview
.getInputMethods((function(inputMethods
) {
500 // Only show globe key to switching between IMEs when there are more
502 this.showGlobeKey
= inputMethods
.length
> 1;
503 this.readyState_
.markStateReady(StateType
.IME_LIST_READY
);
504 this.maybeDispatchSettingsReadyEvent_();
506 inputview
.getInputMethodConfig((function(config
) {
507 this.isQPInputView
= !!config
['isNewQPInputViewEnabled'] ||
508 !!config
['isNewMDInputViewEnabled'];
509 var voiceEnabled
= config
['isVoiceInputEnabled'];
510 if (goog
.isDef(voiceEnabled
)) {
511 this.isVoiceInputEnabled
= !!voiceEnabled
;
513 i18n
.input
.chrome
.inputview
.GlobalFlags
.isQPInputView
=
515 this.readyState_
.markStateReady(StateType
.INPUT_METHOD_CONFIG_READY
);
516 this.maybeDispatchSettingsReadyEvent_();
519 this.readyState_
.markStateReady(StateType
.IME_LIST_READY
);
520 this.readyState_
.markStateReady(StateType
.KEYBOARD_CONFIG_READY
);
521 this.readyState_
.markStateReady(StateType
.INPUT_METHOD_CONFIG_READY
);
524 this.maybeDispatchSettingsReadyEvent_();
529 * Dispatch event SETTINGS_READY if all required bits are flipped.
533 Adapter
.prototype.maybeDispatchSettingsReadyEvent_ = function() {
534 var StateType
= i18n
.input
.chrome
.inputview
.ReadyState
.StateType
;
536 StateType
.KEYBOARD_CONFIG_READY
,
537 StateType
.IME_LIST_READY
,
538 StateType
.INPUT_METHOD_CONFIG_READY
];
540 for (var i
= 0; i
< states
.length
; i
++) {
541 ready
= ready
&& this.readyState_
.isReady(states
[i
]);
544 window
.setTimeout((function() {
545 this.dispatchEvent(new goog
.events
.Event(
546 i18n
.input
.chrome
.inputview
.events
.EventType
.SETTINGS_READY
));
553 * Gets the currently activated input method.
555 * @param {function(string)} callback .
557 Adapter
.prototype.getCurrentInputMethod = function(callback
) {
558 if (window
.inputview
&& inputview
.getCurrentInputMethod
) {
559 inputview
.getCurrentInputMethod(callback
);
567 * Gets the list of all activated input methods.
569 * @param {function(Array.<Object>)} callback .
571 Adapter
.prototype.getInputMethods = function(callback
) {
572 if (window
.inputview
&& inputview
.getInputMethods
) {
573 inputview
.getInputMethods(callback
);
575 // Provides a dummy IME item to enable IME switcher UI.
577 {'indicator': 'DU', 'id': 'DU', 'name': 'Dummy IME', 'command': 1}]);
583 * Switches to the input method with id equals |inputMethodId|.
585 * @param {!string} inputMethodId .
587 Adapter
.prototype.switchToInputMethod = function(inputMethodId
) {
588 if (window
.inputview
&& inputview
.switchToInputMethod
) {
589 inputview
.switchToInputMethod(inputMethodId
);
595 * Callback for visibility change on the input view window.
599 Adapter
.prototype.onVisibilityChange_ = function() {
600 this.isVisible
= !document
.webkitHidden
;
601 this.dispatchEvent(new goog
.events
.Event(i18n
.input
.chrome
.inputview
.
602 events
.EventType
.VISIBILITY_CHANGE
));
603 chrome
.runtime
.sendMessage(goog
.object
.create(
604 Name
.TYPE
, Type
.VISIBILITY_CHANGE
,
605 Name
.VISIBILITY
, !document
.webkitHidden
,
606 Name
.WORKSPACE_HEIGHT
, screen
.height
- window
.innerHeight
));
611 * Sends request for completion.
613 * @param {string} query .
614 * @param {!Object=} opt_spatialData .
616 Adapter
.prototype.sendCompletionRequest = function(query
, opt_spatialData
) {
617 var spatialData
= {};
618 if (opt_spatialData
) {
619 spatialData
[Name
.SOURCES
] = opt_spatialData
.sources
;
620 spatialData
[Name
.POSSIBILITIES
] = opt_spatialData
.possibilities
;
622 chrome
.runtime
.sendMessage(goog
.object
.create(Name
.TYPE
,
623 Type
.COMPLETION
, Name
.TEXT
, query
, Name
.SPATIAL_DATA
, spatialData
));
628 * Selects the candidate.
630 * @param {!Object} candidate .
632 Adapter
.prototype.selectCandidate = function(candidate
) {
633 chrome
.runtime
.sendMessage(goog
.object
.create(
634 Name
.TYPE
, Type
.SELECT_CANDIDATE
, Name
.CANDIDATE
, candidate
));
641 * @param {string} text .
643 Adapter
.prototype.commitText = function(text
) {
644 chrome
.runtime
.sendMessage(goog
.object
.create(
645 Name
.TYPE
, Type
.COMMIT_TEXT
, Name
.TEXT
, text
));
652 * @param {string} language .
654 Adapter
.prototype.setLanguage = function(language
) {
655 chrome
.runtime
.sendMessage(goog
.object
.create(
656 Name
.TYPE
, Type
.SET_LANGUAGE
, Name
.LANGUAGE
, language
));
661 * Callbck when completion is back.
663 * @param {!Object} message .
666 Adapter
.prototype.onCandidatesBack_ = function(message
) {
667 var source
= message
['source'] || '';
668 var candidates
= message
['candidates'] || [];
669 this.dispatchEvent(new CandidatesBackEvent(source
, candidates
));
674 * Hides the keyboard.
676 Adapter
.prototype.hideKeyboard = function() {
677 chrome
.input
.ime
.hideInputView();
682 * Sends DOUBLE_CLICK_ON_SPACE_KEY message.
684 Adapter
.prototype.doubleClickOnSpaceKey = function() {
685 chrome
.runtime
.sendMessage(
688 Type
.DOUBLE_CLICK_ON_SPACE_KEY
));
693 * Sends message to the background when do internal inputtool switch.
695 * @param {boolean} inputToolValue The value of the language flag.
697 Adapter
.prototype.toggleLanguageState = function(inputToolValue
) {
698 chrome
.runtime
.sendMessage(
701 Type
.TOGGLE_LANGUAGE_STATE
,
708 * Processes incoming message from option page or inputview window.
710 * @param {*} request Message from option page or inputview window.
711 * @param {*} sender Information about the script
712 * context that sent the message.
713 * @param {function(*): void} sendResponse Function to call to send a response.
714 * @return {boolean|undefined} {@code true} to keep the message channel open in
715 * order to send a response asynchronously.
718 Adapter
.prototype.onMessage_ = function(request
, sender
, sendResponse
) {
719 var type
= request
[Name
.TYPE
];
720 var msg
= request
[Name
.MSG
];
721 if (!i18n
.input
.chrome
.message
.isFromBackground(type
)) {
725 case Type
.CANDIDATES_BACK
:
726 this.onCandidatesBack_(msg
);
728 case Type
.CONTEXT_FOCUS
:
729 this.onContextFocus_(msg
);
731 case Type
.CONTEXT_BLUR
:
732 this.onContextBlur_();
734 case Type
.SURROUNDING_TEXT_CHANGED
:
735 this.onSurroundingTextChanged_(request
[Name
.TEXT
],
736 request
[Name
.ANCHOR
],
737 request
[Name
.FOCUS
]);
739 case Type
.UPDATE_SETTINGS
:
740 this.onUpdateSettings_(msg
);
742 case Type
.VOICE_STATE_CHANGE
:
743 case Type
.HWT_NETWORK_ERROR
:
744 case Type
.FRONT_TOGGLE_LANGUAGE_STATE
:
745 this.dispatchEvent(new i18n
.input
.chrome
.message
.Event(type
, msg
));
752 * Sends the voice state to background.
754 * @param {boolean} state .
756 Adapter
.prototype.sendVoiceViewStateChange = function(state
) {
757 chrome
.runtime
.sendMessage(goog
.object
.create(
758 Name
.TYPE
, Type
.VOICE_VIEW_STATE_CHANGE
, Name
.MSG
, state
));
763 Adapter
.prototype.disposeInternal = function() {
764 goog
.dispose(this.handler_
);
766 goog
.base(this, 'disposeInternal');
771 * Return the background IME switching state.
775 Adapter
.prototype.isSwitching = function() {
776 return this.isBgControllerSwitching_
;
783 * @param {string} keyset The keyset.
784 * @param {string} languageCode The language code.
786 Adapter
.prototype.setController = function(keyset
, languageCode
) {
787 chrome
.runtime
.sendMessage(
792 {'rawkeyset': keyset
, 'languageCode': languageCode
}));
797 * Unset the inputtool
799 Adapter
.prototype.unsetController = function() {
800 chrome
.runtime
.sendMessage(
803 Type
.UNSET_CONTROLLER
));