gecko-dev/dom/inputmethod/MozKeyboard.js

756 lines
21 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ObjectWrapper.jsm");
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender");
XPCOMUtils.defineLazyServiceGetter(this, "tm",
"@mozilla.org/thread-manager;1", "nsIThreadManager");
// -----------------------------------------------------------------------
// MozKeyboard
// -----------------------------------------------------------------------
function MozKeyboard() { }
MozKeyboard.prototype = {
classID: Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIB2GKeyboard, Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIObserver
]),
classInfo: XPCOMUtils.generateCI({
"classID": Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"),
"contractID": "@mozilla.org/b2g-keyboard;1",
"interfaces": [Ci.nsIB2GKeyboard],
"flags": Ci.nsIClassInfo.DOM_OBJECT,
"classDescription": "B2G Virtual Keyboard"
}),
init: function mozKeyboardInit(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;
}
Services.obs.addObserver(this, "inner-window-destroyed", false);
cpmm.addMessageListener('Keyboard:FocusChange', this);
cpmm.addMessageListener('Keyboard:SelectionChange', this);
this._window = win;
this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
this.innerWindowID = this._utils.currentInnerWindowID;
this._focusHandler = null;
this._selectionHandler = null;
this._selectionStart = -1;
this._selectionEnd = -1;
},
uninit: function mozKeyboardUninit() {
Services.obs.removeObserver(this, "inner-window-destroyed");
cpmm.removeMessageListener('Keyboard:FocusChange', this);
cpmm.removeMessageListener('Keyboard:SelectionChange', this);
this._window = null;
this._utils = null;
this._focusHandler = null;
this._selectionHandler = null;
},
sendKey: function mozKeyboardSendKey(keyCode, charCode) {
charCode = (charCode == undefined) ? keyCode : charCode;
let mainThread = tm.mainThread;
let utils = this._utils;
function send(type) {
mainThread.dispatch(function() {
utils.sendKeyEvent(type, keyCode, charCode, null);
}, mainThread.DISPATCH_NORMAL);
}
send("keydown");
send("keypress");
send("keyup");
},
setSelectedOption: function mozKeyboardSetSelectedOption(index) {
cpmm.sendAsyncMessage('Keyboard:SetSelectedOption', {
'index': index
});
},
setValue: function mozKeyboardSetValue(value) {
cpmm.sendAsyncMessage('Keyboard:SetValue', {
'value': value
});
},
setSelectedOptions: function mozKeyboardSetSelectedOptions(indexes) {
cpmm.sendAsyncMessage('Keyboard:SetSelectedOptions', {
'indexes': indexes
});
},
set onselectionchange(val) {
this._selectionHandler = val;
},
get onselectionchange() {
return this._selectionHandler;
},
get selectionStart() {
return this._selectionStart;
},
get selectionEnd() {
return this._selectionEnd;
},
setSelectionRange: function mozKeyboardSetSelectionRange(start, end) {
cpmm.sendAsyncMessage('Keyboard:SetSelectionRange', {
'selectionStart': start,
'selectionEnd': end
});
},
removeFocus: function mozKeyboardRemoveFocus() {
cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
},
set onfocuschange(val) {
this._focusHandler = val;
},
get onfocuschange() {
return this._focusHandler;
},
replaceSurroundingText: function mozKeyboardReplaceSurroundingText(
text, beforeLength, afterLength) {
cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
'text': text || '',
'beforeLength': (typeof beforeLength === 'number' ? beforeLength : 0),
'afterLength': (typeof afterLength === 'number' ? afterLength: 0)
});
},
receiveMessage: function mozKeyboardReceiveMessage(msg) {
if (msg.name == "Keyboard:FocusChange") {
let msgJson = msg.json;
if (msgJson.type != "blur") {
this._selectionStart = msgJson.selectionStart;
this._selectionEnd = msgJson.selectionEnd;
} else {
this._selectionStart = 0;
this._selectionEnd = 0;
}
let handler = this._focusHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let detail = {
"detail": msgJson
};
let evt = new this._window.CustomEvent("focuschanged",
ObjectWrapper.wrap(detail, this._window));
handler.handleEvent(evt);
} else if (msg.name == "Keyboard:SelectionChange") {
let msgJson = msg.json;
this._selectionStart = msgJson.selectionStart;
this._selectionEnd = msgJson.selectionEnd;
let handler = this._selectionHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let evt = new this._window.CustomEvent("selectionchange",
ObjectWrapper.wrap({}, this._window));
handler.handleEvent(evt);
}
},
observe: function mozKeyboardObserve(subject, topic, data) {
let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (wId == this.innerWindowID)
this.uninit();
}
};
/*
* 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(win) {
this._window = win;
}
MozInputMethodManager.prototype = {
_supportsSwitching: false,
_window: null,
classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIInputMethodManager
]),
classInfo: XPCOMUtils.generateCI({
"classID": Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
"contractID": "@mozilla.org/b2g-imm;1",
"interfaces": [Ci.nsIInputMethodManager],
"flags": Ci.nsIClassInfo.DOM_OBJECT,
"classDescription": "B2G Input Method Manager"
}),
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', {});
}
};
/**
* ==============================================
* InputMethod
* ==============================================
*/
function MozInputMethod() { }
MozInputMethod.prototype = {
_inputcontext: null,
_layouts: {},
_window: null,
classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIInputMethod,
Ci.nsIDOMGlobalPropertyInitializer,
Ci.nsIObserver
]),
classInfo: XPCOMUtils.generateCI({
"classID": Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
"contractID": "@mozilla.org/b2g-inputmethod;1",
"interfaces": [Ci.nsIInputMethod],
"flags": Ci.nsIClassInfo.DOM_OBJECT,
"classDescription": "B2G Input Method"
}),
init: function mozInputMethodInit(win) {
this._window = win;
this._mgmt = new MozInputMethodManager(win);
this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
Services.obs.addObserver(this, "inner-window-destroyed", false);
cpmm.addMessageListener('Keyboard:FocusChange', this);
cpmm.addMessageListener('Keyboard:SelectionChange', this);
cpmm.addMessageListener('Keyboard:GetContext:Result:OK', this);
cpmm.addMessageListener('Keyboard:LayoutsChange', this);
},
uninit: function mozInputMethodUninit() {
Services.obs.removeObserver(this, "inner-window-destroyed");
cpmm.removeMessageListener('Keyboard:FocusChange', this);
cpmm.removeMessageListener('Keyboard:SelectionChange', this);
cpmm.removeMessageListener('Keyboard:GetContext:Result:OK', this);
cpmm.removeMessageListener('Keyboard:LayoutsChange', this);
this._window = null;
this._mgmt = null;
},
receiveMessage: function mozInputMethodReceiveMsg(msg) {
if (!WindowMap.isActive(this._window)) {
return;
}
let json = msg.json;
switch(msg.name) {
case 'Keyboard:FocusChange':
if (json.type !== 'blur') {
// XXX Bug 904339 could receive 'text' event twice
this.setInputContext(json);
}
else {
this.setInputContext(null);
}
break;
case 'Keyboard:SelectionChange':
if (this.inputcontext) {
this._inputcontext.updateSelectionContext(json);
}
break;
case 'Keyboard:GetContext:Result:OK':
this.setInputContext(json);
break;
case 'Keyboard:LayoutsChange':
this._layouts = json;
break;
}
},
observe: function mozInputMethodObserve(subject, topic, data) {
let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (wId == this.innerWindowID)
this.uninit();
},
get mgmt() {
if (!WindowMap.isActive(this._window)) {
return null;
}
return this._mgmt;
},
get inputcontext() {
if (!WindowMap.isActive(this._window)) {
return null;
}
return this._inputcontext;
},
set oninputcontextchange(handler) {
this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler);
},
get oninputcontextchange() {
return this.__DOM_IMPL__.getEventHandler("oninputcontextchange");
},
setInputContext: function mozKeyboardContextChange(data) {
if (this._inputcontext) {
this._inputcontext.destroy();
this._inputcontext = null;
this._mgmt._supportsSwitching = false;
}
if (data) {
this._mgmt._supportsSwitching = this._layouts[data.type] ?
this._layouts[data.type] > 1 :
false;
this._inputcontext = new MozInputContext(data);
this._inputcontext.init(this._window);
}
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);
}
}
}
};
/**
* ==============================================
* InputContext
* ==============================================
*/
function MozInputContext(ctx) {
this._context = {
inputtype: ctx.type,
inputmode: ctx.inputmode,
lang: ctx.lang,
type: ["textarea", "contenteditable"].indexOf(ctx.type) > -1 ?
ctx.type :
"text",
selectionStart: ctx.selectionStart,
selectionEnd: ctx.selectionEnd,
textBeforeCursor: ctx.textBeforeCursor,
textAfterCursor: ctx.textAfterCursor
};
this._contextId = ctx.contextId;
}
MozInputContext.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
_window: null,
_context: null,
_contextId: -1,
classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIB2GInputContext,
Ci.nsIObserver,
Ci.nsISupportsWeakReference
]),
classInfo: XPCOMUtils.generateCI({
"classID": Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
"contractID": "@mozilla.org/b2g-inputcontext;1",
"interfaces": [Ci.nsIB2GInputContext],
"flags": Ci.nsIClassInfo.DOM_OBJECT,
"classDescription": "B2G Input Context"
}),
init: function ic_init(win) {
this._window = win;
this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
this.initDOMRequestHelper(win,
["Keyboard:GetText:Result:OK",
"Keyboard:GetText:Result:Error",
"Keyboard:SetSelectionRange:Result:OK",
"Keyboard:ReplaceSurroundingText:Result:OK",
"Keyboard:SendKey:Result:OK",
"Keyboard:SetComposition:Result:OK",
"Keyboard:EndComposition:Result:OK",
"Keyboard:SequenceError"]);
},
destroy: function ic_destroy() {
let self = this;
// All requests that are still pending need to be invalidated
// because the context is no longer valid.
this.forEachPromiseResolver(function(k) {
self.takePromiseResolver(k).reject("InputContext got destroyed");
});
this.destroyDOMRequestHelper();
// A consuming application might still hold a cached version of
// this object. After destroying all methods will throw because we
// cannot create new promises anymore, but we still hold
// (outdated) information in the context. So let's clear that out.
for (var k in this._context) {
if (this._context.hasOwnProperty(k)) {
this._context[k] = null;
}
}
this._window = null;
},
receiveMessage: function ic_receiveMessage(msg) {
if (!msg || !msg.json) {
dump('InputContext received message without data\n');
return;
}
let json = msg.json;
let resolver = this.takePromiseResolver(json.requestId);
if (!resolver) {
return;
}
switch (msg.name) {
case "Keyboard:SendKey:Result:OK":
resolver.resolve();
break;
case "Keyboard:GetText:Result:OK":
resolver.resolve(json.text);
break;
case "Keyboard:GetText:Result:Error":
resolver.reject(json.error);
break;
case "Keyboard:SetSelectionRange:Result:OK":
case "Keyboard:ReplaceSurroundingText:Result:OK":
resolver.resolve(
ObjectWrapper.wrap(json.selectioninfo, this._window));
break;
case "Keyboard:SequenceError":
// Occurs when a new element got focus, but the inputContext was
// not invalidated yet...
resolver.reject("InputContext has expired");
break;
case "Keyboard:SetComposition:Result:OK": // Fall through.
case "Keyboard:EndComposition:Result:OK":
resolver.resolve();
break;
default:
dump("Could not find a handler for " + msg.name);
resolver.reject();
break;
}
},
updateSelectionContext: function ic_updateSelectionContext(ctx) {
if (!this._context) {
return;
}
let selectionDirty = this._context.selectionStart !== ctx.selectionStart ||
this._context.selectionEnd !== ctx.selectionEnd;
let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor ||
this._context.textAfterCursor !== ctx.textAfterCursor;
this._context.selectionStart = ctx.selectionStart;
this._context.selectionEnd = ctx.selectionEnd;
this._context.textBeforeCursor = ctx.textBeforeCursor;
this._context.textAfterCursor = ctx.textAfterCursor;
if (selectionDirty) {
this._fireEvent("selectionchange", {
selectionStart: ctx.selectionStart,
selectionEnd: ctx.selectionEnd
});
}
if (surroundDirty) {
this._fireEvent("surroundingtextchange", {
beforeString: ctx.textBeforeCursor,
afterString: ctx.textAfterCursor
});
}
},
_fireEvent: function ic_fireEvent(eventName, aDetail) {
let detail = {
detail: aDetail
};
let event = new this._window.Event(eventName,
ObjectWrapper.wrap(aDetail, this._window));
this.__DOM_IMPL__.dispatchEvent(event);
},
// tag name of the input field
get type() {
return this._context.type;
},
// type of the input field
get inputType() {
return this._context.inputtype;
},
get inputMode() {
return this._context.inputmode;
},
get lang() {
return this._context.lang;
},
getText: function ic_getText(offset, length) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:GetText', {
contextId: self._contextId,
requestId: resolverId,
offset: offset,
length: length
});
});
},
get selectionStart() {
return this._context.selectionStart;
},
get selectionEnd() {
return this._context.selectionEnd;
},
get textBeforeCursor() {
return this._context.textBeforeCursor;
},
get textAfterCursor() {
return this._context.textAfterCursor;
},
setSelectionRange: function ic_setSelectionRange(start, length) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", {
contextId: self._contextId,
requestId: resolverId,
selectionStart: start,
selectionEnd: start + length
});
});
},
get onsurroundingtextchange() {
return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange");
},
set onsurroundingtextchange(handler) {
this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange");
},
get onselectionchange() {
return this.__DOM_IMPL__.getEventHandler("onselectionchange");
},
set onselectionchange(handler) {
this.__DOM_IMPL__.setEventHandler("onselectionchange");
},
replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
contextId: self._contextId,
requestId: resolverId,
text: text,
beforeLength: offset || 0,
afterLength: length || 0
});
});
},
deleteSurroundingText: function ic_deleteSurrText(offset, length) {
return this.replaceSurroundingText(null, offset, length);
},
sendKey: function ic_sendKey(keyCode, charCode, modifiers) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:SendKey', {
contextId: self._contextId,
requestId: resolverId,
keyCode: keyCode,
charCode: charCode,
modifiers: modifiers
});
});
},
setComposition: function ic_setComposition(text, cursor, clauses) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmm.sendAsyncMessage('Keyboard:SetComposition', {
contextId: self._contextId,
requestId: resolverId,
text: text,
cursor: cursor || text.length,
clauses: clauses || null
});
});
},
endComposition: function ic_endComposition(text) {
let self = this;
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);
});
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
[MozKeyboard, MozInputMethod]);