flutter_flutter/shell/platform/common/cpp/text_input_model.h
Chris Bracken 9365230ac2
Add support for IME-based text input on Windows (#23853)
This updates the Win32 desktop embedder to support 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.

Windows IME support revolves around two main UI windows: the composition window
and the candidate window. The composition window is a system window overlaid
within the current window bounds which renders the composing string. Flutter
already renders this string itself, so we request that this window be hidden.
The candidate window is a system-rendered dropdown that displays all possible
conversions for the text in the composing region.  Since the contents of this
window are specific to the particular IME in use, and because the user may have
installed one or more third-party IMEs, Flutter does not attempt to render this
as a widget itself, but rather delegates to the system-rendered window.

The lifecycle of IME composing begins follows the following event order:
1. WM_IME_SETCONTEXT: on window creation this event is received. We strip the
   ISC_SHOWUICOMPOSITIONWINDOW bit from the event lparam before passing it to
   DefWindowProc() in order to hide the composition window, which Flutter
   already renders itself.
2. WM_IME_STARTCOMPOSITION: triggered whenever the user begins inputting new
   text. We use this event to set Flutter's TextInputModel into composing mode.
3. WM_IME_COMPOSITION: triggered on each keypress as the user adds, replaces,
   or deletes text in the composing region, navigates with their cursor within
   the composing region, or selects a new conversion candidate from the
   candidates list.
4. WM_IME_ENDCOMPOSITION: triggered when the user has finished editing the text
   in the composing region and decides to commit or abort the composition.

Additionally, the following IME-related events are emitted but not yet handled:
* WM_INPUTLANGCHANGE: triggered whenever the user selects a new language using
  the system language selection menu. Since there some language-specific
  behaviours to IMEs, we may want to make use of this in the future.
* WM_IME_NOTIFY: triggered to notify of various status events such as opening
  or closing the candidate window, setting the conversion mode, etc. None of
  these are relevant to Flutter at the moment.
* WM_IME_REQUEST: triggered to notify of various commands/requests such as
  triggering reconversion of text, which should begin composition mode, insert
  the selected text into the composing region, and allow the user to select new
  alternative candidates for the text in question before re-committing their
  new selection. This patch doesn't support this feature, but it's an important
  feature that we should support in future.
2021-01-24 12:56:08 -08:00

203 lines
7.1 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.
#ifndef FLUTTER_SHELL_PLATFORM_CPP_TEXT_INPUT_MODEL_H_
#define FLUTTER_SHELL_PLATFORM_CPP_TEXT_INPUT_MODEL_H_
#include <memory>
#include <string>
#include "flutter/shell/platform/common/cpp/text_range.h"
namespace flutter {
// Handles underlying text input state, using a simple ASCII model.
//
// Ignores special states like "insert mode" for now.
class TextInputModel {
public:
TextInputModel();
virtual ~TextInputModel();
// Sets the text.
//
// Resets the selection base and extent.
void SetText(const std::string& text);
// Attempts to set the text selection.
//
// Returns false if the selection is not within the bounds of the text.
// While in composing mode, the selection is restricted to the composing
// range; otherwise, it is restricted to the length of the text.
bool SetSelection(const TextRange& range);
// Attempts to set the composing range.
//
// Returns false if the range or offset are out of range for the text, or if
// the offset is outside the composing range.
bool SetComposingRange(const TextRange& range, size_t cursor_offset);
// Begins IME composing mode.
//
// Resets the composing base and extent to the selection start. The existing
// selection is preserved in case composing is aborted with no changes. Until
// |EndComposing| is called, any further changes to selection base and extent
// are restricted to the composing range.
void BeginComposing();
// Replaces the composing range with new UTF-16 text.
//
// If a selection of non-zero length exists, it is deleted if the composing
// text is non-empty. The composing range is adjusted to the length of
// |text| and the selection base and offset are set to the end of the
// composing range.
void UpdateComposingText(const std::u16string& text);
// Replaces the composing range with new UTF-8 text.
//
// If a selection of non-zero length exists, it is deleted if the composing
// text is non-empty. The composing range is adjusted to the length of
// |text| and the selection base and offset are set to the end of the
// composing range.
void UpdateComposingText(const std::string& text);
// Commits composing range to the string.
//
// Causes the composing base and extent to be collapsed to the end of the
// range.
void CommitComposing();
// Ends IME composing mode.
//
// Collapses the composing base and offset to 0.
void EndComposing();
// Adds a Unicode code point.
//
// Either appends after the cursor (when selection base and extent are the
// same), or deletes the selected text, replacing it with the given
// code point.
void AddCodePoint(char32_t c);
// Adds UTF-16 text.
//
// Either appends after the cursor (when selection base and extent are the
// same), or deletes the selected text, replacing it with the given text.
void AddText(const std::u16string& text);
// Adds UTF-8 text.
//
// Either appends after the cursor (when selection base and extent are the
// same), or deletes the selected text, replacing it with the given text.
void AddText(const std::string& text);
// Deletes either the selection, or one character ahead of the cursor.
//
// Deleting one character ahead of the cursor occurs when the selection base
// and extent are the same. When composing is active, deletions are
// restricted to text between the composing base and extent.
//
// Returns true if any deletion actually occurred.
bool Delete();
// Deletes text near the cursor.
//
// A section is made starting at |offset_from_cursor| code points past the
// cursor (negative values go before the cursor). |count| code points are
// removed. The selection may go outside the bounds of the available text and
// will result in only the part selection that covers the available text
// being deleted. The existing selection is ignored and removed after this
// operation. When composing is active, deletions are restricted to the
// composing range.
//
// Returns true if any deletion actually occurred.
bool DeleteSurrounding(int offset_from_cursor, int count);
// Deletes either the selection, or one character behind the cursor.
//
// Deleting one character behind the cursor occurs when the selection base
// and extent are the same. When composing is active, deletions are
// restricted to the text between the composing base and extent.
//
// Returns true if any deletion actually occurred.
bool Backspace();
// Attempts to move the cursor backward.
//
// Returns true if the cursor could be moved. If a selection is active, moves
// to the start of the selection. If composing is active, motion is
// restricted to the composing range.
bool MoveCursorBack();
// Attempts to move the cursor forward.
//
// Returns true if the cursor could be moved. If a selection is active, moves
// to the end of the selection. If composing is active, motion is restricted
// to the composing range.
bool MoveCursorForward();
// Attempts to move the cursor to the beginning.
//
// If composing is active, the cursor is moved to the beginning of the
// composing range; otherwise, it is moved to the beginning of the text. If
// composing is active, motion is restricted to the composing range.
//
// Returns true if the cursor could be moved.
bool MoveCursorToBeginning();
// Attempts to move the cursor to the end.
//
// If composing is active, the cursor is moved to the end of the composing
// range; otherwise, it is moved to the end of the text. If composing is
// active, motion is restricted to the composing range.
//
// Returns true if the cursor could be moved.
bool MoveCursorToEnd();
// Gets the current text as UTF-8.
std::string GetText() const;
// Gets the cursor position as a byte offset in UTF-8 string returned from
// GetText().
int GetCursorOffset() const;
// The current selection.
TextRange selection() const { return selection_; }
// The composing range.
//
// If not in composing mode, returns a collapsed range at position 0.
TextRange composing_range() const { return composing_range_; }
// Whether multi-step input composing mode is active.
bool composing() const { return composing_; }
private:
// Deletes the current selection, if any.
//
// Returns true if any text is deleted. The selection base and extent are
// reset to the start of the selected range.
bool DeleteSelected();
// Returns the currently editable text range.
//
// In composing mode, returns the composing range; otherwise, returns a range
// covering the entire text.
TextRange editable_range() const {
return composing_ ? composing_range_ : text_range();
}
// Returns a range covering the entire text.
TextRange text_range() const { return TextRange(0, text_.length()); }
std::u16string text_;
TextRange selection_ = TextRange(0);
TextRange composing_range_ = TextRange(0);
bool composing_ = false;
};
} // namespace flutter
#endif // FLUTTER_SHELL_PLATFORM_CPP_TEXT_INPUT_MODEL_H_