Bug 905573 - Add setInputMethodActive to browser elements to allow gaia system set the active IME app. r=fabrice

This commit is contained in:
Yuan Xulei 2013-09-23 09:40:59 -04:00
parent 6322ff8a35
commit 840767de6b
11 changed files with 357 additions and 28 deletions

View File

@ -31,7 +31,7 @@ this.Keyboard = {
if (this._messageManager && !Cu.isDeadWrapper(this._messageManager))
return this._messageManager;
throw Error('no message manager set');
return null;
},
set messageManager(mm) {
@ -92,6 +92,10 @@ this.Keyboard = {
// If we get a 'Keyboard:XXX' message, check that the sender has the
// keyboard permission.
if (msg.name.indexOf("Keyboard:") != -1) {
if (!this.messageManager) {
return;
}
let mm;
try {
mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)

View File

@ -199,15 +199,51 @@ MozKeyboard.prototype = {
}
};
const TESTING_ENABLED_PREF = "dom.mozInputMethod.testing";
/*
* A WeakMap to map input method iframe window to its active status.
*/
let WindowMap = {
// WeakMap of <window, boolean> pairs.
_map: null,
/*
* Check if the given window is active.
*/
isActive: function(win) {
if (!this._map || !win) {
return false;
}
return this._map.get(win, false);
},
/*
* Set the active status of the given window.
*/
setActive: function(win, isActive) {
if (!win) {
return;
}
if (!this._map) {
this._map = new WeakMap();
}
this._map.set(win, isActive);
}
};
/**
* ==============================================
* InputMethodManager
* ==============================================
*/
function MozInputMethodManager() { }
function MozInputMethodManager(win) {
this._window = win;
}
MozInputMethodManager.prototype = {
_supportsSwitching: false,
_window: null,
classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
@ -224,18 +260,30 @@ MozInputMethodManager.prototype = {
}),
showAll: function() {
if (!WindowMap.isActive(this._window)) {
return;
}
cpmm.sendAsyncMessage('Keyboard:ShowInputMethodPicker', {});
},
next: function() {
if (!WindowMap.isActive(this._window)) {
return;
}
cpmm.sendAsyncMessage('Keyboard:SwitchToNextInputMethod', {});
},
supportsSwitching: function() {
if (!WindowMap.isActive(this._window)) {
return false;
}
return this._supportsSwitching;
},
hide: function() {
if (!WindowMap.isActive(this._window)) {
return;
}
cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
}
};
@ -250,6 +298,7 @@ function MozInputMethod() { }
MozInputMethod.prototype = {
_inputcontext: null,
_layouts: {},
_window: null,
classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
@ -268,17 +317,26 @@ MozInputMethod.prototype = {
}),
init: function mozInputMethodInit(win) {
let principal = win.document.nodePrincipal;
let perm = Services.perms
.testExactPermissionFromPrincipal(principal, "keyboard");
if (perm != Ci.nsIPermissionManager.ALLOW_ACTION) {
dump("No permission to use the keyboard API for " +
principal.origin + "\n");
return null;
// Check if we're in testing mode.
let isTesting = false;
try {
isTesting = Services.prefs.getBoolPref(TESTING_ENABLED_PREF);
} catch (e) {}
// Don't bypass the permission check if not in testing mode.
if (!isTesting) {
let principal = win.document.nodePrincipal;
let perm = Services.perms
.testExactPermissionFromPrincipal(principal, "keyboard");
if (perm != Ci.nsIPermissionManager.ALLOW_ACTION) {
dump("No permission to use the keyboard API for " +
principal.origin + "\n");
return null;
}
}
this._window = win;
this._mgmt = new MozInputMethodManager();
this._mgmt = new MozInputMethodManager(win);
this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
@ -288,11 +346,6 @@ MozInputMethod.prototype = {
cpmm.addMessageListener('Keyboard:SelectionChange', this);
cpmm.addMessageListener('Keyboard:GetContext:Result:OK', this);
cpmm.addMessageListener('Keyboard:LayoutsChange', this);
// If there already is an active context, then this will trigger
// a GetContext:Result:OK event, and we can initialize ourselves.
// Otherwise silently ignored.
cpmm.sendAsyncMessage("Keyboard:GetContext", {});
},
uninit: function mozInputMethodUninit() {
@ -307,6 +360,10 @@ MozInputMethod.prototype = {
},
receiveMessage: function mozInputMethodReceiveMsg(msg) {
if (!WindowMap.isActive(this._window)) {
return;
}
let json = msg.json;
switch(msg.name) {
@ -338,11 +395,18 @@ MozInputMethod.prototype = {
},
get mgmt() {
if (!WindowMap.isActive(this._window)) {
return null;
}
return this._mgmt;
},
get inputcontext() {
return this._inputcontext;
if (!WindowMap.isActive(this._window)) {
return null;
}
return this._inputcontext;
},
set oninputcontextchange(handler) {
@ -372,6 +436,27 @@ MozInputMethod.prototype = {
let event = new this._window.Event("inputcontextchange",
ObjectWrapper.wrap({}, this._window));
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.
cpmm.sendAsyncMessage("Keyboard:GetContext", {});
} else {
// Deactive current input method.
if (this._inputcontext) {
this.setInputContext(null);
}
}
}
};
@ -400,6 +485,7 @@ function MozInputContext(ctx) {
MozInputContext.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
_window: null,
_context: null,
_contextId: -1,
@ -452,6 +538,8 @@ MozInputContext.prototype = {
this._context[k] = null;
}
}
this._window = null;
},
receiveMessage: function ic_receiveMessage(msg) {
@ -558,8 +646,7 @@ MozInputContext.prototype = {
getText: function ic_getText(offset, length) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:GetText', {
contextId: self._contextId,
requestId: resolverId,
@ -587,8 +674,7 @@ MozInputContext.prototype = {
setSelectionRange: function ic_setSelectionRange(start, length) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", {
contextId: self._contextId,
requestId: resolverId,
@ -616,8 +702,7 @@ MozInputContext.prototype = {
replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
contextId: self._contextId,
requestId: resolverId,
@ -634,8 +719,7 @@ MozInputContext.prototype = {
sendKey: function ic_sendKey(keyCode, charCode, modifiers) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:SendKey', {
contextId: self._contextId,
requestId: resolverId,
@ -648,8 +732,7 @@ MozInputContext.prototype = {
setComposition: function ic_setComposition(text, cursor, clauses) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:SetComposition', {
contextId: self._contextId,
requestId: resolverId,
@ -662,14 +745,26 @@ MozInputContext.prototype = {
endComposition: function ic_endComposition(text) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:EndComposition', {
contextId: self._contextId,
requestId: resolverId,
text: text || ''
});
});
},
_sendPromise: function(callback) {
let self = this;
return this.createPromise(function(resolve, reject) {
let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
if (!WindowMap.isActive(self._window)) {
self.removePromiseResolver(resolverId);
reject('Input method is not active.');
return;
}
callback(resolverId);
});
}
};

View File

@ -287,6 +287,11 @@ this.PermissionsTable = { geolocation: {
privileged: ALLOW_ACTION,
certified: ALLOW_ACTION
},
"inputmethod-manage": {
app: DENY_ACTION,
privileged: DENY_ACTION,
certified: ALLOW_ACTION
},
"wappush": {
app: DENY_ACTION,
privileged: DENY_ACTION,

View File

@ -210,6 +210,7 @@ BrowserElementChild.prototype = {
"owner-visibility-change": this._recvOwnerVisibilityChange,
"exit-fullscreen": this._recvExitFullscreen.bind(this),
"activate-next-paint-listener": this._activateNextPaintListener.bind(this),
"set-input-method-active": this._recvSetInputMethodActive.bind(this),
"deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this)
}
@ -886,6 +887,20 @@ BrowserElementChild.prototype = {
webNav.stop(webNav.STOP_NETWORK);
},
_recvSetInputMethodActive: function(data) {
let msgData = { id: data.json.id };
// Unwrap to access webpage content.
let nav = XPCNativeWrapper.unwrap(content.document.defaultView.navigator);
if (nav.mozInputMethod) {
// Wrap to access the chrome-only attribute setActive.
new XPCNativeWrapper(nav.mozInputMethod).setActive(data.json.args.isActive);
msgData.successRv = null;
} else {
msgData.errorMsg = 'Cannot access mozInputMethod.';
}
sendAsyncMsg('got-set-input-method-active', msgData);
},
_keyEventHandler: function(e) {
if (whitelistedEvents.indexOf(e.keyCode) != -1 && !e.defaultPrevented) {
sendAsyncMsg('keyevent', {

View File

@ -92,6 +92,10 @@ this.BrowserElementParentBuilder = {
}
}
// The active input method iframe.
let activeInputFrame = null;
function BrowserElementParent(frameLoader, hasRemoteFrame) {
debug("Creating new BrowserElementParent object for " + frameLoader);
this._domRequestCounter = 0;
@ -141,6 +145,7 @@ function BrowserElementParent(frameLoader, hasRemoteFrame) {
"exit-fullscreen": this._exitFullscreen,
"got-visible": this._gotDOMRequestResult,
"visibilitychange": this._childVisibilityChange,
"got-set-input-method-active": this._gotDOMRequestResult
}
this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
@ -188,6 +193,13 @@ function BrowserElementParent(frameLoader, hasRemoteFrame) {
defineDOMRequestMethod('getCanGoBack', 'get-can-go-back');
defineDOMRequestMethod('getCanGoForward', 'get-can-go-forward');
let principal = this._frameElement.ownerDocument.nodePrincipal;
let perm = Services.perms
.testExactPermissionFromPrincipal(principal, "inputmethod-manage");
if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) {
defineMethod('setInputMethodActive', this._setInputMethodActive);
}
// Listen to visibilitychange on the iframe's owner window, and forward
// changes down to the child. We want to do this while registering as few
// visibilitychange listeners on _window as possible, because such a listener
@ -581,6 +593,47 @@ BrowserElementParent.prototype = {
this._sendAsyncMsg('deactivate-next-paint-listener');
},
_setInputMethodActive: function(isActive) {
if (typeof isActive !== 'boolean') {
throw Components.Exception("Invalid argument",
Cr.NS_ERROR_INVALID_ARG);
}
let req = Services.DOMRequest.createRequest(this._window);
// Deactivate the old input method if needed.
if (activeInputFrame && isActive) {
let reqOld = XPCNativeWrapper.unwrap(activeInputFrame)
.setInputMethodActive(false);
reqOld.onsuccess = function() {
activeInputFrame = null;
this._sendSetInputMethodActiveDOMRequest(req, isActive);
}.bind(this);
reqOld.onerror = function() {
Services.DOMRequest.fireErrorAsync(req,
'Failed to deactivate the old input method: ' +
reqOld.error + '.');
};
} else {
this._sendSetInputMethodActiveDOMRequest(req, isActive);
}
return req;
},
_sendSetInputMethodActiveDOMRequest: function(req, isActive) {
let id = 'req_' + this._domRequestCounter++;
let data = {
id : id,
args: { isActive: isActive }
};
if (this._sendAsyncMsg('set-input-method-active', data)) {
activeInputFrame = this._frameElement;
this._pendingDOMRequests[id] = req;
} else {
Services.DOMRequest.fireErrorAsync(req, 'fail');
}
},
_fireKeyEvent: function(data) {
let evt = this._window.document.createEvent("KeyboardEvent");
evt.initKeyEvent(data.json.type, true, true, this._window,

View File

@ -163,6 +163,11 @@ MOCHITEST_FILES = \
test_browserElement_inproc_BrowserWindowResize.html \
$(NULL)
# Disabled until we fix bug 906096.
# browserElement_SetInputMethodActive.js \
# test_browserElement_inproc_SetInputMethodActive.html \
# file_inputmethod.html \
# Disabled due to https://bugzilla.mozilla.org/show_bug.cgi?id=774100
# test_browserElement_inproc_Reload.html \
@ -239,4 +244,7 @@ MOCHITEST_FILES += \
test_browserElement_oop_BrowserWindowResize.html \
$(NULL)
# Disabled until we fix bug 906096.
# test_browserElement_oop_SetInputMethodActive.html \
endif #}

View File

@ -0,0 +1,96 @@
/* Any copyright is dedicated to the public domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Bug 905573 - Add setInputMethodActive to browser elements to allow gaia
// system set the active IME app.
'use strict';
SimpleTest.waitForExplicitFinish();
browserElementTestHelpers.setEnabledPref(true);
browserElementTestHelpers.addPermission();
function setup() {
SpecialPowers.setBoolPref("dom.mozInputMethod.enabled", true);
SpecialPowers.setBoolPref("dom.mozInputMethod.testing", true);
SpecialPowers.addPermission('inputmethod-manage', true, document);
}
function tearDown() {
SpecialPowers.setBoolPref("dom.mozInputMethod.enabled", false);
SpecialPowers.setBoolPref("dom.mozInputMethod.testing", false);
SpecialPowers.removePermission("inputmethod-manage", document);
SimpleTest.finish();
}
function runTest() {
// Create an input field to receive string from input method iframes.
let input = document.createElement('input');
input.type = 'text';
document.body.appendChild(input);
// Create two input method iframes.
let frames = [];
for (let i = 0; i < 2; i++) {
frames[i] = document.createElement('iframe');
SpecialPowers.wrap(frames[i]).mozbrowser = true;
// When the input method iframe is activated, it will send the URL
// hash to current focused element. We set different hash to each
// iframe so that iframes can be differentiated by their hash.
frames[i].src = 'file_inputmethod.html#' + i;
frames[i].setAttribute('mozapp', location.href.replace(/[^/]+$/, 'file_inputmethod.webapp'));
document.body.appendChild(frames[i]);
}
let count = 0;
// Set focus to the input field and wait for input methods' inputting.
SpecialPowers.DOMWindowUtils.focus(input);
var timerId = null;
input.oninput = function() {
// The texts sent from the first and the second input method are '#0' and
// '#1' respectively.
switch (count) {
case 1:
is(input.value, '#0', 'Failed to get correct input from the first iframe.');
testNextInputMethod();
break;
case 2:
is(input.value, '#0#1', 'Failed to get correct input from the second iframe.');
// Do nothing and wait for the next input from the second iframe.
count++;
break;
case 3:
is(input.value, '#0#1#1', 'Failed to get correct input from the second iframe.');
// Receive the second input from the second iframe.
count++;
// Deactive the second iframe.
frames[1].setInputMethodActive(false);
// Wait for a short while to ensure the second iframe is not active any
// more.
timerId = setTimeout(function() {
ok(true, 'Successfully deactivate the second iframe.');
tearDown();
}, 1000);
break;
default:
ok(false, 'Failed to deactivate the second iframe.');
clearTimeout(timerId);
tearDown();
break;
}
}
ok(frames[0].setInputMethodActive, 'Cannot access setInputMethodActive.');
function testNextInputMethod() {
frames[count++].setInputMethodActive(true);
}
// Wait for a short while to let input method get ready.
setTimeout(function() {
testNextInputMethod();
}, 500);
}
setup();
addEventListener('testready', runTest);

View File

@ -0,0 +1,21 @@
<html>
<body>
<script>
var im = navigator.mozInputMethod;
if (im) {
var intervalId = null;
// Automatically append location hash to current input field.
im.oninputcontextchange = function() {
var ctx = im.inputcontext;
if (ctx) {
intervalId = setInterval(function() {
ctx.replaceSurroundingText(location.hash);
}, 500);
} else {
clearInterval(intervalId);
}
};
}
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Bug 905573</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="browserElementTestHelpers.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript;version=1.7" src="browserElement_SetInputMethodActive.js">
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for Bug 905573</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="browserElementTestHelpers.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="application/javascript;version=1.7" src="browserElement_SetInputMethodActive.js">
</script>
</body>
</html>

View File

@ -23,6 +23,10 @@ interface MozInputMethod : EventTarget {
// allow to mutate. this attribute should be null when there is no
// text field currently focused.
readonly attribute MozInputContext? inputcontext;
[ChromeOnly]
// Activate or decactive current input method window.
void setActive(boolean isActive);
};
// Manages the list of IMEs, enables/disables IME and switches to an