summaryrefslogtreecommitdiffstats
path: root/dom/inputmethod/Keyboard.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/inputmethod/Keyboard.jsm')
-rw-r--r--dom/inputmethod/Keyboard.jsm644
1 files changed, 644 insertions, 0 deletions
diff --git a/dom/inputmethod/Keyboard.jsm b/dom/inputmethod/Keyboard.jsm
new file mode 100644
index 000000000..22f87ffbc
--- /dev/null
+++ b/dom/inputmethod/Keyboard.jsm
@@ -0,0 +1,644 @@
+/* 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';
+
+this.EXPORTED_SYMBOLS = ['Keyboard'];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "appsService", function() {
+ return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
+});
+
+XPCOMUtils.defineLazyGetter(this, "hardwareKeyHandler", function() {
+#ifdef MOZ_B2G
+ return Cc["@mozilla.org/HardwareKeyHandler;1"]
+ .getService(Ci.nsIHardwareKeyHandler);
+#else
+ return null;
+#endif
+});
+
+var Utils = {
+ getMMFromMessage: function u_getMMFromMessage(msg) {
+ let mm;
+ try {
+ mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ } catch(e) {
+ mm = msg.target;
+ }
+
+ return mm;
+ },
+ checkPermissionForMM: function u_checkPermissionForMM(mm, permName) {
+ return mm.assertPermission(permName);
+ }
+};
+
+this.Keyboard = {
+#ifdef MOZ_B2G
+ // For receving keyboard event fired from hardware before it's dispatched,
+ // |this| object is used to be the listener to get the forwarded event.
+ // As the listener, |this| object must implement nsIHardwareKeyEventListener
+ // and nsSupportsWeakReference.
+ // Please see nsIHardwareKeyHandler.idl to get more information.
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIHardwareKeyEventListener,
+ Ci.nsISupportsWeakReference
+ ]),
+#endif
+ _isConnectedToHardwareKeyHandler: false,
+ _formMM: null, // The current web page message manager.
+ _keyboardMM: null, // The keyboard app message manager.
+ _keyboardID: -1, // The keyboard app's ID number. -1 = invalid
+ _nextKeyboardID: 0, // The ID number counter.
+ _systemMMs: [], // The message managers registered to handle system async
+ // messages.
+ _supportsSwitchingTypes: [],
+ _systemMessageNames: [
+ 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
+ 'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
+ ],
+
+ _messageNames: [
+ 'RemoveFocus',
+ 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
+ 'SwitchToNextInputMethod', 'HideInputMethod',
+ 'SendKey', 'GetContext',
+ 'SetComposition', 'EndComposition',
+ 'RegisterSync', 'Unregister',
+ 'ReplyHardwareKeyEvent'
+ ],
+
+ get formMM() {
+ if (this._formMM && !Cu.isDeadWrapper(this._formMM))
+ return this._formMM;
+
+ return null;
+ },
+
+ set formMM(mm) {
+ this._formMM = mm;
+ },
+
+ sendToForm: function(name, data) {
+ if (!this.formMM) {
+ dump("Keyboard.jsm: Attempt to send message " + name +
+ " to form but no message manager exists.\n");
+
+ return;
+ }
+ try {
+ this.formMM.sendAsyncMessage(name, data);
+ } catch(e) { }
+ },
+
+ sendToKeyboard: function(name, data) {
+ try {
+ this._keyboardMM.sendAsyncMessage(name, data);
+ } catch(e) {
+ return false;
+ }
+ return true;
+ },
+
+ sendToSystem: function(name, data) {
+ if (!this._systemMMs.length) {
+ dump("Keyboard.jsm: Attempt to send message " + name +
+ " to system but no message manager registered.\n");
+
+ return;
+ }
+
+ this._systemMMs.forEach((mm, i) => {
+ data.inputManageId = i;
+ mm.sendAsyncMessage(name, data);
+ });
+ },
+
+ init: function keyboardInit() {
+ Services.obs.addObserver(this, 'inprocess-browser-shown', false);
+ Services.obs.addObserver(this, 'remote-browser-shown', false);
+ Services.obs.addObserver(this, 'oop-frameloader-crashed', false);
+ Services.obs.addObserver(this, 'message-manager-close', false);
+
+ // For receiving the native hardware keyboard event
+ if (hardwareKeyHandler) {
+ hardwareKeyHandler.registerListener(this);
+ }
+
+ for (let name of this._messageNames) {
+ ppmm.addMessageListener('Keyboard:' + name, this);
+ }
+
+ for (let name of this._systemMessageNames) {
+ ppmm.addMessageListener('System:' + name, this);
+ }
+
+ this.inputRegistryGlue = new InputRegistryGlue();
+ },
+
+ // This method will be registered into nsIHardwareKeyHandler:
+ // Send the initialized dictionary retrieved from the native keyboard event
+ // to input-method-app for generating a new event.
+ onHardwareKey: function onHardwareKeyReceived(evt) {
+ return this.sendToKeyboard('Keyboard:ReceiveHardwareKeyEvent', {
+ type: evt.type,
+ keyDict: evt.initDict
+ });
+ },
+
+ observe: function keyboardObserve(subject, topic, data) {
+ let frameLoader = null;
+ let mm = null;
+
+ if (topic == 'message-manager-close') {
+ mm = subject;
+ } else {
+ frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
+ mm = frameLoader.messageManager;
+ }
+
+ if (topic == 'oop-frameloader-crashed' ||
+ topic == 'message-manager-close') {
+ if (this.formMM == mm) {
+ // The application has been closed unexpectingly. Let's tell the
+ // keyboard app that the focus has been lost.
+ this.sendToKeyboard('Keyboard:Blur', {});
+ // Notify system app to hide keyboard.
+ this.sendToSystem('System:Blur', {});
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: 'blur'
+ });
+
+ this.formMM = null;
+ }
+ } else {
+ // Ignore notifications that aren't from a BrowserOrApp
+ if (!frameLoader.ownerIsMozBrowserOrAppFrame) {
+ return;
+ }
+ this.initFormsFrameScript(mm);
+ }
+ },
+
+ initFormsFrameScript: function(mm) {
+ mm.addMessageListener('Forms:Focus', this);
+ mm.addMessageListener('Forms:Blur', this);
+ mm.addMessageListener('Forms:SelectionChange', this);
+ mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
+ mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this);
+ mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
+ mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this);
+ mm.addMessageListener('Forms:SendKey:Result:OK', this);
+ mm.addMessageListener('Forms:SendKey:Result:Error', this);
+ mm.addMessageListener('Forms:SequenceError', this);
+ mm.addMessageListener('Forms:GetContext:Result:OK', this);
+ mm.addMessageListener('Forms:SetComposition:Result:OK', this);
+ mm.addMessageListener('Forms:EndComposition:Result:OK', this);
+ },
+
+ receiveMessage: function keyboardReceiveMessage(msg) {
+ // If we get a 'Keyboard:XXX'/'System:XXX' message, check that the sender
+ // has the required permission.
+ let mm;
+
+ // Assert the permission based on the prefix of the message.
+ let permName;
+ if (msg.name.startsWith("Keyboard:")) {
+ permName = "input";
+ } else if (msg.name.startsWith("System:")) {
+ permName = "input-manage";
+ }
+
+ // There is no permission to check (nor we need to get the mm)
+ // for Form: messages.
+ if (permName) {
+ mm = Utils.getMMFromMessage(msg);
+ if (!mm) {
+ dump("Keyboard.jsm: Message " + msg.name + " has no message manager.");
+ return;
+ }
+ if (!Utils.checkPermissionForMM(mm, permName)) {
+ dump("Keyboard.jsm: Message " + msg.name +
+ " from a content process with no '" + permName + "' privileges.\n");
+ return;
+ }
+ }
+
+ // we don't process kb messages (other than register)
+ // if they come from a kb that we're currently not regsitered for.
+ // this decision is made with the kbID kept by us and kb app
+ let kbID = null;
+ if ('kbID' in msg.data) {
+ kbID = msg.data.kbID;
+ }
+
+ if (0 === msg.name.indexOf('Keyboard:') &&
+ ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID)
+ ) {
+ return;
+ }
+
+ switch (msg.name) {
+ case 'Forms:Focus':
+ this.handleFocus(msg);
+ break;
+ case 'Forms:Blur':
+ this.handleBlur(msg);
+ break;
+ case 'Forms:SelectionChange':
+ case 'Forms:SetSelectionRange:Result:OK':
+ case 'Forms:ReplaceSurroundingText:Result:OK':
+ case 'Forms:SendKey:Result:OK':
+ case 'Forms:SendKey:Result:Error':
+ case 'Forms:SequenceError':
+ case 'Forms:GetContext:Result:OK':
+ case 'Forms:SetComposition:Result:OK':
+ case 'Forms:EndComposition:Result:OK':
+ case 'Forms:SetSelectionRange:Result:Error':
+ case 'Forms:ReplaceSurroundingText:Result:Error':
+ let name = msg.name.replace(/^Forms/, 'Keyboard');
+ this.forwardEvent(name, msg);
+ break;
+
+ case 'System:SetValue':
+ this.setValue(msg);
+ break;
+ case 'Keyboard:RemoveFocus':
+ case 'System:RemoveFocus':
+ this.removeFocus();
+ break;
+ case 'System:RegisterSync': {
+ if (this._systemMMs.length !== 0) {
+ dump('Keyboard.jsm Warning: There are more than one content page ' +
+ 'with input-manage permission. There will be undeterministic ' +
+ 'responses to addInput()/removeInput() if both content pages are ' +
+ 'trying to respond to the same request event.\n');
+ }
+
+ let id = this._systemMMs.length;
+ this._systemMMs.push(mm);
+
+ return id;
+ }
+
+ case 'System:Unregister':
+ this._systemMMs.splice(msg.data.id, 1);
+
+ break;
+ case 'System:SetSelectedOption':
+ this.setSelectedOption(msg);
+ break;
+ case 'System:SetSelectedOptions':
+ this.setSelectedOption(msg);
+ break;
+ case 'System:SetSupportsSwitchingTypes':
+ this.setSupportsSwitchingTypes(msg);
+ break;
+ case 'Keyboard:SetSelectionRange':
+ this.setSelectionRange(msg);
+ break;
+ case 'Keyboard:ReplaceSurroundingText':
+ this.replaceSurroundingText(msg);
+ break;
+ case 'Keyboard:SwitchToNextInputMethod':
+ this.switchToNextInputMethod();
+ break;
+ case 'Keyboard:ShowInputMethodPicker':
+ this.showInputMethodPicker();
+ break;
+ case 'Keyboard:SendKey':
+ this.sendKey(msg);
+ break;
+ case 'Keyboard:GetContext':
+ this.getContext(msg);
+ break;
+ case 'Keyboard:SetComposition':
+ this.setComposition(msg);
+ break;
+ case 'Keyboard:EndComposition':
+ this.endComposition(msg);
+ break;
+ case 'Keyboard:RegisterSync':
+ this._keyboardMM = mm;
+ if (kbID) {
+ // keyboard identifies itself, use its kbID
+ // this msg would be async, so no need to return
+ this._keyboardID = kbID;
+ }else{
+ // generate the id for the keyboard
+ this._keyboardID = this._nextKeyboardID;
+ this._nextKeyboardID++;
+ // this msg is sync,
+ // and we want to return the id back to inputmethod
+ return this._keyboardID;
+ }
+ break;
+ case 'Keyboard:Unregister':
+ this._keyboardMM = null;
+ this._keyboardID = -1;
+ break;
+ case 'Keyboard:ReplyHardwareKeyEvent':
+ if (hardwareKeyHandler) {
+ let reply = msg.data;
+ hardwareKeyHandler.onHandledByInputMethodApp(reply.type,
+ reply.defaultPrevented);
+ }
+ break;
+ }
+ },
+
+ handleFocus: function keyboardHandleFocus(msg) {
+ // Set the formMM to the new message manager received.
+ let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ this.formMM = mm;
+
+ // Notify the nsIHardwareKeyHandler that the input-method-app is active now.
+ if (hardwareKeyHandler && !this._isConnectedToHardwareKeyHandler) {
+ this._isConnectedToHardwareKeyHandler = true;
+ hardwareKeyHandler.onInputMethodAppConnected();
+ }
+
+ // Notify the current active input app to gain focus.
+ this.forwardEvent('Keyboard:Focus', msg);
+
+ // Notify System app, used also to render value selectors for now;
+ // that's why we need the info about choices / min / max here as well...
+ this.sendToSystem('System:Focus', msg.data);
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: msg.data.inputType,
+ value: msg.data.value,
+ choices: JSON.stringify(msg.data.choices),
+ min: msg.data.min,
+ max: msg.data.max
+ });
+ },
+
+ handleBlur: function keyboardHandleBlur(msg) {
+ let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ // A blur message can't be sent to the keyboard if the focus has
+ // already been taken away at first place.
+ // This check is here to prevent problem caused by out-of-order
+ // ipc messages from two processes.
+ if (mm !== this.formMM) {
+ return;
+ }
+
+ // unset formMM
+ this.formMM = null;
+
+ // Notify the nsIHardwareKeyHandler that
+ // the input-method-app is disabled now.
+ if (hardwareKeyHandler && this._isConnectedToHardwareKeyHandler) {
+ this._isConnectedToHardwareKeyHandler = false;
+ hardwareKeyHandler.onInputMethodAppDisconnected();
+ }
+
+ this.forwardEvent('Keyboard:Blur', msg);
+ this.sendToSystem('System:Blur', {});
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: 'blur'
+ });
+ },
+
+ forwardEvent: function keyboardForwardEvent(newEventName, msg) {
+ this.sendToKeyboard(newEventName, msg.data);
+ },
+
+ setSelectedOption: function keyboardSetSelectedOption(msg) {
+ this.sendToForm('Forms:Select:Choice', msg.data);
+ },
+
+ setSelectedOptions: function keyboardSetSelectedOptions(msg) {
+ this.sendToForm('Forms:Select:Choice', msg.data);
+ },
+
+ setSelectionRange: function keyboardSetSelectionRange(msg) {
+ this.sendToForm('Forms:SetSelectionRange', msg.data);
+ },
+
+ setValue: function keyboardSetValue(msg) {
+ this.sendToForm('Forms:Input:Value', msg.data);
+ },
+
+ removeFocus: function keyboardRemoveFocus() {
+ if (!this.formMM) {
+ return;
+ }
+
+ this.sendToForm('Forms:Select:Blur', {});
+ },
+
+ replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
+ this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
+ },
+
+ showInputMethodPicker: function keyboardShowInputMethodPicker() {
+ this.sendToSystem('System:ShowAll', {});
+
+ // XXX: To be removed with mozContentEvent support from shell.js
+ SystemAppProxy.dispatchEvent({
+ type: "inputmethod-showall"
+ });
+ },
+
+ switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
+ this.sendToSystem('System:Next', {});
+
+ // XXX: To be removed with mozContentEvent support from shell.js
+ SystemAppProxy.dispatchEvent({
+ type: "inputmethod-next"
+ });
+ },
+
+ sendKey: function keyboardSendKey(msg) {
+ this.sendToForm('Forms:Input:SendKey', msg.data);
+ },
+
+ getContext: function keyboardGetContext(msg) {
+ if (!this.formMM) {
+ return;
+ }
+
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+ types: this._supportsSwitchingTypes
+ });
+
+ this.sendToForm('Forms:GetContext', msg.data);
+ },
+
+ setComposition: function keyboardSetComposition(msg) {
+ this.sendToForm('Forms:SetComposition', msg.data);
+ },
+
+ endComposition: function keyboardEndComposition(msg) {
+ this.sendToForm('Forms:EndComposition', msg.data);
+ },
+
+ setSupportsSwitchingTypes: function setSupportsSwitchingTypes(msg) {
+ this._supportsSwitchingTypes = msg.data.types;
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', msg.data);
+ },
+ // XXX: To be removed with mozContentEvent support from shell.js
+ setLayouts: function keyboardSetLayouts(layouts) {
+ // The input method plugins may not have loaded yet,
+ // cache the layouts so on init we can respond immediately instead
+ // of going back and forth between keyboard_manager
+ var types = [];
+
+ Object.keys(layouts).forEach((type) => {
+ if (layouts[type] > 1) {
+ types.push(type);
+ }
+ });
+
+ this._supportsSwitchingTypes = types;
+
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+ types: types
+ });
+ }
+};
+
+function InputRegistryGlue() {
+ this._messageId = 0;
+ this._msgMap = new Map();
+
+ ppmm.addMessageListener('InputRegistry:Add', this);
+ ppmm.addMessageListener('InputRegistry:Remove', this);
+ ppmm.addMessageListener('System:InputRegistry:Add:Done', this);
+ ppmm.addMessageListener('System:InputRegistry:Remove:Done', this);
+};
+
+InputRegistryGlue.prototype.receiveMessage = function(msg) {
+ let mm = Utils.getMMFromMessage(msg);
+
+ let permName = msg.name.startsWith("System:") ? "input-mgmt" : "input";
+ if (!Utils.checkPermissionForMM(mm, permName)) {
+ dump("InputRegistryGlue message " + msg.name +
+ " from a content process with no " + permName + " privileges.");
+ return;
+ }
+
+ switch (msg.name) {
+ case 'InputRegistry:Add':
+ this.addInput(msg, mm);
+
+ break;
+
+ case 'InputRegistry:Remove':
+ this.removeInput(msg, mm);
+
+ break;
+
+ case 'System:InputRegistry:Add:Done':
+ case 'System:InputRegistry:Remove:Done':
+ this.returnMessage(msg.data);
+
+ break;
+ }
+};
+
+InputRegistryGlue.prototype.addInput = function(msg, mm) {
+ let msgId = this._messageId++;
+ this._msgMap.set(msgId, {
+ mm: mm,
+ requestId: msg.data.requestId
+ });
+
+ let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
+
+ Keyboard.sendToSystem('System:InputRegistry:Add', {
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId,
+ inputManifest: msg.data.inputManifest
+ });
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputregistry-add',
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId,
+ inputManifest: msg.data.inputManifest
+ });
+};
+
+InputRegistryGlue.prototype.removeInput = function(msg, mm) {
+ let msgId = this._messageId++;
+ this._msgMap.set(msgId, {
+ mm: mm,
+ requestId: msg.data.requestId
+ });
+
+ let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
+
+ Keyboard.sendToSystem('System:InputRegistry:Remove', {
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId
+ });
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputregistry-remove',
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId
+ });
+};
+
+InputRegistryGlue.prototype.returnMessage = function(detail) {
+ if (!this._msgMap.has(detail.id)) {
+ dump('InputRegistryGlue: Ignoring already handled message response. ' +
+ 'id=' + detail.id + '\n');
+ return;
+ }
+
+ let { mm, requestId } = this._msgMap.get(detail.id);
+ this._msgMap.delete(detail.id);
+
+ if (Cu.isDeadWrapper(mm)) {
+ dump('InputRegistryGlue: Message manager has already died.\n');
+ return;
+ }
+
+ if (!('error' in detail)) {
+ mm.sendAsyncMessage('InputRegistry:Result:OK', {
+ requestId: requestId
+ });
+ } else {
+ mm.sendAsyncMessage('InputRegistry:Result:Error', {
+ error: detail.error,
+ requestId: requestId
+ });
+ }
+};
+
+this.Keyboard.init();