Bug 1201407 - Add input-manage-only events for InputMethod API. r=janjongboom, sr=smaug

This commit is contained in:
Tim Chien 2015-09-16 22:11:00 +02:00
parent 346978573f
commit e284a1b7b0
10 changed files with 1185 additions and 27 deletions

View File

@ -45,10 +45,12 @@ this.Keyboard = {
_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'
'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
],
_messageNames: [
@ -57,7 +59,7 @@ this.Keyboard = {
'SwitchToNextInputMethod', 'HideInputMethod',
'GetText', 'SendKey', 'GetContext',
'SetComposition', 'EndComposition',
'Register', 'Unregister'
'RegisterSync', 'Unregister'
],
get formMM() {
@ -89,6 +91,20 @@ this.Keyboard = {
} catch(e) { }
},
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);
@ -124,10 +140,14 @@ this.Keyboard = {
// 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
@ -193,7 +213,7 @@ this.Keyboard = {
}
if (0 === msg.name.indexOf('Keyboard:') &&
('Keyboard:Register' !== msg.name && this._keyboardID !== kbID)
('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID)
) {
return;
}
@ -228,6 +248,24 @@ this.Keyboard = {
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);
@ -265,7 +303,7 @@ this.Keyboard = {
case 'Keyboard:EndComposition':
this.endComposition(msg);
break;
case 'Keyboard:Register':
case 'Keyboard:RegisterSync':
this._keyboardMM = mm;
if (kbID) {
// keyboard identifies itself, use its kbID
@ -293,10 +331,14 @@ this.Keyboard = {
.frameLoader.messageManager;
this.formMM = mm;
// Notify the current active input app to gain focus.
this.forwardEvent('Keyboard:Focus', msg);
// Chrome event, used also to render value selectors; that's why we need
// the info about choices / min / max here as well...
// 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,
@ -322,7 +364,9 @@ this.Keyboard = {
this.formMM = null;
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'
@ -362,12 +406,18 @@ this.Keyboard = {
},
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"
});
@ -432,14 +482,17 @@ function InputRegistryGlue() {
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);
if (!Utils.checkPermissionForMM(mm, 'input')) {
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 'input' privileges.");
" from a content process with no " + permName + " privileges.");
return;
}
@ -452,6 +505,12 @@ InputRegistryGlue.prototype.receiveMessage = function(msg) {
case 'InputRegistry:Remove':
this.removeInput(msg, mm);
break;
case 'System:InputRegistry:Add:Done':
case 'System:InputRegistry:Remove:Done':
this.returnMessage(msg.data);
break;
}
};
@ -465,6 +524,14 @@ InputRegistryGlue.prototype.addInput = function(msg, mm) {
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,
@ -483,6 +550,13 @@ InputRegistryGlue.prototype.removeInput = function(msg, mm) {
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,
@ -493,6 +567,8 @@ InputRegistryGlue.prototype.removeInput = function(msg, mm) {
InputRegistryGlue.prototype.returnMessage = function(detail) {
if (!this._msgMap.has(detail.id)) {
dump('InputRegistryGlue: Ignoring already handled message response. ' +
'id=' + detail.id + '\n');
return;
}
@ -500,6 +576,7 @@ InputRegistryGlue.prototype.returnMessage = function(detail) {
this._msgMap.delete(detail.id);
if (Cu.isDeadWrapper(mm)) {
dump('InputRegistryGlue: Message manager has already died.\n');
return;
}

View File

@ -7,6 +7,7 @@
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");
@ -143,6 +144,54 @@ MozInputMethodManager.prototype = {
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;
@ -175,6 +224,169 @@ MozInputMethodManager.prototype = {
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;
}
};
@ -188,10 +400,13 @@ function MozInputMethod() { }
MozInputMethod.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
_window: null,
_inputcontext: null,
_wrappedInputContext: null,
_mgmt: null,
_wrappedMgmt: null,
_supportsSwitchingTypes: [],
_window: null,
_inputManageId: undefined,
classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
@ -204,6 +419,7 @@ MozInputMethod.prototype = {
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;
@ -217,11 +433,22 @@ MozInputMethod.prototype = {
cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', 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);
@ -231,15 +458,34 @@ MozInputMethod.prototype = {
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('InputRegistry') &&
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;
@ -272,6 +518,30 @@ MozInputMethod.prototype = {
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;
}
},
@ -282,7 +552,7 @@ MozInputMethod.prototype = {
},
get mgmt() {
return this._mgmt;
return this._wrappedMgmt;
},
get inputcontext() {
@ -320,8 +590,7 @@ MozInputMethod.prototype = {
this._window.MozInputContext._create(this._window, this._inputcontext);
}
let event = new this._window.Event("inputcontextchange",
Cu.cloneInto({}, this._window));
let event = new this._window.Event("inputcontextchange");
this.__DOM_IMPL__.dispatchEvent(event);
},
@ -344,9 +613,9 @@ MozInputMethod.prototype = {
// we have to use a synchronous message
var kbID = WindowMap.getKbID(this._window);
if (kbID) {
cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Register', {});
cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {});
} else {
let res = cpmm.sendSyncMessage('Keyboard:Register', {});
let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {});
WindowMap.setKbID(this._window, res[0]);
}
@ -405,6 +674,13 @@ MozInputMethod.prototype = {
removeFocus: function() {
cpmm.sendAsyncMessage('System:RemoveFocus', {});
},
_hasInputManagePerm: function(win) {
let principal = win.document.nodePrincipal;
let perm = Services.perms.testExactPermissionFromPrincipal(principal,
"input-manage");
return (perm === Ci.nsIPermissionManager.ALLOW_ACTION);
}
};

View File

@ -1135,6 +1135,8 @@ function getJSON(element, focusCounter) {
switch (inputTypeLowerCase) {
case "datetime":
case "datetime-local":
case "month":
case "week":
case "range":
inputType = inputTypeLowerCase;
break;

View File

@ -4,7 +4,7 @@ skip-if = (toolkit == 'android' || toolkit == 'gonk') || e10s
support-files =
inputmethod_common.js
file_inputmethod.html
file_inputmethod_1043828.html
file_blank.html
file_test_app.html
file_test_sendkey_cancel.html
file_test_sms_app.html
@ -22,8 +22,11 @@ support-files =
[test_bug1066515.html]
[test_bug1175399.html]
[test_bug1137557.html]
[test_focus_blur_manage_events.html]
[test_input_registry_events.html]
[test_sendkey_cancel.html]
[test_setSupportsSwitching.html]
[test_simple_manage_events.html]
[test_sync_edit.html]
[test_two_inputs.html]
[test_two_selects.html]

View File

@ -84,7 +84,7 @@ function runTest() {
document.body.appendChild(keyboardB);
// simulate two different keyboard apps
let imeUrl = basePath + '/file_inputmethod_1043828.html';
let imeUrl = basePath + '/file_blank.html';
SpecialPowers.pushPermissions([{
type: 'input',

View File

@ -0,0 +1,230 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
-->
<head>
<title>Test inputcontextfocus and inputcontextblur event</title>
<script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a>
<p id="display"></p>
<pre id="test">
<script class="testbody" type="application/javascript;version=1.7">
let contentFrameMM;
function setupTestRunner() {
info('setupTestRunner');
let im = navigator.mozInputMethod;
let expectedEventDetails = [
{ type: 'input', inputType: 'text' },
{ type: 'input', inputType: 'search' },
{ type: 'textarea', inputType: 'textarea' },
{ type: 'contenteditable', inputType: 'textarea' },
{ type: 'input', inputType: 'number' },
{ type: 'input', inputType: 'tel' },
{ type: 'input', inputType: 'url' },
{ type: 'input', inputType: 'email' },
{ type: 'input', inputType: 'password' },
{ type: 'input', inputType: 'datetime' },
{ type: 'input', inputType: 'date',
value: '2015-08-03', min: '1990-01-01', max: '2020-01-01' },
{ type: 'input', inputType: 'month' },
{ type: 'input', inputType: 'week' },
{ type: 'input', inputType: 'time' },
{ type: 'input', inputType: 'datetime-local' },
{ type: 'input', inputType: 'color' },
{ type: 'select', inputType: 'select-one',
choices: {
multiple: false,
choices: [
{ group: false, inGroup: false, text: 'foo',
disabled: false, selected: true, optionIndex: 0 },
{ group: false, inGroup: false, text: 'bar',
disabled: true, selected: false, optionIndex: 1 },
{ group: true, text: 'group', disabled: false },
{ group: false, inGroup: true, text: 'baz',
disabled: false, selected: false, optionIndex: 2 } ] }
},
{ type: 'select', inputType: 'select-multiple',
choices: {
multiple: true,
choices: [
{ group: false, inGroup: false, text: 'foo',
disabled: false, selected: true, optionIndex: 0 },
{ group: false, inGroup: false, text: 'bar',
disabled: true, selected: false, optionIndex: 1 },
{ group: true, text: 'group', disabled: false },
{ group: false, inGroup: true, text: 'baz',
disabled: false, selected: false, optionIndex: 2 } ] }
}
];
let expectBlur = false;
function deepAssertObject(obj, expectedObj, desc) {
for (let prop in expectedObj) {
if (typeof expectedObj[prop] === 'object') {
deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
} else {
is(obj[prop], expectedObj[prop], desc + '.' + prop);
}
}
}
im.mgmt.oninputcontextfocus =
im.mgmt.oninputcontextblur = function(evt) {
if (expectBlur) {
is(evt.type, 'inputcontextblur', 'evt.type');
evt.preventDefault();
expectBlur = false;
return;
}
let expectedEventDetail = expectedEventDetails.shift();
if (!expectedEventDetail) {
ok(false, 'Receving extra events');
inputmethod_cleanup();
return;
}
is(evt.type, 'inputcontextfocus', 'evt.type');
evt.preventDefault();
expectBlur = true;
let detail = evt.detail;
deepAssertObject(detail, expectedEventDetail, 'detail');
if (expectedEventDetails.length) {
contentFrameMM.sendAsyncMessage('test:next');
} else {
im.mgmt.oninputcontextfocus = im.mgmt.oninputcontextblur = null;
inputmethod_cleanup();
}
};
}
function setupInputAppFrame() {
info('setupInputAppFrame');
return new Promise((resolve, reject) => {
let appFrameScript = function appFrameScript() {
let im = content.navigator.mozInputMethod;
im.mgmt.oninputcontextfocus =
im.mgmt.oninputcontextblur = function(evt) {
sendAsyncMessage('text:appEvent', { type: evt.type });
};
content.document.body.textContent = 'I am a input app';
};
let path = location.pathname;
let basePath = location.protocol + '//' + location.host +
path.substring(0, path.lastIndexOf('/'));
let imeUrl = basePath + '/file_blank.html';
let inputAppFrame = document.createElement('iframe');
inputAppFrame.setAttribute('mozbrowser', true);
inputAppFrame.src = imeUrl;
document.body.appendChild(inputAppFrame);
SpecialPowers.pushPermissions([{
type: 'input',
allow: true,
context: {
url: imeUrl,
appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
isInBrowserElement: true
}
}], function() {
let mm = SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
inputAppFrame.addEventListener('mozbrowserloadend', function() {
mm.addMessageListener('text:appEvent', function(msg) {
ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
});
mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
// Set the input app frame to be active
let req = inputAppFrame.setInputMethodActive(true);
resolve(req);
});
});
});
}
function setupContentFrame() {
info('setupContentFrame');
return new Promise((resolve, reject) => {
let contentFrameScript = function contentFrameScript() {
let input = content.document.body.firstElementChild;
let i = 0;
input.focus();
addMessageListener('test:next', function() {
content.document.body.children[++i].focus();
});
};
let iframe = document.createElement('iframe');
iframe.src = 'data:text/html,<html><body>' +
'<input type="text">' +
'<input type="search">' +
'<textarea></textarea>' +
'<p contenteditable></p>' +
'<input type="number">' +
'<input type="tel">' +
'<input type="url">' +
'<input type="email">' +
'<input type="password">' +
'<input type="datetime">' +
'<input type="date" value="2015-08-03" min="1990-01-01" max="2020-01-01">' +
'<input type="month">' +
'<input type="week">' +
'<input type="time">' +
'<input type="datetime-local">' +
'<input type="color">' +
'<select><option selected>foo</option><option disabled>bar</option>' +
'<optgroup label="group"><option>baz</option></optgroup></select>' +
'<select multiple><option selected>foo</option><option disabled>bar</option>' +
'<optgroup label="group"><option>baz</option></optgroup></select>' +
'</body></html>';
iframe.setAttribute('mozbrowser', true);
document.body.appendChild(iframe);
let mm = contentFrameMM =
SpecialPowers.getBrowserFrameMessageManager(iframe);
iframe.addEventListener('mozbrowserloadend', function() {
mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
resolve();
});
});
}
inputmethod_setup(function() {
Promise.resolve()
.then(() => setupTestRunner())
.then(() => setupContentFrame())
.then(() => setupInputAppFrame())
.catch((e) => {
ok(false, 'Error' + e.toString());
console.error(e);
});
});
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,259 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
-->
<head>
<title>Test addinputrequest and removeinputrequest event</title>
<script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a>
<p id="display"></p>
<pre id="test">
<script class="testbody" type="application/javascript;version=1.7">
let appFrameMM;
let nextStep;
function setupInputAppFrame() {
info('setupInputAppFrame');
return new Promise((resolve, reject) => {
let appFrameScript = function appFrameScript() {
let im = content.navigator.mozInputMethod;
addMessageListener('test:callAddInput', function() {
im.addInput('foo', {
launch_path: 'bar.html',
name: 'Foo',
description: 'foobar',
types: ['text', 'password']
})
.then((r) => {
sendAsyncMessage('test:resolved', { resolved: true, result: r });
}, (e) => {
sendAsyncMessage('test:rejected', { rejected: true, error: e });
});
});
addMessageListener('test:callRemoveInput', function() {
im.removeInput('foo')
.then((r) => {
sendAsyncMessage('test:resolved', { resolved: true, result: r });
}, (e) => {
sendAsyncMessage('test:rejected', { rejected: true, error: e });
});
});
im.mgmt.onaddinputrequest =
im.mgmt.onremoveinputrequest = function(evt) {
sendAsyncMessage('test:appEvent', { type: evt.type });
};
content.document.body.textContent = 'I am a input app';
};
let path = location.pathname;
let basePath = location.protocol + '//' + location.host +
path.substring(0, path.lastIndexOf('/'));
let imeUrl = basePath + '/file_blank.html';
let inputAppFrame = document.createElement('iframe');
inputAppFrame.setAttribute('mozbrowser', true);
inputAppFrame.src = imeUrl;
document.body.appendChild(inputAppFrame);
SpecialPowers.pushPermissions([{
type: 'input',
allow: true,
context: {
url: imeUrl,
appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
isInBrowserElement: true
}
}], function() {
let mm = appFrameMM =
SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
inputAppFrame.addEventListener('mozbrowserloadend', function() {
mm.addMessageListener('test:appEvent', function(msg) {
ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
});
mm.addMessageListener('test:resolved', function(msg) {
nextStep && nextStep(msg.data);
});
mm.addMessageListener('test:rejected', function(msg) {
nextStep && nextStep(msg.data);
});
mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
resolve();
});
});
});
}
function Deferred() {
this.promise = new Promise((res, rej) => {
this.resolve = res;
this.reject = rej;
});
return this;
}
function deepAssertObject(obj, expectedObj, desc) {
for (let prop in expectedObj) {
if (typeof expectedObj[prop] === 'object') {
deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
} else {
is(obj[prop], expectedObj[prop], desc + '.' + prop);
}
}
}
function setupTestRunner() {
let im = navigator.mozInputMethod;
let d;
let i = -1;
nextStep = function next(evt) {
i++;
info('Step ' + i);
switch (i) {
case 0:
appFrameMM.sendAsyncMessage('test:callAddInput');
break;
case 1:
is(evt.type, 'addinputrequest', 'evt.type');
deepAssertObject(evt.detail, {
inputId: 'foo',
manifestURL: null, // todo
inputManifest: {
launch_path: 'bar.html',
name: 'Foo',
description: 'foobar',
types: ['text', 'password']
}
}, 'detail');
d = new Deferred();
evt.detail.waitUntil(d.promise);
evt.preventDefault();
Promise.resolve().then(next);
break;
case 2:
d.resolve();
d = null;
break;
case 3:
ok(evt.resolved, 'resolved');
appFrameMM.sendAsyncMessage('test:callAddInput');
break;
case 4:
is(evt.type, 'addinputrequest', 'evt.type');
d = new Deferred();
evt.detail.waitUntil(d.promise);
evt.preventDefault();
Promise.resolve().then(next);
break;
case 5:
d.reject('Foo Error');
d = null;
break;
case 6:
ok(evt.rejected, 'rejected');
is(evt.error, 'Foo Error', 'rejected');
appFrameMM.sendAsyncMessage('test:callRemoveInput');
break;
case 7:
is(evt.type, 'removeinputrequest', 'evt.type');
deepAssertObject(evt.detail, {
inputId: 'foo',
manifestURL: null // todo
}, 'detail');
d = new Deferred();
evt.detail.waitUntil(d.promise);
evt.preventDefault();
Promise.resolve().then(next);
break;
case 8:
d.resolve();
d = null;
break;
case 9:
ok(evt.resolved, 'resolved');
appFrameMM.sendAsyncMessage('test:callRemoveInput');
break;
case 10:
is(evt.type, 'removeinputrequest', 'evt.type');
d = new Deferred();
evt.detail.waitUntil(d.promise);
evt.preventDefault();
Promise.resolve().then(next);
break;
case 11:
d.reject('Foo Error');
d = null;
break;
case 12:
ok(evt.rejected, 'rejected');
is(evt.error, 'Foo Error', 'rejected');
inputmethod_cleanup();
break;
default:
ok(false, 'received extra call.');
inputmethod_cleanup();
break;
}
}
im.mgmt.onaddinputrequest =
im.mgmt.onremoveinputrequest = nextStep;
}
inputmethod_setup(function() {
Promise.resolve()
.then(() => setupTestRunner())
.then(() => setupInputAppFrame())
.then(() => nextStep())
.catch((e) => {
ok(false, 'Error' + e.toString());
console.error(e);
});
});
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,164 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
-->
<head>
<title>Test simple manage notification events on MozInputMethodManager</title>
<script type="application/javascript;version=1.7" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1201407">Mozilla Bug 1201407</a>
<p id="display"></p>
<pre id="test">
<script class="testbody" type="application/javascript;version=1.7">
let appFrameMM;
let nextStep;
function setupTestRunner() {
info('setupTestRunner');
let im = navigator.mozInputMethod;
let i = 0;
im.mgmt.onshowallrequest =
im.mgmt.onnextrequest = nextStep = function(evt) {
i++;
switch (i) {
case 1:
is(evt.type, 'inputcontextchange', '1) inputcontextchange event');
appFrameMM.sendAsyncMessage('test:callShowAll');
break;
case 2:
is(evt.type, 'showallrequest', '2) showallrequest event');
ok(evt.target, im.mgmt, '2) evt.target');
evt.preventDefault();
appFrameMM.sendAsyncMessage('test:callNext');
break;
case 3:
is(evt.type, 'nextrequest', '3) nextrequest event');
ok(evt.target, im.mgmt, '3) evt.target');
evt.preventDefault();
im.mgmt.onshowallrequest =
im.mgmt.onnextrequest = nextStep = null;
inputmethod_cleanup();
break;
default:
ok(false, 'Receving extra events');
inputmethod_cleanup();
break;
}
};
}
function setupInputAppFrame() {
info('setupInputAppFrame');
return new Promise((resolve, reject) => {
let appFrameScript = function appFrameScript() {
let im = content.navigator.mozInputMethod;
addMessageListener('test:callShowAll', function() {
im.mgmt.showAll();
});
addMessageListener('test:callNext', function() {
im.mgmt.next();
});
im.mgmt.onshowallrequest =
im.mgmt.onnextrequest = function(evt) {
sendAsyncMessage('test:appEvent', { type: evt.type });
};
im.oninputcontextchange = function(evt) {
sendAsyncMessage('test:inputcontextchange', {});
};
content.document.body.textContent = 'I am a input app';
};
let path = location.pathname;
let basePath = location.protocol + '//' + location.host +
path.substring(0, path.lastIndexOf('/'));
let imeUrl = basePath + '/file_blank.html';
let inputAppFrame = document.createElement('iframe');
inputAppFrame.setAttribute('mozbrowser', true);
inputAppFrame.src = imeUrl;
document.body.appendChild(inputAppFrame);
SpecialPowers.pushPermissions([{
type: 'input',
allow: true,
context: {
url: imeUrl,
appId: SpecialPowers.Ci.nsIScriptSecurityManager.NO_APP_ID,
isInBrowserElement: true
}
}], function() {
let mm = appFrameMM =
SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
inputAppFrame.addEventListener('mozbrowserloadend', function() {
mm.addMessageListener('test:appEvent', function(msg) {
ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
});
mm.addMessageListener('test:inputcontextchange', function(msg) {
nextStep && nextStep({ type: 'inputcontextchange' });
});
mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
// Set the input app frame to be active
let req = inputAppFrame.setInputMethodActive(true);
resolve(req);
});
});
});
}
function setupContentFrame() {
let contentFrameScript = function contentFrameScript() {
let input = content.document.body.firstElementChild;
input.focus();
};
let iframe = document.createElement('iframe');
iframe.src = 'data:text/html,<html><body><input type="text"></body></html>';
iframe.setAttribute('mozbrowser', true);
document.body.appendChild(iframe);
let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
iframe.addEventListener('mozbrowserloadend', function() {
mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
});
}
inputmethod_setup(function() {
Promise.resolve()
.then(() => setupTestRunner())
.then(() => setupContentFrame())
.then(() => setupInputAppFrame())
.catch((e) => {
ok(false, 'Error' + e.toString());
console.error(e);
});
});
</script>
</pre>
</body>
</html>

View File

@ -120,7 +120,7 @@ interface MozInputMethod : EventTarget {
[JSImplementation="@mozilla.org/b2g-imm;1",
Pref="dom.mozInputMethod.enabled",
CheckAnyPermissions="input input-manage"]
interface MozInputMethodManager {
interface MozInputMethodManager : EventTarget {
/**
* Ask the OS to show a list of available inputs for users to switch from.
* OS should sliently ignore this request if the app is currently not the
@ -165,6 +165,149 @@ interface MozInputMethodManager {
*/
[CheckAllPermissions="input-manage"]
void setSupportsSwitchingTypes(sequence<MozInputMethodInputContextInputTypes> types);
/**
* CustomEvent dispatches to System when there is an input to handle.
* If the API consumer failed to handle and call preventDefault(),
* there will be a message printed on the console.
*
* evt.detail is defined by MozInputContextFocusEventDetail.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler oninputcontextfocus;
/**
* Event dispatches to System when there is no longer an input to handle.
* If the API consumer failed to handle and call preventDefault(),
* there will be a message printed on the console.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler oninputcontextblur;
/**
* Event dispatches to System when there is a showAll() call.
* If the API consumer failed to handle and call preventDefault(),
* there will be a message printed on the console.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler onshowallrequest;
/**
* Event dispatches to System when there is a next() call.
* If the API consumer failed to handle and call preventDefault(),
* there will be a message printed on the console.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler onnextrequest;
/**
* Event dispatches to System when there is a addInput() call.
* The API consumer must call preventDefault() to indicate the event is
* consumed, otherwise the request is not considered handled even if
* waitUntil() was called.
*
* evt.detail is defined by MozInputRegistryEventDetail.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler onaddinputrequest;
/**
* Event dispatches to System when there is a removeInput() call.
* The API consumer must call preventDefault() to indicate the event is
* consumed, otherwise the request is not considered handled even if
* waitUntil() was called.
*
* evt.detail is defined by MozInputRegistryEventDetail.
*/
[CheckAnyPermissions="input-manage"]
attribute EventHandler onremoveinputrequest;
};
/**
* Detail of the inputcontextfocus event.
*/
[JSImplementation="@mozilla.org/b2g-imm-focus;1",
Pref="dom.mozInputMethod.enabled",
CheckAnyPermissions="input-manage"]
interface MozInputContextFocusEventDetail {
/**
* The type of the focused input.
*/
readonly attribute MozInputMethodInputContextTypes type;
/**
* The input type of the focused input.
*/
readonly attribute MozInputMethodInputContextInputTypes inputType;
/**
* The following is only needed for rendering and handling "option" input types,
* in System app.
*/
/**
* Current value of the input/select element.
*/
readonly attribute DOMString? value;
/**
* An object representing all the <optgroup> and <option> elements
* in the <select> element.
*/
[Pure, Cached, Frozen]
readonly attribute MozInputContextChoicesInfo? choices;
/**
* Max/min value of <input>
*/
readonly attribute DOMString? min;
readonly attribute DOMString? max;
};
/**
* Information about the options within the <select> element.
*/
dictionary MozInputContextChoicesInfo {
boolean multiple;
sequence<MozInputMethodChoiceDict> choices;
};
/**
* Content the header (<optgroup>) or an option (<option>).
*/
dictionary MozInputMethodChoiceDict {
boolean group;
DOMString text;
boolean disabled;
boolean? inGroup;
boolean? selected;
long? optionIndex;
};
/**
* detail of addinputrequest or removeinputrequest event.
*/
[JSImplementation="@mozilla.org/b2g-imm-input-registry;1",
Pref="dom.mozInputMethod.enabled",
CheckAnyPermissions="input-manage"]
interface MozInputRegistryEventDetail {
/**
* Manifest URL of the requesting app.
*/
readonly attribute DOMString manifestURL;
/**
* ID of the input
*/
readonly attribute DOMString inputId;
/**
* Input manifest of the input to add.
* Null for removeinputrequest event.
*/
[Pure, Cached, Frozen]
readonly attribute MozInputMethodInputManifest? inputManifest;
/**
* Resolve or Reject the addInput() or removeInput() call when the passed
* promises are resolved.
*/
[Throws]
void waitUntil(Promise<any> p);
};
/**
@ -397,19 +540,20 @@ dictionary CompositionClauseParameters {
* *and* the special keyword "contenteditable" for contenteditable element.
*/
enum MozInputMethodInputContextTypes {
"input", "textarea", "contenteditable"
"input", "textarea", "contenteditable",
/**
* <select> is managed by the API but it's not exposed through InputContext
* yet.
* <select> is managed by the API but it is handled by the System app only,
* so this value is only accessible by System app from inputcontextfocus event.
*/
// "select"
"select"
};
/**
* InputTypes of the input that InputContext is representing. The value
* is inferred from the type attribute of input element.
* is inferred from the type attribute of element.
*
* See https://html.spec.whatwg.org/multipage/forms.html#states-of-the-type-attribute
* for types of HTMLInputElement.
*
* They are divided into groups -- an layout/input capable of handling one type
* in the group is considered as capable of handling all of the types in the
@ -444,12 +588,15 @@ enum MozInputMethodInputContextInputTypes {
* Group "password".
* An non-Latin alphabet layout/input should not be able to handle this type.
*/
"password"
"password",
/**
* Group "option". These types are handled by System app itself currently, and
* not exposed and allowed to handled with input context.
* Group "option". These types are handled by System app itself currently, so
* no input app will be set to active for these input types.
* System app access these types from inputcontextfocus event.
* ("select-one" and "select-multiple" are valid HTMLSelectElement#type.)
*/
//"datetime", "date", "month", "week", "time", "datetime-local", "color",
"datetime", "date", "month", "week", "time", "datetime-local", "color",
"select-one", "select-multiple"
/**
* These types are ignored by the API even though they are valid
* HTMLInputElement#type.