Bug 899999 - Implement new InputContext API. r=yxl

This commit is contained in:
Jan Jongboom 2013-08-07 20:07:41 -04:00
parent 26b36f402d
commit db365c9dce
5 changed files with 687 additions and 31 deletions

View File

@ -194,6 +194,9 @@ let FormAssistant = {
addMessageListener("Forms:Select:Blur", this);
addMessageListener("Forms:SetSelectionRange", this);
addMessageListener("Forms:ReplaceSurroundingText", this);
addMessageListener("Forms:GetText", this);
addMessageListener("Forms:Input:SendKey", this);
addMessageListener("Forms:GetContext", this);
},
ignoredInputTypes: new Set([
@ -203,8 +206,11 @@ let FormAssistant = {
isKeyboardOpened: false,
selectionStart: -1,
selectionEnd: -1,
textBeforeCursor: "",
textAfterCursor: "",
scrollIntoViewTimeout: null,
_focusedElement: null,
_focusCounter: 0, // up one for every time we focus a new element
_documentEncoder: null,
_editor: null,
_editing: false,
@ -218,6 +224,7 @@ let FormAssistant = {
},
set focusedElement(val) {
this._focusCounter++;
this._focusedElement = val;
},
@ -390,12 +397,34 @@ let FormAssistant = {
receiveMessage: function fa_receiveMessage(msg) {
let target = this.focusedElement;
let json = msg.json;
// To not break mozKeyboard contextId is optional
if ('contextId' in json &&
json.contextId !== this._focusCounter &&
json.requestId) {
// Ignore messages that are meant for a previously focused element
sendAsyncMessage("Forms:SequenceError", {
requestId: json.requestId,
error: "Expected contextId " + this._focusCounter +
" but was " + json.contextId
});
return;
}
if (!target) {
switch (msg.name) {
case "Forms:GetText":
sendAsyncMessage("Forms:GetText:Result:Error", {
requestId: json.requestId,
error: "No focused element"
});
break;
}
return;
}
this._editing = true;
let json = msg.json;
switch (msg.name) {
case "Forms:Input:Value": {
target.value = json.value;
@ -406,6 +435,19 @@ let FormAssistant = {
break;
}
case "Forms:Input:SendKey":
["keydown", "keypress", "keyup"].forEach(function(type) {
domWindowUtils.sendKeyEvent(type, json.keyCode, json.charCode,
json.modifiers);
});
if (json.requestId) {
sendAsyncMessage("Forms:SendKey:Result:OK", {
requestId: json.requestId
});
}
break;
case "Forms:Select:Choice":
let options = target.options;
let valueChanged = false;
@ -442,6 +484,13 @@ let FormAssistant = {
let end = json.selectionEnd;
setSelectionRange(target, start, end);
this.updateSelection();
if (json.requestId) {
sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
requestId: json.requestId,
selectioninfo: this.getSelectionInfo()
});
}
break;
}
@ -449,8 +498,44 @@ let FormAssistant = {
let text = json.text;
let beforeLength = json.beforeLength;
let afterLength = json.afterLength;
replaceSurroundingText(target, text, this.selectionStart, beforeLength,
let selectionRange = getSelectionRange(target);
replaceSurroundingText(target, text, selectionRange[0], beforeLength,
afterLength);
if (json.requestId) {
sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
requestId: json.requestId,
selectioninfo: this.getSelectionInfo()
});
}
break;
}
case "Forms:GetText": {
let isPlainTextField = target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement;
let value = isPlainTextField ?
target.value :
getContentEditableText(target);
if (json.offset && json.length) {
value = value.substr(json.offset, json.length);
}
else if (json.offset) {
value = value.substr(json.offset);
}
sendAsyncMessage("Forms:GetText:Result:OK", {
requestId: json.requestId,
text: value
});
break;
}
case "Forms:GetContext": {
let obj = getJSON(target, this._focusCounter);
sendAsyncMessage("Forms:GetContext:Result:OK", obj);
break;
}
}
@ -529,20 +614,47 @@ let FormAssistant = {
return false;
}
sendAsyncMessage("Forms:Input", getJSON(element));
sendAsyncMessage("Forms:Input", getJSON(element, this._focusCounter));
return true;
},
getSelectionInfo: function fa_getSelectionInfo() {
let element = this.focusedElement;
let range = getSelectionRange(element);
let isPlainTextField = element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement;
let text = isPlainTextField ?
element.value :
getContentEditableText(element);
let textAround = getTextAroundCursor(text, range);
let changed = this.selectionStart !== range[0] ||
this.selectionEnd !== range[1] ||
this.textBeforeCursor !== textAround.before ||
this.textAfterCursor !== textAround.after;
this.selectionStart = range[0];
this.selectionEnd = range[1];
this.textBeforeCursor = textAround.before;
this.textAfterCursor = textAround.after;
return {
selectionStart: range[0],
selectionEnd: range[1],
textBeforeCursor: textAround.before,
textAfterCursor: textAround.after,
changed: changed
};
},
// Notify when the selection range changes
updateSelection: function fa_updateSelection() {
let range = getSelectionRange(this.focusedElement);
if (range[0] != this.selectionStart || range[1] != this.selectionEnd) {
this.selectionStart = range[0];
this.selectionEnd = range[1];
sendAsyncMessage("Forms:SelectionChange", {
selectionStart: range[0],
selectionEnd: range[1]
});
let selectionInfo = this.getSelectionInfo();
if (selectionInfo.changed) {
sendAsyncMessage("Forms:SelectionChange", this.getSelectionInfo());
}
}
};
@ -568,7 +680,7 @@ function isContentEditable(element) {
return element.ownerDocument && element.ownerDocument.designMode == "on";
}
function getJSON(element) {
function getJSON(element, focusCounter) {
let type = element.type || "";
let value = element.value || "";
let max = element.max || "";
@ -609,8 +721,11 @@ function getJSON(element) {
}
let range = getSelectionRange(element);
let textAround = getTextAroundCursor(value, range);
return {
"contextId": focusCounter,
"type": type.toLowerCase(),
"choices": getListForElement(element),
"value": value,
@ -618,7 +733,25 @@ function getJSON(element) {
"selectionStart": range[0],
"selectionEnd": range[1],
"max": max,
"min": min
"min": min,
"lang": element.lang || "",
"textBeforeCursor": textAround.before,
"textAfterCursor": textAround.after
};
}
function getTextAroundCursor(value, range) {
let textBeforeCursor = range[0] < 100 ?
value.substr(0, range[0]) :
value.substr(range[0] - 100, 100);
let textAfterCursor = range[1] + 100 > value.length ?
value.substr(range[0], value.length) :
value.substr(range[0], range[1] - range[0] + 100);
return {
before: textBeforeCursor,
after: textAfterCursor
};
}

View File

@ -20,8 +20,8 @@ component {397a7fdf-2254-47be-b74e-76625a1a66d5} MozKeyboard.js
contract @mozilla.org/b2g-keyboard;1 {397a7fdf-2254-47be-b74e-76625a1a66d5}
category JavaScript-navigator-property mozKeyboard @mozilla.org/b2g-keyboard;1
component {5c7f4ce1-a946-4adc-89e6-c908226341a0} MozKeyboard.js
contract @mozilla.org/b2g-inputmethod;1 {5c7f4ce1-a946-4adc-89e6-c908226341a0}
component {4607330d-e7d2-40a4-9eb8-43967eae0142} MozKeyboard.js
contract @mozilla.org/b2g-inputmethod;1 {4607330d-e7d2-40a4-9eb8-43967eae0142}
category JavaScript-navigator-property mozInputMethod @mozilla.org/b2g-inputmethod;1
# DirectoryProvider.js

View File

@ -22,7 +22,8 @@ let Keyboard = {
_messageNames: [
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
'SwitchToNextInputMethod', 'HideInputMethod'
'SwitchToNextInputMethod', 'HideInputMethod',
'GetText', 'SendKey', 'GetContext'
],
get messageManager() {
@ -58,6 +59,13 @@ let Keyboard = {
} else {
mm.addMessageListener('Forms:Input', this);
mm.addMessageListener('Forms:SelectionChange', this);
mm.addMessageListener('Forms:GetText:Result:OK', this);
mm.addMessageListener('Forms:GetText:Result:Error', this);
mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
mm.addMessageListener('Forms:SendKey:Result:OK', this);
mm.addMessageListener('Forms:SequenceError', this);
mm.addMessageListener('Forms:GetContext:Result:OK', this);
// When not running apps OOP, we need to load forms.js here since this
// won't happen from dom/ipc/preload.js
@ -98,11 +106,20 @@ let Keyboard = {
switch (msg.name) {
case 'Forms:Input':
this.handleFormsInput(msg);
this.forwardEvent('Keyboard:FocusChange', msg);
break;
case 'Forms:SelectionChange':
this.handleFormsSelectionChange(msg);
case 'Forms:GetText:Result:OK':
case 'Forms:GetText:Result:Error':
case 'Forms:SetSelectionRange:Result:OK':
case 'Forms:ReplaceSurroundingText:Result:OK':
case 'Forms:SendKey:Result:OK':
case 'Forms:SequenceError':
case 'Forms:GetContext:Result:OK':
let name = msg.name.replace(/^Forms/, 'Keyboard');
this.forwardEvent(name, msg);
break;
case 'Keyboard:SetValue':
this.setValue(msg);
break;
@ -127,21 +144,23 @@ let Keyboard = {
case 'Keyboard:ShowInputMethodPicker':
this.showInputMethodPicker();
break;
case 'Keyboard:GetText':
this.getText(msg);
break;
case 'Keyboard:SendKey':
this.sendKey(msg);
break;
case 'Keyboard:GetContext':
this.getContext(msg);
break;
}
},
handleFormsInput: function keyboardHandleFormsInput(msg) {
forwardEvent: function keyboardForwardEvent(newEventName, msg) {
this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader.messageManager;
ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data);
},
handleFormsSelectionChange: function keyboardHandleFormsSelectionChange(msg) {
this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader.messageManager;
ppmm.broadcastAsyncMessage('Keyboard:SelectionChange', msg.data);
ppmm.broadcastAsyncMessage(newEventName, msg.data);
},
setSelectedOption: function keyboardSetSelectedOption(msg) {
@ -181,6 +200,18 @@ let Keyboard = {
browser.shell.sendChromeEvent({
type: "input-method-switch-to-next"
});
},
getText: function keyboardGetText(msg) {
this.messageManager.sendAsyncMessage('Forms:GetText', msg.data);
},
sendKey: function keyboardSendKey(msg) {
this.messageManager.sendAsyncMessage('Forms:Input:SendKey', msg.data);
},
getContext: function keyboardGetContext(msg) {
this.messageManager.sendAsyncMessage('Forms:GetContext', msg.data);
}
};

View File

@ -10,6 +10,7 @@ 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");
@ -244,15 +245,18 @@ MozInputMethodManager.prototype = {
function MozInputMethod() { }
MozInputMethod.prototype = {
classID: Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"),
_inputcontext: null,
classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIInputMethod,
Ci.nsIDOMGlobalPropertyInitializer
Ci.nsIDOMGlobalPropertyInitializer,
Ci.nsIObserver
]),
classInfo: XPCOMUtils.generateCI({
"classID": Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"),
"classID": Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
"contractID": "@mozilla.org/b2g-inputmethod;1",
"interfaces": [Ci.nsIInputMethod],
"flags": Ci.nsIClassInfo.DOM_OBJECT,
@ -269,11 +273,374 @@ MozInputMethod.prototype = {
return null;
}
this._window = win;
this._mgmt = new MozInputMethodManager();
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);
// 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() {
Services.obs.removeObserver(this, "inner-window-destroyed");
cpmm.removeMessageListener('Keyboard:FocusChange', this);
cpmm.removeMessageListener('Keyboard:SelectionChange', this);
cpmm.removeMessageListener('Keyboard:GetContext:Result:OK', this);
this._window = null;
this._inputcontextHandler = null;
this._mgmt = null;
},
receiveMessage: function mozInputMethodReceiveMsg(msg) {
let json = msg.json;
switch(msg.name) {
case 'Keyboard:FocusChange':
if (json.type !== 'blur') {
this.setInputContext(json);
}
else {
this.setInputContext(null);
}
break;
case 'Keyboard:SelectionChange':
this._inputcontext.updateSelectionContext(json);
break;
case 'Keyboard:GetContext:Result:OK':
this.setInputContext(json);
break;
}
},
observe: function mozInputMethodObserve(subject, topic, data) {
let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
if (wId == this.innerWindowID)
this.uninit();
},
get mgmt() {
return this._mgmt;
},
get inputcontext() {
return this._inputcontext;
},
set oninputcontextchange(val) {
this._inputcontextHandler = val;
},
get oninputcontextchange() {
return this._inputcontextHandler;
},
setInputContext: function mozKeyboardContextChange(data) {
if (this._inputcontext) {
this._inputcontext.destroy();
this._inputcontext = null;
}
if (data) {
this._inputcontext = new MozInputContext(data);
this._inputcontext.init(this._window);
}
let handler = this._inputcontextHandler;
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let evt = new this._window.CustomEvent("inputcontextchange",
ObjectWrapper.wrap({}, this._window));
handler.handleEvent(evt);
}
};
/**
* ==============================================
* 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,
_context: null,
_contextId: -1,
classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIB2GInputContext,
Ci.nsIObserver
]),
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: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.
Object.keys(self._requests).forEach(function(k) {
// takeRequest also does a delete from context
let req = self.takeRequest(k);
Services.DOMRequest.fireError(req, "InputContext got destroyed");
});
this.destroyDOMRequestHelper();
// A consuming application might still hold a cached version of this
// object. After destroying the DOMRequestHelper all methods will throw
// because we cannot create new requests 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;
},
receiveMessage: function ic_receiveMessage(msg) {
if (!msg || !msg.json) {
dump('InputContext received message without data\n');
return;
}
let json = msg.json;
let request = json.requestId ? this.takeRequest(json.requestId) : null;
if (!request) {
return;
}
switch (msg.name) {
case "Keyboard:SendKey:Result:OK":
Services.DOMRequest.fireSuccess(request, null);
break;
case "Keyboard:GetText:Result:OK":
Services.DOMRequest.fireSuccess(request, json.text);
break;
case "Keyboard:GetText:Result:Error":
Services.DOMRequest.fireError(request, json.error);
break;
case "Keyboard:SetSelectionRange:Result:OK":
case "Keyboard:ReplaceSurroundingText:Result:OK":
Services.DOMRequest.fireSuccess(request,
ObjectWrapper.wrap(json.selectioninfo, this._window));
break;
case "Keyboard:SequenceError":
// Occurs when a new element got focus, but the inputContext was
// not invalidated yet...
Services.DOMRequest.fireError(request, "InputContext has expired");
break;
default:
Services.DOMRequest.fireError(request, "Could not find a handler for " +
msg.name);
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(this._onselectionchange, "selectionchange", {
selectionStart: ctx.selectionStart,
selectionEnd: ctx.selectionEnd
});
}
if (surroundDirty) {
this._fireEvent(this._onsurroundingtextchange, "surroundingtextchange", {
beforeString: ctx.textBeforeCursor,
afterString: ctx.textAfterCursor
});
}
},
_fireEvent: function ic_fireEvent(handler, eventName, aDetail) {
if (!handler || !(handler instanceof Ci.nsIDOMEventListener))
return;
let detail = {
detail: aDetail
};
let evt = new this._window.CustomEvent(eventName,
ObjectWrapper.wrap(aDetail, this._window));
handler.handleEvent(evt);
},
// 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 request = this.createRequest();
cpmm.sendAsyncMessage('Keyboard:GetText', {
contextId: this._contextId,
requestId: this.getRequestId(request),
offset: offset,
length: length
});
return request;
},
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 request = this.createRequest();
cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", {
contextId: this._contextId,
requestId: this.getRequestId(request),
selectionStart: start,
selectionEnd: start + length
});
return request;
},
get onsurroundingtextchange() {
return this._onsurroundingtextchange;
},
set onsurroundingtextchange(handler) {
this._onsurroundingtextchange = handler;
},
get onselectionchange() {
return this._onselectionchange;
},
set onselectionchange(handler) {
this._onselectionchange = handler;
},
replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
let request = this.createRequest();
cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
contextId: this._contextId,
requestId: this.getRequestId(request),
text: text,
beforeLength: offset || 0,
afterLength: length || 0
});
return request;
},
deleteSurroundingText: function ic_deleteSurrText(offset, length) {
return this.replaceSurroundingText(null, offset, length);
},
sendKey: function ic_sendKey(keyCode, charCode, modifiers) {
let request = this.createRequest();
cpmm.sendAsyncMessage('Keyboard:SendKey', {
contextId: this._contextId,
requestId: this.getRequestId(request),
keyCode: keyCode,
charCode: charCode,
modifiers: modifiers
});
return request;
},
setComposition: function ic_setComposition(text, cursor) {
throw "Not implemented";
},
endComposition: function ic_endComposition(text) {
throw "Not implemented";
}
};

View File

@ -4,6 +4,8 @@
#include "domstubs.idl"
interface nsIDOMDOMRequest;
[scriptable, uuid(3615a616-571d-4194-bf54-ccf546067b14)]
interface nsIB2GCameraContent : nsISupports
{
@ -11,6 +13,119 @@ interface nsIB2GCameraContent : nsISupports
DOMString getCameraURI([optional] in jsval options);
};
[scriptable, uuid(1e38633d-d08b-4867-9944-afa5c648adb6)]
interface nsIB2GInputContext : nsISupports
{
// The tag name of input field, which is enum of "input", "textarea", or "contenteditable"
readonly attribute DOMString type;
// The type of the input field, which is enum of text, number, password, url, search, email, and so on.
// See http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#states-of-the-type-attribute
readonly attribute DOMString inputType;
/*
* The inputmode string, representing the input mode.
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#input-modalities:-the-inputmode-attribute
*/
readonly attribute DOMString inputMode;
/*
* The primary language for the input field.
* It is the value of HTMLElement.lang.
* See http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#htmlelement
*/
readonly attribute DOMString lang;
/*
* Get the whole text content of the input field.
*/
nsIDOMDOMRequest getText([optional] in long offset, [optional] in long length);
// The start and stop position of the selection.
readonly attribute long selectionStart;
readonly attribute long selectionEnd;
// The start and stop position of the selection.
readonly attribute DOMString textBeforeCursor;
readonly attribute DOMString textAfterCursor;
/*
* Set the selection range of the the editable text.
* Note: This method cannot be used to move the cursor during composition. Calling this
* method will cancel composition.
* @param start The beginning of the selected text.
* @param length The length of the selected text.
*
* Note that the start position should be less or equal to the end position.
* To move the cursor, set the start and end position to the same value.
*/
nsIDOMDOMRequest setSelectionRange(in long start, in long length);
/*
* Commit text to current input field and replace text around cursor position. It will clear the current composition.
*
* @param text The string to be replaced with.
* @param offset The offset from the cursor position where replacing starts. Defaults to 0.
* @param length The length of text to replace. Defaults to 0.
*/
nsIDOMDOMRequest replaceSurroundingText(in DOMString text, [optional] in long offset, [optional] in long length);
/*
*
* Delete text around the cursor.
* @param offset The offset from the cursor position where deletion starts.
* @param length The length of text to delete.
* TODO: maybe updateSurroundingText(DOMString beforeText, DOMString afterText); ?
*/
nsIDOMDOMRequest deleteSurroundingText(in long offset, in long length);
/*
* Notifies when the text around the cursor is changed, due to either text
* editing or cursor movement. If the cursor has been moved, but the text around has not
* changed, the IME won't get notification.
*
* The event handler function is specified as:
* @param beforeString Text before and including cursor position.
* @param afterString Text after and excluing cursor position.
* function(DOMString beforeText, DOMString afterText) {
* ...
* }
*/
attribute nsIDOMEventListener onsurroundingtextchange;
attribute nsIDOMEventListener onselectionchange;
/*
* send a character with its key events.
* @param modifiers see http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl#206
* @return true if succeeds. Otherwise false if the input context becomes void.
* Alternative: sendKey(KeyboardEvent event), but we will likely waste memory for creating the KeyboardEvent object.
*/
nsIDOMDOMRequest sendKey(in long keyCode, in long charCode, in long modifiers);
/*
* Set current composition. It will start or update composition.
* @param cursor Position in the text of the cursor.
*
* The API implementation should automatically ends the composition
* session (with event and confirm the current composition) if
* endComposition is never called. Same apply when the inputContext is lost
* during a unfinished composition session.
*/
nsIDOMDOMRequest setComposition(in DOMString text, in long cursor);
/*
* End composition and actually commit the text. (was |commitText(text, offset, length)|)
* Ending the composition with an empty string will not send any text.
* Note that if composition always ends automatically (with the current composition committed) if the composition
* did not explicitly with |endComposition()| but was interrupted with |sendKey()|, |setSelectionRange()|,
* user moving the cursor, or remove the focus, etc.
*
* @param text The text
*/
nsIDOMDOMRequest endComposition(in DOMString text);
};
[scriptable, uuid(40ad96b2-9efa-41fb-84c7-fbcec9b153f0)]
interface nsIB2GKeyboard : nsISupports
{
@ -103,9 +218,19 @@ interface nsIInputMethodManager : nsISupports
void hide();
};
[scriptable, uuid(5c7f4ce1-a946-4adc-89e6-c908226341a0)]
[scriptable, uuid(4607330d-e7d2-40a4-9eb8-43967eae0142)]
interface nsIInputMethod : nsISupports
{
// Input Method Manager contain a few global methods expose to apps
readonly attribute nsIInputMethodManager mgmt;
// Fired when the input context changes, include changes from and to null.
// The new InputContext instance will be available in the event object under |inputcontext| property.
// When it changes to null it means the app (the user of this API) no longer has the control of the original focused input field.
// Note that if the app saves the original context, it might get void; implementation decides when to void the input context.
readonly attribute nsIB2GInputContext inputcontext;
// An "input context" is mapped to a text field that the app is allow to mutate.
// this attribute should be null when there is no text field currently focused.
attribute nsIDOMEventListener oninputcontextchange;
};