Bug 1234459 - Expose full text in the input box to InputMethod API, r=masayuki, sr=smaug

This commit is contained in:
Tim Chien 2016-01-05 00:37:00 +01:00
parent 830784d6b5
commit f67c190aa1
5 changed files with 193 additions and 132 deletions

View File

@ -57,7 +57,7 @@ this.Keyboard = {
'RemoveFocus',
'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
'SwitchToNextInputMethod', 'HideInputMethod',
'GetText', 'SendKey', 'GetContext',
'SendKey', 'GetContext',
'SetComposition', 'EndComposition',
'RegisterSync', 'Unregister'
],
@ -162,8 +162,6 @@ this.Keyboard = {
mm.addMessageListener('Forms:Focus', this);
mm.addMessageListener('Forms:Blur', 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:SetSelectionRange:Result:Error', this);
mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
@ -226,8 +224,6 @@ this.Keyboard = {
this.handleBlur(msg);
break;
case 'Forms:SelectionChange':
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':
@ -288,9 +284,6 @@ this.Keyboard = {
case 'Keyboard:ShowInputMethodPicker':
this.showInputMethodPicker();
break;
case 'Keyboard:GetText':
this.getText(msg);
break;
case 'Keyboard:SendKey':
this.sendKey(msg);
break;
@ -423,10 +416,6 @@ this.Keyboard = {
});
},
getText: function keyboardGetText(msg) {
this.sendToForm('Forms:GetText', msg.data);
},
sendKey: function keyboardSendKey(msg) {
this.sendToForm('Forms:Input:SendKey', msg.data);
},

View File

@ -739,24 +739,67 @@ InputContextDOMRequestIpcHelper.prototype = {
}
};
function MozInputContextSelectionChangeEventDetail(ctx, ownAction) {
this._ctx = ctx;
this.ownAction = ownAction;
}
MozInputContextSelectionChangeEventDetail.prototype = {
classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"),
QueryInterface: XPCOMUtils.generateQI([]),
ownAction: false,
get selectionStart() {
return this._ctx.selectionStart;
},
get selectionEnd() {
return this._ctx.selectionEnd;
}
};
function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) {
this._ctx = ctx;
this.ownAction = ownAction;
}
MozInputContextSurroundingTextChangeEventDetail.prototype = {
classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"),
QueryInterface: XPCOMUtils.generateQI([]),
ownAction: false,
get text() {
return this._ctx.text;
},
get textBeforeCursor() {
return this._ctx.textBeforeCursor;
},
get textAfterCursor() {
return this._ctx.textAfterCursor;
}
};
/**
* ==============================================
* InputContext
* ==============================================
*/
function MozInputContext(ctx) {
function MozInputContext(data) {
this._context = {
type: ctx.type,
inputType: ctx.inputType,
inputMode: ctx.inputMode,
lang: ctx.lang,
selectionStart: ctx.selectionStart,
selectionEnd: ctx.selectionEnd,
textBeforeCursor: ctx.textBeforeCursor,
textAfterCursor: ctx.textAfterCursor
type: data.type,
inputType: data.inputType,
inputMode: data.inputMode,
lang: data.lang,
selectionStart: data.selectionStart,
selectionEnd: data.selectionEnd,
text: data.value
};
this._contextId = ctx.contextId;
this._contextId = data.contextId;
}
MozInputContext.prototype = {
@ -849,48 +892,45 @@ MozInputContext.prototype = {
}
},
updateSelectionContext: function ic_updateSelectionContext(ctx, ownAction) {
updateSelectionContext: function ic_updateSelectionContext(data, ownAction) {
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;
let selectionDirty =
this._context.selectionStart !== data.selectionStart ||
this._context.selectionEnd !== data.selectionEnd;
let surroundDirty = selectionDirty || data.text !== this._contextId.text;
this._context.selectionStart = ctx.selectionStart;
this._context.selectionEnd = ctx.selectionEnd;
this._context.textBeforeCursor = ctx.textBeforeCursor;
this._context.textAfterCursor = ctx.textAfterCursor;
this._context.text = data.text;
this._context.selectionStart = data.selectionStart;
this._context.selectionEnd = data.selectionEnd;
if (selectionDirty) {
this._fireEvent("selectionchange", {
selectionStart: ctx.selectionStart,
selectionEnd: ctx.selectionEnd,
ownAction: ownAction
});
let selectionChangeDetail =
new MozInputContextSelectionChangeEventDetail(this, ownAction);
let wrappedSelectionChangeDetail =
this._window.MozInputContextSelectionChangeEventDetail
._create(this._window, selectionChangeDetail);
let selectionChangeEvent = new this._window.CustomEvent("selectionchange",
{ cancelable: false, detail: wrappedSelectionChangeDetail });
this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
}
if (surroundDirty) {
this._fireEvent("surroundingtextchange", {
beforeString: ctx.textBeforeCursor,
afterString: ctx.textAfterCursor,
ownAction: ownAction
});
let surroundingTextChangeDetail =
new MozInputContextSurroundingTextChangeEventDetail(this, ownAction);
let wrappedSurroundingTextChangeDetail =
this._window.MozInputContextSurroundingTextChangeEventDetail
._create(this._window, surroundingTextChangeDetail);
let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange",
{ cancelable: false, detail: wrappedSurroundingTextChangeDetail });
this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
}
},
_fireEvent: function ic_fireEvent(eventName, aDetail) {
let detail = {
detail: aDetail
};
let event = new this._window.CustomEvent(eventName,
Cu.cloneInto(detail, this._window));
this.__DOM_IMPL__.dispatchEvent(event);
},
// tag name of the input field
get type() {
return this._context.type;
@ -910,15 +950,16 @@ MozInputContext.prototype = {
},
getText: function ic_getText(offset, length) {
let self = this;
return this._sendPromise(function(resolverId) {
cpmmSendAsyncMessageWithKbID(self, 'Keyboard:GetText', {
contextId: self._contextId,
requestId: resolverId,
offset: offset,
length: length
});
});
let text;
if (offset && length) {
text = this._context.text.substr(offset, length);
} else if (offset) {
text = this._context.text.substr(offset);
} else {
text = this._context.text;
}
return this._window.Promise.resolve(text);
},
get selectionStart() {
@ -929,12 +970,23 @@ MozInputContext.prototype = {
return this._context.selectionEnd;
},
get text() {
return this._context.text;
},
get textBeforeCursor() {
return this._context.textBeforeCursor;
let text = this._context.text;
let start = this._context.selectionStart;
return (start < 100) ?
text.substr(0, start) :
text.substr(start - 100, 100);
},
get textAfterCursor() {
return this._context.textAfterCursor;
let text = this._context.text;
let start = this._context.selectionStart;
let end = this._context.selectionEnd;
return text.substr(start, end - start + 100);
},
setSelectionRange: function ic_setSelectionRange(start, length) {

View File

@ -423,7 +423,6 @@ var 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);
addMessageListener("Forms:SetComposition", this);
@ -438,8 +437,8 @@ var FormAssistant = {
isHandlingFocus: false,
selectionStart: -1,
selectionEnd: -1,
textBeforeCursor: "",
textAfterCursor: "",
text: "",
scrollIntoViewTimeout: null,
_focusedElement: null,
_focusCounter: 0, // up one for every time we focus a new element
@ -524,8 +523,6 @@ var FormAssistant = {
});
if (del && element === self.focusedElement) {
self.unhandleFocus();
self.selectionStart = -1;
self.selectionEnd = -1;
}
});
@ -626,8 +623,6 @@ var FormAssistant = {
case "blur":
if (this.focusedElement) {
this.unhandleFocus();
this.selectionStart = -1;
this.selectionEnd = -1;
}
break;
@ -696,14 +691,6 @@ var FormAssistant = {
}
if (!target) {
switch (msg.name) {
case "Forms:GetText":
sendAsyncMessage("Forms:GetText:Result:Error", {
requestId: json.requestId,
error: "No focused element"
});
break;
}
return;
}
@ -935,24 +922,6 @@ var FormAssistant = {
break;
}
case "Forms:GetText": {
let value = isContentEditable(target) ? getContentEditableText(target)
: target.value;
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);
@ -997,6 +966,9 @@ var FormAssistant = {
unhandleFocus: function fa_unhandleFocus() {
this.setFocusedElement(null);
this.isHandlingFocus = false;
this.selectionStart = -1;
this.selectionEnd = -1;
this.text = "";
sendAsyncMessage("Forms:Blur", {});
},
@ -1036,23 +1008,18 @@ var FormAssistant = {
let text = isContentEditable(element) ? getContentEditableText(element)
: element.value;
let textAround = getTextAroundCursor(text, range);
let changed = this.selectionStart !== range[0] ||
this.selectionEnd !== range[1] ||
this.textBeforeCursor !== textAround.before ||
this.textAfterCursor !== textAround.after;
this.text !== text;
this.selectionStart = range[0];
this.selectionEnd = range[1];
this.textBeforeCursor = textAround.before;
this.textAfterCursor = textAround.after;
this.text = text;
return {
selectionStart: range[0],
selectionEnd: range[1],
textBeforeCursor: textAround.before,
textAfterCursor: textAround.after,
text: text,
changed: changed
};
},
@ -1157,7 +1124,6 @@ function getJSON(element, focusCounter) {
}
let range = getSelectionRange(element);
let textAround = getTextAroundCursor(value, range);
return {
"contextId": focusCounter,
@ -1172,24 +1138,7 @@ function getJSON(element, focusCounter) {
"selectionEnd": range[1],
"max": max,
"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
"lang": element.lang || ""
};
}

View File

@ -42,6 +42,7 @@ function runTest() {
is(gContext.inputType, 'text', 'The inputType should match.');
is(gContext.inputMode, 'verbatim', 'The inputMode should match.');
is(gContext.lang, 'zh', 'The language should match.');
is(gContext.text, 'Yuan', 'Should get the text.');
is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
'Should get the text around the cursor.');
@ -65,6 +66,7 @@ function test_setSelectionRange() {
test_sendKey();
}, function(e) {
ok(false, 'setSelectionRange failed:' + e.name);
console.error(e);
inputmethod_cleanup();
});
}
@ -72,6 +74,8 @@ function test_setSelectionRange() {
function test_sendKey() {
// Add '-' to current cursor posistion and move the cursor position to 3.
gContext.sendKey(0, '-'.charCodeAt(0), 0).then(function() {
is(gContext.text, 'Yu-an',
'sendKey should changed the input field correctly.');
is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yu-an',
'sendKey should changed the input field correctly.');
test_deleteSurroundingText();
@ -86,6 +90,8 @@ function test_deleteSurroundingText() {
// position back to 2.
gContext.deleteSurroundingText(-1, 1).then(function() {
ok(true, 'deleteSurroundingText finished');
is(gContext.text, 'Yuan',
'deleteSurroundingText should changed the input field correctly.');
is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
'deleteSurroundingText should changed the input field correctly.');
test_replaceSurroundingText();
@ -99,6 +105,8 @@ function test_replaceSurroundingText() {
// Replace 'Yuan' with 'Xulei'.
gContext.replaceSurroundingText('Xulei', -2, 4).then(function() {
ok(true, 'replaceSurroundingText finished');
is(gContext.text, 'Xulei',
'replaceSurroundingText changed the input field correctly.');
is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei',
'replaceSurroundingText changed the input field correctly.');
test_setComposition();
@ -120,6 +128,8 @@ function test_setComposition() {
function test_endComposition() {
gContext.endComposition('2013').then(function() {
is(gContext.text, 'Xulei2013',
'endComposition changed the input field correctly.');
is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei2013',
'endComposition changed the input field correctly.');
test_onSelectionChange();
@ -181,8 +191,9 @@ function test_onSurroundingTextChange() {
gContext.onsurroundingtextchange = function(evt) {
ok(true, 'onsurroundingtextchange fired');
is(evt.detail.beforeString, 'Xulei2013jj');
is(evt.detail.afterString, '');
is(evt.detail.text, 'Xulei2013jj');
is(evt.detail.textBeforeCursor, 'Xulei2013jj');
is(evt.detail.textAfterCursor, '');
ok(evt.detail.ownAction);
};

View File

@ -245,7 +245,7 @@ interface MozInputContextFocusEventDetail {
*/
/**
* Current value of the input/select element.
* Current value of the input.
*/
readonly attribute DOMString? value;
/**
@ -270,7 +270,7 @@ dictionary MozInputContextChoicesInfo {
};
/**
* Content the header (<optgroup>) or an option (<option>).
* Content of the option header (<optgroup>) or an option (<option>).
*/
dictionary MozInputMethodChoiceDict {
boolean group;
@ -357,8 +357,16 @@ interface MozInputContext: EventTarget {
readonly attribute long selectionStart;
readonly attribute long selectionEnd;
/**
* The text in the current input.
*/
readonly attribute DOMString? text;
/**
* The text before and after the begining of the selected text.
*
* You should use the text property instead because these properties are
* truncated at 100 characters.
*/
readonly attribute DOMString? textBeforeCursor;
readonly attribute DOMString? textAfterCursor;
@ -380,9 +388,7 @@ interface MozInputContext: EventTarget {
/* User moves the cursor, or changes the selection with other means. If the text around
* cursor has changed, but the cursor has not been moved, the IME won't get notification.
*
* A dict is provided in the detail property of the event containing the new values, and
* an "ownAction" property to denote the event is the result of our own mutation to
* the input field.
* evt.detail is defined by MozInputContextSelectionChangeEventDetail.
*/
attribute EventHandler onselectionchange;
@ -411,9 +417,7 @@ interface MozInputContext: EventTarget {
* editing or cursor movement. If the cursor has been moved, but the text around has not
* changed, the IME won't get notification.
*
* A dict is provided in the detail property of the event containing the new values, and
* an "ownAction" property to denote the event is the result of our own mutation to
* the input field.
* evt.detail is defined by MozInputContextSurroundingTextChangeEventDetail.
*/
attribute EventHandler onsurroundingtextchange;
@ -523,6 +527,62 @@ interface MozInputContext: EventTarget {
optional MozInputMethodKeyboardEventDict dict);
};
/**
* Detail of the selectionchange event.
*/
[JSImplementation="@mozilla.org/b2g-imm-selectionchange;1",
Pref="dom.mozInputMethod.enabled",
CheckAnyPermissions="input"]
interface MozInputContextSelectionChangeEventDetail {
/**
* Indicate whether or not the change is due to our own action from,
* for example, sendKey() call.
*
* Note: this property is untrustworthy because it would still be true even
* if script in the page changed the text synchronously upon responding to
* events trigger by the call.
*/
readonly attribute boolean ownAction;
/**
* The start and stop position of the current selection.
*/
readonly attribute long selectionStart;
readonly attribute long selectionEnd;
};
/**
* Detail of the surroundingtextchange event.
*/
[JSImplementation="@mozilla.org/b2g-imm-surroundingtextchange;1",
Pref="dom.mozInputMethod.enabled",
CheckAnyPermissions="input"]
interface MozInputContextSurroundingTextChangeEventDetail {
/**
* Indicate whether or not the change is due to our own action from,
* for example, sendKey() call.
*
* Note: this property is untrustworthy because it would still be true even
* if script in the page changed the text synchronously upon responding to
* events trigger by the call.
*/
readonly attribute boolean ownAction;
/**
* The text in the current input.
*/
readonly attribute DOMString? text;
/**
* The text before and after the begining of the selected text.
*
* You should use the text property instead because these properties are
* truncated at 100 characters.
*/
readonly attribute DOMString? textBeforeCursor;
readonly attribute DOMString? textAfterCursor;
};
enum CompositionClauseSelectionType {
"raw-input",
"selected-raw-text",