зеркало из 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:Input:SendKey", this);
|
||||
addMessageListener("Forms:GetContext", this);
|
||||
addMessageListener("Forms:SetComposition", this);
|
||||
addMessageListener("Forms:EndComposition", this);
|
||||
},
|
||||
|
||||
ignoredInputTypes: new Set([
|
||||
|
@ -239,6 +241,7 @@ let FormAssistant = {
|
|||
if (this.focusedElement) {
|
||||
this.focusedElement.removeEventListener('mousedown', this);
|
||||
this.focusedElement.removeEventListener('mouseup', this);
|
||||
this.focusedElement.removeEventListener('compositionend', this);
|
||||
if (this._observer) {
|
||||
this._observer.disconnect();
|
||||
this._observer = null;
|
||||
|
@ -263,6 +266,7 @@ let FormAssistant = {
|
|||
if (element) {
|
||||
element.addEventListener('mousedown', this);
|
||||
element.addEventListener('mouseup', this);
|
||||
element.addEventListener('compositionend', this);
|
||||
if (isContentEditable(element)) {
|
||||
this._documentEncoder = getDocumentEncoder(element);
|
||||
}
|
||||
|
@ -423,6 +427,8 @@ let FormAssistant = {
|
|||
break;
|
||||
}
|
||||
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
// Don't monitor the text change resulting from key event.
|
||||
this._ignoreEditActionOnce = true;
|
||||
|
||||
|
@ -438,8 +444,18 @@ let FormAssistant = {
|
|||
break;
|
||||
}
|
||||
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
this._ignoreEditActionOnce = false;
|
||||
break;
|
||||
|
||||
case "compositionend":
|
||||
if (!this.focusedElement) {
|
||||
break;
|
||||
}
|
||||
|
||||
CompositionManager.onCompositionEnd();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -475,6 +491,8 @@ let FormAssistant = {
|
|||
this._editing = true;
|
||||
switch (msg.name) {
|
||||
case "Forms:Input:Value": {
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
target.value = json.value;
|
||||
|
||||
let event = target.ownerDocument.createEvent('HTMLEvents');
|
||||
|
@ -484,6 +502,8 @@ let FormAssistant = {
|
|||
}
|
||||
|
||||
case "Forms:Input:SendKey":
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
["keydown", "keypress", "keyup"].forEach(function(type) {
|
||||
domWindowUtils.sendKeyEvent(type, json.keyCode, json.charCode,
|
||||
json.modifiers);
|
||||
|
@ -528,6 +548,8 @@ let FormAssistant = {
|
|||
}
|
||||
|
||||
case "Forms:SetSelectionRange": {
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
let start = json.selectionStart;
|
||||
let end = json.selectionEnd;
|
||||
setSelectionRange(target, start, end);
|
||||
|
@ -543,6 +565,8 @@ let FormAssistant = {
|
|||
}
|
||||
|
||||
case "Forms:ReplaceSurroundingText": {
|
||||
CompositionManager.endComposition('');
|
||||
|
||||
let text = json.text;
|
||||
let beforeLength = json.beforeLength;
|
||||
let afterLength = json.afterLength;
|
||||
|
@ -583,6 +607,23 @@ let FormAssistant = {
|
|||
sendAsyncMessage("Forms:GetContext:Result:OK", obj);
|
||||
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;
|
||||
|
||||
|
@ -1015,3 +1056,97 @@ function replaceSurroundingText(element, text, selectionStart, beforeLength,
|
|||
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',
|
||||
'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
|
||||
'SwitchToNextInputMethod', 'HideInputMethod',
|
||||
'GetText', 'SendKey', 'GetContext'
|
||||
'GetText', 'SendKey', 'GetContext',
|
||||
'SetComposition', 'EndComposition'
|
||||
],
|
||||
|
||||
get messageManager() {
|
||||
|
@ -66,6 +67,8 @@ let Keyboard = {
|
|||
mm.addMessageListener('Forms:SendKey:Result:OK', this);
|
||||
mm.addMessageListener('Forms:SequenceError', 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
|
||||
// won't happen from dom/ipc/preload.js
|
||||
|
@ -116,6 +119,8 @@ let Keyboard = {
|
|||
case 'Forms:SendKey:Result:OK':
|
||||
case 'Forms:SequenceError':
|
||||
case 'Forms:GetContext:Result:OK':
|
||||
case 'Forms:SetComposition:Result:OK':
|
||||
case 'Forms:EndComposition:Result:OK':
|
||||
let name = msg.name.replace(/^Forms/, 'Keyboard');
|
||||
this.forwardEvent(name, msg);
|
||||
break;
|
||||
|
@ -153,6 +158,12 @@ let Keyboard = {
|
|||
case 'Keyboard:GetContext':
|
||||
this.getContext(msg);
|
||||
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) {
|
||||
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:ReplaceSurroundingText:Result:OK",
|
||||
"Keyboard:SendKey:Result:OK",
|
||||
"Keyboard:SetComposition:Result:OK",
|
||||
"Keyboard:EndComposition:Result:OK",
|
||||
"Keyboard:SequenceError"]);
|
||||
},
|
||||
|
||||
|
@ -472,6 +474,10 @@ MozInputContext.prototype = {
|
|||
// 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();
|
||||
|
@ -627,12 +633,30 @@ MozInputContext.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
setComposition: function ic_setComposition(text, cursor) {
|
||||
throw new this._window.DOMError("NotSupportedError", "Not implemented");
|
||||
setComposition: function ic_setComposition(text, cursor, clauses) {
|
||||
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) {
|
||||
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);
|
||||
|
||||
/*
|
||||
* Set current composition. It will start or update composition.
|
||||
* @param cursor Position in the text of the cursor.
|
||||
* Set current composing text. This method will start composition or update
|
||||
* 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
|
||||
* 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.
|
||||
* The composing text, which is shown with underlined style to distinguish
|
||||
* from the existing text, is used to compose non-ASCII characters from
|
||||
* keystrokes, e.g. Pinyin or Hiragana. The composing text is the
|
||||
* 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)|)
|
||||
* 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.
|
||||
* End composition, clear the composing text and commit given text to
|
||||
* current input field. The text will be committed before the cursor
|
||||
* position.
|
||||
* @param text The text to commited before cursor position. If empty string
|
||||
* 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;
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче