Bug 904530 - Implement composition methods for InputContext API. r=fabrice, r=masayuki

This commit is contained in:
Yuan Xulei 2013-08-30 08:07:16 -04:00
Родитель c03150705d
Коммит 3776b5829d
4 изменённых файлов: 231 добавлений и 18 удалений

Просмотреть файл

@ -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;
}; };