diff options
Diffstat (limited to 'dom/inputmethod/MozKeyboard.js')
-rw-r--r-- | dom/inputmethod/MozKeyboard.js | 1255 |
1 files changed, 1255 insertions, 0 deletions
diff --git a/dom/inputmethod/MozKeyboard.js b/dom/inputmethod/MozKeyboard.js new file mode 100644 index 000000000..3996f3e5d --- /dev/null +++ b/dom/inputmethod/MozKeyboard.js @@ -0,0 +1,1255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender"); + +XPCOMUtils.defineLazyServiceGetter(this, "tm", + "@mozilla.org/thread-manager;1", "nsIThreadManager"); + +/* + * A WeakMap to map input method iframe window to + * it's active status, kbID, and ipcHelper. + */ +var WindowMap = { + // WeakMap of <window, object> pairs. + _map: null, + + /* + * Set the object associated to the window and return it. + */ + _getObjForWin: function(win) { + if (!this._map) { + this._map = new WeakMap(); + } + if (this._map.has(win)) { + return this._map.get(win); + } else { + let obj = { + active: false, + kbID: undefined, + ipcHelper: null + }; + this._map.set(win, obj); + + return obj; + } + }, + + /* + * Check if the given window is active. + */ + isActive: function(win) { + if (!this._map || !win) { + return false; + } + + return this._getObjForWin(win).active; + }, + + /* + * Set the active status of the given window. + */ + setActive: function(win, isActive) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + obj.active = isActive; + }, + + /* + * Get the keyboard ID (assigned by Keyboard.jsm) of the given window. + */ + getKbID: function(win) { + if (!this._map || !win) { + return undefined; + } + + let obj = this._getObjForWin(win); + return obj.kbID; + }, + + /* + * Set the keyboard ID (assigned by Keyboard.jsm) of the given window. + */ + setKbID: function(win, kbID) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + obj.kbID = kbID; + }, + + /* + * Get InputContextDOMRequestIpcHelper instance attached to this window. + */ + getInputContextIpcHelper: function(win) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + if (!obj.ipcHelper) { + obj.ipcHelper = new InputContextDOMRequestIpcHelper(win); + } + return obj.ipcHelper; + }, + + /* + * Unset InputContextDOMRequestIpcHelper instance. + */ + unsetInputContextIpcHelper: function(win) { + if (!win) { + return; + } + let obj = this._getObjForWin(win); + if (!obj.ipcHelper) { + return; + } + obj.ipcHelper = null; + } +}; + +var cpmmSendAsyncMessageWithKbID = function (self, msg, data) { + data.kbID = WindowMap.getKbID(self._window); + cpmm.sendAsyncMessage(msg, data); +}; + +/** + * ============================================== + * InputMethodManager + * ============================================== + */ +function MozInputMethodManager(win) { + this._window = win; +} + +MozInputMethodManager.prototype = { + supportsSwitchingForCurrentInputContext: false, + _window: null, + + classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), + + QueryInterface: XPCOMUtils.generateQI([]), + + set oninputcontextfocus(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextfocus", handler); + }, + + get oninputcontextfocus() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextfocus"); + }, + + set oninputcontextblur(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextblur", handler); + }, + + get oninputcontextblur() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextblur"); + }, + + set onshowallrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onshowallrequest", handler); + }, + + get onshowallrequest() { + return this.__DOM_IMPL__.getEventHandler("onshowallrequest"); + }, + + set onnextrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onnextrequest", handler); + }, + + get onnextrequest() { + return this.__DOM_IMPL__.getEventHandler("onnextrequest"); + }, + + set onaddinputrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onaddinputrequest", handler); + }, + + get onaddinputrequest() { + return this.__DOM_IMPL__.getEventHandler("onaddinputrequest"); + }, + + set onremoveinputrequest(handler) { + this.__DOM_IMPL__.setEventHandler("onremoveinputrequest", handler); + }, + + get onremoveinputrequest() { + return this.__DOM_IMPL__.getEventHandler("onremoveinputrequest"); + }, + + showAll: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ShowInputMethodPicker', {}); + }, + + next: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SwitchToNextInputMethod', {}); + }, + + supportsSwitching: function() { + if (!WindowMap.isActive(this._window)) { + return false; + } + return this.supportsSwitchingForCurrentInputContext; + }, + + hide: function() { + if (!WindowMap.isActive(this._window)) { + return; + } + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {}); + }, + + setSupportsSwitchingTypes: function(types) { + cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', { + types: types + }); + }, + + handleFocus: function(data) { + let detail = new MozInputContextFocusEventDetail(this._window, data); + let wrappedDetail = + this._window.MozInputContextFocusEventDetail._create(this._window, detail); + let event = new this._window.CustomEvent('inputcontextfocus', + { cancelable: true, detail: wrappedDetail }); + + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the inputcontextfocus event dispatched.\n'); + } + }, + + handleBlur: function(data) { + let event = + new this._window.Event('inputcontextblur', { cancelable: true }); + + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the inputcontextblur event dispatched.\n'); + } + }, + + dispatchShowAllRequestEvent: function() { + this._fireSimpleEvent('showallrequest'); + }, + + dispatchNextRequestEvent: function() { + this._fireSimpleEvent('nextrequest'); + }, + + _fireSimpleEvent: function(eventType) { + let event = new this._window.Event(eventType); + let handled = !this.__DOM_IMPL__.dispatchEvent(event, { cancelable: true }); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the ' + eventType + ' event dispatched.\n'); + } + }, + + handleAddInput: function(data) { + let p = this._fireInputRegistryEvent('addinputrequest', data); + if (!p) { + return; + } + + p.then(() => { + cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', { + id: data.id + }); + }, (error) => { + cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', { + id: data.id, + error: error || 'Unknown Error' + }); + }); + }, + + handleRemoveInput: function(data) { + let p = this._fireInputRegistryEvent('removeinputrequest', data); + if (!p) { + return; + } + + p.then(() => { + cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', { + id: data.id + }); + }, (error) => { + cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', { + id: data.id, + error: error || 'Unknown Error' + }); + }); + }, + + _fireInputRegistryEvent: function(eventType, data) { + let detail = new MozInputRegistryEventDetail(this._window, data); + let wrappedDetail = + this._window.MozInputRegistryEventDetail._create(this._window, detail); + let event = new this._window.CustomEvent(eventType, + { cancelable: true, detail: wrappedDetail }); + let handled = !this.__DOM_IMPL__.dispatchEvent(event); + + // A gentle warning if the event is not preventDefault() by the content. + if (!handled) { + dump('MozKeyboard.js: A frame with input-manage permission did not' + + ' handle the ' + eventType + ' event dispatched.\n'); + + return null; + } + return detail.takeChainedPromise(); + } +}; + +function MozInputContextFocusEventDetail(win, data) { + this.type = data.type; + this.inputType = data.inputType; + this.value = data.value; + // Exposed as MozInputContextChoicesInfo dictionary defined in WebIDL + this.choices = data.choices; + this.min = data.min; + this.max = data.max; +} +MozInputContextFocusEventDetail.prototype = { + classID: Components.ID("{e0794208-ac50-40e8-b22e-6ee0b4c4e6e8}"), + QueryInterface: XPCOMUtils.generateQI([]), + + type: undefined, + inputType: undefined, + value: '', + choices: null, + min: undefined, + max: undefined +}; + +function MozInputRegistryEventDetail(win, data) { + this._window = win; + + this.manifestURL = data.manifestURL; + this.inputId = data.inputId; + // Exposed as MozInputMethodInputManifest dictionary defined in WebIDL + this.inputManifest = data.inputManifest; + + this._chainedPromise = Promise.resolve(); +} +MozInputRegistryEventDetail.prototype = { + classID: Components.ID("{02130070-9b3e-4f38-bbd9-f0013aa36717}"), + QueryInterface: XPCOMUtils.generateQI([]), + + _window: null, + + manifestURL: undefined, + inputId: undefined, + inputManifest: null, + + waitUntil: function(p) { + // Need an extra protection here since waitUntil will be an no-op + // when chainedPromise is already returned. + if (!this._chainedPromise) { + throw new this._window.DOMException( + 'Must call waitUntil() within the event handling loop.', + 'InvalidStateError'); + } + + this._chainedPromise = this._chainedPromise + .then(function() { return p; }); + }, + + takeChainedPromise: function() { + var p = this._chainedPromise; + this._chainedPromise = null; + return p; + } +}; + +/** + * ============================================== + * InputMethod + * ============================================== + */ +function MozInputMethod() { } + +MozInputMethod.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + _window: null, + _inputcontext: null, + _wrappedInputContext: null, + _mgmt: null, + _wrappedMgmt: null, + _supportsSwitchingTypes: [], + _inputManageId: undefined, + + classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + init: function mozInputMethodInit(win) { + this._window = win; + this._mgmt = new MozInputMethodManager(win); + this._wrappedMgmt = win.MozInputMethodManager._create(win, this._mgmt); + this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + + Services.obs.addObserver(this, "inner-window-destroyed", false); + + cpmm.addWeakMessageListener('Keyboard:Focus', this); + cpmm.addWeakMessageListener('Keyboard:Blur', this); + cpmm.addWeakMessageListener('Keyboard:SelectionChange', this); + cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this); + cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this); + cpmm.addWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this); + cpmm.addWeakMessageListener('InputRegistry:Result:OK', this); + cpmm.addWeakMessageListener('InputRegistry:Result:Error', this); + + if (this._hasInputManagePerm(win)) { + this._inputManageId = cpmm.sendSyncMessage('System:RegisterSync', {})[0]; + cpmm.addWeakMessageListener('System:Focus', this); + cpmm.addWeakMessageListener('System:Blur', this); + cpmm.addWeakMessageListener('System:ShowAll', this); + cpmm.addWeakMessageListener('System:Next', this); + cpmm.addWeakMessageListener('System:InputRegistry:Add', this); + cpmm.addWeakMessageListener('System:InputRegistry:Remove', this); + } + }, + + uninit: function mozInputMethodUninit() { + this._window = null; + this._mgmt = null; + this._wrappedMgmt = null; + + cpmm.removeWeakMessageListener('Keyboard:Focus', this); + cpmm.removeWeakMessageListener('Keyboard:Blur', this); + cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this); + cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this); + cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this); + cpmm.removeWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this); + cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this); + cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this); + this.setActive(false); + + if (typeof this._inputManageId === 'number') { + cpmm.sendAsyncMessage('System:Unregister', { + 'id': this._inputManageId + }); + cpmm.removeWeakMessageListener('System:Focus', this); + cpmm.removeWeakMessageListener('System:Blur', this); + cpmm.removeWeakMessageListener('System:ShowAll', this); + cpmm.removeWeakMessageListener('System:Next', this); + cpmm.removeWeakMessageListener('System:InputRegistry:Add', this); + cpmm.removeWeakMessageListener('System:InputRegistry:Remove', this); + } + }, + + receiveMessage: function mozInputMethodReceiveMsg(msg) { + if (msg.name.startsWith('Keyboard') && + !WindowMap.isActive(this._window)) { + return; + } + + let data = msg.data; + + if (msg.name.startsWith('System') && + this._inputManageId !== data.inputManageId) { + return; + } + delete data.inputManageId; + + let resolver = ('requestId' in data) ? + this.takePromiseResolver(data.requestId) : null; + + switch(msg.name) { + case 'Keyboard:Focus': + // XXX Bug 904339 could receive 'text' event twice + this.setInputContext(data); + break; + case 'Keyboard:Blur': + this.setInputContext(null); + break; + case 'Keyboard:SelectionChange': + if (this.inputcontext) { + this._inputcontext.updateSelectionContext(data, false); + } + break; + case 'Keyboard:GetContext:Result:OK': + this.setInputContext(data); + break; + case 'Keyboard:SupportsSwitchingTypesChange': + this._supportsSwitchingTypes = data.types; + break; + case 'Keyboard:ReceiveHardwareKeyEvent': + if (!Ci.nsIHardwareKeyHandler) { + break; + } + + let defaultPrevented = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + + // |event.preventDefault()| is allowed to be called only when + // |event.cancelable| is true + if (this._inputcontext && data.keyDict.cancelable) { + defaultPrevented |= this._inputcontext.forwardHardwareKeyEvent(data); + } + + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ReplyHardwareKeyEvent', { + type: data.type, + defaultPrevented: defaultPrevented + }); + break; + case 'InputRegistry:Result:OK': + resolver.resolve(); + + break; + + case 'InputRegistry:Result:Error': + resolver.reject(data.error); + + break; + + case 'System:Focus': + this._mgmt.handleFocus(data); + break; + + case 'System:Blur': + this._mgmt.handleBlur(data); + break; + + case 'System:ShowAll': + this._mgmt.dispatchShowAllRequestEvent(); + break; + + case 'System:Next': + this._mgmt.dispatchNextRequestEvent(); + break; + + case 'System:InputRegistry:Add': + this._mgmt.handleAddInput(data); + break; + + case 'System:InputRegistry:Remove': + this._mgmt.handleRemoveInput(data); + break; + } + }, + + observe: function mozInputMethodObserve(subject, topic, data) { + let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (wId == this.innerWindowID) + this.uninit(); + }, + + get mgmt() { + return this._wrappedMgmt; + }, + + get inputcontext() { + if (!WindowMap.isActive(this._window)) { + return null; + } + return this._wrappedInputContext; + }, + + set oninputcontextchange(handler) { + this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler); + }, + + get oninputcontextchange() { + return this.__DOM_IMPL__.getEventHandler("oninputcontextchange"); + }, + + setInputContext: function mozKeyboardContextChange(data) { + if (this._inputcontext) { + this._inputcontext.destroy(); + this._inputcontext = null; + this._wrappedInputContext = null; + this._mgmt.supportsSwitchingForCurrentInputContext = false; + } + + if (data) { + this._mgmt.supportsSwitchingForCurrentInputContext = + (this._supportsSwitchingTypes.indexOf(data.inputType) !== -1); + + this._inputcontext = new MozInputContext(data); + this._inputcontext.init(this._window); + // inputcontext will be exposed as a WebIDL object. Create its + // content-side object explicitly to avoid Bug 1001325. + this._wrappedInputContext = + this._window.MozInputContext._create(this._window, this._inputcontext); + } + + let event = new this._window.Event("inputcontextchange"); + this.__DOM_IMPL__.dispatchEvent(event); + }, + + setActive: function mozInputMethodSetActive(isActive) { + if (WindowMap.isActive(this._window) === isActive) { + return; + } + + WindowMap.setActive(this._window, isActive); + + if (isActive) { + // Activate current input method. + // If there is already an active context, then this will trigger + // a GetContext:Result:OK event, and we can initialize ourselves. + // Otherwise silently ignored. + + // get keyboard ID from Keyboard.jsm, + // or if we already have it, get it from our map + // Note: if we need to get it from Keyboard.jsm, + // we have to use a synchronous message + var kbID = WindowMap.getKbID(this._window); + if (kbID) { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {}); + } else { + let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {}); + WindowMap.setKbID(this._window, res[0]); + } + + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:GetContext', {}); + } else { + // Deactive current input method. + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Unregister', {}); + if (this._inputcontext) { + this.setInputContext(null); + } + } + }, + + addInput: function(inputId, inputManifest) { + return this.createPromiseWithId(function(resolverId) { + let appId = this._window.document.nodePrincipal.appId; + + cpmm.sendAsyncMessage('InputRegistry:Add', { + requestId: resolverId, + inputId: inputId, + inputManifest: inputManifest, + appId: appId + }); + }.bind(this)); + }, + + removeInput: function(inputId) { + return this.createPromiseWithId(function(resolverId) { + let appId = this._window.document.nodePrincipal.appId; + + cpmm.sendAsyncMessage('InputRegistry:Remove', { + requestId: resolverId, + inputId: inputId, + appId: appId + }); + }.bind(this)); + }, + + setValue: function(value) { + cpmm.sendAsyncMessage('System:SetValue', { + 'value': value + }); + }, + + setSelectedOption: function(index) { + cpmm.sendAsyncMessage('System:SetSelectedOption', { + 'index': index + }); + }, + + setSelectedOptions: function(indexes) { + cpmm.sendAsyncMessage('System:SetSelectedOptions', { + 'indexes': indexes + }); + }, + + removeFocus: function() { + cpmm.sendAsyncMessage('System:RemoveFocus', {}); + }, + + // Only the system app needs that, so instead of testing a permission which + // is allowed for all chrome:// url, we explicitly test that this is the + // system app's start URL. + _hasInputManagePerm: function(win) { + let url = win.location.href; + let systemAppIndex; + try { + systemAppIndex = Services.prefs.getCharPref('b2g.system_startup_url'); + } catch(e) { + dump('MozKeyboard.jsm: no system app startup url set (pref is b2g.system_startup_url)'); + } + + dump(`MozKeyboard.jsm expecting ${systemAppIndex}\n`); + return url == systemAppIndex; + } +}; + +/** + * ============================================== + * InputContextDOMRequestIpcHelper + * ============================================== + */ +function InputContextDOMRequestIpcHelper(win) { + this.initDOMRequestHelper(win, + ["Keyboard:GetText:Result:OK", + "Keyboard:GetText:Result:Error", + "Keyboard:SetSelectionRange:Result:OK", + "Keyboard:ReplaceSurroundingText:Result:OK", + "Keyboard:SendKey:Result:OK", + "Keyboard:SendKey:Result:Error", + "Keyboard:SetComposition:Result:OK", + "Keyboard:EndComposition:Result:OK", + "Keyboard:SequenceError"]); +} + +InputContextDOMRequestIpcHelper.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + _inputContext: null, + + attachInputContext: function(inputCtx) { + if (this._inputContext) { + throw new Error("InputContextDOMRequestIpcHelper: detach the context first."); + } + + this._inputContext = inputCtx; + }, + + // Unset ourselves when the window is destroyed. + uninit: function() { + WindowMap.unsetInputContextIpcHelper(this._window); + }, + + detachInputContext: function() { + // All requests that are still pending need to be invalidated + // because the context is no longer valid. + this.forEachPromiseResolver(k => { + this.takePromiseResolver(k).reject("InputContext got destroyed"); + }); + + this._inputContext = null; + }, + + receiveMessage: function(msg) { + if (!this._inputContext) { + dump('InputContextDOMRequestIpcHelper received message without context attached.\n'); + return; + } + + this._inputContext.receiveMessage(msg); + } +}; + +function MozInputContextSelectionChangeEventDetail(ctx, ownAction) { + this._ctx = ctx; + this.ownAction = ownAction; +} + +MozInputContextSelectionChangeEventDetail.prototype = { + classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"), + QueryInterface: XPCOMUtils.generateQI([]), + + ownAction: false, + + get selectionStart() { + return this._ctx.selectionStart; + }, + + get selectionEnd() { + return this._ctx.selectionEnd; + } +}; + +function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) { + this._ctx = ctx; + this.ownAction = ownAction; +} + +MozInputContextSurroundingTextChangeEventDetail.prototype = { + classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"), + QueryInterface: XPCOMUtils.generateQI([]), + + ownAction: false, + + get text() { + return this._ctx.text; + }, + + get textBeforeCursor() { + return this._ctx.textBeforeCursor; + }, + + get textAfterCursor() { + return this._ctx.textAfterCursor; + } +}; + +/** + * ============================================== + * HardwareInput + * ============================================== + */ +function MozHardwareInput() { +} + +MozHardwareInput.prototype = { + classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), + QueryInterface: XPCOMUtils.generateQI([]), +}; + +/** + * ============================================== + * InputContext + * ============================================== + */ +function MozInputContext(data) { + this._context = { + type: data.type, + inputType: data.inputType, + inputMode: data.inputMode, + lang: data.lang, + selectionStart: data.selectionStart, + selectionEnd: data.selectionEnd, + text: data.value + }; + + this._contextId = data.contextId; +} + +MozInputContext.prototype = { + _window: null, + _context: null, + _contextId: -1, + _ipcHelper: null, + _hardwareinput: null, + _wrappedhardwareinput: null, + + classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]), + + init: function ic_init(win) { + this._window = win; + + this._ipcHelper = WindowMap.getInputContextIpcHelper(win); + this._ipcHelper.attachInputContext(this); + this._hardwareinput = new MozHardwareInput(); + this._wrappedhardwareinput = + this._window.MozHardwareInput._create(this._window, this._hardwareinput); + }, + + destroy: function ic_destroy() { + // A consuming application might still hold a cached version of + // this object. After destroying all methods will throw because we + // cannot create new promises anymore, but we still hold + // (outdated) information in the context. So let's clear that out. + for (var k in this._context) { + if (this._context.hasOwnProperty(k)) { + this._context[k] = null; + } + } + + this._ipcHelper.detachInputContext(); + this._ipcHelper = null; + + this._window = null; + this._hardwareinput = null; + this._wrappedhardwareinput = null; + }, + + receiveMessage: function ic_receiveMessage(msg) { + if (!msg || !msg.json) { + dump('InputContext received message without data\n'); + return; + } + + let json = msg.json; + let resolver = this._ipcHelper.takePromiseResolver(json.requestId); + + if (!resolver) { + dump('InputContext received invalid requestId.\n'); + return; + } + + // Update context first before resolving promise to avoid race condition + if (json.selectioninfo) { + this.updateSelectionContext(json.selectioninfo, true); + } + + switch (msg.name) { + case "Keyboard:SendKey:Result:OK": + resolver.resolve(true); + break; + case "Keyboard:SendKey:Result:Error": + resolver.reject(json.error); + break; + case "Keyboard:GetText:Result:OK": + resolver.resolve(json.text); + break; + case "Keyboard:GetText:Result:Error": + resolver.reject(json.error); + break; + case "Keyboard:SetSelectionRange:Result:OK": + case "Keyboard:ReplaceSurroundingText:Result:OK": + resolver.resolve( + Cu.cloneInto(json.selectioninfo, this._window)); + break; + case "Keyboard:SequenceError": + // Occurs when a new element got focus, but the inputContext was + // not invalidated yet... + resolver.reject("InputContext has expired"); + break; + case "Keyboard:SetComposition:Result:OK": // Fall through. + case "Keyboard:EndComposition:Result:OK": + resolver.resolve(true); + break; + default: + dump("Could not find a handler for " + msg.name); + resolver.reject(); + break; + } + }, + + updateSelectionContext: function ic_updateSelectionContext(data, ownAction) { + if (!this._context) { + return; + } + + let selectionDirty = + this._context.selectionStart !== data.selectionStart || + this._context.selectionEnd !== data.selectionEnd; + let surroundDirty = selectionDirty || data.text !== this._contextId.text; + + this._context.text = data.text; + this._context.selectionStart = data.selectionStart; + this._context.selectionEnd = data.selectionEnd; + + if (selectionDirty) { + let selectionChangeDetail = + new MozInputContextSelectionChangeEventDetail(this, ownAction); + let wrappedSelectionChangeDetail = + this._window.MozInputContextSelectionChangeEventDetail + ._create(this._window, selectionChangeDetail); + let selectionChangeEvent = new this._window.CustomEvent("selectionchange", + { cancelable: false, detail: wrappedSelectionChangeDetail }); + + this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent); + } + + if (surroundDirty) { + let surroundingTextChangeDetail = + new MozInputContextSurroundingTextChangeEventDetail(this, ownAction); + let wrappedSurroundingTextChangeDetail = + this._window.MozInputContextSurroundingTextChangeEventDetail + ._create(this._window, surroundingTextChangeDetail); + let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange", + { cancelable: false, detail: wrappedSurroundingTextChangeDetail }); + + this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent); + } + }, + + // tag name of the input field + get type() { + return this._context.type; + }, + + // type of the input field + get inputType() { + return this._context.inputType; + }, + + get inputMode() { + return this._context.inputMode; + }, + + get lang() { + return this._context.lang; + }, + + getText: function ic_getText(offset, length) { + let text; + if (offset && length) { + text = this._context.text.substr(offset, length); + } else if (offset) { + text = this._context.text.substr(offset); + } else { + text = this._context.text; + } + + return this._window.Promise.resolve(text); + }, + + get selectionStart() { + return this._context.selectionStart; + }, + + get selectionEnd() { + return this._context.selectionEnd; + }, + + get text() { + return this._context.text; + }, + + get textBeforeCursor() { + let text = this._context.text; + let start = this._context.selectionStart; + return (start < 100) ? + text.substr(0, start) : + text.substr(start - 100, 100); + }, + + get textAfterCursor() { + let text = this._context.text; + let start = this._context.selectionStart; + let end = this._context.selectionEnd; + return text.substr(start, end - start + 100); + }, + + get hardwareinput() { + return this._wrappedhardwareinput; + }, + + setSelectionRange: function ic_setSelectionRange(start, length) { + let self = this; + return this._sendPromise(function(resolverId) { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', { + contextId: self._contextId, + requestId: resolverId, + selectionStart: start, + selectionEnd: start + length + }); + }); + }, + + get onsurroundingtextchange() { + return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange"); + }, + + set onsurroundingtextchange(handler) { + this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler); + }, + + get onselectionchange() { + return this.__DOM_IMPL__.getEventHandler("onselectionchange"); + }, + + set onselectionchange(handler) { + this.__DOM_IMPL__.setEventHandler("onselectionchange", handler); + }, + + replaceSurroundingText: function ic_replaceSurrText(text, offset, length) { + let self = this; + return this._sendPromise(function(resolverId) { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:ReplaceSurroundingText', { + contextId: self._contextId, + requestId: resolverId, + text: text, + offset: offset || 0, + length: length || 0 + }); + }); + }, + + deleteSurroundingText: function ic_deleteSurrText(offset, length) { + return this.replaceSurroundingText(null, offset, length); + }, + + sendKey: function ic_sendKey(dictOrKeyCode, charCode, modifiers, repeat) { + if (typeof dictOrKeyCode === 'number') { + // XXX: modifiers are ignored in this API method. + + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'sendKey', + keyCode: dictOrKeyCode, + charCode: charCode, + repeat: repeat + }); + }); + } else if (typeof dictOrKeyCode === 'object') { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'sendKey', + keyboardEventDict: this._getkeyboardEventDict(dictOrKeyCode) + }); + }); + } else { + // XXX: Should not reach here; implies WebIDL binding error. + throw new TypeError('Unknown argument passed.'); + } + }, + + keydown: function ic_keydown(dict) { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'keydown', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + keyup: function ic_keyup(dict) { + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', { + contextId: this._contextId, + requestId: resolverId, + method: 'keyup', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + setComposition: function ic_setComposition(text, cursor, clauses, dict) { + let self = this; + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetComposition', { + contextId: self._contextId, + requestId: resolverId, + text: text, + cursor: (typeof cursor !== 'undefined') ? cursor : text.length, + clauses: clauses || null, + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + endComposition: function ic_endComposition(text, dict) { + let self = this; + return this._sendPromise((resolverId) => { + cpmmSendAsyncMessageWithKbID(self, 'Keyboard:EndComposition', { + contextId: self._contextId, + requestId: resolverId, + text: text || '', + keyboardEventDict: this._getkeyboardEventDict(dict) + }); + }); + }, + + // Generate a new keyboard event by the received keyboard dictionary + // and return defaultPrevented's result of the event after dispatching. + forwardHardwareKeyEvent: function ic_forwardHardwareKeyEvent(data) { + if (!Ci.nsIHardwareKeyHandler) { + return; + } + + if (!this._context) { + return Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + } + let evt = new this._window.KeyboardEvent(data.type, + Cu.cloneInto(data.keyDict, + this._window)); + this._hardwareinput.__DOM_IMPL__.dispatchEvent(evt); + return this._getDefaultPreventedValue(evt); + }, + + _getDefaultPreventedValue: function(evt) { + if (!Ci.nsIHardwareKeyHandler) { + return; + } + + let flags = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED; + + if (evt.defaultPrevented) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED; + } + + if (evt.defaultPreventedByChrome) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CHROME; + } + + if (evt.defaultPreventedByContent) { + flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CONTENT; + } + + return flags; + }, + + _sendPromise: function(callback) { + let self = this; + return this._ipcHelper.createPromiseWithId(function(aResolverId) { + if (!WindowMap.isActive(self._window)) { + self._ipcHelper.removePromiseResolver(aResolverId); + reject('Input method is not active.'); + return; + } + callback(aResolverId); + }); + }, + + // Take a MozInputMethodKeyboardEventDict dict, creates a keyboardEventDict + // object that can be sent to forms.js + _getkeyboardEventDict: function(dict) { + if (typeof dict !== 'object' || !dict.key) { + return; + } + + var keyboardEventDict = { + key: dict.key, + code: dict.code, + repeat: dict.repeat, + flags: 0 + }; + + if (dict.printable) { + keyboardEventDict.flags |= + Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY; + } + + if (/^[a-zA-Z0-9]$/.test(dict.key)) { + // keyCode must follow the key value in this range; + // disregard the keyCode from content. + keyboardEventDict.keyCode = dict.key.toUpperCase().charCodeAt(0); + } else if (typeof dict.keyCode === 'number') { + // Allow keyCode to be specified for other key values. + keyboardEventDict.keyCode = dict.keyCode; + + // Allow keyCode to be explicitly set to zero. + if (dict.keyCode === 0) { + keyboardEventDict.flags |= + Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO; + } + } + + return keyboardEventDict; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]); |