flutter_flutter/shell/platform/common/cpp/text_input_model.cc
Chris Bracken 44ea9677ad
Add multi-step IME support to TextInputModel (#21682)
* 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.
2020-10-15 17:43:16 -07:00

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