mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
* Add multi-step IME support to TextInputModel This updates the platform-independent TextInputModel to add support for input method (abbreviated IM or IME) composing regions. In contrast to languages such as English, where keyboard input is managed keystroke-by-keystroke, languages such as Japanese require a multi-step input process wherein the user begins a composing sequence, during which point their keystrokes are captured by a system input method and converted into a text sequence. During composing, the user is able to edit the composing range and manage the conversion from keyboard input to text before eventually committing the text to the underlying text input field. To illustrate this, in Japanese, this sequence might look something like the following: 1. User types 'k'. The character 'k' is added to the composing region. Typically, the text 'k' will be inserted inline into the underlying text field but the composing range will be highlighted in some manner, frequently with a highlight or underline. 2. User types 'a'. The composing range is replaced with the phonetic kana character 'か' (ka). The composing range continues to be highlighted. 3. User types 'k'. The character 'k' is appended to the composing range such that the highlighted text is now 'かk' 4. User types 'u'. The trailing 'k' is replaced with the phonetic kana character 'く' (ku) such that the composing range now reads 'かく' The composing range continues to be highlighted. 5. The user presses the space bar to convert the kana characters to kanji. The composing range is replaced with '書く' (kaku: to write). 6. The user presses the space bar again to show other conversions. The user's configured input method (for example, ibus) pops up a completions menu populated with alternatives such as 各 (kaku: every), 描く (kaku: to draw), 核 (kaku: pit of a fruit, nucleus), 角 (kaku: angle), etc. 7. The user uses the arrow keys to navigate the completions menu and select the alternative to input. As they do, the inline composing region in the text field is updated. It continues to be highlighted or underlined. 8. The user hits enter to commit the composing region. The text is committed to the underlying text field and the visual highlighting is removed. 9. If the user presses another key, a new composing sequence begins. If a selection is present when composing begins, it is preserved until the first keypress of input is received, at which point the selection is deleted. If a composing sequence is aborted before the first keypress, the selection is preserved. Creating a new selection (with the mouse, for example) aborts composing and the composing region is automatically committed. A composing range and selection, both with an extent, are not permitted to co-exist. During composing, keyboard navigation via the arrow keys, or home and end (or equivalent shortcuts) is restricted to the composing range, as are deletions via backspace and the delete key. This patch adds two new private convenience methods, `editing_range` and `text_range`. The former returns the range for which editing is currently active -- the composing range, if composing, otherwise the full range of the text. The latter, returns a range from position 0 (inclusive) to `text_.length()` exclusive. * Move SetComposingLength to TextRange::set_* Adds set_base, set_extent, set_start, set_end methods to TextRange.
288 lines
8.4 KiB
C++
288 lines
8.4 KiB
C++
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "flutter/shell/platform/common/cpp/text_input_model.h"
|
|
|
|
#include <algorithm>
|
|
#include <codecvt>
|
|
#include <locale>
|
|
|
|
#if defined(_MSC_VER)
|
|
// TODO(naifu): This temporary code is to solve link error.(VS2015/2017)
|
|
// https://social.msdn.microsoft.com/Forums/vstudio/en-US/8f40dcd8-c67f-4eba-9134-a19b9178e481/vs-2015-rc-linker-stdcodecvt-error
|
|
std::locale::id std::codecvt<char16_t, char, _Mbstatet>::id;
|
|
#endif // defined(_MSC_VER)
|
|
|
|
namespace flutter {
|
|
|
|
namespace {
|
|
|
|
// Returns true if |code_point| is a leading surrogate of a surrogate pair.
|
|
bool IsLeadingSurrogate(char32_t code_point) {
|
|
return (code_point & 0xFFFFFC00) == 0xD800;
|
|
}
|
|
// Returns true if |code_point| is a trailing surrogate of a surrogate pair.
|
|
bool IsTrailingSurrogate(char32_t code_point) {
|
|
return (code_point & 0xFFFFFC00) == 0xDC00;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TextInputModel::TextInputModel() = default;
|
|
|
|
TextInputModel::~TextInputModel() = default;
|
|
|
|
void TextInputModel::SetText(const std::string& text) {
|
|
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
|
|
utf16_converter;
|
|
text_ = utf16_converter.from_bytes(text);
|
|
selection_ = TextRange(0);
|
|
composing_range_ = TextRange(0);
|
|
}
|
|
|
|
bool TextInputModel::SetSelection(const TextRange& range) {
|
|
if (composing_ && !range.collapsed()) {
|
|
return false;
|
|
}
|
|
if (!editable_range().Contains(range)) {
|
|
return false;
|
|
}
|
|
selection_ = range;
|
|
return true;
|
|
}
|
|
|
|
bool TextInputModel::SetComposingRange(const TextRange& range,
|
|
size_t cursor_offset) {
|
|
if (!composing_ || !text_range().Contains(range)) {
|
|
return false;
|
|
}
|
|
composing_range_ = range;
|
|
selection_ = TextRange(range.start() + cursor_offset);
|
|
return true;
|
|
}
|
|
|
|
void TextInputModel::BeginComposing() {
|
|
composing_ = true;
|
|
composing_range_ = TextRange(selection_.start());
|
|
}
|
|
|
|
void TextInputModel::UpdateComposingText(const std::string& composing_text) {
|
|
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
|
|
utf16_converter;
|
|
std::u16string text = utf16_converter.from_bytes(composing_text);
|
|
|
|
// Preserve selection if we get a no-op update to the composing region.
|
|
if (text.length() == 0 && composing_range_.collapsed()) {
|
|
return;
|
|
}
|
|
DeleteSelected();
|
|
text_.replace(composing_range_.start(), composing_range_.length(), text);
|
|
composing_range_.set_end(composing_range_.start() + text.length());
|
|
selection_ = TextRange(composing_range_.end());
|
|
}
|
|
|
|
void TextInputModel::CommitComposing() {
|
|
// Preserve selection if no composing text was entered.
|
|
if (composing_range_.collapsed()) {
|
|
return;
|
|
}
|
|
composing_range_ = TextRange(composing_range_.end());
|
|
selection_ = composing_range_;
|
|
}
|
|
|
|
void TextInputModel::EndComposing() {
|
|
composing_ = false;
|
|
composing_range_ = TextRange(0);
|
|
}
|
|
|
|
bool TextInputModel::DeleteSelected() {
|
|
if (selection_.collapsed()) {
|
|
return false;
|
|
}
|
|
size_t start = selection_.start();
|
|
text_.erase(start, selection_.length());
|
|
selection_ = TextRange(start);
|
|
if (composing_) {
|
|
// This occurs only immediately after composing has begun with a selection.
|
|
composing_range_ = selection_;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TextInputModel::AddCodePoint(char32_t c) {
|
|
if (c <= 0xFFFF) {
|
|
AddText(std::u16string({static_cast<char16_t>(c)}));
|
|
} else {
|
|
char32_t to_decompose = c - 0x10000;
|
|
AddText(std::u16string({
|
|
// High surrogate.
|
|
static_cast<char16_t>((to_decompose >> 10) + 0xd800),
|
|
// Low surrogate.
|
|
static_cast<char16_t>((to_decompose % 0x400) + 0xdc00),
|
|
}));
|
|
}
|
|
}
|
|
|
|
void TextInputModel::AddText(const std::u16string& text) {
|
|
DeleteSelected();
|
|
if (composing_) {
|
|
// Delete the current composing text, set the cursor to composing start.
|
|
text_.erase(composing_range_.start(), composing_range_.length());
|
|
selection_ = TextRange(composing_range_.start());
|
|
composing_range_.set_end(composing_range_.start() + text.length());
|
|
}
|
|
size_t position = selection_.position();
|
|
text_.insert(position, text);
|
|
selection_ = TextRange(position + text.length());
|
|
}
|
|
|
|
void TextInputModel::AddText(const std::string& text) {
|
|
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
|
|
utf16_converter;
|
|
AddText(utf16_converter.from_bytes(text));
|
|
}
|
|
|
|
bool TextInputModel::Backspace() {
|
|
if (DeleteSelected()) {
|
|
return true;
|
|
}
|
|
// There is no selection. Delete the preceding codepoint.
|
|
size_t position = selection_.position();
|
|
if (position != editable_range().start()) {
|
|
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
|
|
text_.erase(position - count, count);
|
|
selection_ = TextRange(position - count);
|
|
if (composing_) {
|
|
composing_range_.set_end(composing_range_.end() - count);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TextInputModel::Delete() {
|
|
if (DeleteSelected()) {
|
|
return true;
|
|
}
|
|
// There is no selection. Delete the preceding codepoint.
|
|
size_t position = selection_.position();
|
|
if (position < editable_range().end()) {
|
|
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
|
|
text_.erase(position, count);
|
|
if (composing_) {
|
|
composing_range_.set_end(composing_range_.end() - count);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
|
|
size_t max_pos = editable_range().end();
|
|
size_t start = selection_.extent();
|
|
if (offset_from_cursor < 0) {
|
|
for (int i = 0; i < -offset_from_cursor; i++) {
|
|
// If requested start is before the available text then reduce the
|
|
// number of characters to delete.
|
|
if (start == editable_range().start()) {
|
|
count = i;
|
|
break;
|
|
}
|
|
start -= IsTrailingSurrogate(text_.at(start - 1)) ? 2 : 1;
|
|
}
|
|
} else {
|
|
for (int i = 0; i < offset_from_cursor && start != max_pos; i++) {
|
|
start += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
|
|
}
|
|
}
|
|
|
|
auto end = start;
|
|
for (int i = 0; i < count && end != max_pos; i++) {
|
|
end += IsLeadingSurrogate(text_.at(start)) ? 2 : 1;
|
|
}
|
|
|
|
if (start == end) {
|
|
return false;
|
|
}
|
|
|
|
auto deleted_length = end - start;
|
|
text_.erase(start, deleted_length);
|
|
|
|
// Cursor moves only if deleted area is before it.
|
|
selection_ = TextRange(offset_from_cursor <= 0 ? start : selection_.start());
|
|
|
|
// Adjust composing range.
|
|
if (composing_) {
|
|
composing_range_.set_end(composing_range_.end() - deleted_length);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TextInputModel::MoveCursorToBeginning() {
|
|
size_t min_pos = editable_range().start();
|
|
if (selection_.collapsed() && selection_.position() == min_pos) {
|
|
return false;
|
|
}
|
|
selection_ = TextRange(min_pos);
|
|
return true;
|
|
}
|
|
|
|
bool TextInputModel::MoveCursorToEnd() {
|
|
size_t max_pos = editable_range().end();
|
|
if (selection_.collapsed() && selection_.position() == max_pos) {
|
|
return false;
|
|
}
|
|
selection_ = TextRange(max_pos);
|
|
return true;
|
|
}
|
|
|
|
bool TextInputModel::MoveCursorForward() {
|
|
// If there's a selection, move to the end of the selection.
|
|
if (!selection_.collapsed()) {
|
|
selection_ = TextRange(selection_.end());
|
|
return true;
|
|
}
|
|
// Otherwise, move the cursor forward.
|
|
size_t position = selection_.position();
|
|
if (position != editable_range().end()) {
|
|
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
|
|
selection_ = TextRange(position + count);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TextInputModel::MoveCursorBack() {
|
|
// If there's a selection, move to the beginning of the selection.
|
|
if (!selection_.collapsed()) {
|
|
selection_ = TextRange(selection_.start());
|
|
return true;
|
|
}
|
|
// Otherwise, move the cursor backward.
|
|
size_t position = selection_.position();
|
|
if (position != editable_range().start()) {
|
|
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
|
|
selection_ = TextRange(position - count);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string TextInputModel::GetText() const {
|
|
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
|
|
utf8_converter;
|
|
return utf8_converter.to_bytes(text_);
|
|
}
|
|
|
|
int TextInputModel::GetCursorOffset() const {
|
|
// Measure the length of the current text up to the selection extent.
|
|
// There is probably a much more efficient way of doing this.
|
|
auto leading_text = text_.substr(0, selection_.extent());
|
|
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
|
|
utf8_converter;
|
|
return utf8_converter.to_bytes(leading_text).size();
|
|
}
|
|
|
|
} // namespace flutter
|