diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 40a5df92483d1..a2cc16781ef02 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -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 diff --git a/shell/platform/common/cpp/text_input_model.cc b/shell/platform/common/cpp/text_input_model.cc index 649baede20f51..70d8001c1651d 100644 --- a/shell/platform/common/cpp/text_input_model.cc +++ b/shell/platform/common/cpp/text_input_model.cc @@ -67,11 +67,7 @@ void TextInputModel::BeginComposing() { composing_range_ = TextRange(selection_.start()); } -void TextInputModel::UpdateComposingText(const std::string& composing_text) { - std::wstring_convert, 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, 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()) { diff --git a/shell/platform/common/cpp/text_input_model.h b/shell/platform/common/cpp/text_input_model.h index a9560b7b1483d..340a2b62b8738 100644 --- a/shell/platform/common/cpp/text_input_model.h +++ b/shell/platform/common/cpp/text_input_model.h @@ -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. // diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index a1ce9f169c8b8..ad4d4ee8e3135 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -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 += [ diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index a5c709357305b..2f3b4c282698c 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -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, diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 0998f04912685..c868a4c7b1ee2 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -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, diff --git a/shell/platform/windows/key_event_handler.cc b/shell/platform/windows/key_event_handler.cc index b4b1b8149c3fd..67942e1e12edb 100644 --- a/shell/platform/windows/key_event_handler.cc +++ b/shell/platform/windows/key_event_handler.cc @@ -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 diff --git a/shell/platform/windows/key_event_handler.h b/shell/platform/windows/key_event_handler.h index 86433e6f6322a..62f1b768afcd8 100644 --- a/shell/platform/windows/key_event_handler.h +++ b/shell/platform/windows/key_event_handler.h @@ -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); diff --git a/shell/platform/windows/keyboard_hook_handler.h b/shell/platform/windows/keyboard_hook_handler.h index 37bcbafbeb2b3..527a83687f6dc 100644 --- a/shell/platform/windows/keyboard_hook_handler.h +++ b/shell/platform/windows/keyboard_hook_handler.h @@ -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 diff --git a/shell/platform/windows/testing/mock_win32_window.h b/shell/platform/windows/testing/mock_win32_window.h index dc7765e95959a..08417e144990f 100644 --- a/shell/platform/windows/testing/mock_win32_window.h +++ b/shell/platform/windows/testing/mock_win32_window.h @@ -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 diff --git a/shell/platform/windows/text_input_manager.cc b/shell/platform/windows/text_input_manager.cc new file mode 100644 index 0000000000000..9e9cfb7ae0639 --- /dev/null +++ b/shell/platform/windows/text_input_manager.cc @@ -0,0 +1,139 @@ +// 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 + +#include + +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 TextInputManager::GetComposingString() const { + return GetString(GCS_COMPSTR); +} + +std::optional TextInputManager::GetResultString() const { + return GetString(GCS_RESULTSTR); +} + +std::optional 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 diff --git a/shell/platform/windows/text_input_manager.h b/shell/platform/windows/text_input_manager.h new file mode 100644 index 0000000000000..cf21809f4e8c2 --- /dev/null +++ b/shell/platform/windows/text_input_manager.h @@ -0,0 +1,101 @@ +// 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 +#include + +#include +#include + +#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 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 GetResultString() const; + + private: + // Returns either the composing string or result string based on the value of + // the |type| parameter. + std::optional 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_ diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index 69c66de50ba97..59dbe59c63273 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -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& method_call, std::unique_ptr> 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); diff --git a/shell/platform/windows/text_input_plugin.h b/shell/platform/windows/text_input_plugin.h index 47415365507bc..a18f025097135 100644 --- a/shell/platform/windows/text_input_plugin.h +++ b/shell/platform/windows/text_input_plugin.h @@ -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); diff --git a/shell/platform/windows/win32_flutter_window.cc b/shell/platform/windows/win32_flutter_window.cc index 5915cd03b7a32..ba4e3880dad1d 100644 --- a/shell/platform/windows/win32_flutter_window.cc +++ b/shell/platform/windows/win32_flutter_window.cc @@ -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 diff --git a/shell/platform/windows/win32_flutter_window.h b/shell/platform/windows/win32_flutter_window.h index 3e86d9da7c1de..15fd108fb2905 100644 --- a/shell/platform/windows/win32_flutter_window.h +++ b/shell/platform/windows/win32_flutter_window.h @@ -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; diff --git a/shell/platform/windows/win32_flutter_window_unittests.cc b/shell/platform/windows/win32_flutter_window_unittests.cc index 8ba4b15f8d425..c53843e9a437e 100644 --- a/shell/platform/windows/win32_flutter_window_unittests.cc +++ b/shell/platform/windows/win32_flutter_window_unittests.cc @@ -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 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) {} diff --git a/shell/platform/windows/win32_window.cc b/shell/platform/windows/win32_window.cc index 01432cfb0f6fe..92be90f4dda3a 100644 --- a/shell/platform/windows/win32_window.cc +++ b/shell/platform/windows/win32_window.cc @@ -4,10 +4,17 @@ #include "flutter/shell/platform/windows/win32_window.h" +#include + #include #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(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 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 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(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() { diff --git a/shell/platform/windows/win32_window.h b/shell/platform/windows/win32_window.h index 2d964bb62540f..b1366877cb80d 100644 --- a/shell/platform/windows/win32_window.h +++ b/shell/platform/windows/win32_window.h @@ -11,6 +11,8 @@ #include #include +#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 diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index 8ea914bde3e9e..8b8b1acc7669e 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -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,