diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 41ed21d3bff57..40a5df92483d1 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1475,6 +1475,7 @@ FILE: ../../../flutter/shell/platform/windows/flutter_windows_win32.cc FILE: ../../../flutter/shell/platform/windows/flutter_windows_winuwp.cc FILE: ../../../flutter/shell/platform/windows/key_event_handler.cc FILE: ../../../flutter/shell/platform/windows/key_event_handler.h +FILE: ../../../flutter/shell/platform/windows/key_event_handler_unittests.cc FILE: ../../../flutter/shell/platform/windows/keyboard_hook_handler.h FILE: ../../../flutter/shell/platform/windows/platform_handler.cc FILE: ../../../flutter/shell/platform/windows/platform_handler.h diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index e9e33d6efa518..a1ce9f169c8b8 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -167,11 +167,14 @@ if (target_os == "winuwp") { "flutter_project_bundle_unittests.cc", "flutter_windows_engine_unittests.cc", "flutter_windows_texture_registrar_unittests.cc", + "key_event_handler_unittests.cc", "string_conversion_unittests.cc", "system_utils_unittests.cc", "testing/engine_embedder_api_modifier.h", "testing/mock_win32_window.cc", "testing/mock_win32_window.h", + "testing/mock_window_binding_handler.cc", + "testing/mock_window_binding_handler.h", "testing/win32_flutter_window_test.cc", "testing/win32_flutter_window_test.h", "win32_dpi_utils_unittests.cc", diff --git a/shell/platform/windows/flutter_windows_engine_unittests.cc b/shell/platform/windows/flutter_windows_engine_unittests.cc index 5837e32330200..fa6d87e72b186 100644 --- a/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -102,7 +102,7 @@ TEST(FlutterWindowsEngine, SendPlatformMessageWithoutResponse) { const char* channel = "test"; const std::vector test_message = {1, 2, 3, 4}; - // Without a respones, SendPlatformMessage should be a simple passthrough. + // Without a response, SendPlatformMessage should be a simple pass-through. bool called = false; modifier.embedder_api().SendPlatformMessage = MOCK_ENGINE_PROC( SendPlatformMessage, ([&called, test_message](auto engine, auto message) { diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index a9315d6f58cda..a5c709357305b 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -35,10 +35,7 @@ void FlutterWindowsView::SetEngine( // Set up the system channel handlers. auto internal_plugin_messenger = internal_plugin_registrar_->messenger(); - keyboard_hook_handlers_.push_back( - std::make_unique(internal_plugin_messenger)); - keyboard_hook_handlers_.push_back(std::make_unique( - internal_plugin_messenger, this)); + RegisterKeyboardHookHandlers(internal_plugin_messenger); platform_handler_ = PlatformHandler::Create(internal_plugin_messenger, this); cursor_handler_ = std::make_unique( internal_plugin_messenger, binding_handler_.get()); @@ -49,6 +46,18 @@ void FlutterWindowsView::SetEngine( binding_handler_->GetDpiScale()); } +void FlutterWindowsView::RegisterKeyboardHookHandlers( + flutter::BinaryMessenger* messenger) { + AddKeyboardHookHandler(std::make_unique(messenger)); + AddKeyboardHookHandler( + std::make_unique(messenger, this)); +} + +void FlutterWindowsView::AddKeyboardHookHandler( + std::unique_ptr handler) { + keyboard_hook_handlers_.push_back(std::move(handler)); +} + uint32_t FlutterWindowsView::GetFrameBufferId(size_t width, size_t height) { // Called on an engine-controlled (non-platform) thread. std::unique_lock lock(resize_mutex_); @@ -120,11 +129,12 @@ void FlutterWindowsView::OnText(const std::u16string& text) { SendText(text); } -void FlutterWindowsView::OnKey(int key, +bool FlutterWindowsView::OnKey(int key, int scancode, int action, - char32_t character) { - SendKey(key, scancode, action, character); + char32_t character, + bool extended) { + return SendKey(key, scancode, action, character, extended); } void FlutterWindowsView::OnScroll(double x, @@ -215,13 +225,19 @@ void FlutterWindowsView::SendText(const std::u16string& text) { } } -void FlutterWindowsView::SendKey(int key, +bool FlutterWindowsView::SendKey(int key, int scancode, int action, - char32_t character) { + char32_t character, + bool extended) { for (const auto& handler : keyboard_hook_handlers_) { - handler->KeyboardHook(this, key, scancode, action, character); + if (handler->KeyboardHook(this, key, scancode, action, character, + extended)) { + // key event was handled, so don't send to other handlers. + return true; + } } + return false; } void FlutterWindowsView::SendScroll(double x, diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index d6373016c505e..0998f04912685 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -38,14 +38,14 @@ inline constexpr uint32_t kWindowFrameBufferID = 0; class FlutterWindowsView : public WindowBindingHandlerDelegate, public TextInputPluginDelegate { public: - // Creates a FlutterWindowsView with the given implementator of + // Creates a FlutterWindowsView with the given implementor of // WindowBindingHandler. // // In order for object to render Flutter content the SetEngine method must be // called with a valid FlutterWindowsEngine instance. FlutterWindowsView(std::unique_ptr window_binding); - ~FlutterWindowsView(); + virtual ~FlutterWindowsView(); // Configures the window instance with an instance of a running Flutter // engine. @@ -100,7 +100,11 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, void OnText(const std::u16string&) override; // |WindowBindingHandlerDelegate| - void OnKey(int key, int scancode, int action, char32_t character) override; + bool OnKey(int key, + int scancode, + int action, + char32_t character, + bool extended) override; // |WindowBindingHandlerDelegate| void OnScroll(double x, @@ -112,6 +116,15 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // |TextInputPluginDelegate| void OnCursorRectUpdated(const Rect& rect) override; + protected: + // Called to create the keyboard hook handlers. + virtual void RegisterKeyboardHookHandlers( + flutter::BinaryMessenger* messenger); + + // Used by RegisterKeyboardHookHandlers to add a new keyboard hook handler. + void AddKeyboardHookHandler( + std::unique_ptr handler); + private: // Struct holding the mouse state. The engine doesn't keep track of which // mouse buttons have been pressed, so it's the embedding's responsibility. @@ -166,7 +179,11 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, void SendText(const std::u16string&); // Reports a raw keyboard message to Flutter engine. - void SendKey(int key, int scancode, int action, char32_t character); + bool SendKey(int key, + int scancode, + int action, + char32_t character, + bool extended); // Reports scroll wheel events to Flutter engine. void SendScroll(double x, diff --git a/shell/platform/windows/key_event_handler.cc b/shell/platform/windows/key_event_handler.cc index 9b562f8f0944e..b4b1b8149c3fd 100644 --- a/shell/platform/windows/key_event_handler.cc +++ b/shell/platform/windows/key_event_handler.cc @@ -10,6 +10,10 @@ #include "flutter/shell/platform/common/cpp/json_message_codec.h" +namespace flutter { + +namespace { + static constexpr char kChannelName[] = "flutter/keyevent"; static constexpr char kKeyCodeKey[] = "keyCode"; @@ -18,33 +22,35 @@ static constexpr char kCharacterCodePointKey[] = "characterCodePoint"; static constexpr char kModifiersKey[] = "modifiers"; static constexpr char kKeyMapKey[] = "keymap"; static constexpr char kTypeKey[] = "type"; +static constexpr char kHandledKey[] = "handled"; static constexpr char kWindowsKeyMap[] = "windows"; static constexpr char kKeyUp[] = "keyup"; static constexpr char kKeyDown[] = "keydown"; -namespace flutter { +// The maximum number of pending events to keep before +// emitting a warning on the console about unhandled events. +static constexpr int kMaxPendingEvents = 1000; // Re-definition of the modifiers for compatibility with the Flutter framework. // These have to be in sync with the framework's RawKeyEventDataWindows // modifiers definition. // https://github.com/flutter/flutter/blob/19ff596979e407c484a32f4071420fca4f4c885f/packages/flutter/lib/src/services/raw_keyboard_windows.dart#L203 -const int kShift = 1 << 0; -const int kShiftLeft = 1 << 1; -const int kShiftRight = 1 << 2; -const int kControl = 1 << 3; -const int kControlLeft = 1 << 4; -const int kControlRight = 1 << 5; -const int kAlt = 1 << 6; -const int kAltLeft = 1 << 7; -const int kAltRight = 1 << 8; -const int kWinLeft = 1 << 9; -const int kWinRight = 1 << 10; -const int kCapsLock = 1 << 11; -const int kNumLock = 1 << 12; -const int kScrollLock = 1 << 13; +static constexpr int kShift = 1 << 0; +static constexpr int kShiftLeft = 1 << 1; +static constexpr int kShiftRight = 1 << 2; +static constexpr int kControl = 1 << 3; +static constexpr int kControlLeft = 1 << 4; +static constexpr int kControlRight = 1 << 5; +static constexpr int kAlt = 1 << 6; +static constexpr int kAltLeft = 1 << 7; +static constexpr int kAltRight = 1 << 8; +static constexpr int kWinLeft = 1 << 9; +static constexpr int kWinRight = 1 << 10; +static constexpr int kCapsLock = 1 << 11; +static constexpr int kNumLock = 1 << 12; +static constexpr int kScrollLock = 1 << 13; -namespace { /// Calls GetKeyState() an all modifier keys and packs the result in an int, /// with the re-defined values declared above for compatibility with the Flutter /// framework. @@ -81,25 +87,130 @@ int GetModsForKeyState() { mods |= kScrollLock; return mods; } + +// This uses event data instead of generating a serial number because +// information can't be attached to the redispatched events, so it has to be +// possible to compute an ID from the identifying data in the event when it is +// received again in order to differentiate between events that are new, and +// events that have been redispatched. +// +// Another alternative would be to compute a checksum from all the data in the +// event (just compute it over the bytes in the struct, probably skipping +// timestamps), but the fields used below are enough to differentiate them, and +// since Windows does some processing on the events (coming up with virtual key +// codes, setting timestamps, etc.), it's not clear that the redispatched +// events would have the same checksums. +uint64_t CalculateEventId(int scancode, int action, bool extended) { + // Calculate a key event ID based on the scan code of the key pressed, + // and the flags we care about. + return scancode | (((action == WM_KEYUP ? KEYEVENTF_KEYUP : 0x0) | + (extended ? KEYEVENTF_EXTENDEDKEY : 0x0)) + << 16); +} + } // namespace -KeyEventHandler::KeyEventHandler(flutter::BinaryMessenger* messenger) +KeyEventHandler::KeyEventHandler(flutter::BinaryMessenger* messenger, + KeyEventHandler::SendInputDelegate send_input) : channel_( std::make_unique>( messenger, kChannelName, - &flutter::JsonMessageCodec::GetInstance())) {} + &flutter::JsonMessageCodec::GetInstance())), + send_input_(send_input) { + assert(send_input != nullptr); +} KeyEventHandler::~KeyEventHandler() = default; void KeyEventHandler::TextHook(FlutterWindowsView* view, const std::u16string& code_point) {} -void KeyEventHandler::KeyboardHook(FlutterWindowsView* view, +KEYBDINPUT* KeyEventHandler::FindPendingEvent(uint64_t id) { + if (pending_events_.empty()) { + return nullptr; + } + for (auto iter = pending_events_.begin(); iter != pending_events_.end(); + ++iter) { + if (iter->first == id) { + return &iter->second; + } + } + return nullptr; +} + +void KeyEventHandler::RemovePendingEvent(uint64_t id) { + for (auto iter = pending_events_.begin(); iter != pending_events_.end(); + ++iter) { + if (iter->first == id) { + pending_events_.erase(iter); + return; + } + } + std::cerr << "Tried to remove pending event with id " << id + << ", but the event was not found." << std::endl; +} + +void KeyEventHandler::AddPendingEvent(uint64_t id, + int scancode, + int action, + bool extended) { + if (pending_events_.size() > kMaxPendingEvents) { + std::cerr + << "There are " << pending_events_.size() + << " keyboard events that have not yet received a response from the " + << "framework. Are responses being sent?" << std::endl; + } + KEYBDINPUT key_event = KEYBDINPUT{0}; + key_event.wScan = scancode; + key_event.dwFlags = KEYEVENTF_SCANCODE | + (extended ? KEYEVENTF_EXTENDEDKEY : 0x0) | + (action == WM_KEYUP ? KEYEVENTF_KEYUP : 0x0); + pending_events_.push_back(std::make_pair(id, key_event)); +} + +void KeyEventHandler::HandleResponse(bool handled, + uint64_t id, + int action, + bool extended, + int scancode, + int character) { + if (handled) { + this->RemovePendingEvent(id); + } else { + // Since the framework didn't handle the event, we inject a newly + // synthesized one. We let Windows figure out the virtual key and + // character for the given scancode, as well as a new timestamp. + const KEYBDINPUT* key_event = this->FindPendingEvent(id); + if (key_event == nullptr) { + std::cerr << "Unable to find event " << id << " in pending events queue."; + return; + } + INPUT input_event; + input_event.type = INPUT_KEYBOARD; + input_event.ki = *key_event; + UINT accepted = send_input_(1, &input_event, sizeof(input_event)); + if (accepted != 1) { + std::cerr << "Unable to synthesize event for unhandled keyboard event " + "with scancode " + << scancode << " (character " << character << ")" << std::endl; + } + } +} + +bool KeyEventHandler::KeyboardHook(FlutterWindowsView* view, int key, int scancode, int action, - char32_t character) { + char32_t character, + bool extended) { + const uint64_t id = CalculateEventId(scancode, action, extended); + if (FindPendingEvent(id) != nullptr) { + // Don't pass messages that we synthesized to the framework again. + RemovePendingEvent(id); + return false; + } + // TODO: Translate to a cross-platform key code system rather than passing // the native key code. rapidjson::Document event(rapidjson::kObjectType); @@ -119,9 +230,17 @@ void KeyEventHandler::KeyboardHook(FlutterWindowsView* view, break; default: std::cerr << "Unknown key event action: " << action << std::endl; - return; + return false; } - channel_->Send(event); + AddPendingEvent(id, scancode, action, extended); + channel_->Send(event, [this, id, action, extended, scancode, character]( + const uint8_t* reply, size_t reply_size) { + auto decoded = flutter::JsonMessageCodec::GetInstance().DecodeMessage( + reply, reply_size); + bool handled = (*decoded)[kHandledKey].GetBool(); + this->HandleResponse(handled, id, action, extended, scancode, character); + }); + return true; } } // namespace flutter diff --git a/shell/platform/windows/key_event_handler.h b/shell/platform/windows/key_event_handler.h index 9a1b2752288e9..86433e6f6322a 100644 --- a/shell/platform/windows/key_event_handler.h +++ b/shell/platform/windows/key_event_handler.h @@ -5,6 +5,7 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_KEY_EVENT_HANDLER_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_KEY_EVENT_HANDLER_H_ +#include #include #include @@ -23,24 +24,48 @@ class FlutterWindowsView; // Handles key events and forwards them to the Flutter engine. class KeyEventHandler : public KeyboardHookHandler { public: - explicit KeyEventHandler(flutter::BinaryMessenger* messenger); + using SendInputDelegate = + std::function; + + explicit KeyEventHandler(flutter::BinaryMessenger* messenger, + SendInputDelegate delegate = SendInput); virtual ~KeyEventHandler(); // |KeyboardHookHandler| - void KeyboardHook(FlutterWindowsView* window, + bool KeyboardHook(FlutterWindowsView* window, int key, int scancode, int action, - char32_t character) override; + char32_t character, + bool extended) override; // |KeyboardHookHandler| void TextHook(FlutterWindowsView* window, const std::u16string& text) override; private: + KEYBDINPUT* FindPendingEvent(uint64_t id); + void RemovePendingEvent(uint64_t id); + void AddPendingEvent(uint64_t id, int scancode, int action, bool extended); + void HandleResponse(bool handled, + uint64_t id, + int action, + bool extended, + int scancode, + int character); + // The Flutter system channel for key event messages. std::unique_ptr> channel_; + + // The queue of key events that have been sent to the framework but have not + // yet received a response. + std::deque> pending_events_; + + // A function used to dispatch synthesized events. Used in testing to inject a + // test function to collect events. Defaults to the Windows function + // SendInput. + SendInputDelegate send_input_; }; } // namespace flutter diff --git a/shell/platform/windows/key_event_handler_unittests.cc b/shell/platform/windows/key_event_handler_unittests.cc new file mode 100644 index 0000000000000..398afc5efc68a --- /dev/null +++ b/shell/platform/windows/key_event_handler_unittests.cc @@ -0,0 +1,118 @@ +// 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/key_event_handler.h" + +#include +#include + +#include "flutter/shell/platform/common/cpp/json_message_codec.h" +#include "flutter/shell/platform/windows/flutter_windows_view.h" +#include "flutter/shell/platform/windows/testing/test_binary_messenger.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +static constexpr char kScanCodeKey[] = "scanCode"; +static constexpr int kHandledScanCode = 20; +static constexpr int kUnhandledScanCode = 21; + +std::unique_ptr> CreateResponse(bool handled) { + auto response_doc = + std::make_unique(rapidjson::kObjectType); + auto& allocator = response_doc->GetAllocator(); + response_doc->AddMember("handled", handled, allocator); + return JsonMessageCodec::GetInstance().EncodeMessage(*response_doc); +} + +TEST(KeyEventHandlerTest, KeyboardHookHandling) { + auto handled_message = CreateResponse(true); + auto unhandled_message = CreateResponse(false); + int received_scancode = 0; + + TestBinaryMessenger messenger( + [&received_scancode, &handled_message, &unhandled_message]( + const std::string& channel, const uint8_t* message, + size_t message_size, BinaryReply reply) { + if (channel == "flutter/keyevent") { + auto message_doc = JsonMessageCodec::GetInstance().DecodeMessage( + message, message_size); + received_scancode = (*message_doc)[kScanCodeKey].GetInt(); + if (received_scancode == kHandledScanCode) { + reply(handled_message->data(), handled_message->size()); + } else { + reply(unhandled_message->data(), unhandled_message->size()); + } + } + }); + + int redispatch_scancode = 0; + KeyEventHandler handler(&messenger, + [&redispatch_scancode](UINT cInputs, LPINPUT pInputs, + int cbSize) -> UINT { + EXPECT_TRUE(cbSize > 0); + redispatch_scancode = pInputs->ki.wScan; + return 1; + }); + + handler.KeyboardHook(nullptr, 64, kHandledScanCode, WM_KEYDOWN, L'a', false); + EXPECT_EQ(received_scancode, kHandledScanCode); + EXPECT_EQ(redispatch_scancode, 0); + received_scancode = 0; + handler.KeyboardHook(nullptr, 64, kUnhandledScanCode, WM_KEYDOWN, L'b', + false); + EXPECT_EQ(received_scancode, kUnhandledScanCode); + EXPECT_EQ(redispatch_scancode, kUnhandledScanCode); +} + +TEST(KeyEventHandlerTest, ExtendedKeysAreSentToRedispatch) { + auto handled_message = CreateResponse(true); + auto unhandled_message = CreateResponse(false); + int received_scancode = 0; + bool is_extended_key = false; + + TestBinaryMessenger messenger( + [&received_scancode, &handled_message, &unhandled_message]( + const std::string& channel, const uint8_t* message, + size_t message_size, BinaryReply reply) { + if (channel == "flutter/keyevent") { + auto message_doc = JsonMessageCodec::GetInstance().DecodeMessage( + message, message_size); + received_scancode = (*message_doc)[kScanCodeKey].GetInt(); + if (received_scancode == kHandledScanCode) { + reply(handled_message->data(), handled_message->size()); + } else { + reply(unhandled_message->data(), unhandled_message->size()); + } + } + }); + + int redispatch_scancode = 0; + KeyEventHandler handler( + &messenger, + [&redispatch_scancode, &is_extended_key](UINT cInputs, LPINPUT pInputs, + int cbSize) -> UINT { + EXPECT_TRUE(cbSize > 0); + redispatch_scancode = pInputs->ki.wScan; + is_extended_key = (pInputs->ki.dwFlags & KEYEVENTF_EXTENDEDKEY) != 0; + return 1; + }); + + // Extended key flag is passed to redispatched events if set. + handler.KeyboardHook(nullptr, 64, kUnhandledScanCode, WM_KEYDOWN, L'b', true); + EXPECT_EQ(received_scancode, kUnhandledScanCode); + EXPECT_EQ(redispatch_scancode, kUnhandledScanCode); + EXPECT_EQ(is_extended_key, true); + + // Extended key flag is not passed to redispatched events if not set. + handler.KeyboardHook(nullptr, 64, kUnhandledScanCode, WM_KEYDOWN, L'b', + false); + EXPECT_EQ(received_scancode, kUnhandledScanCode); + EXPECT_EQ(redispatch_scancode, kUnhandledScanCode); + EXPECT_EQ(is_extended_key, false); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/keyboard_hook_handler.h b/shell/platform/windows/keyboard_hook_handler.h index b36ad3a6d7d82..37bcbafbeb2b3 100644 --- a/shell/platform/windows/keyboard_hook_handler.h +++ b/shell/platform/windows/keyboard_hook_handler.h @@ -19,11 +19,15 @@ class KeyboardHookHandler { virtual ~KeyboardHookHandler() = default; // A function for hooking into keyboard input. - virtual void KeyboardHook(FlutterWindowsView* view, + // + // Returns true if the key event has been handled, to indicate that other + // handlers should not be called for this event. + virtual bool KeyboardHook(FlutterWindowsView* view, int key, int scancode, int action, - char32_t character) = 0; + char32_t character, + bool extended) = 0; // A function for hooking into Unicode text input. virtual void TextHook(FlutterWindowsView* view, diff --git a/shell/platform/windows/testing/mock_win32_window.cc b/shell/platform/windows/testing/mock_win32_window.cc index 68e8e3f516f06..f17964aa3fc3c 100644 --- a/shell/platform/windows/testing/mock_win32_window.cc +++ b/shell/platform/windows/testing/mock_win32_window.cc @@ -15,10 +15,10 @@ UINT MockWin32Window::GetDpi() { return GetCurrentDPI(); } -void MockWin32Window::InjectWindowMessage(UINT const message, - WPARAM const wparam, - LPARAM const lparam) { - HandleMessage(message, wparam, lparam); +LRESULT MockWin32Window::InjectWindowMessage(UINT const message, + WPARAM const wparam, + LPARAM const lparam) { + return HandleMessage(message, wparam, lparam); } } // namespace testing diff --git a/shell/platform/windows/testing/mock_win32_window.h b/shell/platform/windows/testing/mock_win32_window.h index 224aaf9090510..dc7765e95959a 100644 --- a/shell/platform/windows/testing/mock_win32_window.h +++ b/shell/platform/windows/testing/mock_win32_window.h @@ -2,6 +2,9 @@ // 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_MOCK_WIN32_WINDOW_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WIN32_WINDOW_H_ + #include #include "flutter/shell/platform/windows/win32_window.h" @@ -24,9 +27,9 @@ class MockWin32Window : public Win32Window { UINT GetDpi(); // Simulates a WindowProc message from the OS. - void InjectWindowMessage(UINT const message, - WPARAM const wparam, - LPARAM const lparam); + LRESULT InjectWindowMessage(UINT const message, + WPARAM const wparam, + LPARAM const lparam); MOCK_METHOD1(OnDpiScale, void(unsigned int)); MOCK_METHOD2(OnResize, void(unsigned int, unsigned int)); @@ -36,9 +39,11 @@ class MockWin32Window : public Win32Window { MOCK_METHOD0(OnPointerLeave, void()); MOCK_METHOD0(OnSetCursor, void()); MOCK_METHOD1(OnText, void(const std::u16string&)); - MOCK_METHOD4(OnKey, void(int, int, int, char32_t)); + MOCK_METHOD5(OnKey, bool(int, int, int, char32_t, bool)); MOCK_METHOD2(OnScroll, void(double, double)); }; } // namespace testing } // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WIN32_WINDOW_H_ diff --git a/shell/platform/windows/testing/mock_window_binding_handler.cc b/shell/platform/windows/testing/mock_window_binding_handler.cc new file mode 100644 index 0000000000000..7b7f0e17fdae7 --- /dev/null +++ b/shell/platform/windows/testing/mock_window_binding_handler.cc @@ -0,0 +1,15 @@ +// 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/mock_window_binding_handler.h" + +namespace flutter { +namespace testing { + +MockWindowBindingHandler::MockWindowBindingHandler() : WindowBindingHandler(){}; + +MockWindowBindingHandler::~MockWindowBindingHandler() = default; + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/testing/mock_window_binding_handler.h b/shell/platform/windows/testing/mock_window_binding_handler.h new file mode 100644 index 0000000000000..50786a4b5da51 --- /dev/null +++ b/shell/platform/windows/testing/mock_window_binding_handler.h @@ -0,0 +1,38 @@ +// 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_MOCK_WINDOW_BINDING_HANDLER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ + +#include + +#include "flutter/shell/platform/windows/window_binding_handler.h" +#include "gmock/gmock.h" + +namespace flutter { +namespace testing { + +/// Mock for the Win32Window base class. +class MockWindowBindingHandler : public WindowBindingHandler { + public: + MockWindowBindingHandler(); + virtual ~MockWindowBindingHandler(); + + // Prevent copying. + MockWindowBindingHandler(MockWindowBindingHandler const&) = delete; + MockWindowBindingHandler& operator=(MockWindowBindingHandler const&) = delete; + + MOCK_METHOD1(SetView, void(WindowBindingHandlerDelegate* view)); + MOCK_METHOD0(GetRenderTarget, WindowsRenderTarget()); + MOCK_METHOD0(GetDpiScale, float()); + MOCK_METHOD0(OnWindowResized, void()); + MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds()); + MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name)); + MOCK_METHOD1(UpdateCursorRect, void(const Rect& rect)); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_ diff --git a/shell/platform/windows/testing/win32_flutter_window_test.h b/shell/platform/windows/testing/win32_flutter_window_test.h index c48bda6311a8b..bf42b6c92a0a8 100644 --- a/shell/platform/windows/testing/win32_flutter_window_test.h +++ b/shell/platform/windows/testing/win32_flutter_window_test.h @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_WIN32_FLUTTER_WINDOW_TEST_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_WIN32_FLUTTER_WINDOW_TEST_H_ #include "flutter/shell/platform/windows/win32_flutter_window.h" @@ -18,10 +19,9 @@ class Win32FlutterWindowTest : public Win32FlutterWindow { // Prevent copying. Win32FlutterWindowTest(Win32FlutterWindowTest const&) = delete; Win32FlutterWindowTest& operator=(Win32FlutterWindowTest const&) = delete; - - private: - bool on_font_change_called_ = false; }; } // namespace testing } // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_WIN32_FLUTTER_WINDOW_TEST_H_ diff --git a/shell/platform/windows/text_input_plugin.cc b/shell/platform/windows/text_input_plugin.cc index 43fbd40689a53..69c66de50ba97 100644 --- a/shell/platform/windows/text_input_plugin.cc +++ b/shell/platform/windows/text_input_plugin.cc @@ -61,13 +61,14 @@ void TextInputPlugin::TextHook(FlutterWindowsView* view, SendStateUpdate(*active_model_); } -void TextInputPlugin::KeyboardHook(FlutterWindowsView* view, +bool TextInputPlugin::KeyboardHook(FlutterWindowsView* view, int key, int scancode, int action, - char32_t character) { + char32_t character, + bool extended) { if (active_model_ == nullptr) { - return; + return false; } if (action == WM_KEYDOWN) { // Most editing keys (arrow keys, backspace, delete, etc.) are handled in @@ -80,6 +81,7 @@ void TextInputPlugin::KeyboardHook(FlutterWindowsView* view, break; } } + return false; } TextInputPlugin::TextInputPlugin(flutter::BinaryMessenger* messenger, diff --git a/shell/platform/windows/text_input_plugin.h b/shell/platform/windows/text_input_plugin.h index acf0241d3d98c..47415365507bc 100644 --- a/shell/platform/windows/text_input_plugin.h +++ b/shell/platform/windows/text_input_plugin.h @@ -33,11 +33,12 @@ class TextInputPlugin : public KeyboardHookHandler { virtual ~TextInputPlugin(); // |KeyboardHookHandler| - void KeyboardHook(FlutterWindowsView* view, + bool KeyboardHook(FlutterWindowsView* view, int key, int scancode, int action, - char32_t character) override; + char32_t character, + bool extended) override; // |KeyboardHookHandler| void TextHook(FlutterWindowsView* view, const std::u16string& text) override; diff --git a/shell/platform/windows/win32_flutter_window.cc b/shell/platform/windows/win32_flutter_window.cc index 0ec6f416549a9..5915cd03b7a32 100644 --- a/shell/platform/windows/win32_flutter_window.cc +++ b/shell/platform/windows/win32_flutter_window.cc @@ -162,11 +162,13 @@ void Win32FlutterWindow::OnText(const std::u16string& text) { binding_handler_delegate_->OnText(text); } -void Win32FlutterWindow::OnKey(int key, +bool Win32FlutterWindow::OnKey(int key, int scancode, int action, - char32_t character) { - binding_handler_delegate_->OnKey(key, scancode, action, character); + char32_t character, + bool extended) { + return binding_handler_delegate_->OnKey(key, scancode, action, character, + extended); } void Win32FlutterWindow::OnScroll(double delta_x, double delta_y) { diff --git a/shell/platform/windows/win32_flutter_window.h b/shell/platform/windows/win32_flutter_window.h index e3f02ea22d43d..3e86d9da7c1de 100644 --- a/shell/platform/windows/win32_flutter_window.h +++ b/shell/platform/windows/win32_flutter_window.h @@ -55,7 +55,11 @@ class Win32FlutterWindow : public Win32Window, public WindowBindingHandler { void OnText(const std::u16string& text) override; // |Win32Window| - void OnKey(int key, int scancode, int action, char32_t character) override; + bool OnKey(int key, + int scancode, + int action, + char32_t character, + bool extended) 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 e03470dcd34ef..8ba4b15f8d425 100644 --- a/shell/platform/windows/win32_flutter_window_unittests.cc +++ b/shell/platform/windows/win32_flutter_window_unittests.cc @@ -2,16 +2,448 @@ // 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/cpp/json_message_codec.h" +#include "flutter/shell/platform/embedder/embedder.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/testing/engine_embedder_api_modifier.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" #include "flutter/shell/platform/windows/testing/win32_flutter_window_test.h" +#include "flutter/shell/platform/windows/text_input_plugin_delegate.h" + +#include "gmock/gmock.h" #include "gtest/gtest.h" +#include + +using testing::_; +using testing::Invoke; + namespace flutter { namespace testing { +namespace { +// Creates a valid Windows LPARAM for WM_KEYDOWN and WM_CHAR from parameters +// given. +static LPARAM CreateKeyEventLparam(USHORT ScanCode, + bool extended = false, + USHORT RepeatCount = 1, + bool ContextCode = 0, + bool PreviousKeyState = 1, + bool TransitionState = 1) { + return ((LPARAM(TransitionState) << 31) | (LPARAM(PreviousKeyState) << 30) | + (LPARAM(ContextCode) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | + (LPARAM(ScanCode) << 16) | LPARAM(RepeatCount)); +} + +// A struc to hold simulated events that will be delivered after the framework +// response is handled. +struct SimulatedEvent { + UINT message; + WPARAM wparam; + LPARAM lparam; +}; + +// A key event handler that can be spied on while it forwards calls to the real +// key event handler. +class SpyKeyEventHandler : public KeyboardHookHandler { + public: + SpyKeyEventHandler(flutter::BinaryMessenger* messenger, + KeyEventHandler::SendInputDelegate delegate) { + real_implementation_ = + std::make_unique(messenger, delegate); + ON_CALL(*this, KeyboardHook(_, _, _, _, _, _)) + .WillByDefault( + Invoke(real_implementation_.get(), &KeyEventHandler::KeyboardHook)); + ON_CALL(*this, TextHook(_, _)) + .WillByDefault( + Invoke(real_implementation_.get(), &KeyEventHandler::TextHook)); + } + + MOCK_METHOD6(KeyboardHook, + bool(FlutterWindowsView* window, + int key, + int scancode, + int action, + char32_t character, + bool extended)); + MOCK_METHOD2(TextHook, + void(FlutterWindowsView* window, const std::u16string& text)); + + private: + std::unique_ptr real_implementation_; +}; + +// A text input plugin that can be spied on while it forwards calls to the real +// text input plugin. +class SpyTextInputPlugin : public KeyboardHookHandler, + public TextInputPluginDelegate { + public: + SpyTextInputPlugin(flutter::BinaryMessenger* messenger) { + real_implementation_ = std::make_unique(messenger, this); + ON_CALL(*this, KeyboardHook(_, _, _, _, _, _)) + .WillByDefault( + Invoke(real_implementation_.get(), &TextInputPlugin::KeyboardHook)); + ON_CALL(*this, TextHook(_, _)) + .WillByDefault( + Invoke(real_implementation_.get(), &TextInputPlugin::TextHook)); + } + + MOCK_METHOD6(KeyboardHook, + bool(FlutterWindowsView* window, + int key, + int scancode, + int action, + char32_t character, + bool extended)); + MOCK_METHOD2(TextHook, + void(FlutterWindowsView* window, const std::u16string& text)); + + virtual void OnCursorRectUpdated(const Rect& rect) {} + + private: + std::unique_ptr real_implementation_; +}; + +class MockWin32FlutterWindow : public Win32FlutterWindow { + public: + MockWin32FlutterWindow() : Win32FlutterWindow(800, 600) {} + virtual ~MockWin32FlutterWindow() {} + + // Prevent copying. + MockWin32FlutterWindow(MockWin32FlutterWindow const&) = delete; + MockWin32FlutterWindow& operator=(MockWin32FlutterWindow 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); + } + + 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)); +}; + +// A FlutterWindowsView that overrides the RegisterKeyboardHookHandlers function +// to register the keyboard hook handlers that can be spied upon. +class TestFlutterWindowsView : public FlutterWindowsView { + public: + TestFlutterWindowsView(std::unique_ptr window_binding, + WPARAM virtual_key, + bool is_printable = true) + : FlutterWindowsView(std::move(window_binding)), + virtual_key_(virtual_key), + is_printable_(is_printable) {} + + SpyKeyEventHandler* key_event_handler; + SpyTextInputPlugin* text_input_plugin; + + void InjectPendingEvents(MockWin32FlutterWindow* win32window) { + while (pending_events_.size() > 0) { + SimulatedEvent event = pending_events_.front(); + win32window->InjectWindowMessage(event.message, event.wparam, + event.lparam); + pending_events_.pop_front(); + } + } + + protected: + void RegisterKeyboardHookHandlers( + flutter::BinaryMessenger* messenger) override { + auto spy_key_event_handler = std::make_unique( + messenger, [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { + return this->SendInput(cInputs, pInputs, cbSize); + }); + auto spy_text_input_plugin = + std::make_unique(messenger); + key_event_handler = spy_key_event_handler.get(); + text_input_plugin = spy_text_input_plugin.get(); + AddKeyboardHookHandler(std::move(spy_key_event_handler)); + AddKeyboardHookHandler(std::move(spy_text_input_plugin)); + } + + 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 LPARAM lparam = CreateKeyEventLparam( + kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY); + // 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 + // Windows will fill it in). + pending_events_.push_back(SimulatedEvent{message, virtual_key_, lparam}); + if (is_printable_ && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) { + pending_events_.push_back(SimulatedEvent{WM_CHAR, virtual_key_, lparam}); + } + return 1; + } + + std::deque pending_events_; + WPARAM virtual_key_; + bool is_printable_; +}; + +// 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; +}; + +// The static value to return as the "handled" value from the framework for key +// events. Individual tests set this to change the framework response that the +// test engine simulates. +static bool test_response = false; + +// 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); + + EngineEmbedderApiModifier modifier(engine.get()); + // Force the non-AOT path unless overridden by the test. + modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; + + 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().SendPlatformMessage = + [](FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterPlatformMessage* message) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + document.SetObject(); + document.AddMember("handled", test_response, allocator); + auto encoded = + flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); + const TestResponseHandle* response_handle = + reinterpret_cast( + message->response_handle); + if (response_handle->callback != nullptr) { + response_handle->callback(encoded->data(), encoded->size(), + response_handle->user_data); + } + 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; + }; + + return engine; +} + +} // namespace + TEST(Win32FlutterWindowTest, CreateDestroy) { Win32FlutterWindowTest window(800, 600); ASSERT_TRUE(TRUE); } +// Tests key event propagation of non-printable, non-modifier key down events. +TEST(Win32FlutterWindowTest, NonPrintableKeyDownPropagation) { + constexpr WPARAM virtual_key = VK_LEFT; + constexpr WPARAM scan_code = 10; + constexpr char32_t character = 0; + MockWin32FlutterWindow win32window; + std::deque pending_events; + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + TestFlutterWindowsView flutter_windows_view( + std::move(window_binding_handler), virtual_key, false /* is_printable */); + win32window.SetView(&flutter_windows_view); + LPARAM lparam = CreateKeyEventLparam(scan_code); + + // 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 /* extended */)) + .Times(2) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) + .Times(0); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) + .Times(0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } + + // 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 /* extended */)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _)) + .Times(0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } +} + +// Tests key event propagation of printable character key down events. These +// differ from non-printable characters in that they follow a different code +// path in the WndProc (HandleMessage), producing a follow-on WM_CHAR event. +TEST(Win32FlutterWindowTest, CharKeyDownPropagation) { + constexpr WPARAM virtual_key = 65; // The "A" key, which produces a character + constexpr WPARAM scan_code = 30; + constexpr char32_t character = 65; + + MockWin32FlutterWindow win32window; + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + TestFlutterWindowsView flutter_windows_view( + std::move(window_binding_handler), virtual_key, true /* is_printable */); + win32window.SetView(&flutter_windows_view); + LPARAM lparam = CreateKeyEventLparam(scan_code); + + // 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)) + .Times(2) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } + + // 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 */)) + .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); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) + .Times(0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } +} + +// Tests key event propagation of modifier key down events. This are different +// from non-printable events in that they call MapVirtualKey, resulting in a +// slightly different code path. +TEST(Win32FlutterWindowTest, ModifierKeyDownPropagation) { + constexpr WPARAM virtual_key = VK_LSHIFT; + constexpr WPARAM scan_code = 20; + constexpr char32_t character = 0; + MockWin32FlutterWindow win32window; + std::deque pending_events; + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + TestFlutterWindowsView flutter_windows_view( + std::move(window_binding_handler), virtual_key, false /* is_printable */); + win32window.SetView(&flutter_windows_view); + LPARAM lparam = CreateKeyEventLparam(scan_code); + + // 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 /* extended */)) + .Times(2) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) + .Times(0); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) + .Times(0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } + + // 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 /* extended */)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(*flutter_windows_view.text_input_plugin, + KeyboardHook(_, _, _, _, _, _)) + .Times(0); + EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), + 0); + flutter_windows_view.InjectPendingEvents(&win32window); + } +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/win32_window.cc b/shell/platform/windows/win32_window.cc index 7ed1f7c0ccd6f..01432cfb0f6fe 100644 --- a/shell/platform/windows/win32_window.cc +++ b/shell/platform/windows/win32_window.cc @@ -225,6 +225,26 @@ Win32Window::HandleMessage(UINT const message, s_pending_high_surrogate = 0; } + // All key presses that generate a character 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) { + const unsigned int scancode = (lparam >> 16) & 0xff; + const bool extended = ((lparam >> 24) & 0x01) == 0x01; + + bool handled = OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN, + code_point, extended); + keycode_for_char_message_ = 0; + if (handled) { + // If the OnKey handler handles the message, then return so we don't + // pass it to OnText, because handling the message indicates that + // OnKey either just sent it to the framework to be processed, or the + // framework handled the key in its response, so it shouldn't also be + // added as text. + return 0; + } + } + // Of the messages handled here, only WM_CHAR should be treated as // characters. WM_SYS*CHAR are not part of text input, and WM_DEADCHAR // will be incorporated into a later WM_CHAR with the full character. @@ -236,15 +256,6 @@ Win32Window::HandleMessage(UINT const message, character >= u' ') { OnText(text); } - - // All key presses that generate a character 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) { - const unsigned int scancode = (lparam >> 16) & 0xff; - OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN, code_point); - keycode_for_char_message_ = 0; - } break; } case WM_KEYDOWN: @@ -263,12 +274,15 @@ Win32Window::HandleMessage(UINT const message, } unsigned int keyCode(wparam); 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); } const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP; - OnKey(keyCode, scancode, action, 0); + if (OnKey(keyCode, scancode, action, 0, extended)) { + return 0; + } break; } diff --git a/shell/platform/windows/win32_window.h b/shell/platform/windows/win32_window.h index 453bc0e90480f..2d964bb62540f 100644 --- a/shell/platform/windows/win32_window.h +++ b/shell/platform/windows/win32_window.h @@ -92,7 +92,14 @@ class Win32Window { virtual void OnText(const std::u16string& text) = 0; // Called when raw keyboard input occurs. - virtual void OnKey(int key, int scancode, int action, char32_t character) = 0; + // + // Returns true if the event was handled, indicating that DefWindowProc should + // not be called on the event by the main message loop. + virtual bool OnKey(int key, + int scancode, + int action, + char32_t character, + bool extended) = 0; // Called when mouse scrollwheel input occurs. virtual void OnScroll(double delta_x, double delta_y) = 0; diff --git a/shell/platform/windows/win32_window_unittests.cc b/shell/platform/windows/win32_window_unittests.cc index ea860114e53ed..325d0ca81315e 100644 --- a/shell/platform/windows/win32_window_unittests.cc +++ b/shell/platform/windows/win32_window_unittests.cc @@ -5,8 +5,26 @@ #include "flutter/shell/platform/windows/testing/mock_win32_window.h" #include "gtest/gtest.h" +using testing::_; + namespace flutter { namespace testing { +namespace { + +// Creates a valid Windows LPARAM for WM_KEYDOWN and WM_KEYUP from parameters +// given. +static LPARAM CreateKeyEventLparam(USHORT ScanCode, + bool extended = false, + USHORT RepeatCount = 1, + bool ContextCode = 0, + bool PreviousKeyState = 1, + bool TransitionState = 1) { + return ((LPARAM(TransitionState) << 31) | (LPARAM(PreviousKeyState) << 30) | + (LPARAM(ContextCode) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | + (LPARAM(ScanCode) << 16) | LPARAM(RepeatCount)); +} + +} // namespace TEST(MockWin32Window, CreateDestroy) { MockWin32Window window; @@ -37,5 +55,34 @@ TEST(MockWin32Window, HorizontalScroll) { window.InjectWindowMessage(WM_MOUSEHWHEEL, MAKEWPARAM(0, scroll_amount), 0); } +TEST(MockWin32Window, KeyDown) { + MockWin32Window window; + EXPECT_CALL(window, OnKey(_, _, _, _, _)).Times(1); + LPARAM lparam = CreateKeyEventLparam(42); + // send a "Shift" key down event. + window.InjectWindowMessage(WM_KEYDOWN, 16, lparam); +} + +TEST(MockWin32Window, KeyUp) { + MockWin32Window window; + EXPECT_CALL(window, OnKey(_, _, _, _, _)).Times(1); + LPARAM lparam = CreateKeyEventLparam(42); + // send a "Shift" key up event. + window.InjectWindowMessage(WM_KEYUP, 16, lparam); +} + +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)).Times(0); + // send a "A" key down event. + window.InjectWindowMessage(WM_KEYDOWN, 65, lparam); + + EXPECT_CALL(window, OnKey(65, 30, WM_KEYDOWN, 65, false)).Times(1); + EXPECT_CALL(window, OnText(_)).Times(1); + window.InjectWindowMessage(WM_CHAR, 65, lparam); +} + } // namespace testing } // namespace flutter diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index 61218822ca7ce..8ea914bde3e9e 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -41,9 +41,14 @@ class WindowBindingHandlerDelegate { // Typically called by currently configured WindowBindingHandler virtual void OnText(const std::u16string&) = 0; - // Notifies delegate that backing window size has received key press. - // Typically called by currently configured WindowBindingHandler - virtual void OnKey(int key, int scancode, int action, char32_t character) = 0; + // Notifies delegate that backing window size has received key press. Should + // return true if the event was handled and should not be propagated. + // Typically called by currently configured WindowBindingHandler. + virtual bool OnKey(int key, + int scancode, + int action, + char32_t character, + bool extended) = 0; // Notifies delegate that backing window size has recevied scroll. // Typically called by currently configured WindowBindingHandler