michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "cpmm", michael@0: "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "tm", michael@0: "@mozilla.org/thread-manager;1", "nsIThreadManager"); michael@0: michael@0: /* michael@0: * A WeakMap to map input method iframe window to its active status. michael@0: */ michael@0: let WindowMap = { michael@0: // WeakMap of pairs. michael@0: _map: null, michael@0: michael@0: /* michael@0: * Check if the given window is active. michael@0: */ michael@0: isActive: function(win) { michael@0: if (!this._map || !win) { michael@0: return false; michael@0: } michael@0: return this._map.get(win, false); michael@0: }, michael@0: michael@0: /* michael@0: * Set the active status of the given window. michael@0: */ michael@0: setActive: function(win, isActive) { michael@0: if (!win) { michael@0: return; michael@0: } michael@0: if (!this._map) { michael@0: this._map = new WeakMap(); michael@0: } michael@0: this._map.set(win, isActive); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * ============================================== michael@0: * InputMethodManager michael@0: * ============================================== michael@0: */ michael@0: function MozInputMethodManager(win) { michael@0: this._window = win; michael@0: } michael@0: michael@0: MozInputMethodManager.prototype = { michael@0: _supportsSwitching: false, michael@0: _window: null, michael@0: michael@0: classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([]), michael@0: michael@0: showAll: function() { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return; michael@0: } michael@0: cpmm.sendAsyncMessage('Keyboard:ShowInputMethodPicker', {}); michael@0: }, michael@0: michael@0: next: function() { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return; michael@0: } michael@0: cpmm.sendAsyncMessage('Keyboard:SwitchToNextInputMethod', {}); michael@0: }, michael@0: michael@0: supportsSwitching: function() { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return false; michael@0: } michael@0: return this._supportsSwitching; michael@0: }, michael@0: michael@0: hide: function() { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return; michael@0: } michael@0: cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {}); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * ============================================== michael@0: * InputMethod michael@0: * ============================================== michael@0: */ michael@0: function MozInputMethod() { } michael@0: michael@0: MozInputMethod.prototype = { michael@0: _inputcontext: null, michael@0: _layouts: {}, michael@0: _window: null, michael@0: _isSystem: false, michael@0: _isKeyboard: true, michael@0: michael@0: classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIDOMGlobalPropertyInitializer, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference michael@0: ]), michael@0: michael@0: init: function mozInputMethodInit(win) { michael@0: this._window = win; michael@0: this._mgmt = new MozInputMethodManager(win); michael@0: this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils) michael@0: .currentInnerWindowID; michael@0: michael@0: Services.obs.addObserver(this, "inner-window-destroyed", false); michael@0: michael@0: let principal = win.document.nodePrincipal; michael@0: let perm = Services.perms.testExactPermissionFromPrincipal(principal, michael@0: "input-manage"); michael@0: if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: this._isSystem = true; michael@0: } michael@0: michael@0: // Check if we can use keyboard related APIs. michael@0: let testing = false; michael@0: try { michael@0: testing = Services.prefs.getBoolPref("dom.mozInputMethod.testing"); michael@0: } catch (e) { michael@0: } michael@0: perm = Services.perms.testExactPermissionFromPrincipal(principal, "input"); michael@0: if (!testing && perm !== Ci.nsIPermissionManager.ALLOW_ACTION) { michael@0: this._isKeyboard = false; michael@0: return; michael@0: } michael@0: michael@0: cpmm.addWeakMessageListener('Keyboard:FocusChange', this); michael@0: cpmm.addWeakMessageListener('Keyboard:SelectionChange', this); michael@0: cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this); michael@0: cpmm.addWeakMessageListener('Keyboard:LayoutsChange', this); michael@0: }, michael@0: michael@0: uninit: function mozInputMethodUninit() { michael@0: this._window = null; michael@0: this._mgmt = null; michael@0: Services.obs.removeObserver(this, "inner-window-destroyed"); michael@0: if (!this._isKeyboard) { michael@0: return; michael@0: } michael@0: michael@0: cpmm.removeWeakMessageListener('Keyboard:FocusChange', this); michael@0: cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this); michael@0: cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this); michael@0: cpmm.removeWeakMessageListener('Keyboard:LayoutsChange', this); michael@0: this.setActive(false); michael@0: }, michael@0: michael@0: receiveMessage: function mozInputMethodReceiveMsg(msg) { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return; michael@0: } michael@0: michael@0: let json = msg.json; michael@0: michael@0: switch(msg.name) { michael@0: case 'Keyboard:FocusChange': michael@0: if (json.type !== 'blur') { michael@0: // XXX Bug 904339 could receive 'text' event twice michael@0: this.setInputContext(json); michael@0: } michael@0: else { michael@0: this.setInputContext(null); michael@0: } michael@0: break; michael@0: case 'Keyboard:SelectionChange': michael@0: if (this.inputcontext) { michael@0: this._inputcontext.updateSelectionContext(json); michael@0: } michael@0: break; michael@0: case 'Keyboard:GetContext:Result:OK': michael@0: this.setInputContext(json); michael@0: break; michael@0: case 'Keyboard:LayoutsChange': michael@0: this._layouts = json; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: observe: function mozInputMethodObserve(subject, topic, data) { michael@0: let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; michael@0: if (wId == this.innerWindowID) michael@0: this.uninit(); michael@0: }, michael@0: michael@0: get mgmt() { michael@0: return this._mgmt; michael@0: }, michael@0: michael@0: get inputcontext() { michael@0: if (!WindowMap.isActive(this._window)) { michael@0: return null; michael@0: } michael@0: return this._inputcontext; michael@0: }, michael@0: michael@0: set oninputcontextchange(handler) { michael@0: this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler); michael@0: }, michael@0: michael@0: get oninputcontextchange() { michael@0: return this.__DOM_IMPL__.getEventHandler("oninputcontextchange"); michael@0: }, michael@0: michael@0: setInputContext: function mozKeyboardContextChange(data) { michael@0: if (this._inputcontext) { michael@0: this._inputcontext.destroy(); michael@0: this._inputcontext = null; michael@0: this._mgmt._supportsSwitching = false; michael@0: } michael@0: michael@0: if (data) { michael@0: this._mgmt._supportsSwitching = this._layouts[data.type] ? michael@0: this._layouts[data.type] > 1 : michael@0: false; michael@0: michael@0: this._inputcontext = new MozInputContext(data); michael@0: this._inputcontext.init(this._window); michael@0: } michael@0: michael@0: let event = new this._window.Event("inputcontextchange", michael@0: Cu.cloneInto({}, this._window)); michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: }, michael@0: michael@0: setActive: function mozInputMethodSetActive(isActive) { michael@0: if (WindowMap.isActive(this._window) === isActive) { michael@0: return; michael@0: } michael@0: michael@0: WindowMap.setActive(this._window, isActive); michael@0: michael@0: if (isActive) { michael@0: // Activate current input method. michael@0: // If there is already an active context, then this will trigger michael@0: // a GetContext:Result:OK event, and we can initialize ourselves. michael@0: // Otherwise silently ignored. michael@0: cpmm.sendAsyncMessage('Keyboard:Register', {}); michael@0: cpmm.sendAsyncMessage("Keyboard:GetContext", {}); michael@0: } else { michael@0: // Deactive current input method. michael@0: cpmm.sendAsyncMessage('Keyboard:Unregister', {}); michael@0: if (this._inputcontext) { michael@0: this.setInputContext(null); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: setValue: function(value) { michael@0: this._ensureIsSystem(); michael@0: cpmm.sendAsyncMessage('System:SetValue', { michael@0: 'value': value michael@0: }); michael@0: }, michael@0: michael@0: setSelectedOption: function(index) { michael@0: this._ensureIsSystem(); michael@0: cpmm.sendAsyncMessage('System:SetSelectedOption', { michael@0: 'index': index michael@0: }); michael@0: }, michael@0: michael@0: setSelectedOptions: function(indexes) { michael@0: this._ensureIsSystem(); michael@0: cpmm.sendAsyncMessage('System:SetSelectedOptions', { michael@0: 'indexes': indexes michael@0: }); michael@0: }, michael@0: michael@0: removeFocus: function() { michael@0: this._ensureIsSystem(); michael@0: cpmm.sendAsyncMessage('System:RemoveFocus', {}); michael@0: }, michael@0: michael@0: _ensureIsSystem: function() { michael@0: if (!this._isSystem) { michael@0: throw new this._window.DOMError("Security", michael@0: "Should have 'input-manage' permssion."); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * ============================================== michael@0: * InputContext michael@0: * ============================================== michael@0: */ michael@0: function MozInputContext(ctx) { michael@0: this._context = { michael@0: inputtype: ctx.type, michael@0: inputmode: ctx.inputmode, michael@0: lang: ctx.lang, michael@0: type: ["textarea", "contenteditable"].indexOf(ctx.type) > -1 ? michael@0: ctx.type : michael@0: "text", michael@0: selectionStart: ctx.selectionStart, michael@0: selectionEnd: ctx.selectionEnd, michael@0: textBeforeCursor: ctx.textBeforeCursor, michael@0: textAfterCursor: ctx.textAfterCursor michael@0: }; michael@0: michael@0: this._contextId = ctx.contextId; michael@0: } michael@0: michael@0: MozInputContext.prototype = { michael@0: __proto__: DOMRequestIpcHelper.prototype, michael@0: michael@0: _window: null, michael@0: _context: null, michael@0: _contextId: -1, michael@0: michael@0: classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference michael@0: ]), michael@0: michael@0: init: function ic_init(win) { michael@0: this._window = win; michael@0: this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: this.initDOMRequestHelper(win, michael@0: ["Keyboard:GetText:Result:OK", michael@0: "Keyboard:GetText:Result:Error", michael@0: "Keyboard:SetSelectionRange:Result:OK", michael@0: "Keyboard:ReplaceSurroundingText:Result:OK", michael@0: "Keyboard:SendKey:Result:OK", michael@0: "Keyboard:SendKey:Result:Error", michael@0: "Keyboard:SetComposition:Result:OK", michael@0: "Keyboard:EndComposition:Result:OK", michael@0: "Keyboard:SequenceError"]); michael@0: }, michael@0: michael@0: destroy: function ic_destroy() { michael@0: let self = this; michael@0: michael@0: // All requests that are still pending need to be invalidated michael@0: // because the context is no longer valid. michael@0: this.forEachPromiseResolver(function(k) { michael@0: self.takePromiseResolver(k).reject("InputContext got destroyed"); michael@0: }); michael@0: this.destroyDOMRequestHelper(); michael@0: michael@0: // A consuming application might still hold a cached version of michael@0: // this object. After destroying all methods will throw because we michael@0: // cannot create new promises anymore, but we still hold michael@0: // (outdated) information in the context. So let's clear that out. michael@0: for (var k in this._context) { michael@0: if (this._context.hasOwnProperty(k)) { michael@0: this._context[k] = null; michael@0: } michael@0: } michael@0: michael@0: this._window = null; michael@0: }, michael@0: michael@0: receiveMessage: function ic_receiveMessage(msg) { michael@0: if (!msg || !msg.json) { michael@0: dump('InputContext received message without data\n'); michael@0: return; michael@0: } michael@0: michael@0: let json = msg.json; michael@0: let resolver = this.takePromiseResolver(json.requestId); michael@0: michael@0: if (!resolver) { michael@0: return; michael@0: } michael@0: michael@0: switch (msg.name) { michael@0: case "Keyboard:SendKey:Result:OK": michael@0: resolver.resolve(); michael@0: break; michael@0: case "Keyboard:SendKey:Result:Error": michael@0: resolver.reject(json.error); michael@0: break; michael@0: case "Keyboard:GetText:Result:OK": michael@0: resolver.resolve(json.text); michael@0: break; michael@0: case "Keyboard:GetText:Result:Error": michael@0: resolver.reject(json.error); michael@0: break; michael@0: case "Keyboard:SetSelectionRange:Result:OK": michael@0: case "Keyboard:ReplaceSurroundingText:Result:OK": michael@0: resolver.resolve( michael@0: Cu.cloneInto(json.selectioninfo, this._window)); michael@0: break; michael@0: case "Keyboard:SequenceError": michael@0: // Occurs when a new element got focus, but the inputContext was michael@0: // not invalidated yet... michael@0: resolver.reject("InputContext has expired"); michael@0: break; michael@0: case "Keyboard:SetComposition:Result:OK": // Fall through. michael@0: case "Keyboard:EndComposition:Result:OK": michael@0: resolver.resolve(); michael@0: break; michael@0: default: michael@0: dump("Could not find a handler for " + msg.name); michael@0: resolver.reject(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: updateSelectionContext: function ic_updateSelectionContext(ctx) { michael@0: if (!this._context) { michael@0: return; michael@0: } michael@0: michael@0: let selectionDirty = this._context.selectionStart !== ctx.selectionStart || michael@0: this._context.selectionEnd !== ctx.selectionEnd; michael@0: let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor || michael@0: this._context.textAfterCursor !== ctx.textAfterCursor; michael@0: michael@0: this._context.selectionStart = ctx.selectionStart; michael@0: this._context.selectionEnd = ctx.selectionEnd; michael@0: this._context.textBeforeCursor = ctx.textBeforeCursor; michael@0: this._context.textAfterCursor = ctx.textAfterCursor; michael@0: michael@0: if (selectionDirty) { michael@0: this._fireEvent("selectionchange", { michael@0: selectionStart: ctx.selectionStart, michael@0: selectionEnd: ctx.selectionEnd michael@0: }); michael@0: } michael@0: michael@0: if (surroundDirty) { michael@0: this._fireEvent("surroundingtextchange", { michael@0: beforeString: ctx.textBeforeCursor, michael@0: afterString: ctx.textAfterCursor michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: _fireEvent: function ic_fireEvent(eventName, aDetail) { michael@0: let detail = { michael@0: detail: aDetail michael@0: }; michael@0: michael@0: let event = new this._window.Event(eventName, michael@0: Cu.cloneInto(aDetail, this._window)); michael@0: this.__DOM_IMPL__.dispatchEvent(event); michael@0: }, michael@0: michael@0: // tag name of the input field michael@0: get type() { michael@0: return this._context.type; michael@0: }, michael@0: michael@0: // type of the input field michael@0: get inputType() { michael@0: return this._context.inputtype; michael@0: }, michael@0: michael@0: get inputMode() { michael@0: return this._context.inputmode; michael@0: }, michael@0: michael@0: get lang() { michael@0: return this._context.lang; michael@0: }, michael@0: michael@0: getText: function ic_getText(offset, length) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage('Keyboard:GetText', { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: offset: offset, michael@0: length: length michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: get selectionStart() { michael@0: return this._context.selectionStart; michael@0: }, michael@0: michael@0: get selectionEnd() { michael@0: return this._context.selectionEnd; michael@0: }, michael@0: michael@0: get textBeforeCursor() { michael@0: return this._context.textBeforeCursor; michael@0: }, michael@0: michael@0: get textAfterCursor() { michael@0: return this._context.textAfterCursor; michael@0: }, michael@0: michael@0: setSelectionRange: function ic_setSelectionRange(start, length) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: selectionStart: start, michael@0: selectionEnd: start + length michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: get onsurroundingtextchange() { michael@0: return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange"); michael@0: }, michael@0: michael@0: set onsurroundingtextchange(handler) { michael@0: this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler); michael@0: }, michael@0: michael@0: get onselectionchange() { michael@0: return this.__DOM_IMPL__.getEventHandler("onselectionchange"); michael@0: }, michael@0: michael@0: set onselectionchange(handler) { michael@0: this.__DOM_IMPL__.setEventHandler("onselectionchange", handler); michael@0: }, michael@0: michael@0: replaceSurroundingText: function ic_replaceSurrText(text, offset, length) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: text: text, michael@0: offset: offset || 0, michael@0: length: length || 0 michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: deleteSurroundingText: function ic_deleteSurrText(offset, length) { michael@0: return this.replaceSurroundingText(null, offset, length); michael@0: }, michael@0: michael@0: sendKey: function ic_sendKey(keyCode, charCode, modifiers, repeat) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage('Keyboard:SendKey', { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: keyCode: keyCode, michael@0: charCode: charCode, michael@0: modifiers: modifiers, michael@0: repeat: repeat michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: setComposition: function ic_setComposition(text, cursor, clauses) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage('Keyboard:SetComposition', { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: text: text, michael@0: cursor: cursor || text.length, michael@0: clauses: clauses || null michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: endComposition: function ic_endComposition(text) { michael@0: let self = this; michael@0: return this._sendPromise(function(resolverId) { michael@0: cpmm.sendAsyncMessage('Keyboard:EndComposition', { michael@0: contextId: self._contextId, michael@0: requestId: resolverId, michael@0: text: text || '' michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: _sendPromise: function(callback) { michael@0: let self = this; michael@0: return this.createPromise(function(resolve, reject) { michael@0: let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject }); michael@0: if (!WindowMap.isActive(self._window)) { michael@0: self.removePromiseResolver(resolverId); michael@0: reject('Input method is not active.'); michael@0: return; michael@0: } michael@0: callback(resolverId); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]);