diff --git a/lib/ui/key.dart b/lib/ui/key.dart index 50e07703fb977..ac6b8b7755a90 100644 --- a/lib/ui/key.dart +++ b/lib/ui/key.dart @@ -149,9 +149,17 @@ class KeyData { } } + String? _quotedCharCode() { + if (character == null) + return ''; + final Iterable hexChars = character!.codeUnits + .map((int code) => code.toRadixString(16).padLeft(2, '0')); + return '0x${hexChars.join(' ')}'; + } + @override String toString() => 'KeyData(key ${_typeToString(type)}, physical: 0x${physical.toRadixString(16)}, ' - 'logical: ${_logicalToString()}, character: ${_escapeCharacter()})'; + 'logical: ${_logicalToString()}, character: ${_escapeCharacter()}${_quotedCharCode()}${synthesized ? ', synthesized' : ''})'; /// Returns a complete textual description of the information in this object. String toStringFull() { diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 4ed1b0a8e7cfe..3a9f5fa0c3c20 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -243,12 +243,15 @@ executable("flutter_windows_unittests") { "keyboard_key_channel_handler_unittests.cc", "keyboard_key_embedder_handler_unittests.cc", "keyboard_key_handler_unittests.cc", + "keyboard_unittests.cc", "testing/flutter_window_win32_test.cc", "testing/flutter_window_win32_test.h", "testing/mock_window_binding_handler.cc", "testing/mock_window_binding_handler.h", "testing/mock_window_win32.cc", "testing/mock_window_win32.h", + "testing/test_keyboard.cc", + "testing/test_keyboard.h", "text_input_plugin_unittest.cc", "window_proc_delegate_manager_win32_unittests.cc", "window_win32_unittests.cc", diff --git a/shell/platform/windows/flutter_window_win32_unittests.cc b/shell/platform/windows/flutter_window_win32_unittests.cc index 7f01ed5c9930e..0fca408911421 100644 --- a/shell/platform/windows/flutter_window_win32_unittests.cc +++ b/shell/platform/windows/flutter_window_win32_unittests.cc @@ -30,8 +30,8 @@ namespace { // Creates a valid Windows LPARAM for WM_KEYDOWN and WM_CHAR from parameters // given. static LPARAM CreateKeyEventLparam(USHORT scancode, - bool extended = false, - bool was_down = 1, + bool extended, + bool was_down, USHORT repeat_count = 1, bool context_code = 0, bool transition_state = 1) { @@ -215,7 +215,11 @@ class TestFlutterWindowsView : public FlutterWindowsView { } protected: - void RegisterKeyboardHandlers(flutter::BinaryMessenger* messenger) override { + void RegisterKeyboardHandlers( + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) + override { auto spy_key_event_handler = std::make_unique( messenger, [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { return this->SendInput(cInputs, pInputs, cbSize); @@ -235,8 +239,9 @@ class TestFlutterWindowsView : public FlutterWindowsView { const KEYBDINPUT kbdinput = pInputs->ki; const UINT message = (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; + const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP; const LPARAM lparam = CreateKeyEventLparam( - kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY); + kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up); // Windows would normally fill in the virtual key code for us, so we // simulate it for the test with the key we know is in the test. The // KBDINPUT we're passed doesn't have it filled in (on purpose, so that @@ -345,8 +350,7 @@ TEST(FlutterWindowWin32Test, NonPrintableKeyDownPropagation) { TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code, false /* extended */, - false /* PrevState */); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); // Test an event not handled by the framework { @@ -406,16 +410,15 @@ TEST(FlutterWindowWin32Test, CharKeyDownPropagation) { TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, true /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code, false /* extended */, - true /* PrevState */); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); + flutter_windows_view.SetEngine(std::move(GetTestEngine())); // Test an event not handled by the framework { test_response = false; - flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false, true)) + false, false)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, @@ -433,21 +436,21 @@ TEST(FlutterWindowWin32Test, CharKeyDownPropagation) { EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } - return; // Test an event handled by the framework { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* is_printable */, true)) + false, false)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) - .Times(0); + .Times(1) + .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), @@ -471,7 +474,7 @@ TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); - LPARAM lparam = CreateKeyEventLparam(scan_code); + LPARAM lparam = CreateKeyEventLparam(scan_code, false, false); // Test an event not handled by the framework { @@ -479,7 +482,7 @@ TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* extended */, true)) + false /* extended */, false)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, @@ -500,7 +503,7 @@ TEST(FlutterWindowWin32Test, ModifierKeyDownPropagation) { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, - false /* extended */, true)) + false /* extended */, false)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index 55cbc43299dce..fee32e5f7da18 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -55,7 +55,17 @@ void FlutterWindowsView::SetEngine( // Set up the system channel handlers. auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); - RegisterKeyboardHandlers(internal_plugin_messenger); +#ifdef WINUWP + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event = nullptr; + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = + nullptr; +#else + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event = SendInput; + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = + GetKeyState; +#endif + RegisterKeyboardHandlers(internal_plugin_messenger, dispatch_event, + get_key_state); platform_handler_ = PlatformHandler::Create(internal_plugin_messenger, this); cursor_handler_ = std::make_unique( internal_plugin_messenger, binding_handler_.get()); @@ -67,7 +77,9 @@ void FlutterWindowsView::SetEngine( } void FlutterWindowsView::RegisterKeyboardHandlers( - flutter::BinaryMessenger* messenger) { + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) { // There must be only one handler that receives |SendInput|, i.e. only one // handler that might redispatch events. (See the documentation of // |KeyboardKeyHandler| to learn about redispatching.) @@ -76,17 +88,8 @@ void FlutterWindowsView::RegisterKeyboardHandlers( // of the event. In order to allow the same real event in the future, the // handler is "toggled" when events pass through, therefore the redispatching // algorithm does not allow more than 1 handler that takes |SendInput|. -#ifdef WINUWP - flutter::KeyboardKeyHandler::EventDispatcher redispatch_event = nullptr; - flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = - nullptr; -#else - flutter::KeyboardKeyHandler::EventDispatcher redispatch_event = SendInput; - flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state = - GetKeyState; -#endif auto key_handler = - std::make_unique(redispatch_event); + std::make_unique(dispatch_event); key_handler->AddDelegate(std::make_unique( [this](const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* user_data) { diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index f70d1b1aa62d6..f3432c0575062 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -19,6 +19,8 @@ #include "flutter/shell/platform/windows/cursor_handler.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/keyboard_handler_base.h" +#include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_handler.h" #include "flutter/shell/platform/windows/platform_handler.h" #include "flutter/shell/platform/windows/public/flutter_windows.h" #include "flutter/shell/platform/windows/text_input_plugin_delegate.h" @@ -144,7 +146,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, protected: // Called to create the keyboard hook handlers. - virtual void RegisterKeyboardHandlers(flutter::BinaryMessenger* messenger); + virtual void RegisterKeyboardHandlers( + flutter::BinaryMessenger* messenger, + flutter::KeyboardKeyHandler::EventDispatcher dispatch_event, + flutter::KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state); // Used by RegisterKeyboardHandlers to add a new keyboard hook handler. void AddKeyboardHandler( diff --git a/shell/platform/windows/keyboard_key_embedder_handler.cc b/shell/platform/windows/keyboard_key_embedder_handler.cc index b1aa06d1bbddb..1c4bf4d2037ae 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler.cc @@ -267,10 +267,10 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( continue; } assert(key_info.logical_key != 0); - SHORT state = get_key_state_(virtual_key); // Check toggling state first, because it might alter pressing state. if (key_info.check_toggled) { + SHORT state = get_key_state_(virtual_key); bool should_toggled = state & kStateMaskToggled; if (virtual_key == toggle_virtual_key) { key_info.toggled_on = !key_info.toggled_on; @@ -312,8 +312,8 @@ void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates() { continue; } assert(key_info.logical_key != 0); - SHORT state = get_key_state_(virtual_key); if (key_info.check_pressed) { + SHORT state = get_key_state_(virtual_key); auto recorded_pressed_iter = pressingRecords_.find(key_info.physical_key); bool recorded_pressed = recorded_pressed_iter != pressingRecords_.end(); bool should_pressed = state & kStateMaskPressed; diff --git a/shell/platform/windows/keyboard_key_handler.cc b/shell/platform/windows/keyboard_key_handler.cc index 4264039d658aa..34c3515ba6ae3 100644 --- a/shell/platform/windows/keyboard_key_handler.cc +++ b/shell/platform/windows/keyboard_key_handler.cc @@ -62,7 +62,7 @@ static bool IsKeyDownAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_LMENU && extended && action == WM_KEYDOWN; + return virtual_key == VK_RMENU && extended && action == WM_KEYDOWN; #endif } @@ -73,7 +73,7 @@ static bool IsKeyUpAltRight(int action, int virtual_key, bool extended) { #ifdef WINUWP return false; #else - return virtual_key == VK_LMENU && extended && action == WM_KEYUP; + return virtual_key == VK_RMENU && extended && action == WM_KEYUP; #endif } @@ -220,9 +220,11 @@ bool KeyboardKeyHandler::KeyboardHook(FlutterWindowsView* view, } pending_responds_.push_back(std::move(incoming)); + const bool is_deadchar = character & 0x80000000; + const uint32_t key_character = is_deadchar ? 0 : character; for (const auto& delegate : delegates_) { - delegate->KeyboardHook(key, scancode, action, character, extended, was_down, - [sequence_id, this](bool handled) { + delegate->KeyboardHook(key, scancode, action, key_character, extended, + was_down, [sequence_id, this](bool handled) { ResolvePendingEvent(sequence_id, handled); }); } @@ -233,7 +235,7 @@ bool KeyboardKeyHandler::KeyboardHook(FlutterWindowsView* view, // return true at this time, preventing this event from affecting // others. - return true; + return !is_deadchar; } bool KeyboardKeyHandler::RemoveRedispatchedEvent(const PendingEvent& incoming) { @@ -261,7 +263,9 @@ void KeyboardKeyHandler::ResolvePendingEvent(uint64_t sequence_id, if (event.unreplied == 0) { std::unique_ptr event_ptr = std::move(*iter); pending_responds_.erase(iter); - if (!event_ptr->any_handled) { + const bool is_deadchar = event.character & 0x80000000; + const bool should_redispatch = !event_ptr->any_handled && !is_deadchar; + if (should_redispatch) { RedispatchEvent(std::move(event_ptr)); } } diff --git a/shell/platform/windows/keyboard_unittests.cc b/shell/platform/windows/keyboard_unittests.cc new file mode 100644 index 0000000000000..7a98fb6ee00d4 --- /dev/null +++ b/shell/platform/windows/keyboard_unittests.cc @@ -0,0 +1,390 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/common/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/embedder/test_utils/proc_table_replacement.h" +#include "flutter/shell/platform/windows/flutter_windows_engine.h" +#include "flutter/shell/platform/windows/keyboard_key_channel_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" +#include "flutter/shell/platform/windows/keyboard_key_handler.h" +#include "flutter/shell/platform/windows/testing/engine_modifier.h" +#include "flutter/shell/platform/windows/testing/flutter_window_win32_test.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" +#include "flutter/shell/platform/windows/testing/test_keyboard.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include + +using testing::_; +using testing::Invoke; +using testing::Return; +using namespace ::flutter::testing::keycodes; + +namespace flutter { +namespace testing { + +namespace { + +constexpr SHORT kStateMaskToggled = 0x01; +constexpr SHORT kStateMaskPressed = 0x80; + +struct SimulatedEvent { + UINT message; + WPARAM wparam; + LPARAM lparam; +}; + +static LPARAM CreateKeyEventLparam(USHORT scancode, + bool extended, + bool was_down, + USHORT repeat_count = 1, + bool context_code = 0, + bool transition_state = 1) { + return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) | + (LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | + (LPARAM(scancode) << 16) | LPARAM(repeat_count)); +} + +class MockFlutterWindowWin32 : public FlutterWindowWin32 { + public: + typedef std::function U16StringHandler; + + MockFlutterWindowWin32(U16StringHandler on_text) + : FlutterWindowWin32(800, 600), on_text_(std::move(on_text)) { + ON_CALL(*this, GetDpiScale()) + .WillByDefault(Return(this->FlutterWindowWin32::GetDpiScale())); + } + virtual ~MockFlutterWindowWin32() {} + + // Prevent copying. + MockFlutterWindowWin32(MockFlutterWindowWin32 const&) = delete; + MockFlutterWindowWin32& operator=(MockFlutterWindowWin32 const&) = delete; + + // Wrapper for GetCurrentDPI() which is a protected method. + UINT GetDpi() { return GetCurrentDPI(); } + + // Simulates a WindowProc message from the OS. + LRESULT InjectWindowMessage(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + return HandleMessage(message, wparam, lparam); + } + + void OnText(const std::u16string& text) override { on_text_(text); } + + MOCK_METHOD1(OnDpiScale, void(unsigned int)); + MOCK_METHOD2(OnResize, void(unsigned int, unsigned int)); + MOCK_METHOD2(OnPointerMove, void(double, double)); + MOCK_METHOD3(OnPointerDown, void(double, double, UINT)); + MOCK_METHOD3(OnPointerUp, void(double, double, UINT)); + MOCK_METHOD0(OnPointerLeave, void()); + MOCK_METHOD0(OnSetCursor, void()); + MOCK_METHOD2(OnScroll, void(double, double)); + MOCK_METHOD4(DefaultWindowProc, LRESULT(HWND, UINT, WPARAM, LPARAM)); + MOCK_METHOD0(GetDpiScale, float()); + MOCK_METHOD0(IsVisible, bool()); + MOCK_METHOD1(UpdateCursorRect, void(const Rect&)); + + private: + U16StringHandler on_text_; +}; + +class TestKeystate { + public: + void Set(int virtual_key, bool pressed, bool toggled_on = false) { + state_[virtual_key] = (pressed ? kStateMaskPressed : 0) | + (toggled_on ? kStateMaskToggled : 0); + } + + SHORT Get(int virtual_key) { return state_[virtual_key]; } + + KeyboardKeyEmbedderHandler::GetKeyStateHandler Getter() { + return [this](int virtual_key) { return Get(virtual_key); }; + } + + private: + std::map state_; +}; + +// A FlutterWindowsView that overrides the RegisterKeyboardHandlers function +// to register the keyboard hook handlers that can be spied upon. +class TestFlutterWindowsView : public FlutterWindowsView { + public: + TestFlutterWindowsView() + // The WindowBindingHandler is used for window size and such, and doesn't + // affect keyboard. + : FlutterWindowsView( + std::make_unique<::testing::NiceMock>()), + is_printable(true) {} + + void InjectPendingEvents(MockFlutterWindowWin32* win32window) { + while (pending_responds_.size() > 0) { + SimulatedEvent event = pending_responds_.front(); + win32window->InjectWindowMessage(event.message, event.wparam, + event.lparam); + pending_responds_.pop_front(); + } + } + + bool is_printable; + + protected: + void RegisterKeyboardHandlers( + BinaryMessenger* messenger, + KeyboardKeyHandler::EventDispatcher dispatch_event, + KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) override { + FlutterWindowsView::RegisterKeyboardHandlers( + messenger, + [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { + return this->SendInput(cInputs, pInputs, cbSize); + }, + key_state_.Getter()); + } + + private: + UINT SendInput(UINT cInputs, LPINPUT pInputs, int cbSize) { + // Simulate the event loop by just sending the event sent to + // "SendInput" directly to the window. + const KEYBDINPUT kbdinput = pInputs->ki; + const UINT message = + (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; + const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP; + const LPARAM lparam = CreateKeyEventLparam( + kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up); + pending_responds_.push_back(SimulatedEvent{message, kbdinput.wVk, lparam}); + if (is_printable && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) { + pending_responds_.push_back( + SimulatedEvent{WM_CHAR, kbdinput.wVk, lparam}); + } + return 1; + } + + std::deque pending_responds_; + TestKeystate key_state_; +}; + +// 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; +}; + +static bool test_response = false; + +typedef enum { + kKeyCallOnKey, + kKeyCallOnText, +} KeyCallType; + +typedef struct { + KeyCallType type; + + // Only one of the following fields should be assigned. + FlutterKeyEvent key_event; + std::u16string text; +} KeyCall; + +static std::vector key_calls; + +void clear_key_calls() { + for (KeyCall& key_call : key_calls) { + if (key_call.type == kKeyCallOnKey && + key_call.key_event.character != nullptr) { + delete[] key_call.key_event.character; + } + } + key_calls.clear(); +} + +std::unique_ptr> keyHandlingResponse(bool handled) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + document.SetObject(); + document.AddMember("handled", test_response, allocator); + return flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); +} + +// Returns an engine instance configured with dummy project path values, and +// overridden methods for sending platform messages, so that the engine can +// respond as if the framework were connected. +std::unique_ptr GetTestEngine() { + FlutterDesktopEngineProperties properties = {}; + properties.assets_path = L"C:\\foo\\flutter_assets"; + properties.icu_data_path = L"C:\\foo\\icudtl.dat"; + properties.aot_library_path = L"C:\\foo\\aot.so"; + FlutterProjectBundle project(properties); + auto engine = std::make_unique(project); + + EngineModifier modifier(engine.get()); + + // 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; + } + if (std::string(message->channel) == std::string("flutter/keyevent")) { + auto response = keyHandlingResponse(true); + 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; + }; + + // This mock handles key events sent through the embedder API, + // and records it in `key_calls`. + modifier.embedder_api().SendKeyEvent = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, void* user_data) { + FlutterKeyEvent clone_event = *event; + clone_event.character = event->character == nullptr + ? nullptr + : clone_string(event->character); + key_calls.push_back(KeyCall{ + .type = kKeyCallOnKey, + .key_event = clone_event, + }); + if (callback != nullptr) { + callback(test_response, user_data); + } + return kSuccess; + }; + + // 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(); + response_handle->user_data = user_data; + response_handle->callback = data_callback; + *response_out = reinterpret_cast( + 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; + return kSuccess; + }; + + // The following mocks allows RunWithEntrypoint to be run, which creates a + // non-empty FlutterEngine and enables SendKeyEvent. + + modifier.embedder_api().Run = + [](size_t version, const FlutterRendererConfig* config, + const FlutterProjectArgs* args, void* user_data, + FLUTTER_API_SYMBOL(FlutterEngine) * engine_out) { + *engine_out = reinterpret_cast(1); + + return kSuccess; + }; + modifier.embedder_api().UpdateLocales = + [](auto engine, const FlutterLocale** locales, size_t locales_count) { + return kSuccess; + }; + modifier.embedder_api().SendWindowMetricsEvent = + [](auto engine, const FlutterWindowMetricsEvent* event) { + return kSuccess; + }; + modifier.embedder_api().Shutdown = [](auto engine) { return kSuccess; }; + + engine->RunWithEntrypoint(nullptr); + return engine; +} + +class KeyboardTester { + public: + explicit KeyboardTester() { + view_ = std::make_unique(); + view_->SetEngine(std::move(GetTestEngine())); + window_ = std::make_unique( + [](const std::u16string& text) { + key_calls.push_back(KeyCall{ + .type = kKeyCallOnText, + .text = text, + }); + }); + window_->SetView(view_.get()); + } + + KeyboardTester& Responding(bool response) { + test_response = response; + return *this; + } + + LRESULT InjectWindowMessage(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + return window_->InjectWindowMessage(message, wparam, lparam); + } + + private: + std::unique_ptr view_; + std::unique_ptr window_; +}; + +constexpr uint64_t kScanCodeKeyA = 0x1e; +// constexpr uint64_t kScanCodeNumpad1 = 0x4f; +// constexpr uint64_t kScanCodeNumLock = 0x45; +// constexpr uint64_t kScanCodeControl = 0x1d; +// constexpr uint64_t kScanCodeShiftLeft = 0x2a; +// constexpr uint64_t kScanCodeShiftRight = 0x36; + +constexpr uint64_t kVirtualKeyA = 0x41; + +constexpr bool kExtended = true; +constexpr bool kNotExtended = false; +constexpr bool kWasDown = true; +constexpr bool kWasUp = false; +constexpr bool kSynthesized = true; +constexpr bool kNotSynthesized = false; + +} // namespace + +#define EXPECT_CALL_IS_EVENT(_key_call, ...) \ + EXPECT_EQ(_key_call.type, kKeyCallOnKey); \ + EXPECT_EVENT_EQUALS(_key_call.key_event, __VA_ARGS__); + +TEST(KeyboardTest, LowerCaseA) { + KeyboardTester tester; + + tester.Responding(true).InjectWindowMessage( + WM_KEYDOWN, kVirtualKeyA, + CreateKeyEventLparam(kScanCodeKeyA, kNotExtended, kWasUp)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA, + kLogicalKeyA, "A", kNotSynthesized); + clear_key_calls(); + + tester.Responding(true).InjectWindowMessage( + WM_KEYUP, kVirtualKeyA, + CreateKeyEventLparam(kScanCodeKeyA, kNotExtended, kWasDown)); + + EXPECT_EQ(key_calls.size(), 1); + EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA, + kLogicalKeyA, "", kNotSynthesized); + clear_key_calls(); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/test_keyboard.cc b/shell/platform/windows/testing/test_keyboard.cc new file mode 100644 index 0000000000000..a28aa72067c7f --- /dev/null +++ b/shell/platform/windows/testing/test_keyboard.cc @@ -0,0 +1,21 @@ +// 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/testing/test_keyboard.h" + +namespace flutter { +namespace testing { + +char* clone_string(const char* string) { + if (string == nullptr) { + return nullptr; + } + size_t len = strlen(string); + char* result = new char[len + 1]; + strcpy(result, string); + return result; +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/test_keyboard.h b/shell/platform/windows/testing/test_keyboard.h new file mode 100644 index 0000000000000..a7728ce015b03 --- /dev/null +++ b/shell/platform/windows/testing/test_keyboard.h @@ -0,0 +1,74 @@ +// 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_TESTING_TEST_KEYBOARD_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_TEST_KEYBOARD_H_ + +#include + +#include "flutter/shell/platform/embedder/embedder.h" + +#include "gtest/gtest.h" + +#define _RETURN_IF_NOT_EQUALS(val1, val2) \ + if ((val1) != (val2)) { \ + return ::testing::AssertionFailure() \ + << "Expected equality of these values:\n " #val1 "\n To be: " \ + << val2 << "\n Actual: \n " << val1; \ + } + +namespace flutter { +namespace testing { + +namespace { + +std::string _print_character(const char* s) { + if (s == nullptr) { + return "nullptr"; + } + return std::string("\"") + s + "\""; +} + +::testing::AssertionResult _EventEquals(const char* expr_event, + const char* expr_expected, + const FlutterKeyEvent& event, + const FlutterKeyEvent& expected) { + _RETURN_IF_NOT_EQUALS(event.struct_size, sizeof(FlutterKeyEvent)); + _RETURN_IF_NOT_EQUALS(event.type, expected.type); + _RETURN_IF_NOT_EQUALS(event.physical, expected.physical); + _RETURN_IF_NOT_EQUALS(event.logical, expected.logical); + if ((event.character == nullptr) != (expected.character == nullptr) || + strcmp(event.character, expected.character) != 0) { + return ::testing::AssertionFailure() + << "Expected equality of these values:\n expected.character\n " + << _print_character(expected.character) << "\n Actual: \n " + << _print_character(event.character); + } + _RETURN_IF_NOT_EQUALS(event.synthesized, expected.synthesized); + return ::testing::AssertionSuccess(); +} + +} // namespace + +// Clone string onto the heap. +// +// If #string is nullptr, returns nullptr. Otherwise, the returned pointer must +// be freed with delete[]. +char* clone_string(const char* string); + +} // namespace testing +} // namespace flutter + +#define EXPECT_EVENT_EQUALS(_target, _type, _physical, _logical, _character, \ + _synthesized) \ + EXPECT_PRED_FORMAT2(_EventEquals, _target, \ + (FlutterKeyEvent{ \ + .type = _type, \ + .physical = _physical, \ + .logical = _logical, \ + .character = _character, \ + .synthesized = _synthesized, \ + })); + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_TEST_KEYBOARD_H_ diff --git a/shell/platform/windows/window_win32.cc b/shell/platform/windows/window_win32.cc index f7c8e231a3a48..1ed1333f45f16 100644 --- a/shell/platform/windows/window_win32.cc +++ b/shell/platform/windows/window_win32.cc @@ -172,6 +172,25 @@ void WindowWin32::UpdateCursorRect(const Rect& rect) { text_input_manager_.UpdateCaretRect(rect); } +static uint16_t ResolveKeyCode(uint16_t original, + bool extended, + uint8_t scancode) { + switch (original) { + case VK_SHIFT: + case VK_LSHIFT: + return MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX); + ; + case VK_MENU: + case VK_LMENU: + return extended ? VK_RMENU : VK_LMENU; + case VK_CONTROL: + case VK_LCONTROL: + return extended ? VK_RCONTROL : VK_LCONTROL; + default: + return original; + } +} + LRESULT WindowWin32::HandleMessage(UINT const message, WPARAM const wparam, @@ -313,6 +332,9 @@ WindowWin32::HandleMessage(UINT const message, case WM_SYSDEADCHAR: case WM_CHAR: case WM_SYSCHAR: { + if (ignore_next_event) { + break; + } static wchar_t s_pending_high_surrogate = 0; wchar_t character = static_cast(wparam); @@ -329,10 +351,11 @@ WindowWin32::HandleMessage(UINT const message, s_pending_high_surrogate = 0; } - // All key presses that generate a character should be sent from + // Key presses that generate a non-surrogate should be sent from // WM_CHAR. In order to send the full key press information, the keycode // is persisted in keycode_for_char_message_ obtained from WM_KEYDOWN. - if (keycode_for_char_message_ != 0) { + if (keycode_for_char_message_ != 0 && + (IS_HIGH_SURROGATE(character) || IS_LOW_SURROGATE(character))) { const unsigned int scancode = (lparam >> 16) & 0xff; const bool extended = ((lparam >> 24) & 0x01) == 0x01; const bool was_down = lparam & 0x40000000; @@ -366,19 +389,16 @@ WindowWin32::HandleMessage(UINT const message, case WM_SYSKEYDOWN: case WM_KEYUP: case WM_SYSKEYUP: + handled_for_char_message_ = false; const bool is_keydown_message = (message == WM_KEYDOWN || message == WM_SYSKEYDOWN); - // Check if this key produces a character. If so, the key press should - // be sent with the character produced at WM_CHAR. Store the produced + // Check if this key produces a surrogate. If so, the key press should + // be resolved with the character produced at WM_CHAR. Store the produced // keycode (it's not accessible from WM_CHAR) to be used in WM_CHAR. - // - // Messages with Control or Win modifiers down are never considered as - // character messages. This allows key combinations such as "CTRL + Digit" - // to properly produce key down events even though `MapVirtualKey` returns - // a valid character. See https://github.com/flutter/flutter/issues/85587. unsigned int character = MapVirtualKey(wparam, MAPVK_VK_TO_CHAR); - if (character > 0 && is_keydown_message && GetKeyState(VK_CONTROL) >= 0 && - GetKeyState(VK_LWIN) >= 0 && GetKeyState(VK_RWIN) >= 0) { + keycode_for_char_message_ = 0; + if (character > 0 && is_keydown_message && + (IS_HIGH_SURROGATE(character) || IS_LOW_SURROGATE(character))) { keycode_for_char_message_ = wparam; break; } @@ -386,13 +406,14 @@ WindowWin32::HandleMessage(UINT const message, const unsigned int scancode = (lparam >> 16) & 0xff; const bool extended = ((lparam >> 24) & 0x01) == 0x01; // If the key is a modifier, get its side. - if (keyCode == VK_SHIFT || keyCode == VK_MENU || keyCode == VK_CONTROL) { - keyCode = MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX); - } + keyCode = ResolveKeyCode(keyCode, extended, scancode); const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP; const bool was_down = lparam & 0x40000000; - if (OnKey(keyCode, scancode, action, 0, extended, was_down)) { + if (OnKey(keyCode, scancode, action, character, extended, was_down)) { + handled_for_char_message_ = true; return 0; + } else { + ignore_next_event = true; } break; } diff --git a/shell/platform/windows/window_win32.h b/shell/platform/windows/window_win32.h index 37d8d9aa18719..ca7a975e0ffbc 100644 --- a/shell/platform/windows/window_win32.h +++ b/shell/platform/windows/window_win32.h @@ -194,6 +194,12 @@ class WindowWin32 { // message. int keycode_for_char_message_ = 0; + // Keeps track of the event result produced by a WM_KEYDOWN or WM_SYSKEYDOWN + // message. + bool handled_for_char_message_ = false; + + bool ignore_next_event = false; + // Manages IME state. TextInputManagerWin32 text_input_manager_; }; diff --git a/shell/platform/windows/window_win32_unittests.cc b/shell/platform/windows/window_win32_unittests.cc index ce54155cd7a96..9947a2c9efc6d 100644 --- a/shell/platform/windows/window_win32_unittests.cc +++ b/shell/platform/windows/window_win32_unittests.cc @@ -74,12 +74,13 @@ TEST(MockWin32Window, KeyUp) { TEST(MockWin32Window, KeyDownPrintable) { MockWin32Window window; LPARAM lparam = CreateKeyEventLparam(30); - // OnKey shouldn't be called until the WM_CHAR message. - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); + // OnKey shoulde called during the WM_KEYDOWN message. + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); // send a "A" key down event. window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); + // OnKey shouldn't be called during the WM_CHAR message. + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); EXPECT_CALL(window, OnText(_)).Times(1); window.InjectWindowMessage(WM_CHAR, 65, lparam); } @@ -97,7 +98,7 @@ TEST(MockWin32Window, KeyDownWithCtrl) { // Expect OnKey, but not OnText, because Control + Key is not followed by // WM_CHAR - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 0, false, true)).Times(1); + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 'A', false, true)).Times(1); EXPECT_CALL(window, OnText(_)).Times(0); window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); @@ -117,12 +118,13 @@ TEST(MockWin32Window, KeyDownWithCtrlToggled) { LPARAM lparam = CreateKeyEventLparam(30); - // OnKey shouldn't be called until the WM_CHAR message. - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); + // OnKey should be called during the WM_KEYDOWN message. + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); // send a "A" key down event. window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); - EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(1); + // OnKey shouldn't be called during the WM_CHAR message. + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false, true)).Times(0); EXPECT_CALL(window, OnText(_)).Times(1); window.InjectWindowMessage(WM_CHAR, 65, lparam);