未验证 提交 9365230a 编写于 作者: C Chris Bracken 提交者: GitHub

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.
上级 f3c5687c
......@@ -1497,6 +1497,8 @@ FILE: ../../../flutter/shell/platform/windows/task_runner_win32.cc
FILE: ../../../flutter/shell/platform/windows/task_runner_win32.h
FILE: ../../../flutter/shell/platform/windows/task_runner_winuwp.cc
FILE: ../../../flutter/shell/platform/windows/task_runner_winuwp.h
FILE: ../../../flutter/shell/platform/windows/text_input_manager.cc
FILE: ../../../flutter/shell/platform/windows/text_input_manager.h
FILE: ../../../flutter/shell/platform/windows/text_input_plugin.cc
FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h
FILE: ../../../flutter/shell/platform/windows/text_input_plugin_delegate.h
......
......@@ -67,11 +67,7 @@ void TextInputModel::BeginComposing() {
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);
void TextInputModel::UpdateComposingText(const std::u16string& text) {
// Preserve selection if we get a no-op update to the composing region.
if (text.length() == 0 && composing_range_.collapsed()) {
return;
......@@ -82,6 +78,12 @@ void TextInputModel::UpdateComposingText(const std::string& composing_text) {
selection_ = TextRange(composing_range_.end());
}
void TextInputModel::UpdateComposingText(const std::string& text) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
utf16_converter;
UpdateComposingText(utf16_converter.from_bytes(text));
}
void TextInputModel::CommitComposing() {
// Preserve selection if no composing text was entered.
if (composing_range_.collapsed()) {
......
......@@ -46,13 +46,21 @@ class TextInputModel {
// are restricted to the composing range.
void BeginComposing();
// Replaces the composing range with new text.
// 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
// |composing_text| and the selection base and offset are set to the end of
// the composing range.
void UpdateComposingText(const std::string& composing_text);
// |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.
//
......
......@@ -68,6 +68,8 @@ source_set("flutter_windows_source") {
"string_conversion.h",
"system_utils.h",
"task_runner.h",
"text_input_manager.cc",
"text_input_manager.h",
"text_input_plugin.cc",
"text_input_plugin.h",
"window_binding_handler.h",
......@@ -103,7 +105,10 @@ source_set("flutter_windows_source") {
"win32_window_proc_delegate_manager.h",
]
libs = [ "dwmapi.lib" ]
libs = [
"dwmapi.lib",
"imm32.lib",
]
}
configs += [
......
......@@ -137,6 +137,19 @@ bool FlutterWindowsView::OnKey(int key,
return SendKey(key, scancode, action, character, extended);
}
void FlutterWindowsView::OnComposeBegin() {
SendComposeBegin();
}
void FlutterWindowsView::OnComposeEnd() {
SendComposeEnd();
}
void FlutterWindowsView::OnComposeChange(const std::u16string& text,
int cursor_pos) {
SendComposeChange(text, cursor_pos);
}
void FlutterWindowsView::OnScroll(double x,
double y,
double delta_x,
......@@ -240,6 +253,25 @@ bool FlutterWindowsView::SendKey(int key,
return false;
}
void FlutterWindowsView::SendComposeBegin() {
for (const auto& handler : keyboard_hook_handlers_) {
handler->ComposeBeginHook();
}
}
void FlutterWindowsView::SendComposeEnd() {
for (const auto& handler : keyboard_hook_handlers_) {
handler->ComposeEndHook();
}
}
void FlutterWindowsView::SendComposeChange(const std::u16string& text,
int cursor_pos) {
for (const auto& handler : keyboard_hook_handlers_) {
handler->ComposeChangeHook(text, cursor_pos);
}
}
void FlutterWindowsView::SendScroll(double x,
double y,
double delta_x,
......
......@@ -106,6 +106,15 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
char32_t character,
bool extended) override;
// |WindowBindingHandlerDelegate|
void OnComposeBegin() override;
// |WindowBindingHandlerDelegate|
void OnComposeEnd() override;
// |WindowBindingHandlerDelegate|
void OnComposeChange(const std::u16string& text, int cursor_pos) override;
// |WindowBindingHandlerDelegate|
void OnScroll(double x,
double y,
......@@ -185,6 +194,24 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
char32_t character,
bool extended);
// Reports an IME compose begin event.
//
// Triggered when the user begins editing composing text using a multi-step
// input method such as in CJK text input.
void SendComposeBegin();
// Reports an IME compose end event.
//
// Triggered when the user commits the composing text while using a multi-step
// input method such as in CJK text input.
void SendComposeEnd();
// Reports an IME composing region change event.
//
// Triggered when the user edits the composing text while using a multi-step
// input method such as in CJK text input.
void SendComposeChange(const std::u16string& text, int cursor_pos);
// Reports scroll wheel events to Flutter engine.
void SendScroll(double x,
double y,
......
......@@ -243,4 +243,17 @@ bool KeyEventHandler::KeyboardHook(FlutterWindowsView* view,
return true;
}
void KeyEventHandler::ComposeBeginHook() {
// Ignore.
}
void KeyEventHandler::ComposeEndHook() {
// Ignore.
}
void KeyEventHandler::ComposeChangeHook(const std::u16string& text,
int cursor_pos) {
// Ignore.
}
} // namespace flutter
......@@ -44,6 +44,15 @@ class KeyEventHandler : public KeyboardHookHandler {
void TextHook(FlutterWindowsView* window,
const std::u16string& text) override;
// |KeyboardHookHandler|
void ComposeBeginHook() override;
// |KeyboardHookHandler|
void ComposeEndHook() override;
// |KeyboardHookHandler|
void ComposeChangeHook(const std::u16string& text, int cursor_pos) override;
private:
KEYBDINPUT* FindPendingEvent(uint64_t id);
void RemovePendingEvent(uint64_t id);
......
......@@ -32,6 +32,25 @@ class KeyboardHookHandler {
// A function for hooking into Unicode text input.
virtual void TextHook(FlutterWindowsView* view,
const std::u16string& text) = 0;
// Handler for IME compose begin events.
//
// Triggered when the user begins editing composing text using a multi-step
// input method such as in CJK text input.
virtual void ComposeBeginHook() = 0;
// Handler for IME compose end events.
//
// Triggered when the user commits the composing text while using a multi-step
// input method such as in CJK text input.
virtual void ComposeEndHook() = 0;
// Handler for IME compose change events.
//
// Triggered when the user edits the composing text while using a multi-step
// input method such as in CJK text input.
virtual void ComposeChangeHook(const std::u16string& text,
int cursor_pos) = 0;
};
} // namespace flutter
......
......@@ -41,6 +41,9 @@ class MockWin32Window : public Win32Window {
MOCK_METHOD1(OnText, void(const std::u16string&));
MOCK_METHOD5(OnKey, bool(int, int, int, char32_t, bool));
MOCK_METHOD2(OnScroll, void(double, double));
MOCK_METHOD0(OnComposeBegin, void());
MOCK_METHOD0(OnComposeEnd, void());
MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int));
};
} // namespace testing
......
// 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.
// 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/windows/text_input_manager.h"
#include <imm.h>
#include <memory>
namespace flutter {
void TextInputManager::SetWindowHandle(HWND window_handle) {
window_handle_ = window_handle;
}
void TextInputManager::CreateImeWindow() {
if (window_handle_ == nullptr) {
return;
}
// Some IMEs ignore calls to ::ImmSetCandidateWindow() and use the position of
// the current system caret instead via ::GetCaretPos(). In order to behave
// as expected with these IMEs, we create a temporary system caret.
if (!ime_active_) {
::CreateCaret(window_handle_, nullptr, 1, 1);
}
ime_active_ = true;
// Set the position of the IME windows.
UpdateImeWindow();
}
void TextInputManager::DestroyImeWindow() {
if (window_handle_ == nullptr) {
return;
}
// Destroy the system caret created in CreateImeWindow().
if (ime_active_) {
::DestroyCaret();
}
ime_active_ = false;
}
void TextInputManager::UpdateImeWindow() {
if (window_handle_ == nullptr) {
return;
}
HIMC imm_context = ::ImmGetContext(window_handle_);
if (imm_context) {
MoveImeWindow(imm_context);
::ImmReleaseContext(window_handle_, imm_context);
}
}
void TextInputManager::UpdateCaretRect(const Rect& rect) {
caret_rect_ = rect;
if (window_handle_ == nullptr) {
return;
}
// TODO(cbracken): wrap these in an RAII container.
HIMC imm_context = ::ImmGetContext(window_handle_);
if (imm_context) {
MoveImeWindow(imm_context);
::ImmReleaseContext(window_handle_, imm_context);
}
}
long TextInputManager::GetComposingCursorPosition() const {
if (window_handle_ == nullptr) {
return false;
}
HIMC imm_context = ::ImmGetContext(window_handle_);
if (imm_context) {
// Read the cursor position within the composing string.
const int pos =
ImmGetCompositionStringW(imm_context, GCS_CURSORPOS, nullptr, 0);
::ImmReleaseContext(window_handle_, imm_context);
return pos;
}
return -1;
}
std::optional<std::u16string> TextInputManager::GetComposingString() const {
return GetString(GCS_COMPSTR);
}
std::optional<std::u16string> TextInputManager::GetResultString() const {
return GetString(GCS_RESULTSTR);
}
std::optional<std::u16string> TextInputManager::GetString(int type) const {
if (window_handle_ == nullptr || !ime_active_) {
return std::nullopt;
}
HIMC imm_context = ::ImmGetContext(window_handle_);
if (imm_context) {
// Read the composing string length.
const long compose_bytes =
::ImmGetCompositionString(imm_context, type, nullptr, 0);
const long compose_length = compose_bytes / sizeof(wchar_t);
if (compose_length <= 0) {
::ImmReleaseContext(window_handle_, imm_context);
return std::nullopt;
}
std::u16string text(compose_length, '\0');
::ImmGetCompositionString(imm_context, type, &text[0], compose_bytes);
::ImmReleaseContext(window_handle_, imm_context);
return text;
}
return std::nullopt;
}
void TextInputManager::MoveImeWindow(HIMC imm_context) {
if (GetFocus() != window_handle_ || !ime_active_) {
return;
}
LONG x = caret_rect_.left();
LONG y = caret_rect_.top();
::SetCaretPos(x, y);
COMPOSITIONFORM cf = {CFS_POINT, {x, y}};
::ImmSetCompositionWindow(imm_context, &cf);
CANDIDATEFORM candidate_form = {0, CFS_CANDIDATEPOS, {x, y}, {0, 0, 0, 0}};
::ImmSetCandidateWindow(imm_context, &candidate_form);
}
} // namespace flutter
// 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_WINDOWS_TEXT_INPUT_MANAGER_H_
#define FLUTTER_SHELL_PLATFORM_WINDOWS_TEXT_INPUT_MANAGER_H_
#include <Windows.h>
#include <Windowsx.h>
#include <optional>
#include <string>
#include "flutter/shell/platform/common/cpp/geometry.h"
namespace flutter {
// Management interface for IME-based text input on Windows.
//
// When inputting text in CJK languages, text is entered via a multi-step
// process, where direct keyboard input is buffered into a composing string,
// which is then converted into the desired characters by selecting from a
// candidates list and committing the change to the string.
//
// This implementation wraps the Win32 IMM32 APIs and provides a mechanism for
// creating and positioning the IME window, a system caret, and the candidates
// list as well as for accessing composing and results string contents.
class TextInputManager {
public:
TextInputManager() noexcept = default;
~TextInputManager() = default;
TextInputManager(const TextInputManager&) = delete;
TextInputManager& operator=(const TextInputManager&) = delete;
// Sets the window handle with which the IME is associated.
void SetWindowHandle(HWND window_handle);
// Creates a new IME window and system caret.
//
// This method should be invoked in response to the WM_IME_SETCONTEXT and
// WM_IME_STARTCOMPOSITION events.
void CreateImeWindow();
// Destroys the current IME window and system caret.
//
// This method should be invoked in response to the WM_IME_ENDCOMPOSITION
// event.
void DestroyImeWindow();
// Updates the current IME window and system caret position.
//
// This method should be invoked when handling user input via
// WM_IME_COMPOSITION events.
void UpdateImeWindow();
// Updates the current IME window and system caret position.
//
// This method should be invoked when handling cursor position/size updates.
void UpdateCaretRect(const Rect& rect);
// Returns the cursor position relative to the start of the composing range.
long GetComposingCursorPosition() const;
// Returns the contents of the composing string.
//
// This may be called in response to WM_IME_COMPOSITION events where the
// GCS_COMPSTR flag is set in the lparam. In some IMEs, this string may also
// be set in events where the GCS_RESULTSTR flag is set. This contains the
// in-progress composing string.
std::optional<std::u16string> GetComposingString() const;
// Returns the contents of the result string.
//
// This may be called in response to WM_IME_COMPOSITION events where the
// GCS_RESULTSTR flag is set in the lparam. This contains the final string to
// be committed in the composing region when composition is ended.
std::optional<std::u16string> GetResultString() const;
private:
// Returns either the composing string or result string based on the value of
// the |type| parameter.
std::optional<std::u16string> GetString(int type) const;
// Moves the IME composing and candidates windows to the current caret
// position.
void MoveImeWindow(HIMC imm_context);
// The window with which the IME windows are associated.
HWND window_handle_ = nullptr;
// True if IME-based composing is active.
bool ime_active_ = false;
// The system caret rect.
Rect caret_rect_ = {{0, 0}, {0, 0}};
};
} // namespace flutter
#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TEXT_INPUT_MANAGER_H_
......@@ -102,6 +102,26 @@ TextInputPlugin::TextInputPlugin(flutter::BinaryMessenger* messenger,
TextInputPlugin::~TextInputPlugin() = default;
void TextInputPlugin::ComposeBeginHook() {
active_model_->BeginComposing();
SendStateUpdate(*active_model_);
}
void TextInputPlugin::ComposeEndHook() {
active_model_->CommitComposing();
active_model_->EndComposing();
SendStateUpdate(*active_model_);
}
void TextInputPlugin::ComposeChangeHook(const std::u16string& text,
int cursor_pos) {
active_model_->AddText(text);
cursor_pos += active_model_->composing_range().base();
active_model_->UpdateComposingText(text);
active_model_->SetSelection(TextRange(cursor_pos, cursor_pos));
SendStateUpdate(*active_model_);
}
void TextInputPlugin::HandleMethodCall(
const flutter::MethodCall<rapidjson::Document>& method_call,
std::unique_ptr<flutter::MethodResult<rapidjson::Document>> result) {
......@@ -167,23 +187,41 @@ void TextInputPlugin::HandleMethodCall(
"Set editing state has been invoked, but without text.");
return;
}
auto selection_base = args.FindMember(kSelectionBaseKey);
auto selection_extent = args.FindMember(kSelectionExtentKey);
if (selection_base == args.MemberEnd() || selection_base->value.IsNull() ||
selection_extent == args.MemberEnd() ||
selection_extent->value.IsNull()) {
auto base = args.FindMember(kSelectionBaseKey);
auto extent = args.FindMember(kSelectionExtentKey);
if (base == args.MemberEnd() || base->value.IsNull() ||
extent == args.MemberEnd() || extent->value.IsNull()) {
result->Error(kInternalConsistencyError,
"Selection base/extent values invalid.");
return;
}
// Flutter uses -1/-1 for invalid; translate that to 0/0 for the model.
int base = selection_base->value.GetInt();
int extent = selection_extent->value.GetInt();
if (base == -1 && extent == -1) {
base = extent = 0;
int selection_base = base->value.GetInt();
int selection_extent = extent->value.GetInt();
if (selection_base == -1 && selection_extent == -1) {
selection_base = selection_extent = 0;
}
active_model_->SetText(text->value.GetString());
active_model_->SetSelection(TextRange(base, extent));
active_model_->SetSelection(TextRange(selection_base, selection_extent));
base = args.FindMember(kComposingBaseKey);
extent = args.FindMember(kComposingExtentKey);
if (base == args.MemberEnd() || base->value.IsNull() ||
extent == args.MemberEnd() || extent->value.IsNull()) {
result->Error(kInternalConsistencyError,
"Composing base/extent values invalid.");
return;
}
int composing_base = base->value.GetInt();
int composing_extent = base->value.GetInt();
if (composing_base == -1 && composing_extent == -1) {
active_model_->EndComposing();
} else {
int composing_start = std::min(composing_base, composing_extent);
int cursor_offset = selection_base - composing_start;
active_model_->SetComposingRange(
TextRange(composing_base, composing_extent), cursor_offset);
}
} else if (method.compare(kSetMarkedTextRect) == 0) {
if (!method_call.arguments() || method_call.arguments()->IsNull()) {
result->Error(kBadArgumentError, "Method invoked without args");
......@@ -259,13 +297,17 @@ void TextInputPlugin::SendStateUpdate(const TextInputModel& model) {
TextRange selection = model.selection();
rapidjson::Value editing_state(rapidjson::kObjectType);
editing_state.AddMember(kComposingBaseKey, -1, allocator);
editing_state.AddMember(kComposingExtentKey, -1, allocator);
editing_state.AddMember(kSelectionAffinityKey, kAffinityDownstream,
allocator);
editing_state.AddMember(kSelectionBaseKey, selection.base(), allocator);
editing_state.AddMember(kSelectionExtentKey, selection.extent(), allocator);
editing_state.AddMember(kSelectionIsDirectionalKey, false, allocator);
int composing_base = model.composing() ? model.composing_range().base() : -1;
int composing_extent =
model.composing() ? model.composing_range().extent() : -1;
editing_state.AddMember(kComposingBaseKey, composing_base, allocator);
editing_state.AddMember(kComposingExtentKey, composing_extent, allocator);
editing_state.AddMember(
kTextKey, rapidjson::Value(model.GetText(), allocator).Move(), allocator);
args->PushBack(editing_state, allocator);
......
......@@ -43,6 +43,15 @@ class TextInputPlugin : public KeyboardHookHandler {
// |KeyboardHookHandler|
void TextHook(FlutterWindowsView* view, const std::u16string& text) override;
// |KeyboardHookHandler|
void ComposeBeginHook() override;
// |KeyboardHookHandler|
void ComposeEndHook() override;
// |KeyboardHookHandler|
void ComposeChangeHook(const std::u16string& text, int cursor_pos) override;
private:
// Sends the current state of the given model to the Flutter engine.
void SendStateUpdate(const TextInputModel& model);
......
......@@ -171,6 +171,19 @@ bool Win32FlutterWindow::OnKey(int key,
extended);
}
void Win32FlutterWindow::OnComposeBegin() {
binding_handler_delegate_->OnComposeBegin();
}
void Win32FlutterWindow::OnComposeEnd() {
binding_handler_delegate_->OnComposeEnd();
}
void Win32FlutterWindow::OnComposeChange(const std::u16string& text,
int cursor_pos) {
binding_handler_delegate_->OnComposeChange(text, cursor_pos);
}
void Win32FlutterWindow::OnScroll(double delta_x, double delta_y) {
POINT point;
GetCursorPos(&point);
......@@ -181,7 +194,7 @@ void Win32FlutterWindow::OnScroll(double delta_x, double delta_y) {
}
void Win32FlutterWindow::UpdateCursorRect(const Rect& rect) {
// TODO(cbracken): Implement IMM candidate window positioning.
text_input_manager_.UpdateCaretRect(rect);
}
} // namespace flutter
......@@ -61,6 +61,15 @@ class Win32FlutterWindow : public Win32Window, public WindowBindingHandler {
char32_t character,
bool extended) override;
// |Win32Window|
void OnComposeBegin() override;
// |Win32Window|
void OnComposeEnd() override;
// |Win32Window|
void OnComposeChange(const std::u16string& text, int cursor_pos) override;
// |Win32Window|
void OnScroll(double delta_x, double delta_y) override;
......
......@@ -69,6 +69,9 @@ class SpyKeyEventHandler : public KeyboardHookHandler {
bool extended));
MOCK_METHOD2(TextHook,
void(FlutterWindowsView* window, const std::u16string& text));
MOCK_METHOD0(ComposeBeginHook, void());
MOCK_METHOD0(ComposeEndHook, void());
MOCK_METHOD2(ComposeChangeHook, void(const std::u16string& text, int cursor_pos));
private:
std::unique_ptr<KeyEventHandler> real_implementation_;
......@@ -98,6 +101,9 @@ class SpyTextInputPlugin : public KeyboardHookHandler,
bool extended));
MOCK_METHOD2(TextHook,
void(FlutterWindowsView* window, const std::u16string& text));
MOCK_METHOD0(ComposeBeginHook, void());
MOCK_METHOD0(ComposeEndHook, void());
MOCK_METHOD2(ComposeChangeHook, void(const std::u16string& text, int cursor_pos));
virtual void OnCursorRectUpdated(const Rect& rect) {}
......
......@@ -4,10 +4,17 @@
#include "flutter/shell/platform/windows/win32_window.h"
#include <imm.h>
#include <cstring>
#include "win32_dpi_utils.h"
// KeyCode used to indicate key events to be handled by the IME. These include
// the kana key, fullwidth/halfwidth (zenkaku/hankaku) key, and keypresses when
// the IME is in composing mode.
static constexpr int kImeComposingKeyCode = 229;
namespace flutter {
namespace {
......@@ -91,6 +98,7 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
auto that = static_cast<Win32Window*>(cs->lpCreateParams);
that->window_handle_ = window;
that->text_input_manager_.SetWindowHandle(window);
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->HandleMessage(message, wparam, lparam);
}
......@@ -109,10 +117,72 @@ void Win32Window::TrackMouseLeaveEvent(HWND hwnd) {
}
}
void Win32Window::OnImeSetContext(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
if (wparam != 0) {
text_input_manager_.CreateImeWindow();
}
}
void Win32Window::OnImeStartComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
text_input_manager_.CreateImeWindow();
OnComposeBegin();
}
void Win32Window::OnImeComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
// Update the IME window position.
text_input_manager_.UpdateImeWindow();
if (lparam & GCS_COMPSTR) {
// Read the in-progress composing string.
long pos = text_input_manager_.GetComposingCursorPosition();
std::optional<std::u16string> text =
text_input_manager_.GetComposingString();
if (text) {
OnComposeChange(text.value(), pos);
}
} else if (lparam & GCS_RESULTSTR) {
// Read the committed composing string.
long pos = text_input_manager_.GetComposingCursorPosition();
std::optional<std::u16string> text = text_input_manager_.GetResultString();
if (text) {
OnComposeChange(text.value(), pos);
}
// Next, try reading the composing string. Some Japanese IMEs send a message
// containing both a GCS_RESULTSTR and a GCS_COMPSTR when one composition is
// committed and another immediately started.
text = text_input_manager_.GetResultString();
if (text) {
OnComposeChange(text.value(), pos);
}
}
}
void Win32Window::OnImeEndComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
text_input_manager_.DestroyImeWindow();
OnComposeEnd();
}
void Win32Window::OnImeRequest(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
// TODO(cbracken): Handle IMR_RECONVERTSTRING, IMR_DOCUMENTFEED,
// and IMR_QUERYCHARPOSITION messages.
// https://github.com/flutter/flutter/issues/74547
}
LRESULT
Win32Window::HandleMessage(UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
LPARAM result_lparam = lparam;
int xPos = 0, yPos = 0;
UINT width = 0, height = 0;
UINT button_pressed = 0;
......@@ -152,6 +222,12 @@ Win32Window::HandleMessage(UINT const message,
}
break;
}
case WM_SETFOCUS:
::CreateCaret(window_handle_, nullptr, 1, 1);
break;
case WM_KILLFOCUS:
::DestroyCaret();
break;
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
......@@ -198,6 +274,40 @@ Win32Window::HandleMessage(UINT const message,
static_cast<double>(WHEEL_DELTA)),
0.0);
break;
case WM_INPUTLANGCHANGE:
// TODO(cbracken): pass this to TextInputManager to aid with
// language-specific issues.
break;
case WM_IME_SETCONTEXT:
OnImeSetContext(message, wparam, lparam);
// Strip the ISC_SHOWUICOMPOSITIONWINDOW bit from lparam before passing it
// to DefWindowProc() so that the composition window is hidden since
// Flutter renders the composing string itself.
result_lparam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
break;
case WM_IME_STARTCOMPOSITION:
OnImeStartComposition(message, wparam, lparam);
// Suppress further processing by DefWindowProc() so that the default
// system IME style isn't used, but rather the one set in the
// WM_IME_SETCONTEXT handler.
return TRUE;
case WM_IME_COMPOSITION:
OnImeComposition(message, wparam, lparam);
if (lparam & GCS_RESULTSTR || lparam & GCS_COMPSTR) {
// Suppress further processing by DefWindowProc() since otherwise it
// will emit the result string as WM_CHAR messages on commit. Instead,
// committing the composing text to the EditableText string is handled
// in TextInputModel::CommitComposing, triggered by
// OnImeEndComposition().
return TRUE;
}
break;
case WM_IME_ENDCOMPOSITION:
OnImeEndComposition(message, wparam, lparam);
return TRUE;
case WM_IME_REQUEST:
OnImeRequest(message, wparam, lparam);
break;
case WM_UNICHAR: {
// Tell third-pary app, we can support Unicode.
if (wparam == UNICODE_NOCHAR)
......@@ -273,6 +383,12 @@ Win32Window::HandleMessage(UINT const message,
break;
}
unsigned int keyCode(wparam);
if (keyCode == kImeComposingKeyCode) {
// This is an IME composing mode keypress that will be handled via
// WM_IME_* messages, which update the framework via updates to the text
// and composing range in text editing update messages.
break;
}
const unsigned int scancode = (lparam >> 16) & 0xff;
const bool extended = ((lparam >> 24) & 0x01) == 0x01;
// If the key is a modifier, get its side.
......@@ -286,7 +402,7 @@ Win32Window::HandleMessage(UINT const message,
break;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
return DefWindowProc(window_handle_, message, wparam, result_lparam);
}
UINT Win32Window::GetCurrentDPI() {
......
......@@ -11,6 +11,8 @@
#include <memory>
#include <string>
#include "flutter/shell/platform/windows/text_input_manager.h"
namespace flutter {
// A class abstraction for a high DPI aware Win32 Window. Intended to be
......@@ -101,6 +103,43 @@ class Win32Window {
char32_t character,
bool extended) = 0;
// Called when IME composing begins.
virtual void OnComposeBegin() = 0;
// Called when IME composing ends.
virtual void OnComposeEnd() = 0;
// Called when IME composing text or cursor position changes.
virtual void OnComposeChange(const std::u16string& text, int cursor_pos) = 0;
// Called when a window is activated in order to configure IME support for
// multi-step text input.
void OnImeSetContext(UINT const message,
WPARAM const wparam,
LPARAM const lparam);
// Called when multi-step text input begins when using an IME.
void OnImeStartComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam);
// Called when edits/commit of multi-step text input occurs when using an IME.
void OnImeComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam);
// Called when multi-step text input ends when using an IME.
void OnImeEndComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam);
// Called when the user triggers an IME-specific request such as input
// reconversion, where an existing input sequence is returned to composing
// mode to select an alternative candidate conversion.
void OnImeRequest(UINT const message,
WPARAM const wparam,
LPARAM const lparam);
// Called when mouse scrollwheel input occurs.
virtual void OnScroll(double delta_x, double delta_y) = 0;
......@@ -142,6 +181,9 @@ class Win32Window {
// Keeps track of the last key code produced by a WM_KEYDOWN or WM_SYSKEYDOWN
// message.
int keycode_for_char_message_ = 0;
protected:
TextInputManager text_input_manager_;
};
} // namespace flutter
......
......@@ -50,6 +50,24 @@ class WindowBindingHandlerDelegate {
char32_t character,
bool extended) = 0;
// Notifies the delegate that IME composing mode has begun.
//
// Triggered when the user begins editing composing text using a multi-step
// input method such as in CJK text input.
virtual void OnComposeBegin() = 0;
// Notifies the delegate that IME composing mode has ended.
//
// Triggered when the user commits the composing text while using a multi-step
// input method such as in CJK text input.
virtual void OnComposeEnd() = 0;
// Notifies the delegate that IME composing region contents have changed.
//
// Triggered when the user edits the composing text while using a multi-step
// input method such as in CJK text input.
virtual void OnComposeChange(const std::u16string& text, int cursor_pos) = 0;
// Notifies delegate that backing window size has recevied scroll.
// Typically called by currently configured WindowBindingHandler
virtual void OnScroll(double x,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册