зеркало из https://github.com/mozilla/gecko-dev.git
Bug 904530 - Implement composition methods for InputContext API. r=fabrice, r=masayuki
This commit is contained in:
Родитель
c03150705d
Коммит
3776b5829d
|
@ -197,6 +197,8 @@ let FormAssistant = {
|
||||||
addMessageListener("Forms:GetText", this);
|
addMessageListener("Forms:GetText", this);
|
||||||
addMessageListener("Forms:Input:SendKey", this);
|
addMessageListener("Forms:Input:SendKey", this);
|
||||||
addMessageListener("Forms:GetContext", this);
|
addMessageListener("Forms:GetContext", this);
|
||||||
|
addMessageListener("Forms:SetComposition", this);
|
||||||
|
addMessageListener("Forms:EndComposition", this);
|
||||||
},
|
},
|
||||||
|
|
||||||
ignoredInputTypes: new Set([
|
ignoredInputTypes: new Set([
|
||||||
|
@ -239,6 +241,7 @@ let FormAssistant = {
|
||||||
if (this.focusedElement) {
|
if (this.focusedElement) {
|
||||||
this.focusedElement.removeEventListener('mousedown', this);
|
this.focusedElement.removeEventListener('mousedown', this);
|
||||||
this.focusedElement.removeEventListener('mouseup', this);
|
this.focusedElement.removeEventListener('mouseup', this);
|
||||||
|
this.focusedElement.removeEventListener('compositionend', this);
|
||||||
if (this._observer) {
|
if (this._observer) {
|
||||||
this._observer.disconnect();
|
this._observer.disconnect();
|
||||||
this._observer = null;
|
this._observer = null;
|
||||||
|
@ -263,6 +266,7 @@ let FormAssistant = {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.addEventListener('mousedown', this);
|
element.addEventListener('mousedown', this);
|
||||||
element.addEventListener('mouseup', this);
|
element.addEventListener('mouseup', this);
|
||||||
|
element.addEventListener('compositionend', this);
|
||||||
if (isContentEditable(element)) {
|
if (isContentEditable(element)) {
|
||||||
this._documentEncoder = getDocumentEncoder(element);
|
this._documentEncoder = getDocumentEncoder(element);
|
||||||
}
|
}
|
||||||
|
@ -423,6 +427,8 @@ let FormAssistant = {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
// Don't monitor the text change resulting from key event.
|
// Don't monitor the text change resulting from key event.
|
||||||
this._ignoreEditActionOnce = true;
|
this._ignoreEditActionOnce = true;
|
||||||
|
|
||||||
|
@ -438,8 +444,18 @@ let FormAssistant = {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
this._ignoreEditActionOnce = false;
|
this._ignoreEditActionOnce = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "compositionend":
|
||||||
|
if (!this.focusedElement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionManager.onCompositionEnd();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -475,6 +491,8 @@ let FormAssistant = {
|
||||||
this._editing = true;
|
this._editing = true;
|
||||||
switch (msg.name) {
|
switch (msg.name) {
|
||||||
case "Forms:Input:Value": {
|
case "Forms:Input:Value": {
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
target.value = json.value;
|
target.value = json.value;
|
||||||
|
|
||||||
let event = target.ownerDocument.createEvent('HTMLEvents');
|
let event = target.ownerDocument.createEvent('HTMLEvents');
|
||||||
|
@ -484,6 +502,8 @@ let FormAssistant = {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "Forms:Input:SendKey":
|
case "Forms:Input:SendKey":
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
["keydown", "keypress", "keyup"].forEach(function(type) {
|
["keydown", "keypress", "keyup"].forEach(function(type) {
|
||||||
domWindowUtils.sendKeyEvent(type, json.keyCode, json.charCode,
|
domWindowUtils.sendKeyEvent(type, json.keyCode, json.charCode,
|
||||||
json.modifiers);
|
json.modifiers);
|
||||||
|
@ -528,6 +548,8 @@ let FormAssistant = {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "Forms:SetSelectionRange": {
|
case "Forms:SetSelectionRange": {
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
let start = json.selectionStart;
|
let start = json.selectionStart;
|
||||||
let end = json.selectionEnd;
|
let end = json.selectionEnd;
|
||||||
setSelectionRange(target, start, end);
|
setSelectionRange(target, start, end);
|
||||||
|
@ -543,6 +565,8 @@ let FormAssistant = {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "Forms:ReplaceSurroundingText": {
|
case "Forms:ReplaceSurroundingText": {
|
||||||
|
CompositionManager.endComposition('');
|
||||||
|
|
||||||
let text = json.text;
|
let text = json.text;
|
||||||
let beforeLength = json.beforeLength;
|
let beforeLength = json.beforeLength;
|
||||||
let afterLength = json.afterLength;
|
let afterLength = json.afterLength;
|
||||||
|
@ -583,6 +607,23 @@ let FormAssistant = {
|
||||||
sendAsyncMessage("Forms:GetContext:Result:OK", obj);
|
sendAsyncMessage("Forms:GetContext:Result:OK", obj);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "Forms:SetComposition": {
|
||||||
|
CompositionManager.setComposition(target, json.text, json.cursor,
|
||||||
|
json.clauses);
|
||||||
|
sendAsyncMessage("Forms:SetComposition:Result:OK", {
|
||||||
|
requestId: json.requestId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Forms:EndComposition": {
|
||||||
|
CompositionManager.endComposition(json.text);
|
||||||
|
sendAsyncMessage("Forms:EndComposition:Result:OK", {
|
||||||
|
requestId: json.requestId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._editing = false;
|
this._editing = false;
|
||||||
|
|
||||||
|
@ -1015,3 +1056,97 @@ function replaceSurroundingText(element, text, selectionStart, beforeLength,
|
||||||
editor.insertText(text);
|
editor.insertText(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let CompositionManager = {
|
||||||
|
_isStarted: false,
|
||||||
|
_text: '',
|
||||||
|
_clauseAttrMap: {
|
||||||
|
'raw-input': domWindowUtils.COMPOSITION_ATTR_RAWINPUT,
|
||||||
|
'selected-raw-text': domWindowUtils.COMPOSITION_ATTR_SELECTEDRAWTEXT,
|
||||||
|
'converted-text': domWindowUtils.COMPOSITION_ATTR_CONVERTEDTEXT,
|
||||||
|
'selected-converted-text': domWindowUtils.COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT
|
||||||
|
},
|
||||||
|
|
||||||
|
setComposition: function cm_setComposition(element, text, cursor, clauses) {
|
||||||
|
// Check parameters.
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let len = text.length;
|
||||||
|
if (cursor < 0) {
|
||||||
|
cursor = 0;
|
||||||
|
} else if (cursor > len) {
|
||||||
|
cursor = len;
|
||||||
|
}
|
||||||
|
let clauseLens = [len, 0, 0];
|
||||||
|
let clauseAttrs = [domWindowUtils.COMPOSITION_ATTR_RAWINPUT,
|
||||||
|
domWindowUtils.COMPOSITION_ATTR_RAWINPUT,
|
||||||
|
domWindowUtils.COMPOSITION_ATTR_RAWINPUT];
|
||||||
|
if (clauses) {
|
||||||
|
let remainingLength = len;
|
||||||
|
// Currently we don't support 4 or more clauses composition string.
|
||||||
|
let clauseNum = Math.min(3, clauses.length);
|
||||||
|
for (let i = 0; i < clauseNum; i++) {
|
||||||
|
if (clauses[i]) {
|
||||||
|
let clauseLength = clauses[i].length || 0;
|
||||||
|
// Make sure the total clauses length is not bigger than that of the
|
||||||
|
// composition string.
|
||||||
|
if (clauseLength > remainingLength) {
|
||||||
|
clauseLength = remainingLength;
|
||||||
|
}
|
||||||
|
remainingLength -= clauseLength;
|
||||||
|
clauseLens[i] = clauseLength;
|
||||||
|
clauseAttrs[i] = this._clauseAttrMap[clauses[i].selectionType] ||
|
||||||
|
domWindowUtils.COMPOSITION_ATTR_RAWINPUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the total clauses length is less than that of the composition
|
||||||
|
// string, extend the last clause to the end of the composition string.
|
||||||
|
if (remainingLength > 0) {
|
||||||
|
clauseLens[2] += remainingLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start composition if need to.
|
||||||
|
if (!this._isStarted) {
|
||||||
|
this._isStarted = true;
|
||||||
|
domWindowUtils.sendCompositionEvent('compositionstart', '', '');
|
||||||
|
this._text = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the composing text.
|
||||||
|
if (this._text !== text) {
|
||||||
|
this._text = text;
|
||||||
|
domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
|
||||||
|
}
|
||||||
|
domWindowUtils.sendTextEvent(text,
|
||||||
|
clauseLens[0], clauseAttrs[0],
|
||||||
|
clauseLens[1], clauseAttrs[1],
|
||||||
|
clauseLens[2], clauseAttrs[2],
|
||||||
|
cursor, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
endComposition: function cm_endComposition(text) {
|
||||||
|
if (!this._isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update the composing text.
|
||||||
|
if (this._text !== text) {
|
||||||
|
domWindowUtils.sendCompositionEvent('compositionupdate', text, '');
|
||||||
|
}
|
||||||
|
domWindowUtils.sendTextEvent(text, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
domWindowUtils.sendCompositionEvent('compositionend', text, '');
|
||||||
|
this._text = '';
|
||||||
|
this._isStarted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Composition ends due to external actions.
|
||||||
|
onCompositionEnd: function cm_onCompositionEnd() {
|
||||||
|
if (!this._isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._text = '';
|
||||||
|
this._isStarted = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -23,7 +23,8 @@ let Keyboard = {
|
||||||
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
|
'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
|
||||||
'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
|
'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
|
||||||
'SwitchToNextInputMethod', 'HideInputMethod',
|
'SwitchToNextInputMethod', 'HideInputMethod',
|
||||||
'GetText', 'SendKey', 'GetContext'
|
'GetText', 'SendKey', 'GetContext',
|
||||||
|
'SetComposition', 'EndComposition'
|
||||||
],
|
],
|
||||||
|
|
||||||
get messageManager() {
|
get messageManager() {
|
||||||
|
@ -66,6 +67,8 @@ let Keyboard = {
|
||||||
mm.addMessageListener('Forms:SendKey:Result:OK', this);
|
mm.addMessageListener('Forms:SendKey:Result:OK', this);
|
||||||
mm.addMessageListener('Forms:SequenceError', this);
|
mm.addMessageListener('Forms:SequenceError', this);
|
||||||
mm.addMessageListener('Forms:GetContext:Result:OK', this);
|
mm.addMessageListener('Forms:GetContext:Result:OK', this);
|
||||||
|
mm.addMessageListener('Forms:SetComposition:Result:OK', this);
|
||||||
|
mm.addMessageListener('Forms:EndComposition:Result:OK', this);
|
||||||
|
|
||||||
// When not running apps OOP, we need to load forms.js here since this
|
// When not running apps OOP, we need to load forms.js here since this
|
||||||
// won't happen from dom/ipc/preload.js
|
// won't happen from dom/ipc/preload.js
|
||||||
|
@ -116,6 +119,8 @@ let Keyboard = {
|
||||||
case 'Forms:SendKey:Result:OK':
|
case 'Forms:SendKey:Result:OK':
|
||||||
case 'Forms:SequenceError':
|
case 'Forms:SequenceError':
|
||||||
case 'Forms:GetContext:Result:OK':
|
case 'Forms:GetContext:Result:OK':
|
||||||
|
case 'Forms:SetComposition:Result:OK':
|
||||||
|
case 'Forms:EndComposition:Result:OK':
|
||||||
let name = msg.name.replace(/^Forms/, 'Keyboard');
|
let name = msg.name.replace(/^Forms/, 'Keyboard');
|
||||||
this.forwardEvent(name, msg);
|
this.forwardEvent(name, msg);
|
||||||
break;
|
break;
|
||||||
|
@ -153,6 +158,12 @@ let Keyboard = {
|
||||||
case 'Keyboard:GetContext':
|
case 'Keyboard:GetContext':
|
||||||
this.getContext(msg);
|
this.getContext(msg);
|
||||||
break;
|
break;
|
||||||
|
case 'Keyboard:SetComposition':
|
||||||
|
this.setComposition(msg);
|
||||||
|
break;
|
||||||
|
case 'Keyboard:EndComposition':
|
||||||
|
this.endComposition(msg);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -223,6 +234,14 @@ let Keyboard = {
|
||||||
|
|
||||||
getContext: function keyboardGetContext(msg) {
|
getContext: function keyboardGetContext(msg) {
|
||||||
this.messageManager.sendAsyncMessage('Forms:GetContext', msg.data);
|
this.messageManager.sendAsyncMessage('Forms:GetContext', msg.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
setComposition: function keyboardSetComposition(msg) {
|
||||||
|
this.messageManager.sendAsyncMessage('Forms:SetComposition', msg.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
endComposition: function keyboardEndComposition(msg) {
|
||||||
|
this.messageManager.sendAsyncMessage('Forms:EndComposition', msg.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -415,6 +415,8 @@ MozInputContext.prototype = {
|
||||||
"Keyboard:SetSelectionRange:Result:OK",
|
"Keyboard:SetSelectionRange:Result:OK",
|
||||||
"Keyboard:ReplaceSurroundingText:Result:OK",
|
"Keyboard:ReplaceSurroundingText:Result:OK",
|
||||||
"Keyboard:SendKey:Result:OK",
|
"Keyboard:SendKey:Result:OK",
|
||||||
|
"Keyboard:SetComposition:Result:OK",
|
||||||
|
"Keyboard:EndComposition:Result:OK",
|
||||||
"Keyboard:SequenceError"]);
|
"Keyboard:SequenceError"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -472,6 +474,10 @@ MozInputContext.prototype = {
|
||||||
// not invalidated yet...
|
// not invalidated yet...
|
||||||
resolver.reject("InputContext has expired");
|
resolver.reject("InputContext has expired");
|
||||||
break;
|
break;
|
||||||
|
case "Keyboard:SetComposition:Result:OK": // Fall through.
|
||||||
|
case "Keyboard:EndComposition:Result:OK":
|
||||||
|
resolver.resolve();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
dump("Could not find a handler for " + msg.name);
|
dump("Could not find a handler for " + msg.name);
|
||||||
resolver.reject();
|
resolver.reject();
|
||||||
|
@ -627,12 +633,30 @@ MozInputContext.prototype = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setComposition: function ic_setComposition(text, cursor) {
|
setComposition: function ic_setComposition(text, cursor, clauses) {
|
||||||
throw new this._window.DOMError("NotSupportedError", "Not implemented");
|
let self = this;
|
||||||
|
return this.createPromise(function(resolver) {
|
||||||
|
let resolverId = self.getPromiseResolverId(resolver);
|
||||||
|
cpmm.sendAsyncMessage('Keyboard:SetComposition', {
|
||||||
|
contextId: self._contextId,
|
||||||
|
requestId: resolverId,
|
||||||
|
text: text,
|
||||||
|
cursor: cursor || text.length,
|
||||||
|
clauses: clauses || null
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
endComposition: function ic_endComposition(text) {
|
endComposition: function ic_endComposition(text) {
|
||||||
throw new this._window.DOMError("NotSupportedError", "Not implemented");
|
let self = this;
|
||||||
|
return this.createPromise(function(resolver) {
|
||||||
|
let resolverId = self.getPromiseResolverId(resolver);
|
||||||
|
cpmm.sendAsyncMessage('Keyboard:EndComposition', {
|
||||||
|
contextId: self._contextId,
|
||||||
|
requestId: resolverId,
|
||||||
|
text: text || ''
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -146,24 +146,59 @@ interface MozInputContext: EventTarget {
|
||||||
Promise sendKey(long keyCode, long charCode, long modifiers);
|
Promise sendKey(long keyCode, long charCode, long modifiers);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set current composition. It will start or update composition.
|
* Set current composing text. This method will start composition or update
|
||||||
* @param cursor Position in the text of the cursor.
|
* composition if it has started. The composition will be started right
|
||||||
|
* before the cursor position and any selected text will be replaced by the
|
||||||
|
* composing text. When the composition is started, calling this method can
|
||||||
|
* update the text and move cursor winthin the range of the composing text.
|
||||||
|
* @param text The new composition text to show.
|
||||||
|
* @param cursor The new cursor position relative to the start of the
|
||||||
|
* composition text. The cursor should be positioned within the composition
|
||||||
|
* text. This means the value should be >= 0 and <= the length of
|
||||||
|
* composition text. Defaults to the lenght of composition text, i.e., the
|
||||||
|
* cursor will be positioned after the composition text.
|
||||||
|
* @param clauses The array of composition clause information. If not set,
|
||||||
|
* only one clause is supported.
|
||||||
*
|
*
|
||||||
* The API implementation should automatically ends the composition
|
* The composing text, which is shown with underlined style to distinguish
|
||||||
* session (with event and confirm the current composition) if
|
* from the existing text, is used to compose non-ASCII characters from
|
||||||
* endComposition is never called. Same apply when the inputContext is lost
|
* keystrokes, e.g. Pinyin or Hiragana. The composing text is the
|
||||||
* during a unfinished composition session.
|
* intermediate text to help input complex character and is not committed to
|
||||||
|
* current input field. Therefore if any text operation other than
|
||||||
|
* composition is performed, the composition will automatically end. Same
|
||||||
|
* apply when the inputContext is lost during an unfinished composition
|
||||||
|
* session.
|
||||||
|
*
|
||||||
|
* To finish composition and commit text to current input field, an IME
|
||||||
|
* should call |endComposition|.
|
||||||
*/
|
*/
|
||||||
Promise setComposition(DOMString text, long cursor);
|
Promise setComposition(DOMString text, optional long cursor,
|
||||||
|
optional sequence<CompositionClauseParameters> clauses);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* End composition and actually commit the text. (was |commitText(text, offset, length)|)
|
* End composition, clear the composing text and commit given text to
|
||||||
* Ending the composition with an empty string will not send any text.
|
* current input field. The text will be committed before the cursor
|
||||||
* Note that if composition always ends automatically (with the current composition committed)
|
* position.
|
||||||
* if the composition did not explicitly with |endComposition()| but was interrupted with
|
* @param text The text to commited before cursor position. If empty string
|
||||||
* |sendKey()|, |setSelectionRange()|, user moving the cursor, or remove the focus, etc.
|
* is given, no text will be committed.
|
||||||
*
|
*
|
||||||
* @param text The text
|
* Note that composition always ends automatically with nothing to commit if
|
||||||
|
* the composition does not explicitly end by calling |endComposition|, but
|
||||||
|
* is interrupted by |sendKey|, |setSelectionRange|,
|
||||||
|
* |replaceSurroundingText|, |deleteSurroundingText|, user moving the
|
||||||
|
* cursor, changing the focus, etc.
|
||||||
*/
|
*/
|
||||||
Promise endComposition(DOMString text);
|
Promise endComposition(optional DOMString text);
|
||||||
|
};
|
||||||
|
|
||||||
|
enum CompositionClauseSelectionType {
|
||||||
|
"raw-input",
|
||||||
|
"selected-raw-text",
|
||||||
|
"converted-text",
|
||||||
|
"selected-converted-text"
|
||||||
|
};
|
||||||
|
|
||||||
|
dictionary CompositionClauseParameters {
|
||||||
|
DOMString selectionType = "raw-input";
|
||||||
|
long length;
|
||||||
};
|
};
|
||||||
|
|
Загрузка…
Ссылка в новой задаче