From 3538465b2b15e7a7c796430cdb992c77e9fcc8bd Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 24 Jan 2022 12:18:35 -0800 Subject: [PATCH] [Win32, Keyboard] Fix text submission (flutter/engine#30990) Fixes text submission not working on win32. --- .../platform/windows/flutter_windows_view.cc | 2 +- .../windows/keyboard_win32_unittests.cc | 83 ++++++++++++++++++- .../platform/windows/testing/test_keyboard.cc | 74 +++++++++-------- .../platform/windows/testing/test_keyboard.h | 21 ++++- 4 files changed, 139 insertions(+), 41 deletions(-) diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc index 538353344f..9c3190ad20 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc @@ -381,7 +381,7 @@ void FlutterWindowsView::SendKey(int key, KeyEventCallback callback) { keyboard_key_handler_->KeyboardHook( key, scancode, action, character, extended, was_down, - [&, callback = std::move(callback)](bool handled) { + [=, callback = std::move(callback)](bool handled) { if (!handled) { text_input_plugin_->KeyboardHook(key, scancode, action, character, extended, was_down); diff --git a/engine/src/flutter/shell/platform/windows/keyboard_win32_unittests.cc b/engine/src/flutter/shell/platform/windows/keyboard_win32_unittests.cc index 7d48886420..e3aecbdef3 100644 --- a/engine/src/flutter/shell/platform/windows/keyboard_win32_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/keyboard_win32_unittests.cc @@ -2,6 +2,7 @@ // 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/json_message_codec.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/embedder/test_utils/key_codes.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" @@ -16,6 +17,8 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" #include #include @@ -188,6 +191,36 @@ class TestFlutterWindowsView : public FlutterWindowsView { key_state_.Set(key, pressed, toggled_on); } + void HandleMessage(const char* channel, + const char* method, + const char* args) { + rapidjson::Document args_doc; + args_doc.Parse(args); + assert(!args_doc.HasParseError()); + + rapidjson::Document message_doc(rapidjson::kObjectType); + auto& allocator = message_doc.GetAllocator(); + message_doc.AddMember("method", rapidjson::Value(method, allocator), + allocator); + message_doc.AddMember("args", args_doc, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + message_doc.Accept(writer); + + std::unique_ptr> data = + JsonMessageCodec::GetInstance().EncodeMessage(message_doc); + FlutterPlatformMessageResponseHandle response_handle; + const FlutterPlatformMessage message = { + sizeof(FlutterPlatformMessage), // struct_size + channel, // channel + data->data(), // message + data->size(), // message_size + &response_handle, // response_handle + }; + GetEngine()->HandlePlatformMessage(&message); + } + protected: std::unique_ptr CreateKeyboardKeyHandler( BinaryMessenger* messenger, @@ -204,14 +237,16 @@ class TestFlutterWindowsView : public FlutterWindowsView { typedef enum { kKeyCallOnKey, kKeyCallOnText, + kKeyCallTextMethodCall, } KeyCallType; typedef struct { KeyCallType type; // Only one of the following fields should be assigned. - FlutterKeyEvent key_event; - std::u16string text; + FlutterKeyEvent key_event; // For kKeyCallOnKey + std::u16string text; // For kKeyCallOnText + std::string text_method_call; // For kKeyCallTextMethodCall } KeyCall; static std::vector key_calls; @@ -256,6 +291,8 @@ class KeyboardTester { window_ = std::make_unique(view_.get()); } + TestFlutterWindowsView& GetView() { return *view_; } + void SetKeyState(uint32_t key, bool pressed, bool toggled_on) { view_->SetKeyState(key, pressed, toggled_on); } @@ -322,6 +359,16 @@ class KeyboardTester { std::make_shared(); key_response_controller->SetEmbedderResponse( std::move(embedder_callback_handler)); + key_response_controller->SetTextInputResponse( + [](std::unique_ptr document) { + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document->Accept(writer); + key_calls.push_back(KeyCall{ + .type = kKeyCallTextMethodCall, + .text_method_call = buffer.GetString(), + }); + }); MockEmbedderApiForKeyboard(modifier, key_response_controller); @@ -355,6 +402,7 @@ constexpr uint64_t kScanCodeShiftLeft = 0x2a; constexpr uint64_t kScanCodeShiftRight = 0x36; constexpr uint64_t kScanCodeBracketLeft = 0x1a; constexpr uint64_t kScanCodeArrowLeft = 0x4b; +constexpr uint64_t kScanCodeEnter = 0x1c; constexpr uint64_t kVirtualDigit1 = 0x31; constexpr uint64_t kVirtualKeyA = 0x41; @@ -379,6 +427,10 @@ constexpr bool kNotSynthesized = false; EXPECT_EQ(_key_call.type, kKeyCallOnText); \ EXPECT_EQ(_key_call.text, u16_string); +#define EXPECT_CALL_IS_TEXT_METHOD_CALL(_key_call, json_string) \ + EXPECT_EQ(_key_call.type, kKeyCallTextMethodCall); \ + EXPECT_STREQ(_key_call.text_method_call.c_str(), json_string); + TEST(KeyboardTest, LowerCaseAHandled) { KeyboardTester tester; tester.Responding(true); @@ -1749,5 +1801,32 @@ TEST(KeyboardTest, SlowFrameworkResponse) { clear_key_calls(); } +TEST(KeyboardTest, TextInputSubmit) { + KeyboardTester tester; + tester.Responding(false); + + // US Keyboard layout + + tester.GetView().HandleMessage( + "flutter/textinput", "TextInput.setClient", + R"|([108, {"inputAction": "TextInputAction.none"}])|"); + + // Press Enter + tester.InjectMessages( + 1, WmKeyDownInfo{VK_RETURN, kScanCodeEnter, kNotExtended, kWasUp}.Build( + kWmResultZero)); + + EXPECT_EQ(key_calls.size(), 2); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalEnter, + kLogicalEnter, "", kNotSynthesized); + EXPECT_CALL_IS_TEXT_METHOD_CALL( + key_calls[1], + "{" + R"|("method":"TextInputClient.performAction",)|" + R"|("args":[108,"TextInputAction.none"])|" + "}"); + clear_key_calls(); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/testing/test_keyboard.cc b/engine/src/flutter/shell/platform/windows/testing/test_keyboard.cc index eca499a26c..6b2030afb4 100644 --- a/engine/src/flutter/shell/platform/windows/testing/test_keyboard.cc +++ b/engine/src/flutter/shell/platform/windows/testing/test_keyboard.cc @@ -50,15 +50,6 @@ static std::string ordinal(int num) { return "th"; } } - -// A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the -// callbacks and user data passed to the engine's -// PlatformMessageCreateResponseHandle for use in the SendPlatformMessage -// overridden function. -struct TestResponseHandle { - FlutterDesktopBinaryReply callback; - void* user_data; -}; } // namespace #define _RETURN_IF_NOT_EQUALS(val1, val2) \ @@ -111,28 +102,35 @@ void MockEmbedderApiForKeyboard( std::shared_ptr response_controller) { stored_response_controller = response_controller; // This mock handles channel messages. - modifier.embedder_api().SendPlatformMessage = - [](FLUTTER_API_SYMBOL(FlutterEngine) engine, - const FlutterPlatformMessage* message) { - if (std::string(message->channel) == std::string("flutter/settings")) { - return kSuccess; + modifier.embedder_api() + .SendPlatformMessage = [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterPlatformMessage* message) { + if (std::string(message->channel) == std::string("flutter/settings")) { + return kSuccess; + } + if (std::string(message->channel) == std::string("flutter/keyevent")) { + stored_response_controller->HandleChannelMessage([message](bool handled) { + auto response = _keyHandlingResponse(handled); + auto response_handle = message->response_handle; + if (response_handle->callback != nullptr) { + response_handle->callback(response->data(), response->size(), + response_handle->user_data); } - if (std::string(message->channel) == std::string("flutter/keyevent")) { - stored_response_controller->HandleChannelMessage( - [message](bool handled) { - auto response = _keyHandlingResponse(handled); - const TestResponseHandle* response_handle = - reinterpret_cast( - message->response_handle); - if (response_handle->callback != nullptr) { - response_handle->callback(response->data(), response->size(), - response_handle->user_data); - } - }); - return kSuccess; - } - return kSuccess; - }; + }); + return kSuccess; + } + if (std::string(message->channel) == std::string("flutter/textinput")) { + std::unique_ptr document = + flutter::JsonMessageCodec::GetInstance().DecodeMessage( + message->message, message->message_size); + if (document == nullptr) { + return kInvalidArguments; + } + stored_response_controller->HandleTextInputMessage(std::move(document)); + return kSuccess; + } + return kSuccess; + }; // This mock handles key events sent through the embedder API. modifier.embedder_api().SendKeyEvent = @@ -150,23 +148,27 @@ void MockEmbedderApiForKeyboard( // The following mocks enable channel mocking. modifier.embedder_api().PlatformMessageCreateResponseHandle = [](auto engine, auto data_callback, auto user_data, auto response_out) { - TestResponseHandle* response_handle = new TestResponseHandle(); + auto response_handle = new FlutterPlatformMessageResponseHandle(); response_handle->user_data = user_data; response_handle->callback = data_callback; - *response_out = reinterpret_cast( - response_handle); + *response_out = response_handle; return kSuccess; }; modifier.embedder_api().PlatformMessageReleaseResponseHandle = [](FLUTTER_API_SYMBOL(FlutterEngine) engine, FlutterPlatformMessageResponseHandle* response) { - const TestResponseHandle* response_handle = - reinterpret_cast(response); - delete response_handle; + delete response; return kSuccess; }; + // The following mock disables responses for method channels sent from the + // embedding to the framework. (No code uses the response yet.) + modifier.embedder_api().SendPlatformMessageResponse = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterPlatformMessageResponseHandle* handle, + const uint8_t* data, size_t data_length) { return kSuccess; }; + // The following mocks allows RunWithEntrypoint to be run, which creates a // non-empty FlutterEngine and enables SendKeyEvent. diff --git a/engine/src/flutter/shell/platform/windows/testing/test_keyboard.h b/engine/src/flutter/shell/platform/windows/testing/test_keyboard.h index f36b490460..868b9f1ba8 100644 --- a/engine/src/flutter/shell/platform/windows/testing/test_keyboard.h +++ b/engine/src/flutter/shell/platform/windows/testing/test_keyboard.h @@ -16,9 +16,13 @@ #include "gtest/gtest.h" +struct _FlutterPlatformMessageResponseHandle { + FlutterDesktopBinaryReply callback; + void* user_data; +}; + namespace flutter { namespace testing { - ::testing::AssertionResult _EventEquals(const char* expr_event, const char* expr_expected, const FlutterKeyEvent& event, @@ -48,10 +52,14 @@ class MockKeyResponseController { using EmbedderCallbackHandler = std::function; using ChannelCallbackHandler = std::function; + using TextInputCallbackHandler = + std::function)>; MockKeyResponseController() : channel_response_(ChannelRespondFalse), - embedder_response_(EmbedderRespondFalse) {} + embedder_response_(EmbedderRespondFalse), + text_input_response_( + [](std::unique_ptr document) {}) {} void SetChannelResponse(ChannelCallbackHandler handler) { channel_response_ = std::move(handler); @@ -61,6 +69,10 @@ class MockKeyResponseController { embedder_response_ = std::move(handler); } + void SetTextInputResponse(TextInputCallbackHandler handler) { + text_input_response_ = std::move(handler); + } + void HandleChannelMessage(ResponseCallback callback) { channel_response_(callback); } @@ -70,9 +82,14 @@ class MockKeyResponseController { embedder_response_(event, std::move(callback)); } + void HandleTextInputMessage(std::unique_ptr document) { + text_input_response_(std::move(document)); + } + private: EmbedderCallbackHandler embedder_response_; ChannelCallbackHandler channel_response_; + TextInputCallbackHandler text_input_response_; static void ChannelRespondFalse(ResponseCallback callback) { callback(false);