diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index ce45fcf252124b4a6b34a990f009f52d7d37cc21..c967b4e1790344a91e84e2ddbeae60d8a525ecc8 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1465,10 +1465,20 @@ FILE: ../../../flutter/shell/platform/linux/fl_json_message_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_json_message_codec_test.cc FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_json_method_codec_test.cc -FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.cc -FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin.h -FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin_private.h -FILE: ../../../flutter/shell/platform/linux/fl_key_event_plugin_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_channel_responder.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_channel_responder.h +FILE: ../../../flutter/shell/platform/linux/fl_key_channel_responder_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_embedder_responder.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_embedder_responder.h +FILE: ../../../flutter/shell/platform/linux/fl_key_embedder_responder_private.h +FILE: ../../../flutter/shell/platform/linux/fl_key_embedder_responder_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_event.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_event.h +FILE: ../../../flutter/shell/platform/linux/fl_key_responder.cc +FILE: ../../../flutter/shell/platform/linux/fl_key_responder.h +FILE: ../../../flutter/shell/platform/linux/fl_keyboard_manager.cc +FILE: ../../../flutter/shell/platform/linux/fl_keyboard_manager.h +FILE: ../../../flutter/shell/platform/linux/fl_keyboard_manager_test.cc FILE: ../../../flutter/shell/platform/linux/fl_message_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_message_codec_test.cc FILE: ../../../flutter/shell/platform/linux/fl_method_call.cc @@ -1513,6 +1523,8 @@ FILE: ../../../flutter/shell/platform/linux/fl_view.cc FILE: ../../../flutter/shell/platform/linux/fl_view_accessible.cc FILE: ../../../flutter/shell/platform/linux/fl_view_accessible.h FILE: ../../../flutter/shell/platform/linux/fl_view_private.h +FILE: ../../../flutter/shell/platform/linux/key_mapping.cc +FILE: ../../../flutter/shell/platform/linux/key_mapping.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_binary_codec.h FILE: ../../../flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h diff --git a/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h index b40492a3550fba624597cab9115cadebf3220b03..911695e7adaa8b948da03974776a6198154c12bd 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h @@ -12,7 +12,7 @@ * A primary responder of |FlutterKeyboardManager| that handles events by * sending the raw information through the method channel. * - * This class corresponds to the RawKeyboard API in the framework. + * This class communicates with the RawKeyboard API in the framework. */ @interface FlutterChannelKeyResponder : NSObject diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h index 5cd68954d0df9266c1932d7a0d7bdfb3d0e4f5ab..ab2b1c107d7852fbb32fbcef551ca4ce96924690 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEmbedderKeyResponder.h @@ -19,7 +19,7 @@ typedef void (^FlutterSendEmbedderKeyEvent)(const FlutterKeyEvent& /* event */, * A primary responder of |FlutterKeyboardManager| that handles events by * sending the converted events through the embedder API. * - * This class corresponds to the HardwareKeyboard API in the framework. + * This class communicates with the HardwareKeyboard API in the framework. */ @interface FlutterEmbedderKeyResponder : NSObject diff --git a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h index 922f1ad0721f4d4d841ab7d2d99fa9672dcbf8a2..2ddae205e109dc3b2636dad4a9fa07b0bdf53c32 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h @@ -17,7 +17,7 @@ * secondary responders. * * An event that is received by |handleEvent| is first dispatched to *all* - * primary resopnders. Each primary responder responds *ascynchronously* with a + * primary responders. Each primary responder responds *ascynchronously* with a * boolean, indicating whether it handles the event. * * An event that is not handled by any primary responders is then passed to to @@ -35,7 +35,7 @@ * because in reality this class will only support 2 hardcoded ones (channel * and embedder), where the only purpose of supporting two is to support the * legacy API (channel) during the deprecation window, after which the channel - * resopnder should be removed. + * responder should be removed. */ @interface FlutterKeyboardManager : NSObject @@ -50,7 +50,7 @@ - (nonnull instancetype)initWithOwner:(nonnull NSResponder*)weakOwner; /** - * Add a primary resopnder, which asynchronously decides whether to handle an + * Add a primary responder, which asynchronously decides whether to handle an * event. */ - (void)addPrimaryResponder:(nonnull id)responder; diff --git a/shell/platform/embedder/test_utils/proc_table_replacement.h b/shell/platform/embedder/test_utils/proc_table_replacement.h index 6981ee97b42ae4e6a5b7d8e23fa4a14f3b73d06b..2d64f374cea6d8ed93d35b9512cc24fec4164ce5 100644 --- a/shell/platform/embedder/test_utils/proc_table_replacement.h +++ b/shell/platform/embedder/test_utils/proc_table_replacement.h @@ -13,7 +13,7 @@ // to run multiple times (e.g., using gtest_repeat). // // |proc| should be the name of an entry in FlutterEngineProcTable, such as -// "initialize". |mock_impl| should be a lamba that replaces its implementation, +// "Initialize". |mock_impl| should be a lamba that replaces its implementation, // taking the same arguments and returning the same type. #define MOCK_ENGINE_PROC(proc, mock_impl) \ ([&]() { \ diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index b7ed3e4946d62ceaab469115f011736407b4c5ca..a8109be879bc860f17f522d22cddcb42655bccba 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -74,12 +74,18 @@ source_set("flutter_linux_sources") { "fl_binary_messenger_private.h", "fl_dart_project_private.h", "fl_engine_private.h", - "fl_key_event_plugin_private.h", + "fl_keyboard_manager.h", + "fl_key_event.h", + "fl_key_responder.h", + "fl_key_channel_responder.h", + "fl_key_embedder_responder.h", + "fl_key_embedder_responder_private.h", "fl_method_call_private.h", "fl_method_channel_private.h", "fl_method_codec_private.h", "fl_plugin_registrar_private.h", "fl_standard_message_codec_private.h", + "key_mapping.h", ] configs += [ "//flutter/shell/platform/linux/config:gtk" ] @@ -97,7 +103,11 @@ source_set("flutter_linux_sources") { "fl_gl_area.cc", "fl_json_message_codec.cc", "fl_json_method_codec.cc", - "fl_key_event_plugin.cc", + "fl_key_channel_responder.cc", + "fl_key_embedder_responder.cc", + "fl_key_event.cc", + "fl_key_responder.cc", + "fl_keyboard_manager.cc", "fl_message_codec.cc", "fl_method_call.cc", "fl_method_channel.cc", @@ -120,6 +130,7 @@ source_set("flutter_linux_sources") { "fl_value.cc", "fl_view.cc", "fl_view_accessible.cc", + "key_mapping.cc", ] # Set flag to stop headers being directly included (library users should not do this) @@ -166,7 +177,9 @@ executable("flutter_linux_unittests") { "fl_event_channel_test.cc", "fl_json_message_codec_test.cc", "fl_json_method_codec_test.cc", - "fl_key_event_plugin_test.cc", + "fl_key_channel_responder_test.cc", + "fl_key_embedder_responder_test.cc", + "fl_keyboard_manager_test.cc", "fl_message_codec_test.cc", "fl_method_channel_test.cc", "fl_method_codec_test.cc", diff --git a/shell/platform/linux/fl_engine.cc b/shell/platform/linux/fl_engine.cc index 8028d21005a582a67753f15d3233951af56db0bc..116fd3f5c7e1850c73abd30db8cea563736a785d 100644 --- a/shell/platform/linux/fl_engine.cc +++ b/shell/platform/linux/fl_engine.cc @@ -639,6 +639,19 @@ void fl_engine_send_mouse_pointer_event(FlEngine* self, self->embedder_api.SendPointerEvent(self->engine, &fl_event, 1); } +void fl_engine_send_key_event(FlEngine* self, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data) { + g_return_if_fail(FL_IS_ENGINE(self)); + + if (self->engine == nullptr) { + return; + } + + self->embedder_api.SendKeyEvent(self->engine, event, callback, user_data); +} + void fl_engine_dispatch_semantics_action(FlEngine* self, uint64_t id, FlutterSemanticsAction action, diff --git a/shell/platform/linux/fl_engine_private.h b/shell/platform/linux/fl_engine_private.h index 95e6daa19f294cdbd356384ba88facb8ca97e207..b5e0081eb0fbb3e53238dc54757e8213259f37a9 100644 --- a/shell/platform/linux/fl_engine_private.h +++ b/shell/platform/linux/fl_engine_private.h @@ -163,6 +163,14 @@ void fl_engine_send_mouse_pointer_event(FlEngine* engine, double scroll_delta_y, int64_t buttons); +/** + * fl_engine_send_key_event: + */ +void fl_engine_send_key_event(FlEngine* engine, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data); + /** * fl_engine_dispatch_semantics_action: * @engine: an #FlEngine. diff --git a/shell/platform/linux/fl_key_channel_responder.cc b/shell/platform/linux/fl_key_channel_responder.cc new file mode 100644 index 0000000000000000000000000000000000000000..1a7ff137b12f41ed3b409d7755d633d7c448f8d4 --- /dev/null +++ b/shell/platform/linux/fl_key_channel_responder.cc @@ -0,0 +1,279 @@ +// 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/linux/fl_key_channel_responder.h" + +#include +#include + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" + +static constexpr char kChannelName[] = "flutter/keyevent"; +static constexpr char kTypeKey[] = "type"; +static constexpr char kTypeValueUp[] = "keyup"; +static constexpr char kTypeValueDown[] = "keydown"; +static constexpr char kKeymapKey[] = "keymap"; +static constexpr char kKeyCodeKey[] = "keyCode"; +static constexpr char kScanCodeKey[] = "scanCode"; +static constexpr char kModifiersKey[] = "modifiers"; +static constexpr char kToolkitKey[] = "toolkit"; +static constexpr char kUnicodeScalarValuesKey[] = "unicodeScalarValues"; + +static constexpr char kGtkToolkit[] = "gtk"; +static constexpr char kLinuxKeymap[] = "linux"; + +/* Declare and define FlKeyChannelUserData */ + +/** + * FlKeyChannelUserData: + * The user_data used when #FlKeyChannelResponder sends message through the + * channel. + */ +G_DECLARE_FINAL_TYPE(FlKeyChannelUserData, + fl_key_channel_user_data, + FL, + KEY_CHANNEL_USER_DATA, + GObject); + +struct _FlKeyChannelUserData { + GObject parent_instance; + + // The current responder. + FlKeyChannelResponder* responder; + // The callback provided by the caller #FlKeyboardManager. + FlKeyResponderAsyncCallback callback; + // The user_data provided by the caller #FlKeyboardManager. + gpointer user_data; +}; + +// Definition for FlKeyChannelUserData private class. +G_DEFINE_TYPE(FlKeyChannelUserData, fl_key_channel_user_data, G_TYPE_OBJECT) + +// Dispose method for FlKeyChannelUserData private class. +static void fl_key_channel_user_data_dispose(GObject* object) { + g_return_if_fail(FL_IS_KEY_CHANNEL_USER_DATA(object)); + FlKeyChannelUserData* self = FL_KEY_CHANNEL_USER_DATA(object); + if (self->responder != nullptr) { + g_object_remove_weak_pointer( + G_OBJECT(self->responder), + reinterpret_cast(&(self->responder))); + self->responder = nullptr; + } +} + +// Class initialization method for FlKeyChannelUserData private class. +static void fl_key_channel_user_data_class_init( + FlKeyChannelUserDataClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_key_channel_user_data_dispose; +} + +// Instance initialization method for FlKeyChannelUserData private class. +static void fl_key_channel_user_data_init(FlKeyChannelUserData* self) {} + +// Creates a new FlKeyChannelUserData private class with all information. +// +// The callback and the user_data might be nullptr. +static FlKeyChannelUserData* fl_key_channel_user_data_new( + FlKeyChannelResponder* responder, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyChannelUserData* self = FL_KEY_CHANNEL_USER_DATA( + g_object_new(fl_key_channel_user_data_get_type(), nullptr)); + + self->responder = responder; + // Add a weak pointer so we can know if the key event responder disappeared + // while the framework was responding. + g_object_add_weak_pointer(G_OBJECT(responder), + reinterpret_cast(&(self->responder))); + self->callback = callback; + self->user_data = user_data; + return self; +} + +/* Define FlKeyChannelResponder */ + +// Definition of the FlKeyChannelResponder GObject class. +struct _FlKeyChannelResponder { + GObject parent_instance; + + FlBasicMessageChannel* channel; + + FlKeyChannelResponderMock* mock; +}; + +static void fl_key_channel_responder_iface_init(FlKeyResponderInterface* iface); + +G_DEFINE_TYPE_WITH_CODE( + FlKeyChannelResponder, + fl_key_channel_responder, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(FL_TYPE_KEY_RESPONDER, + fl_key_channel_responder_iface_init)) + +static void fl_key_channel_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data); + +static void fl_key_channel_responder_iface_init( + FlKeyResponderInterface* iface) { + iface->handle_event = fl_key_channel_responder_handle_event; +} + +/* Implement FlKeyChannelResponder */ + +// Handles a response from the method channel to a key event sent to the +// framework earlier. +static void handle_response(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(FlKeyChannelUserData) data = FL_KEY_CHANNEL_USER_DATA(user_data); + + // Will also return if the weak pointer has been destroyed. + if (data->responder == nullptr) { + return; + } + + FlKeyChannelResponder* self = data->responder; + + g_autoptr(GError) error = nullptr; + FlBasicMessageChannel* messageChannel = FL_BASIC_MESSAGE_CHANNEL(object); + FlValue* message = + fl_basic_message_channel_send_finish(messageChannel, result, &error); + if (self->mock != nullptr && self->mock->value_converter != nullptr) { + message = self->mock->value_converter(message); + } + if (error != nullptr) { + g_warning("Unable to retrieve framework response: %s", error->message); + return; + } + g_autoptr(FlValue) handled_value = fl_value_lookup_string(message, "handled"); + bool handled = fl_value_get_bool(handled_value); + + data->callback(handled, data->user_data); +} + +// Disposes of an FlKeyChannelResponder instance. +static void fl_key_channel_responder_dispose(GObject* object) { + FlKeyChannelResponder* self = FL_KEY_CHANNEL_RESPONDER(object); + + g_clear_object(&self->channel); + + G_OBJECT_CLASS(fl_key_channel_responder_parent_class)->dispose(object); +} + +// Initializes the FlKeyChannelResponder class methods. +static void fl_key_channel_responder_class_init( + FlKeyChannelResponderClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_key_channel_responder_dispose; +} + +// Initializes an FlKeyChannelResponder instance. +static void fl_key_channel_responder_init(FlKeyChannelResponder* self) {} + +// Creates a new FlKeyChannelResponder instance, with a messenger used to send +// messages to the framework, and an FlTextInputPlugin that is used to handle +// key events that the framework doesn't handle. Mainly for testing purposes, it +// also takes an optional callback to call when a response is received, and an +// optional channel name to use when sending messages. +FlKeyChannelResponder* fl_key_channel_responder_new( + FlBinaryMessenger* messenger, + FlKeyChannelResponderMock* mock) { + g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); + + FlKeyChannelResponder* self = FL_KEY_CHANNEL_RESPONDER( + g_object_new(fl_key_channel_responder_get_type(), nullptr)); + self->mock = mock; + + g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new(); + const char* channel_name = + mock == nullptr ? kChannelName : mock->channel_name; + self->channel = fl_basic_message_channel_new(messenger, channel_name, + FL_MESSAGE_CODEC(codec)); + + return self; +} + +// Sends a key event to the framework. +static void fl_key_channel_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyChannelResponder* self = FL_KEY_CHANNEL_RESPONDER(responder); + g_return_if_fail(event != nullptr); + g_return_if_fail(callback != nullptr); + + const gchar* type = event->is_press ? kTypeValueDown : kTypeValueUp; + int64_t scan_code = event->keycode; + int64_t unicode_scarlar_values = gdk_keyval_to_unicode(event->keyval); + + // For most modifier keys, GTK keeps track of the "pressed" state of the + // modifier keys. Flutter uses this information to keep modifier keys from + // being "stuck" when a key-up event is lost because it happens after the app + // loses focus. + // + // For Lock keys (ShiftLock, CapsLock, NumLock), however, GTK keeps track of + // the state of the locks themselves, not the "pressed" state of the key. + // + // Since Flutter expects the "pressed" state of the modifier keys, the lock + // state for these keys is discarded here, and it is substituted for the + // pressed state of the key. + // + // This code has the flaw that if a key event is missed due to the app losing + // focus, then this state will still think the key is pressed when it isn't, + // but that is no worse than for "regular" keys until we implement the + // sync/cancel events on app focus changes. + // + // This is necessary to do here instead of in the framework because Flutter + // does modifier key syncing in the framework, and will turn on/off these keys + // as being "pressed" whenever the lock is on, which breaks a lot of + // interactions (for example, if shift-lock is on, tab traversal is broken). + + // Remove lock states from state mask. + guint state = event->state & ~(GDK_LOCK_MASK | GDK_MOD2_MASK); + + static bool shift_lock_pressed = FALSE; + static bool caps_lock_pressed = FALSE; + static bool num_lock_pressed = FALSE; + switch (event->keyval) { + case GDK_KEY_Num_Lock: + num_lock_pressed = event->is_press; + break; + case GDK_KEY_Caps_Lock: + caps_lock_pressed = event->is_press; + break; + case GDK_KEY_Shift_Lock: + shift_lock_pressed = event->is_press; + break; + } + + // Add back in the state matching the actual pressed state of the lock keys, + // not the lock states. + state |= (shift_lock_pressed || caps_lock_pressed) ? GDK_LOCK_MASK : 0x0; + state |= num_lock_pressed ? GDK_MOD2_MASK : 0x0; + + g_autoptr(FlValue) message = fl_value_new_map(); + fl_value_set_string_take(message, kTypeKey, fl_value_new_string(type)); + fl_value_set_string_take(message, kKeymapKey, + fl_value_new_string(kLinuxKeymap)); + fl_value_set_string_take(message, kScanCodeKey, fl_value_new_int(scan_code)); + fl_value_set_string_take(message, kToolkitKey, + fl_value_new_string(kGtkToolkit)); + fl_value_set_string_take(message, kKeyCodeKey, + fl_value_new_int(event->keyval)); + fl_value_set_string_take(message, kModifiersKey, fl_value_new_int(state)); + if (unicode_scarlar_values != 0) { + fl_value_set_string_take(message, kUnicodeScalarValuesKey, + fl_value_new_int(unicode_scarlar_values)); + } + + FlKeyChannelUserData* data = + fl_key_channel_user_data_new(self, callback, user_data); + // Send the message off to the framework for handling (or not). + fl_basic_message_channel_send(self->channel, message, nullptr, + handle_response, data); +} diff --git a/shell/platform/linux/fl_key_channel_responder.h b/shell/platform/linux/fl_key_channel_responder.h new file mode 100644 index 0000000000000000000000000000000000000000..7337dfdc9819e418ebd6028d85ad78e8e857f11a --- /dev/null +++ b/shell/platform/linux/fl_key_channel_responder.h @@ -0,0 +1,71 @@ +// 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_LINUX_FL_KEY_CHANNEL_RESPONDER_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_CHANNEL_RESPONDER_H_ + +#include + +#include "flutter/shell/platform/linux/fl_key_responder.h" +#include "flutter/shell/platform/linux/fl_keyboard_manager.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" + +typedef FlValue* (*FlValueConverter)(FlValue*); + +/** + * FlKeyChannelResponderMock: + * + * Allows mocking of FlKeyChannelResponder methods and values. Only used in + * unittests. + */ +typedef struct _FlKeyChannelResponderMock { + /** + * FlKeyChannelResponderMock::value_converter: + * If #value_converter is not nullptr, then this function is applied to the + * reply of the message, whose return value is taken as the message reply. + */ + FlValueConverter value_converter; + + /** + * FlKeyChannelResponderMock::channel_name: + * Mocks the channel name to send the message. + */ + const char* channel_name; +} FlKeyChannelResponderMock; + +G_BEGIN_DECLS + +#define FL_TYPE_KEY_CHANNEL_RESPONDER fl_key_channel_responder_get_type() +G_DECLARE_FINAL_TYPE(FlKeyChannelResponder, + fl_key_channel_responder, + FL, + KEY_CHANNEL_RESPONDER, + GObject); + +/** + * FlKeyChannelResponder: + * + * A #FlKeyResponder that handles events by sending the raw event data + * in JSON through the message channel. + * + * This class communicates with the RawKeyboard API in the framework. + */ + +/** + * fl_key_channel_responder_new: + * @messenger: the messenger that the message channel should be built on. + * @mock: options to mock several functionalities. Only used in unittests. + * + * Creates a new #FlKeyChannelResponder. + * + * Returns: a new #FlKeyChannelResponder. + */ +FlKeyChannelResponder* fl_key_channel_responder_new( + FlBinaryMessenger* messenger, + FlKeyChannelResponderMock* mock = nullptr); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_CHANNEL_RESPONDER_H_ diff --git a/shell/platform/linux/fl_key_channel_responder_test.cc b/shell/platform/linux/fl_key_channel_responder_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..c0fefe5effae0784c3db6ad1d97397cb761abdfd --- /dev/null +++ b/shell/platform/linux/fl_key_channel_responder_test.cc @@ -0,0 +1,205 @@ +// 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/linux/fl_key_channel_responder.h" + +#include "gtest/gtest.h" + +#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/testing/fl_test.h" + +static const char* expected_value = nullptr; +static gboolean expected_handled = FALSE; + +static FlValue* echo_response_cb(FlValue* echoed_value) { + gchar* text = fl_value_to_string(echoed_value); + EXPECT_STREQ(text, expected_value); + g_free(text); + + FlValue* value = fl_value_new_map(); + fl_value_set_string_take(value, "handled", + fl_value_new_bool(expected_handled)); + return value; +} + +static void responder_callback(bool handled, gpointer user_data) { + EXPECT_EQ(handled, expected_handled); + g_main_loop_quit(static_cast(user_data)); +} + +// Clone string onto the heap. +// +// If #string is nullptr, returns nullptr. Otherwise, the returned pointer must +// be freed with g_free. +static char* clone_string(const char* string) { + if (string == nullptr) { + return nullptr; + } + size_t len = strlen(string); + char* result = g_new(char, len + 1); + strcpy(result, string); + return result; +} + +namespace { +// A global variable to store new event. It is a global variable so that it can +// be returned by #fl_key_event_new_by_mock for easy use. +FlKeyEvent _g_key_event; +} // namespace + +// Create a new #FlKeyEvent with the given information. +// +// This event is passed to #fl_key_responder_handle_event, +// which assumes that the event is managed by callee. +// Therefore #fl_key_event_new_by_mock doesn't need to +// dynamically allocate, but reuses the same global object. +static FlKeyEvent* fl_key_event_new_by_mock(guint32 time_in_milliseconds, + bool is_press, + guint keyval, + guint16 keycode, + int state, + const char* string, + gboolean is_modifier) { + if (_g_key_event.string != nullptr) { + g_free(const_cast(_g_key_event.string)); + } + _g_key_event.is_press = is_press; + _g_key_event.time = time_in_milliseconds; + _g_key_event.state = state; + _g_key_event.keyval = keyval; + _g_key_event.string = clone_string(string); + _g_key_event.keycode = keycode; + _g_key_event.origin = nullptr; + _g_key_event.dispose_origin = nullptr; + return &_g_key_event; +} + +// Test sending a letter "A"; +TEST(FlKeyChannelResponderTest, SendKeyEvent) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlBinaryMessenger) messenger = fl_binary_messenger_new(engine); + FlKeyChannelResponderMock mock{ + .value_converter = echo_response_cb, + .channel_name = "test/echo", + }; + g_autoptr(FlKeyResponder) responder = + FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger, &mock)); + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12345, true, GDK_KEY_A, 0x04, 0x0, "A", false), + responder_callback, loop); + expected_value = + "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, " + "modifiers: 0, unicodeScalarValues: 65}"; + expected_handled = FALSE; + + // Blocks here until echo_response_cb is called. + g_main_loop_run(loop); + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(23456, false, GDK_KEY_A, 0x04, 0x0, "A", false), + responder_callback, loop); + expected_value = + "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, " + "modifiers: 0, unicodeScalarValues: 65}"; + expected_handled = FALSE; + + // Blocks here until echo_response_cb is called. + g_main_loop_run(loop); +} + +void test_lock_event(guint key_code, + const char* down_expected, + const char* up_expected) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlBinaryMessenger) messenger = fl_binary_messenger_new(engine); + FlKeyChannelResponderMock mock{ + .value_converter = echo_response_cb, + .channel_name = "test/echo", + }; + g_autoptr(FlKeyResponder) responder = + FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger, &mock)); + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12345, true, key_code, 0x04, 0x0, nullptr, + false), + responder_callback, loop); + expected_value = down_expected; + expected_handled = FALSE; + + // Blocks here until echo_response_cb is called. + g_main_loop_run(loop); + + expected_value = up_expected; + expected_handled = FALSE; + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12346, false, key_code, 0x04, 0x0, nullptr, + false), + responder_callback, loop); + + // Blocks here until echo_response_cb is called. + g_main_loop_run(loop); +} + +// Test sending a "NumLock" keypress. +TEST(FlKeyChannelResponderTest, SendNumLockKeyEvent) { + test_lock_event(GDK_KEY_Num_Lock, + "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65407, modifiers: 16}", + "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65407, modifiers: 0}"); +} + +// Test sending a "CapsLock" keypress. +TEST(FlKeyChannelResponderTest, SendCapsLockKeyEvent) { + test_lock_event(GDK_KEY_Caps_Lock, + "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65509, modifiers: 2}", + "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65509, modifiers: 0}"); +} + +// Test sending a "ShiftLock" keypress. +TEST(FlKeyChannelResponderTest, SendShiftLockKeyEvent) { + test_lock_event(GDK_KEY_Shift_Lock, + "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65510, modifiers: 2}", + "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65510, modifiers: 0}"); +} + +TEST(FlKeyChannelResponderTest, TestKeyEventHandledByFramework) { + g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); + + g_autoptr(FlEngine) engine = make_mock_engine(); + g_autoptr(FlBinaryMessenger) messenger = fl_binary_messenger_new(engine); + FlKeyChannelResponderMock mock{ + .value_converter = echo_response_cb, + .channel_name = "test/echo", + }; + g_autoptr(FlKeyResponder) responder = + FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger, &mock)); + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12345, true, GDK_KEY_A, 0x04, 0x0, nullptr, + false), + responder_callback, loop); + expected_handled = TRUE; + expected_value = + "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " + "keyCode: 65, modifiers: 0, unicodeScalarValues: 65}"; + + // Blocks here until echo_response_cb is called. + g_main_loop_run(loop); +} diff --git a/shell/platform/linux/fl_key_embedder_responder.cc b/shell/platform/linux/fl_key_embedder_responder.cc new file mode 100644 index 0000000000000000000000000000000000000000..a8cb78aa0911c1fd1a541a01261185225feea37b --- /dev/null +++ b/shell/platform/linux/fl_key_embedder_responder.cc @@ -0,0 +1,747 @@ +// 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/linux/fl_key_embedder_responder.h" + +#include +#include + +#include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/fl_key_embedder_responder_private.h" +#include "flutter/shell/platform/linux/key_mapping.h" + +// The code prefix for unrecognized keys that are unique to Gtk, generated from +// platform-specific codes. +constexpr uint64_t kGtkKeyIdPlane = 0x00600000000; + +constexpr uint64_t kMicrosecondsPerMillisecond = 1000; + +// Look up a hash table that maps a uint64_t to a uint64_t. +// +// Returns 0 if not found. +// +// Both key and value should be directly hashed. +static uint64_t lookup_hash_table(GHashTable* table, uint64_t key) { + return gpointer_to_uint64( + g_hash_table_lookup(table, uint64_to_gpointer(key))); +} + +static uint64_t to_lower(uint64_t n) { + constexpr uint64_t lower_a = 0x61; + constexpr uint64_t upper_a = 0x41; + constexpr uint64_t upper_z = 0x5a; + + constexpr uint64_t lower_a_grave = 0xe0; + constexpr uint64_t upper_a_grave = 0xc0; + constexpr uint64_t upper_thorn = 0xde; + constexpr uint64_t division = 0xf7; + + // ASCII range. + if (n >= upper_a && n <= upper_z) { + return n - upper_a + lower_a; + } + + // EASCII range. + if (n >= upper_a_grave && n <= upper_thorn && n != division) { + return n - upper_a_grave + lower_a_grave; + } + + return n; +} + +/* Define FlKeyEmbedderUserData */ + +/** + * FlKeyEmbedderUserData: + * The user_data used when #FlKeyEmbedderResponder sends message through the + * embedder.SendKeyEvent API. + */ +#define FL_TYPE_EMBEDDER_USER_DATA fl_key_embedder_user_data_get_type() +G_DECLARE_FINAL_TYPE(FlKeyEmbedderUserData, + fl_key_embedder_user_data, + FL, + KEY_EMBEDDER_USER_DATA, + GObject); + +struct _FlKeyEmbedderUserData { + GObject parent_instance; + + FlKeyResponderAsyncCallback callback; + gpointer user_data; +}; + +G_DEFINE_TYPE(FlKeyEmbedderUserData, fl_key_embedder_user_data, G_TYPE_OBJECT) + +static void fl_key_embedder_user_data_dispose(GObject* object); + +static void fl_key_embedder_user_data_class_init( + FlKeyEmbedderUserDataClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_key_embedder_user_data_dispose; +} + +static void fl_key_embedder_user_data_init(FlKeyEmbedderUserData* self) {} + +static void fl_key_embedder_user_data_dispose(GObject* object) { + // The following line suppresses a warning for unused function + // FL_IS_KEY_EMBEDDER_USER_DATA. + g_return_if_fail(FL_IS_KEY_EMBEDDER_USER_DATA(object)); +} + +// Creates a new FlKeyChannelUserData private class with all information. +// +// The callback and the user_data might be nullptr. +static FlKeyEmbedderUserData* fl_key_embedder_user_data_new( + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyEmbedderUserData* self = FL_KEY_EMBEDDER_USER_DATA( + g_object_new(FL_TYPE_EMBEDDER_USER_DATA, nullptr)); + + self->callback = callback; + self->user_data = user_data; + return self; +} + +/* Define FlKeyEmbedderResponder */ + +namespace { + +typedef enum { + kStateLogicUndecided, + kStateLogicNormal, + kStateLogicReversed, +} StateLogicInferrence; + +} + +struct _FlKeyEmbedderResponder { + GObject parent_instance; + + // A weak pointer to the engine the responder is attached to. + FlEngine* engine; + + // Internal record for states of whether a key is pressed. + // + // It is a map from Flutter physical key to Flutter logical key. Both keys + // and values are directly stored uint64s. This table is freed by the + // responder. + GHashTable* pressing_records; + + // Internal record for states of whether a lock mode is enabled. + // + // It is a bit mask composed of GTK mode bits. + guint lock_records; + + // Internal record for the last observed key mapping. + // + // It stores the physical key last seen during a key down event for a logical + // key. It is used to synthesize modifier keys and lock keys. + // + // It is a map from Flutter logical key to physical key. Both keys and + // values are directly stored uint64s. This table is freed by the responder. + GHashTable* mapping_records; + + // The inferred logic type indicating whether the CapsLock state logic is + // reversed on this platform. + // + // For more information, see #update_caps_lock_state_logic_inferrence. + StateLogicInferrence caps_lock_state_logic_inferrence; + + // A static map from GTK modifier bits to #FlKeyEmbedderCheckedKey to + // configure the modifier keys that needs to be tracked and kept synchronous + // on. + // + // The keys are directly stored guints. The values must be freed with g_free. + // This table is freed by the responder. + GHashTable* modifier_bit_to_checked_keys; + + // A static map from GTK modifier bits to #FlKeyEmbedderCheckedKey to + // configure the lock mode bits that needs to be tracked and kept synchronous + // on. + // + // The keys are directly stored guints. The values must be freed with g_free. + // This table is freed by the responder. + GHashTable* lock_bit_to_checked_keys; + + // A static map generated by reverse mapping lock_bit_to_checked_keys. + // + // It is a map from primary physical keys to lock bits. Both keys and values + // are directly stored uint64s. This table is freed by the responder. + GHashTable* logical_key_to_lock_bit; +}; + +static void fl_key_embedder_responder_iface_init( + FlKeyResponderInterface* iface); +static void fl_key_embedder_responder_dispose(GObject* object); + +#define FL_TYPE_EMBEDDER_RESPONDER_USER_DATA \ + fl_key_embedder_responder_get_type() +G_DEFINE_TYPE_WITH_CODE( + FlKeyEmbedderResponder, + fl_key_embedder_responder, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(FL_TYPE_KEY_RESPONDER, + fl_key_embedder_responder_iface_init)) + +static void fl_key_embedder_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data); + +static void fl_key_embedder_responder_iface_init( + FlKeyResponderInterface* iface) { + iface->handle_event = fl_key_embedder_responder_handle_event; +} + +// Initializes the FlKeyEmbedderResponder class methods. +static void fl_key_embedder_responder_class_init( + FlKeyEmbedderResponderClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_key_embedder_responder_dispose; +} + +// Initializes an FlKeyEmbedderResponder instance. +static void fl_key_embedder_responder_init(FlKeyEmbedderResponder* self) {} + +// Disposes of an FlKeyEmbedderResponder instance. +static void fl_key_embedder_responder_dispose(GObject* object) { + FlKeyEmbedderResponder* self = FL_KEY_EMBEDDER_RESPONDER(object); + + g_clear_pointer(&self->pressing_records, g_hash_table_unref); + g_clear_pointer(&self->mapping_records, g_hash_table_unref); + g_clear_pointer(&self->modifier_bit_to_checked_keys, g_hash_table_unref); + g_clear_pointer(&self->lock_bit_to_checked_keys, g_hash_table_unref); + g_clear_pointer(&self->logical_key_to_lock_bit, g_hash_table_unref); + + G_OBJECT_CLASS(fl_key_embedder_responder_parent_class)->dispose(object); +} + +// Fill in #logical_key_to_lock_bit by associating a logical key with +// its corresponding modifier bit. +// +// This is used as the body of a loop over #lock_bit_to_checked_keys. +static void initialize_logical_key_to_lock_bit_loop_body(gpointer lock_bit, + gpointer value, + gpointer user_data) { + FlKeyEmbedderCheckedKey* checked_key = + reinterpret_cast(value); + GHashTable* table = reinterpret_cast(user_data); + g_hash_table_insert(table, + uint64_to_gpointer(checked_key->primary_logical_key), + GUINT_TO_POINTER(lock_bit)); +} + +// Creates a new FlKeyEmbedderResponder instance with an engine. +FlKeyEmbedderResponder* fl_key_embedder_responder_new(FlEngine* engine) { + g_return_val_if_fail(FL_IS_ENGINE(engine), nullptr); + + FlKeyEmbedderResponder* self = FL_KEY_EMBEDDER_RESPONDER( + g_object_new(FL_TYPE_EMBEDDER_RESPONDER_USER_DATA, nullptr)); + + self->engine = engine; + // Add a weak pointer so we can know if the key event responder disappeared + // while the framework was responding. + g_object_add_weak_pointer(G_OBJECT(engine), + reinterpret_cast(&(self->engine))); + + self->pressing_records = g_hash_table_new(g_direct_hash, g_direct_equal); + self->mapping_records = g_hash_table_new(g_direct_hash, g_direct_equal); + self->lock_records = 0; + self->caps_lock_state_logic_inferrence = kStateLogicUndecided; + + self->modifier_bit_to_checked_keys = + g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); + initialize_modifier_bit_to_checked_keys(self->modifier_bit_to_checked_keys); + + self->lock_bit_to_checked_keys = + g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); + initialize_lock_bit_to_checked_keys(self->lock_bit_to_checked_keys); + + self->logical_key_to_lock_bit = + g_hash_table_new(g_direct_hash, g_direct_equal); + g_hash_table_foreach(self->lock_bit_to_checked_keys, + initialize_logical_key_to_lock_bit_loop_body, + self->logical_key_to_lock_bit); + + return self; +} + +/* Implement FlKeyEmbedderUserData */ + +static uint64_t event_to_physical_key(const FlKeyEvent* event) { + auto found = xkb_to_physical_key_map.find(event->keycode); + if (found != xkb_to_physical_key_map.end()) { + return found->second; + } + return kGtkKeyIdPlane | event->keycode; +} + +static uint64_t event_to_logical_key(const FlKeyEvent* event) { + guint keyval = event->keyval; + auto found = gtk_keyval_to_logical_key_map.find(keyval); + if (found != gtk_keyval_to_logical_key_map.end()) { + return found->second; + } + // EASCII range + if (keyval < 256) { + return to_lower(keyval); + } + // Auto-generate key + return kGtkKeyIdPlane | keyval; +} + +static uint64_t event_to_timestamp(const FlKeyEvent* event) { + return kMicrosecondsPerMillisecond * (double)event->time; +} + +// Returns a newly accocated UTF-8 string from event->keyval that must be +// freed later with g_free(). +static char* event_to_character(const FlKeyEvent* event) { + gunichar unicodeChar = gdk_keyval_to_unicode(event->keyval); + glong items_written; + gchar* result = g_ucs4_to_utf8(&unicodeChar, 1, NULL, &items_written, NULL); + if (items_written == 0) { + if (result != NULL) + g_free(result); + return nullptr; + } + return result; +} + +// Handles a response from the embedder API to a key event sent to the framework +// earlier. +static void handle_response(bool handled, gpointer user_data) { + g_autoptr(FlKeyEmbedderUserData) data = FL_KEY_EMBEDDER_USER_DATA(user_data); + + g_return_if_fail(data->callback != nullptr); + + data->callback(handled, data->user_data); +} + +// Sends a synthesized event to the engine with no demand for callback. +static void synthesize_simple_event(FlKeyEmbedderResponder* self, + FlutterKeyEventType type, + uint64_t physical, + uint64_t logical, + double timestamp) { + FlutterKeyEvent out_event; + out_event.struct_size = sizeof(out_event); + out_event.timestamp = timestamp; + out_event.type = type; + out_event.physical = physical; + out_event.logical = logical; + out_event.character = nullptr; + out_event.synthesized = true; + if (self->engine != nullptr) { + fl_engine_send_key_event(self->engine, &out_event, nullptr, nullptr); + } +} + +namespace { + +// Context variables for the foreach call used to synchronize pressing states +// and lock states. +typedef struct { + FlKeyEmbedderResponder* self; + guint state; + uint64_t event_logical_key; + bool is_down; + double timestamp; +} SyncStateLoopContext; + +} // namespace + +// Update the pressing record. +// +// If `logical_key` is 0, the record will be set as "released". Otherwise, the +// record will be set as "pressed" with this logical key. This function asserts +// that the key is pressed if the caller asked to release, and vice versa. +static void update_pressing_state(FlKeyEmbedderResponder* self, + uint64_t physical_key, + uint64_t logical_key) { + if (logical_key != 0) { + g_return_if_fail(lookup_hash_table(self->pressing_records, physical_key) == + 0); + g_hash_table_insert(self->pressing_records, + uint64_to_gpointer(physical_key), + uint64_to_gpointer(logical_key)); + } else { + g_return_if_fail(lookup_hash_table(self->pressing_records, physical_key) != + 0); + g_hash_table_remove(self->pressing_records, + uint64_to_gpointer(physical_key)); + } +} + +// Update the lock record. +// +// If `is_down` is false, this function is a no-op. Otherwise, this function +// finds the lock bit corresponding to `physical_key`, and flips its bit. +static void possibly_update_lock_bit(FlKeyEmbedderResponder* self, + uint64_t logical_key, + bool is_down) { + if (!is_down) { + return; + } + const guint mode_bit = GPOINTER_TO_UINT(g_hash_table_lookup( + self->logical_key_to_lock_bit, uint64_to_gpointer(logical_key))); + if (mode_bit != 0) { + self->lock_records ^= mode_bit; + } +} + +static void update_mapping_record(FlKeyEmbedderResponder* self, + uint64_t physical_key, + uint64_t logical_key) { + g_hash_table_insert(self->mapping_records, uint64_to_gpointer(logical_key), + uint64_to_gpointer(physical_key)); +} + +// Synchronizes the pressing state of a key to its state from the event by +// synthesizing events. +// +// This is used as the body of a loop over #modifier_bit_to_checked_keys. +static void synchronize_pressed_states_loop_body(gpointer key, + gpointer value, + gpointer user_data) { + SyncStateLoopContext* context = + reinterpret_cast(user_data); + FlKeyEmbedderCheckedKey* checked_key = + reinterpret_cast(value); + + const guint modifier_bit = GPOINTER_TO_INT(key); + FlKeyEmbedderResponder* self = context->self; + const uint64_t logical_keys[] = { + checked_key->primary_logical_key, + checked_key->secondary_logical_key, + }; + const guint length = checked_key->secondary_logical_key == 0 ? 1 : 2; + + const bool pressed_by_state = (context->state & modifier_bit) != 0; + + bool pressed_by_record = false; + + // Traverse each logical key of this modifier bit for 2 purposes: + // + // 1. Find if this logical key is pressed before the event, + // and synthesize a release event if needed. + // 2. Find if any logical key of this modifier is pressed + // before the event (#pressed_by_record), so that we can decide + // whether to synthesize a press event later. + for (guint logical_key_idx = 0; logical_key_idx < length; logical_key_idx++) { + const uint64_t logical_key = logical_keys[logical_key_idx]; + const uint64_t recorded_physical_key = + lookup_hash_table(self->mapping_records, logical_key); + const uint64_t pressed_logical_key_before_event = + recorded_physical_key == 0 + ? 0 + : lookup_hash_table(self->pressing_records, recorded_physical_key); + const bool this_key_pressed_before_event = + pressed_logical_key_before_event != 0; + + g_return_if_fail(pressed_logical_key_before_event == 0 || + pressed_logical_key_before_event == logical_key); + + pressed_by_record = pressed_by_record || this_key_pressed_before_event; + + if (this_key_pressed_before_event && !pressed_by_state) { + synthesize_simple_event(self, kFlutterKeyEventTypeUp, + recorded_physical_key, logical_key, + context->timestamp); + update_pressing_state(self, recorded_physical_key, 0); + } + } + // If the modifier should be pressed, press its primary key. + if (pressed_by_state && !pressed_by_record) { + const uint64_t logical_key = checked_key->primary_logical_key; + const uint64_t recorded_physical_key = + lookup_hash_table(self->mapping_records, logical_key); + // The physical key is derived from past mapping record if possible. + // + // The event to be synthesized is a key down event. There might not have + // been a mapping record, in which case the hard-coded #primary_physical_key + // is used. + const uint64_t physical_key = recorded_physical_key != 0 + ? recorded_physical_key + : checked_key->primary_physical_key; + if (recorded_physical_key == 0) { + update_mapping_record(self, physical_key, logical_key); + } + synthesize_simple_event(self, kFlutterKeyEventTypeDown, physical_key, + logical_key, context->timestamp); + update_pressing_state(self, physical_key, logical_key); + } +} + +// Find the stage # by the current record, which should be the recorded stage +// before the event. +static int find_stage_by_record(bool is_down, bool is_enabled) { + constexpr int stage_by_record_index[] = { + 0, // is_down: 0, is_enabled: 0 + 2, // 0 1 + 3, // 1 0 + 1 // 1 1 + }; + return stage_by_record_index[(is_down << 1) + is_enabled]; +} + +// Find the stage # by an event for the target key, which should be inferred +// stage before the event. +static int find_stage_by_self_event(int stage_by_record, + bool is_down_event, + bool is_state_on, + bool reverse_state_logic) { + if (!is_state_on) { + return reverse_state_logic ? 2 : 0; + } + if (is_down_event) { + return reverse_state_logic ? 0 : 2; + } + return stage_by_record; +} + +// Find the stage # by an event for a non-target key, which should be inferred +// stage during the event. +static int find_stage_by_others_event(int stage_by_record, bool is_state_on) { + g_return_val_if_fail(stage_by_record >= 0 && stage_by_record < 4, + stage_by_record); + if (!is_state_on) { + return 0; + } + if (stage_by_record == 0) { + return 1; + } + return stage_by_record; +} + +// Infer the logic type of CapsLock on the current platform if applicable. +// +// In most cases, when a lock key is pressed or released, its event has the +// key's state as 0-1-1-1 for the 4 stages (as documented in +// #synchronize_lock_states_loop_body) respectively. But in very rare cases it +// produces 1-1-0-1, which we call "reversed state logic". This is observed +// when using Chrome Remote Desktop on macOS (likely a bug). +// +// To detect whether the current platform behaves normally or reversed, this +// function is called on the first down event of CapsLock before calculating +// stages. This function then store the inferred mode as +// self->caps_lock_state_logic_inferrence. +// +// This does not help if the same app session is used alternatively between a +// reversed platform and a normal platform. But this is the best we can do. +static void update_caps_lock_state_logic_inferrence( + FlKeyEmbedderResponder* self, + bool is_down_event, + bool enabled_by_state, + int stage_by_record) { + if (self->caps_lock_state_logic_inferrence != kStateLogicUndecided) { + return; + } + if (!is_down_event) { + return; + } + const int stage_by_event = find_stage_by_self_event( + stage_by_record, is_down_event, enabled_by_state, false); + if ((stage_by_event == 0 && stage_by_record == 2) || + (stage_by_event == 2 && stage_by_record == 0)) { + self->caps_lock_state_logic_inferrence = kStateLogicReversed; + } else { + self->caps_lock_state_logic_inferrence = kStateLogicNormal; + } +} + +// Synchronizes the lock state of a key to its state from the event by +// synthesizing events. +// +// This is used as the body of a loop over #lock_bit_to_checked_keys. +// +// This function might modify #caps_lock_state_logic_inferrence. +static void synchronize_lock_states_loop_body(gpointer key, + gpointer value, + gpointer user_data) { + SyncStateLoopContext* context = + reinterpret_cast(user_data); + FlKeyEmbedderCheckedKey* checked_key = + reinterpret_cast(value); + + guint modifier_bit = GPOINTER_TO_INT(key); + FlKeyEmbedderResponder* self = context->self; + + const uint64_t logical_key = checked_key->primary_logical_key; + const uint64_t recorded_physical_key = + lookup_hash_table(self->mapping_records, logical_key); + // The physical key is derived from past mapping record if possible. + // + // If the event to be synthesized is a key up event, then there must have + // been a key down event before, which has updated the mapping record. + // If the event to be synthesized is a key down event, then there might + // not have been a mapping record, in which case the hard-coded + // #primary_physical_key is used. + const uint64_t physical_key = recorded_physical_key != 0 + ? recorded_physical_key + : checked_key->primary_physical_key; + + // A lock mode key can be at any of a 4-stage cycle, depending on whether it's + // pressed and enabled. The following table lists the definition of each + // stage (TruePressed and TrueEnabled), the event of the lock key between + // every 2 stages (SelfType and SelfState), and the event of other keys at + // each stage (OthersState). On certain platforms SelfState uses a reversed + // rule for certain keys (SelfState(rvsd), as documented in + // #update_caps_lock_state_logic_inferrence). + // + // # [0] [1] [2] [3] + // TruePressed: Released Pressed Released Pressed + // TrueEnabled: Disabled Enabled Enabled Disabled + // SelfType: Down Up Down Up + // SelfState: 0 1 1 1 + // SelfState(rvsd): 1 1 0 1 + // OthersState: 0 1 1 1 + // + // When the exact stage can't be derived, choose the stage that requires the + // minimal synthesization. + + const uint64_t pressed_logical_key = + recorded_physical_key == 0 + ? 0 + : lookup_hash_table(self->pressing_records, recorded_physical_key); + + g_return_if_fail(pressed_logical_key == 0 || + pressed_logical_key == logical_key); + const int stage_by_record = find_stage_by_record( + pressed_logical_key != 0, (self->lock_records & modifier_bit) != 0); + + const bool enabled_by_state = (context->state & modifier_bit) != 0; + const bool this_key_is_event_key = logical_key == context->event_logical_key; + if (this_key_is_event_key && checked_key->is_caps_lock) { + update_caps_lock_state_logic_inferrence(self, context->is_down, + enabled_by_state, stage_by_record); + g_return_if_fail(self->caps_lock_state_logic_inferrence != + kStateLogicUndecided); + } + const bool reverse_state_logic = + checked_key->is_caps_lock && + self->caps_lock_state_logic_inferrence == kStateLogicReversed; + const int stage_by_event = + this_key_is_event_key + ? find_stage_by_self_event(stage_by_record, context->is_down, + enabled_by_state, reverse_state_logic) + : find_stage_by_others_event(stage_by_record, enabled_by_state); + + // The destination stage is equal to stage_by_event but shifted cyclically to + // be no less than stage_by_record. + constexpr int kNumStages = 4; + const int destination_stage = stage_by_event >= stage_by_record + ? stage_by_event + : stage_by_event + kNumStages; + + g_return_if_fail(stage_by_record <= destination_stage); + if (stage_by_record == destination_stage) { + return; + } + for (int current_stage = stage_by_record; current_stage < destination_stage; + current_stage += 1) { + if (current_stage == 9) { + return; + } + + const int standard_current_stage = current_stage % kNumStages; + const bool is_down_event = + standard_current_stage == 0 || standard_current_stage == 2; + if (is_down_event && recorded_physical_key == 0) { + update_mapping_record(self, physical_key, logical_key); + } + FlutterKeyEventType type = + is_down_event ? kFlutterKeyEventTypeDown : kFlutterKeyEventTypeUp; + update_pressing_state(self, physical_key, is_down_event ? logical_key : 0); + possibly_update_lock_bit(self, logical_key, is_down_event); + synthesize_simple_event(self, type, physical_key, logical_key, + context->timestamp); + } +} + +// Sends a key event to the framework. +static void fl_key_embedder_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyEmbedderResponder* self = FL_KEY_EMBEDDER_RESPONDER(responder); + + g_return_if_fail(event != nullptr); + g_return_if_fail(callback != nullptr); + + const uint64_t physical_key = event_to_physical_key(event); + const uint64_t logical_key = event_to_logical_key(event); + const double timestamp = event_to_timestamp(event); + const bool is_down_event = event->is_press; + + SyncStateLoopContext sync_pressed_state_context; + sync_pressed_state_context.self = self; + sync_pressed_state_context.state = event->state; + sync_pressed_state_context.timestamp = timestamp; + sync_pressed_state_context.is_down = is_down_event; + sync_pressed_state_context.event_logical_key = logical_key; + + // Update lock mode states + g_hash_table_foreach(self->lock_bit_to_checked_keys, + synchronize_lock_states_loop_body, + &sync_pressed_state_context); + + // Update pressing states + g_hash_table_foreach(self->modifier_bit_to_checked_keys, + synchronize_pressed_states_loop_body, + &sync_pressed_state_context); + + // Construct the real event + const uint64_t last_logical_record = + lookup_hash_table(self->pressing_records, physical_key); + + FlutterKeyEvent out_event; + out_event.struct_size = sizeof(out_event); + out_event.timestamp = timestamp; + out_event.physical = physical_key; + out_event.logical = logical_key; + out_event.character = nullptr; + out_event.synthesized = false; + + g_autofree char* character_to_free = nullptr; + if (is_down_event) { + if (last_logical_record) { + // A key has been pressed that has the exact physical key as a currently + // pressed one, usually indicating multiple keyboards are pressing keys + // with the same physical key, or the up event was lost during a loss of + // focus. The down event is ignored. + callback(true, user_data); + return; + } else { + out_event.type = kFlutterKeyEventTypeDown; + character_to_free = event_to_character(event); // Might be null + out_event.character = character_to_free; + } + } else { // is_down_event false + if (!last_logical_record) { + // The physical key has been released before. It might indicate a missed + // event due to loss of focus, or multiple keyboards pressed keys with the + // same physical key. Ignore the up event. + callback(true, user_data); + return; + } else { + out_event.type = kFlutterKeyEventTypeUp; + } + } + + update_pressing_state(self, physical_key, is_down_event ? logical_key : 0); + possibly_update_lock_bit(self, logical_key, is_down_event); + if (is_down_event) { + update_mapping_record(self, physical_key, logical_key); + } + if (self->engine != nullptr) { + FlKeyEmbedderUserData* response_data = + fl_key_embedder_user_data_new(callback, user_data); + fl_engine_send_key_event(self->engine, &out_event, handle_response, + response_data); + } else { + callback(true, user_data); + } +} diff --git a/shell/platform/linux/fl_key_embedder_responder.h b/shell/platform/linux/fl_key_embedder_responder.h new file mode 100644 index 0000000000000000000000000000000000000000..e9ac6d5125a2045ecbf90cf1118e43b8b14840d4 --- /dev/null +++ b/shell/platform/linux/fl_key_embedder_responder.h @@ -0,0 +1,48 @@ +// 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_LINUX_FL_KEY_EMBEDDER_RESPONDER_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EMBEDDER_RESPONDER_H_ + +#include + +#include "flutter/shell/platform/linux/fl_key_responder.h" +#include "flutter/shell/platform/linux/fl_keyboard_manager.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" + +constexpr int kMaxConvertedKeyData = 3; + +G_BEGIN_DECLS + +#define FL_TYPE_KEY_EMBEDDER_RESPONDER fl_key_embedder_responder_get_type() +G_DECLARE_FINAL_TYPE(FlKeyEmbedderResponder, + fl_key_embedder_responder, + FL, + KEY_EMBEDDER_RESPONDER, + GObject); + +/** + * FlKeyEmbedderResponder: + * + * A #FlKeyResponder that handles events by sending the converted events + * through the embedder API. + * + * This class communicates with the HardwareKeyboard API in the framework. + */ + +/** + * fl_key_embedder_responder_new: + * @engine: The #FlEngine, whose the embedder API will be used to send + * the event. + * + * Creates a new #FlKeyEmbedderResponder. + * + * Returns: a new #FlKeyEmbedderResponder. + */ +FlKeyEmbedderResponder* fl_key_embedder_responder_new(FlEngine* engine); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EMBEDDER_RESPONDER_H_ diff --git a/shell/platform/linux/fl_key_embedder_responder_private.h b/shell/platform/linux/fl_key_embedder_responder_private.h new file mode 100644 index 0000000000000000000000000000000000000000..635cbc672e4af0cda9c300e8c99f367361ce390d --- /dev/null +++ b/shell/platform/linux/fl_key_embedder_responder_private.h @@ -0,0 +1,46 @@ +// 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_LINUX_FL_KEY_EMBEDDER_RESPONDER_PRIVATE_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EMBEDDER_RESPONDER_PRIVATE_H_ + +#include + +#include "flutter/shell/platform/linux/fl_key_responder.h" +#include "flutter/shell/platform/linux/fl_keyboard_manager.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" + +/** + * FlKeyEmbedderCheckedKey: + * + * The information for a key that #FlKeyEmbedderResponder should keep state + * synchronous on. For every record of #FlKeyEmbedderCheckedKey, the responder + * will check the #GdkEvent::state and the internal state, and synchronize + * events if they don't match. + * + * #FlKeyEmbedderCheckedKey can synchronize pressing states (such as + * whether ControlLeft is pressed) or lock states (such as whether CapsLock + * is enabled). + * + * #FlKeyEmbedderCheckedKey has a "primary key". For pressing states, the + * primary key is the left of the modifiers. For lock states, the primary + * key is the key. + * + * #FlKeyEmbedderCheckedKey may also have a "secondary key". It is only + * available to pressing states, which is the right of the modifiers. + */ +typedef struct { + // The physical key for the primary key. + uint64_t primary_physical_key; + // The logical key for the primary key. + uint64_t primary_logical_key; + // The logical key for the secondary key. + uint64_t secondary_logical_key; + // Whether this key is CapsLock. CapsLock uses a different event model in GDK + // and needs special treatment. + bool is_caps_lock; +} FlKeyEmbedderCheckedKey; + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EMBEDDER_RESPONDER_PRIVATE_H_ diff --git a/shell/platform/linux/fl_key_embedder_responder_test.cc b/shell/platform/linux/fl_key_embedder_responder_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..740980d5a63a25fa17a823647d7342b60252730f --- /dev/null +++ b/shell/platform/linux/fl_key_embedder_responder_test.cc @@ -0,0 +1,1528 @@ +// 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/linux/fl_key_embedder_responder.h" + +#include "gtest/gtest.h" + +#include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" +#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" +#include "flutter/shell/platform/linux/fl_engine_private.h" +#include "flutter/shell/platform/linux/testing/fl_test.h" + +namespace { +constexpr gboolean kRelease = FALSE; +constexpr gboolean kPress = TRUE; + +constexpr gboolean kIsModifier = TRUE; +constexpr gboolean kIsNotModifier = FALSE; + +constexpr guint16 kKeyCodeKeyA = 0x26u; +constexpr guint16 kKeyCodeShiftRight = 0x3Eu; +constexpr guint16 kKeyCodeNumpad1 = 0x57u; +constexpr guint16 kKeyCodeNumLock = 0x4Du; +constexpr guint16 kKeyCodeCapsLock = 0x42u; +constexpr guint16 kKeyCodeControlLeft = 0x25u; +constexpr guint16 kKeyCodeControlRight = 0x69u; + +constexpr uint64_t kPhysicalKeyA = 0x00070004; +constexpr uint64_t kPhysicalControlLeft = 0x000700e0; +constexpr uint64_t kPhysicalShiftRight = 0x000700E5; +constexpr uint64_t kPhysicalNumpad1 = 0x00070059; +constexpr uint64_t kPhysicalNumLock = 0x00070053; +constexpr uint64_t kPhysicalCapsLock = 0x00070039; + +constexpr uint64_t kLogicalKeyA = 0x00000061; +constexpr uint64_t kLogicalKeyQ = 0x00000071; +constexpr uint64_t kLogicalControlLeft = 0x30000000105; +constexpr uint64_t kLogicalShiftRight = 0x4000000010D; +constexpr uint64_t kLogicalNumpad1 = 0x50000000031; +constexpr uint64_t kLogicalNumLock = 0x100000010A; +constexpr uint64_t kLogicalCapsLock = 0x1000000104; +} // namespace + +static void g_ptr_array_clear(GPtrArray* array) { + g_ptr_array_remove_range(array, 0, array->len); +} + +G_DECLARE_FINAL_TYPE(FlKeyEmbedderCallRecord, + fl_key_embedder_call_record, + FL, + KEY_EMBEDDER_CALL_RECORD, + GObject); + +struct _FlKeyEmbedderCallRecord { + GObject parent_instance; + + FlutterKeyEvent* event; + FlutterKeyEventCallback callback; + gpointer user_data; +}; + +G_DEFINE_TYPE(FlKeyEmbedderCallRecord, + fl_key_embedder_call_record, + G_TYPE_OBJECT) + +static void fl_key_embedder_call_record_init(FlKeyEmbedderCallRecord* self) {} + +// Dispose method for FlKeyEmbedderCallRecord. +static void fl_key_embedder_call_record_dispose(GObject* object) { + g_return_if_fail(FL_IS_KEY_EMBEDDER_CALL_RECORD(object)); + + FlKeyEmbedderCallRecord* self = FL_KEY_EMBEDDER_CALL_RECORD(object); + if (self->event != nullptr) { + g_free(const_cast(self->event->character)); + g_free(self->event); + } + G_OBJECT_CLASS(fl_key_embedder_call_record_parent_class)->dispose(object); +} + +// Class Initialization method for FlKeyEmbedderCallRecord class. +static void fl_key_embedder_call_record_class_init( + FlKeyEmbedderCallRecordClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_key_embedder_call_record_dispose; +} + +static FlKeyEmbedderCallRecord* fl_key_embedder_call_record_new( + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + gpointer user_data) { + g_return_val_if_fail(event != nullptr, nullptr); + + FlKeyEmbedderCallRecord* self = FL_KEY_EMBEDDER_CALL_RECORD( + g_object_new(fl_key_embedder_call_record_get_type(), nullptr)); + + FlutterKeyEvent* clone_event = g_new(FlutterKeyEvent, 1); + *clone_event = *event; + if (event->character != nullptr) { + size_t character_length = strlen(event->character); + char* clone_character = g_new(char, character_length + 1); + strcpy(clone_character, event->character); + clone_event->character = clone_character; + } + self->event = clone_event; + self->callback = callback; + self->user_data = user_data; + + return self; +} + +namespace { +// A global variable to store new event. It is a global variable so that it can +// be returned by #fl_key_event_new_by_mock for easy use. +FlKeyEvent _g_key_event; +} // namespace + +// Create a new #FlKeyEvent with the given information. +// +// This event is passed to #fl_key_responder_handle_event, +// which assumes that the event is managed by callee. +// Therefore #fl_key_event_new_by_mock doesn't need to +// dynamically allocate, but reuses the same global object. +static FlKeyEvent* fl_key_event_new_by_mock(guint32 time_in_milliseconds, + bool is_press, + guint keyval, + guint16 keycode, + int state, + gboolean is_modifier) { + _g_key_event.is_press = is_press; + _g_key_event.time = time_in_milliseconds; + _g_key_event.state = state; + _g_key_event.keyval = keyval; + _g_key_event.string = nullptr; + _g_key_event.keycode = keycode; + _g_key_event.origin = nullptr; + _g_key_event.dispose_origin = nullptr; + return &_g_key_event; +} + +static gboolean g_expected_handled; +static gpointer g_expected_user_data; + +static void verify_response_handled(bool handled, gpointer user_data) { + EXPECT_EQ(handled, g_expected_handled); +} + +static void invoke_record_callback_and_verify(FlKeyEmbedderCallRecord* record, + bool expected_handled, + void* expected_user_data) { + g_return_if_fail(record->callback != nullptr); + g_expected_handled = expected_handled; + g_expected_user_data = expected_user_data; + record->callback(expected_handled, record->user_data); +} + +namespace { +GPtrArray* g_call_records; +} + +static FlEngine* make_mock_engine_with_records() { + FlEngine* engine = make_mock_engine(); + FlutterEngineProcTable* embedder_api = fl_engine_get_embedder_api(engine); + embedder_api->SendKeyEvent = [](auto engine, const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data) { + if (g_call_records != nullptr) { + g_ptr_array_add(g_call_records, fl_key_embedder_call_record_new( + event, callback, user_data)); + } + return kSuccess; + }; + + return engine; +} + +static void clear_g_call_records() { + g_ptr_array_free(g_call_records, TRUE); + g_call_records = nullptr; +} + +// Basic key presses +TEST(FlKeyEmbedderResponderTest, SendKeyEvent) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // On a QWERTY keyboard, press key Q (physically key A), and release. + // Key down + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12345, kPress, GDK_KEY_a, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->struct_size, sizeof(FlutterKeyEvent)); + EXPECT_EQ(record->event->timestamp, 12345000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "a"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Skip testing key repeats, which is not present on GDK. + + // Key up + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12346, kRelease, GDK_KEY_a, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->struct_size, sizeof(FlutterKeyEvent)); + EXPECT_EQ(record->event->timestamp, 12346000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, FALSE, &user_data); + g_ptr_array_clear(g_call_records); + + // On an AZERTY keyboard, press key Q (physically key A), and release. + // Key down + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12347, kPress, GDK_KEY_q, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->struct_size, sizeof(FlutterKeyEvent)); + EXPECT_EQ(record->event->timestamp, 12347000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyQ); + EXPECT_STREQ(record->event->character, "q"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Key up + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(12348, kRelease, GDK_KEY_q, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->struct_size, sizeof(FlutterKeyEvent)); + EXPECT_EQ(record->event->timestamp, 12348000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyQ); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, FALSE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + // TODO(dkwingsmt): Convert `engine` and `responder` to `g_autofree`. + // The current implementation is because `responder` must be unreferenced + // after `engine`, otherwise crash will *consistantly* occur on CI even if + // everything passes locally. This is a strange bug I've tried to track for + // dozens of hours in vain. It shouldn't affect real application anyway, + // since it seems to appear only during the "reboot" of the engine. + g_object_unref(engine); + g_object_unref(responder); +} + +// Press Shift, key A, then release Shift, key A. +TEST(FlKeyEmbedderResponderTest, PressShiftDuringLetterKeyTap) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press shift right + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Shift_R, kKeyCodeShiftRight, + 0, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftRight); + EXPECT_EQ(record->event->logical, kLogicalShiftRight); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press key A + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_A, kKeyCodeKeyA, 0x1, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release shift right + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Shift_R, + kKeyCodeShiftRight, 0x1, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalShiftRight); + EXPECT_EQ(record->event->logical, kLogicalShiftRight); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release key A + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_A, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Press or release Numpad 1 between presses/releases of NumLock. +// +// This tests interaction between lock keys and non-lock keys in cases that do +// not have events missed. +// +// This also tests the result of Numpad keys across NumLock taps, which is +// test-worthy because the keyval for the numpad key will change before and +// after the NumLock tap, which should not alter the resulting logical key. +TEST(FlKeyEmbedderResponderTest, TapNumPadKeysBetweenNumLockEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press Numpad 1 (stage 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_KP_End, kKeyCodeNumpad1, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumpad1); + EXPECT_EQ(record->event->logical, kLogicalNumpad1); + EXPECT_STREQ(record->event->character, nullptr); // TODO + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press NumLock (stage 0 -> 1) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_Num_Lock, kKeyCodeNumLock, + 0, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release numpad 1 (stage 1) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_KP_1, kKeyCodeNumpad1, + 0x10, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumpad1); + EXPECT_EQ(record->event->logical, kLogicalNumpad1); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release NumLock (stage 1 -> 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Num_Lock, kKeyCodeNumLock, + 0x10, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press Numpad 1 (stage 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_KP_End, kKeyCodeNumpad1, + 0x10, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumpad1); + EXPECT_EQ(record->event->logical, kLogicalNumpad1); + EXPECT_STREQ(record->event->character, nullptr); // TODO + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press NumLock (stage 2 -> 3) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_Num_Lock, kKeyCodeNumLock, + 0x10, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release numpad 1 (stage 3) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_KP_1, kKeyCodeNumpad1, + 0x10, kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumpad1); + EXPECT_EQ(record->event->logical, kLogicalNumpad1); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release NumLock (stage 3 -> 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Num_Lock, kKeyCodeNumLock, + 0x10, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Press or release letter key between presses/releases of CapsLock. +// +// This tests interaction between lock keys and non-lock keys in cases that do +// not have events missed. +TEST(FlKeyEmbedderResponderTest, TapLetterKeysBetweenCapsLockEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press CapsLock (stage 0 -> 1) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Caps_Lock, kKeyCodeCapsLock, + 0x0, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press key A (stage 1) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_A, kKeyCodeKeyA, 0x2, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release CapsLock (stage 1 -> 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Caps_Lock, + kKeyCodeCapsLock, 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release key A (stage 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_A, kKeyCodeKeyA, 0x2, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press CapsLock (stage 2 -> 3) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(105, kPress, GDK_KEY_Caps_Lock, kKeyCodeCapsLock, + 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press key A (stage 3) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(106, kPress, GDK_KEY_A, kKeyCodeKeyA, 0x2, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release CapsLock (stage 3 -> 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(107, kRelease, GDK_KEY_Caps_Lock, + kKeyCodeCapsLock, 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release key A (stage 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(108, kRelease, GDK_KEY_a, kKeyCodeKeyA, 0x0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Press or release letter key between presses/releases of CapsLock, on +// a platform with reversed logic. +// +// This happens when using a Chrome remote desktop on MacOS. +TEST(FlKeyEmbedderResponderTest, TapLetterKeysBetweenCapsLockEventsReversed) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press key A (stage 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_a, kKeyCodeKeyA, 0x0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "a"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press CapsLock (stage 0 -> 1) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_Caps_Lock, kKeyCodeCapsLock, + 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release CapsLock (stage 1 -> 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Caps_Lock, + kKeyCodeCapsLock, 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release key A (stage 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_A, kKeyCodeKeyA, 0x2, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press key A (stage 2) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(105, kPress, GDK_KEY_A, kKeyCodeKeyA, 0x2, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press CapsLock (stage 2 -> 3) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(106, kPress, GDK_KEY_Caps_Lock, kKeyCodeCapsLock, + 0x0, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release CapsLock (stage 3 -> 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(107, kRelease, GDK_KEY_Caps_Lock, + kKeyCodeCapsLock, 0x2, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalCapsLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release key A (stage 0) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(108, kRelease, GDK_KEY_a, kKeyCodeKeyA, 0x0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +TEST(FlKeyEmbedderResponderTest, IgnoreDuplicateDownEvent) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press KeyA + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_a, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Press KeyA again (with different logical key, which is not necessari but + // for coverage). + g_expected_handled = true; // The ignored event is always handled. + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_q, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 0u); + + // Release KeyA + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_q, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +TEST(FlKeyEmbedderResponderTest, IgnoreAbruptUpEvent) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + // Release KeyA before it was even pressed. + g_expected_handled = true; // The ignored event is always handled. + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_q, kKeyCodeKeyA, 0, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 0u); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Test if missed modifier keys can be detected and synthesized with state +// information upon events that are for this modifier key. +TEST(FlKeyEmbedderResponderTest, SynthesizeForDesyncPressingStateOnSelfEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Test 1: synthesize key down. + + // A key down of control left is missed. + guint state = GDK_CONTROL_MASK; + + // Send a ControlLeft up + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kRelease, GDK_KEY_Control_L, + kKeyCodeControlLeft, state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Test 2: synthesize key up. + + // Send a ControlLeft down. + state = 0; + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_Control_L, + kKeyCodeControlLeft, state, kIsModifier), + verify_response_handled, &user_data); + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // A key up of control left is missed. + state = 0; + + // Send another ControlLeft down + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kPress, GDK_KEY_Control_L, + kKeyCodeControlLeft, state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 103000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 103000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Send a ControlLeft up to clear up state. + state = GDK_CONTROL_MASK; + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(104, kRelease, GDK_KEY_Control_L, + kKeyCodeControlLeft, state, kIsModifier), + verify_response_handled, &user_data); + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Test 3: synthesize by right modifier. + + // A key down of control right is missed. + state = GDK_CONTROL_MASK; + + // Send a ControlRight up. + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(105, kRelease, GDK_KEY_Control_R, + kKeyCodeControlRight, state, kIsModifier), + verify_response_handled, &user_data); + + // A ControlLeft down is synthesized, with no non-synthesized event. + // Reason: The ControlLeft down is synthesized to synchronize the state + // showing Control as pressed. The ControlRight event is ignored because + // the event is considered a duplicate up event. + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 105000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Test if missed modifier keys can be detected and synthesized with state +// information upon events that are not for this modifier key. +TEST(FlKeyEmbedderResponderTest, + SynthesizeForDesyncPressingStateOnNonSelfEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // A key down of control left is missed. + guint state = GDK_CONTROL_MASK; + + // Send a normal event (KeyA down) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_a, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "a"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // A key up of control left is missed. + state = 0; + + // Send a normal event (KeyA up) + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kRelease, GDK_KEY_A, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Test non-default key mapping. + + // Press a key with physical CapsLock and logical ControlLeft. + state = 0; + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Control_L, kKeyCodeCapsLock, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // The key up of the control left press is missed. + state = 0; + + // Send a normal event (KeyA down). + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_A, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + // The synthesized event should have physical CapsLock and logical + // ControlLeft. + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Test if missed modifier keys can be detected and synthesized with state +// information upon events that do not have the standard key mapping. +TEST(FlKeyEmbedderResponderTest, + SynthesizeForDesyncPressingStateOnRemappedEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // Press a key with physical CapsLock and logical ControlLeft. + guint state = 0; + + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Control_L, kKeyCodeCapsLock, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // The key up of the control left press is missed. + state = 0; + + // Send a normal event (KeyA down). + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_A, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + // The synthesized event should have physical CapsLock and logical + // ControlLeft. + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalCapsLock); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "A"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Test if missed lock keys can be detected and synthesized with state +// information upon events that are not for this modifier key. +TEST(FlKeyEmbedderResponderTest, SynthesizeForDesyncLockModeOnNonSelfEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // The NumLock is desynchronized by being enabled. + guint state = GDK_MOD2_MASK; + + // Send a normal event + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_a, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, "a"); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // The NumLock is desynchronized by being disabled. + state = 0; + + // Release key A + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kRelease, GDK_KEY_A, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 4u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 2)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 3)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalKeyA); + EXPECT_EQ(record->event->logical, kLogicalKeyA); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // Release NumLock. Since the previous event should have synthesized NumLock + // to be released, this should result in no events. + g_expected_handled = true; + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(103, kRelease, GDK_KEY_Num_Lock, kKeyCodeNumLock, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 0u); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Test if missed lock keys can be detected and synthesized with state +// information upon events that are for this modifier key. +TEST(FlKeyEmbedderResponderTest, SynthesizeForDesyncLockModeOnSelfEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // The NumLock is desynchronized by being enabled. + guint state = GDK_MOD2_MASK; + + // NumLock down + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kPress, GDK_KEY_Num_Lock, kKeyCodeNumLock, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 3u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 2)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + // The NumLock is desynchronized by being enabled in a press event. + state = GDK_MOD2_MASK; + + // NumLock up + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(102, kPress, GDK_KEY_Num_Lock, kKeyCodeNumLock, + state, kIsModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 4u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 2)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeUp); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 3)); + EXPECT_EQ(record->event->timestamp, 102000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, false); + + invoke_record_callback_and_verify(record, TRUE, &user_data); + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} + +// Ensures that even if the primary event is ignored (due to duplicate +// key up or down events), key synthesization is still performed. +TEST(FlKeyEmbedderResponderTest, SynthesizationOccursOnIgnoredEvents) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlEngine* engine = make_mock_engine_with_records(); + FlKeyResponder* responder = + FL_KEY_RESPONDER(fl_key_embedder_responder_new(engine)); + int user_data = 123; // Arbitrary user data + + FlKeyEmbedderCallRecord* record; + + // The NumLock is desynchronized by being enabled, and Control is pressed. + guint state = GDK_MOD2_MASK | GDK_CONTROL_MASK; + + // Send a KeyA up event, which will be ignored. + g_expected_handled = true; // The ignored event is always handled. + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(101, kRelease, GDK_KEY_a, kKeyCodeKeyA, state, + kIsNotModifier), + verify_response_handled, &user_data); + + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalNumLock); + EXPECT_EQ(record->event->logical, kLogicalNumLock); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->timestamp, 101000); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalControlLeft); + EXPECT_EQ(record->event->logical, kLogicalControlLeft); + EXPECT_STREQ(record->event->character, nullptr); + EXPECT_EQ(record->event->synthesized, true); + + g_ptr_array_clear(g_call_records); + + clear_g_call_records(); + g_object_unref(engine); + g_object_unref(responder); +} diff --git a/shell/platform/linux/fl_key_event.cc b/shell/platform/linux/fl_key_event.cc new file mode 100644 index 0000000000000000000000000000000000000000..b823a3377ae16786a3cc9b9745beaf3c45bc8af3 --- /dev/null +++ b/shell/platform/linux/fl_key_event.cc @@ -0,0 +1,57 @@ +// 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/linux/fl_key_event.h" + +static void dispose_origin_from_gdk_event(gpointer origin) { + g_return_if_fail(origin != nullptr); + gdk_event_free(reinterpret_cast(origin)); +} + +static char* clone_string(const char* source) { + if (source == nullptr) { + return nullptr; + } + size_t length = strlen(source); + char* result = g_new(char, length + 1); + strcpy(result, source); + return result; +} + +FlKeyEvent* fl_key_event_new_from_gdk_event(GdkEvent* raw_event) { + g_return_val_if_fail(raw_event != nullptr, nullptr); + GdkEventKey* event = reinterpret_cast(raw_event); + GdkEventType type = event->type; + g_return_val_if_fail(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE, + nullptr); + FlKeyEvent* result = g_new(FlKeyEvent, 1); + + result->time = event->time; + result->is_press = type == GDK_KEY_PRESS; + result->keycode = event->hardware_keycode; + result->keyval = event->keyval; + result->state = event->state; + result->string = clone_string(event->string); + result->origin = event; + result->dispose_origin = dispose_origin_from_gdk_event; + + return result; +} + +void fl_key_event_dispose(FlKeyEvent* event) { + if (event->string != nullptr) { + g_free(const_cast(event->string)); + } + if (event->dispose_origin != nullptr) { + event->dispose_origin(event->origin); + } + g_free(event); +} + +FlKeyEvent* fl_key_event_clone(const FlKeyEvent* event) { + FlKeyEvent* new_event = g_new(FlKeyEvent, 1); + *new_event = *event; + new_event->string = clone_string(event->string); + return new_event; +} diff --git a/shell/platform/linux/fl_key_event.h b/shell/platform/linux/fl_key_event.h new file mode 100644 index 0000000000000000000000000000000000000000..2fd6240b240f07f14a1632028af693092651407c --- /dev/null +++ b/shell/platform/linux/fl_key_event.h @@ -0,0 +1,78 @@ +// 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_LINUX_FL_KEY_EVENT_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_H_ + +#include + +/** + * FlKeyEventDispose: + * @origin: the #FlKeyEvent::origin to dispose. + * + * The signature for #FlKeyEvent::dispose_origin, which + * frees #FlKeyEvent::origin. + **/ +typedef void (*FlKeyEventDisposeOrigin)(gpointer origin); + +/** + * FlKeyEvent: + * A struct that stores information from GdkEvent. + * + * This is a class only used within the GTK embedding, created by + * FlView and consumed by FlKeyboardManager. It is not sent to + * the embedder. + * + * This object contains information from GdkEvent as well as an origin event + * object, so that Flutter can create an event object in unit tests even after + * migrating to GDK 4.0 which stops supporting creating GdkEvent. + */ +typedef struct _FlKeyEvent { + // Time in milliseconds. + guint32 time; + // True if is a press event, otherwise a release event. + bool is_press; + // Hardware keycode. + guint16 keycode; + // Keyval. + guint keyval; + // Modifier state. + int state; + // String, null-terminated. + // + // Can be nullptr. + const char* string; + // An opaque pointer to the original event. + // + // This is used when dispatching. For native events, this is #GdkEvent + // pointer. For unit tests, this is a dummy pointer. + gpointer origin; + // A callback to free #origin, called in #fl_key_event_dispose. + // + // Can be nullptr. + FlKeyEventDisposeOrigin dispose_origin; +} FlKeyEvent; + +/** + * fl_key_event_new_from_gdk_event: + * @event: the #GdkEvent this #FlKeyEvent is based on. The #event must be a + * #GdkEventKey, and will be destroyed by #fl_key_event_dispose. + * + * Create a new #FlKeyEvent based on a #GdkEvent. + * + * Returns: a new #FlKeyEvent. Must be freed with #fl_key_event_dispose. + */ +FlKeyEvent* fl_key_event_new_from_gdk_event(GdkEvent* event); + +/** + * fl_key_event_dispose: + * @event: the event to dispose. + * + * Properly disposes the content of #event and then the pointer. + */ +void fl_key_event_dispose(FlKeyEvent* event); + +FlKeyEvent* fl_key_event_clone(const FlKeyEvent* source); + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_H_ diff --git a/shell/platform/linux/fl_key_event_plugin.cc b/shell/platform/linux/fl_key_event_plugin.cc deleted file mode 100644 index 1cabc6a1658e4e0ad172d29402dddfc2119153c2..0000000000000000000000000000000000000000 --- a/shell/platform/linux/fl_key_event_plugin.cc +++ /dev/null @@ -1,432 +0,0 @@ -// 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/linux/fl_key_event_plugin.h" -#include "flutter/shell/platform/linux/fl_key_event_plugin_private.h" - -#include -#include - -#include "flutter/shell/platform/linux/fl_text_input_plugin.h" -#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h" -#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" - -static constexpr char kChannelName[] = "flutter/keyevent"; -static constexpr char kTypeKey[] = "type"; -static constexpr char kTypeValueUp[] = "keyup"; -static constexpr char kTypeValueDown[] = "keydown"; -static constexpr char kKeymapKey[] = "keymap"; -static constexpr char kKeyCodeKey[] = "keyCode"; -static constexpr char kScanCodeKey[] = "scanCode"; -static constexpr char kModifiersKey[] = "modifiers"; -static constexpr char kToolkitKey[] = "toolkit"; -static constexpr char kUnicodeScalarValuesKey[] = "unicodeScalarValues"; - -static constexpr char kGtkToolkit[] = "gtk"; -static constexpr char kLinuxKeymap[] = "linux"; - -static constexpr uint64_t kMaxPendingEvents = 1000; - -// Definition of the FlKeyEventPlugin GObject class. - -struct _FlKeyEventPlugin { - GObject parent_instance; - - FlBasicMessageChannel* channel = nullptr; - FlTextInputPlugin* text_input_plugin = nullptr; - FlKeyEventPluginCallback response_callback = nullptr; - GPtrArray* pending_events; -}; - -G_DEFINE_TYPE(FlKeyEventPlugin, fl_key_event_plugin, G_TYPE_OBJECT) - -// Declare and define a private pair object to bind the id and the event -// together. - -G_DECLARE_FINAL_TYPE(FlKeyEventPair, - fl_key_event_pair, - FL, - KEY_EVENT_PAIR, - GObject); - -struct _FlKeyEventPair { - GObject parent_instance; - - uint64_t id; - GdkEventKey* event; -}; - -G_DEFINE_TYPE(FlKeyEventPair, fl_key_event_pair, G_TYPE_OBJECT) - -// Dispose method for FlKeyEventPair. -static void fl_key_event_pair_dispose(GObject* object) { - // Redundant, but added so that we don't get a warning about unused function - // for FL_IS_KEY_EVENT_PAIR. - g_return_if_fail(FL_IS_KEY_EVENT_PAIR(object)); - - FlKeyEventPair* self = FL_KEY_EVENT_PAIR(object); - g_clear_pointer(&self->event, gdk_event_free); - G_OBJECT_CLASS(fl_key_event_pair_parent_class)->dispose(object); -} - -// Class Initialization method for FlKeyEventPair class. -static void fl_key_event_pair_class_init(FlKeyEventPairClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_key_event_pair_dispose; -} - -// Initialization for FlKeyEventPair instances. -static void fl_key_event_pair_init(FlKeyEventPair* self) {} - -// Creates a new FlKeyEventPair instance, given a unique ID, and an event struct -// to keep. -FlKeyEventPair* fl_key_event_pair_new(uint64_t id, GdkEventKey* event) { - FlKeyEventPair* self = - FL_KEY_EVENT_PAIR(g_object_new(fl_key_event_pair_get_type(), nullptr)); - - // Copy the event to preserve refcounts for referenced values (mainly the - // window). - GdkEventKey* event_copy = reinterpret_cast( - gdk_event_copy(reinterpret_cast(event))); - self->id = id; - self->event = event_copy; - return self; -} - -// Declare and define a private class to hold response data from the framework. -G_DECLARE_FINAL_TYPE(FlKeyEventResponseData, - fl_key_event_response_data, - FL, - KEY_EVENT_RESPONSE_DATA, - GObject); - -struct _FlKeyEventResponseData { - GObject parent_instance; - - FlKeyEventPlugin* plugin; - uint64_t id; - gpointer user_data; -}; - -// Definition for FlKeyEventResponseData private class. -G_DEFINE_TYPE(FlKeyEventResponseData, fl_key_event_response_data, G_TYPE_OBJECT) - -// Dispose method for FlKeyEventResponseData private class. -static void fl_key_event_response_data_dispose(GObject* object) { - g_return_if_fail(FL_IS_KEY_EVENT_RESPONSE_DATA(object)); - FlKeyEventResponseData* self = FL_KEY_EVENT_RESPONSE_DATA(object); - if (self->plugin != nullptr) { - g_object_remove_weak_pointer(G_OBJECT(self->plugin), - reinterpret_cast(&(self->plugin))); - self->plugin = nullptr; - } -} - -// Class initialization method for FlKeyEventResponseData private class. -static void fl_key_event_response_data_class_init( - FlKeyEventResponseDataClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_key_event_response_data_dispose; -} - -// Instance initialization method for FlKeyEventResponseData private class. -static void fl_key_event_response_data_init(FlKeyEventResponseData* self) {} - -// Creates a new FlKeyEventResponseData private class with a plugin that created -// the request, a unique ID for tracking, and optional user data. -// Will keep a weak pointer to the plugin. -FlKeyEventResponseData* fl_key_event_response_data_new(FlKeyEventPlugin* plugin, - uint64_t id, - gpointer user_data) { - FlKeyEventResponseData* self = FL_KEY_EVENT_RESPONSE_DATA( - g_object_new(fl_key_event_response_data_get_type(), nullptr)); - - self->plugin = plugin; - // Add a weak pointer so we can know if the key event plugin disappeared - // while the framework was responding. - g_object_add_weak_pointer(G_OBJECT(plugin), - reinterpret_cast(&(self->plugin))); - self->id = id; - self->user_data = user_data; - return self; -} - -// Calculates a unique ID for a given GdkEventKey object to use for -// identification of responses from the framework. -uint64_t fl_key_event_plugin_get_event_id(GdkEventKey* event) { - // Combine the event timestamp, the type of event, and the hardware keycode - // (scan code) of the event to come up with a unique id for this event that - // can be derived solely from the event data itself, so that we can identify - // whether or not we have seen this event already. - return (event->time & 0xffffffff) | - (static_cast(event->type) & 0xffff) << 32 | - (static_cast(event->hardware_keycode) & 0xffff) << 48; -} - -// Finds an event in the event queue that was sent to the framework by its ID. -GdkEventKey* fl_key_event_plugin_find_pending_event(FlKeyEventPlugin* self, - uint64_t id) { - for (guint i = 0; i < self->pending_events->len; ++i) { - if (FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, i))->id == - id) { - return FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, i)) - ->event; - } - } - return nullptr; -} - -// Removes an event from the pending event queue. -static void remove_pending_event(FlKeyEventPlugin* self, uint64_t id) { - for (guint i = 0; i < self->pending_events->len; ++i) { - if (FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, i))->id == - id) { - g_ptr_array_remove_index(self->pending_events, i); - return; - } - } - g_warning("Tried to remove pending event with id %" PRIu64 - ", but the event was not found.", - id); -} - -// Adds an GdkEventKey to the pending event queue, with a unique ID, and the -// plugin that added it. -static void add_pending_event(FlKeyEventPlugin* self, - uint64_t id, - GdkEventKey* event) { - if (self->pending_events->len > kMaxPendingEvents) { - g_warning( - "There are %d keyboard events that have not yet received a " - "response from the framework. Are responses being sent?", - self->pending_events->len); - } - g_ptr_array_add(self->pending_events, fl_key_event_pair_new(id, event)); -} - -// Handles a response from the framework to a key event sent to the framework -// earlier. -static void handle_response(GObject* object, - GAsyncResult* result, - gpointer user_data) { - g_autoptr(FlKeyEventResponseData) data = - FL_KEY_EVENT_RESPONSE_DATA(user_data); - - // Will also return if the weak pointer has been destroyed. - if (data->plugin == nullptr) { - return; - } - - FlKeyEventPlugin* self = data->plugin; - - g_autoptr(GError) error = nullptr; - FlBasicMessageChannel* messageChannel = FL_BASIC_MESSAGE_CHANNEL(object); - FlValue* message = - fl_basic_message_channel_send_finish(messageChannel, result, &error); - if (error != nullptr) { - g_warning("Unable to retrieve framework response: %s", error->message); - return; - } - g_autoptr(FlValue) handled_value = fl_value_lookup_string(message, "handled"); - bool handled = FALSE; - if (handled_value != nullptr) { - GdkEventKey* event = fl_key_event_plugin_find_pending_event(self, data->id); - if (event == nullptr) { - g_warning("Event response for event id %" PRIu64 - " received, but pending event was not found.", - data->id); - } else { - handled = fl_value_get_bool(handled_value); - if (!handled) { - if (self->text_input_plugin != nullptr) { - // Propagate the event to the text input plugin. - handled = fl_text_input_plugin_filter_keypress( - self->text_input_plugin, event); - } - // Dispatch the event to other GTK windows if the text input plugin - // didn't handle it. We keep track of the event id so we can recognize - // the event when our window receives it again and not respond to it. If - // the response callback is set, then use that instead. - if (!handled && self->response_callback == nullptr) { - gdk_event_put(reinterpret_cast(event)); - } - } - } - } - - if (handled) { - // Because the event was handled, we no longer need to track it. Unhandled - // events will be removed when the event is re-dispatched to the window. - remove_pending_event(self, data->id); - } - - if (self->response_callback != nullptr) { - self->response_callback(object, message, handled, data->user_data); - } -} - -// Disposes of an FlKeyEventPlugin instance. -static void fl_key_event_plugin_dispose(GObject* object) { - FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(object); - - g_clear_object(&self->channel); - if (self->text_input_plugin != nullptr) { - g_object_remove_weak_pointer( - G_OBJECT(self->text_input_plugin), - reinterpret_cast(&(self->text_input_plugin))); - self->text_input_plugin = nullptr; - } - g_ptr_array_free(self->pending_events, TRUE); - - G_OBJECT_CLASS(fl_key_event_plugin_parent_class)->dispose(object); -} - -// Initializes the FlKeyEventPlugin class methods. -static void fl_key_event_plugin_class_init(FlKeyEventPluginClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_key_event_plugin_dispose; -} - -// Initializes an FlKeyEventPlugin instance. -static void fl_key_event_plugin_init(FlKeyEventPlugin* self) {} - -// Creates a new FlKeyEventPlugin instance, with a messenger used to send -// messages to the framework, an FlTextInputPlugin used to handle key events -// that the framework doesn't handle. Mainly for testing purposes, it also takes -// an optional callback to call when a response is received, and an optional -// channel name to use when sending messages. -FlKeyEventPlugin* fl_key_event_plugin_new( - FlBinaryMessenger* messenger, - FlTextInputPlugin* text_input_plugin, - FlKeyEventPluginCallback response_callback, - const char* channel_name) { - g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); - g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(text_input_plugin), nullptr); - - FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN( - g_object_new(fl_key_event_plugin_get_type(), nullptr)); - - g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new(); - self->channel = fl_basic_message_channel_new( - messenger, channel_name == nullptr ? kChannelName : channel_name, - FL_MESSAGE_CODEC(codec)); - self->response_callback = response_callback; - // Add a weak pointer so we know if the text input plugin goes away. - g_object_add_weak_pointer( - G_OBJECT(text_input_plugin), - reinterpret_cast(&(self->text_input_plugin))); - self->text_input_plugin = text_input_plugin; - - self->pending_events = g_ptr_array_new_with_free_func(g_object_unref); - return self; -} - -// Sends a key event to the framework. -bool fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self, - GdkEventKey* event, - gpointer user_data) { - g_return_val_if_fail(FL_IS_KEY_EVENT_PLUGIN(self), FALSE); - g_return_val_if_fail(event != nullptr, FALSE); - - // Get an ID for the event, so we can match them up when we get a response - // from the framework. Use the event time, type, and hardware keycode as a - // unique ID, since they are part of the event structure that we can look up - // when we receive a random event that may or may not have been - // tracked/produced by this code. - uint64_t id = fl_key_event_plugin_get_event_id(event); - if (self->pending_events->len != 0 && - fl_key_event_plugin_find_pending_event(self, id) != nullptr) { - // If the event is in the queue of pending events we've seen, then we know - // that this is a re-dispatched event, and we shouldn't respond to it, but - // we should remove it from tracking. - remove_pending_event(self, id); - return FALSE; - } - - const gchar* type; - switch (event->type) { - case GDK_KEY_PRESS: - type = kTypeValueDown; - break; - case GDK_KEY_RELEASE: - type = kTypeValueUp; - break; - default: - return FALSE; - } - - int64_t scan_code = event->hardware_keycode; - int64_t unicodeScalarValues = gdk_keyval_to_unicode(event->keyval); - - // For most modifier keys, GTK keeps track of the "pressed" state of the - // modifier keys. Flutter uses this information to keep modifier keys from - // being "stuck" when a key-up event is lost because it happens after the app - // loses focus. - // - // For Lock keys (ShiftLock, CapsLock, NumLock), however, GTK keeps track of - // the state of the locks themselves, not the "pressed" state of the key. - // - // Since Flutter expects the "pressed" state of the modifier keys, the lock - // state for these keys is discarded here, and it is substituted for the - // pressed state of the key. - // - // This code has the flaw that if a key event is missed due to the app losing - // focus, then this state will still think the key is pressed when it isn't, - // but that is no worse than for "regular" keys until we implement the - // sync/cancel events on app focus changes. - // - // This is necessary to do here instead of in the framework because Flutter - // does modifier key syncing in the framework, and will turn on/off these keys - // as being "pressed" whenever the lock is on, which breaks a lot of - // interactions (for example, if shift-lock is on, tab traversal is broken). - // - // TODO(gspencergoog): get rid of this tracked state when we are tracking the - // state of all keys and sending sync/cancel events when focus is gained/lost. - - // Remove lock states from state mask. - guint state = event->state & ~(GDK_LOCK_MASK | GDK_MOD2_MASK); - - static bool shift_lock_pressed = FALSE; - static bool caps_lock_pressed = FALSE; - static bool num_lock_pressed = FALSE; - switch (event->keyval) { - case GDK_KEY_Num_Lock: - num_lock_pressed = event->type == GDK_KEY_PRESS; - break; - case GDK_KEY_Caps_Lock: - caps_lock_pressed = event->type == GDK_KEY_PRESS; - break; - case GDK_KEY_Shift_Lock: - shift_lock_pressed = event->type == GDK_KEY_PRESS; - break; - } - - // Add back in the state matching the actual pressed state of the lock keys, - // not the lock states. - state |= (shift_lock_pressed || caps_lock_pressed) ? GDK_LOCK_MASK : 0x0; - state |= num_lock_pressed ? GDK_MOD2_MASK : 0x0; - - g_autoptr(FlValue) message = fl_value_new_map(); - fl_value_set_string_take(message, kTypeKey, fl_value_new_string(type)); - fl_value_set_string_take(message, kKeymapKey, - fl_value_new_string(kLinuxKeymap)); - fl_value_set_string_take(message, kScanCodeKey, fl_value_new_int(scan_code)); - fl_value_set_string_take(message, kToolkitKey, - fl_value_new_string(kGtkToolkit)); - fl_value_set_string_take(message, kKeyCodeKey, - fl_value_new_int(event->keyval)); - fl_value_set_string_take(message, kModifiersKey, fl_value_new_int(state)); - if (unicodeScalarValues != 0) { - fl_value_set_string_take(message, kUnicodeScalarValuesKey, - fl_value_new_int(unicodeScalarValues)); - } - - // Track the event as pending a response from the framework. - add_pending_event(self, id, event); - FlKeyEventResponseData* data = - fl_key_event_response_data_new(self, id, user_data); - // Send the message off to the framework for handling (or not). - fl_basic_message_channel_send(self->channel, message, nullptr, - handle_response, data); - // Return true before we know what the framework will do, because if it - // doesn't handle the key, we'll re-dispatch it later. - return TRUE; -} diff --git a/shell/platform/linux/fl_key_event_plugin.h b/shell/platform/linux/fl_key_event_plugin.h deleted file mode 100644 index 18ee36a8a99d3762ace5b5b7cc02e61324afd1bc..0000000000000000000000000000000000000000 --- a/shell/platform/linux/fl_key_event_plugin.h +++ /dev/null @@ -1,89 +0,0 @@ -// 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_LINUX_FL_KEY_EVENT_PLUGIN_H_ -#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_ - -#include "flutter/shell/platform/linux/fl_text_input_plugin.h" -#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" -#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" - -#include - -G_BEGIN_DECLS - -G_DECLARE_FINAL_TYPE(FlKeyEventPlugin, - fl_key_event_plugin, - FL, - KEY_EVENT_PLUGIN, - GObject); - -/** - * FlKeyEventPlugin: - * - * #FlKeyEventPlugin is a plugin that implements the shell side - * of SystemChannels.keyEvent from the Flutter services library. - */ - -/** - * FlKeyEventPluginCallback: - * @source_object: (nullable): the object the key event was started with. - * @message: the message returned from the framework. - * @handled: a boolean indicating whether the key event was handled in the - * framework. - * @user_data: user data passed to the callback. - * - * Type definition for a function that will be called when a key event is - * received from the engine. - **/ -typedef void (*FlKeyEventPluginCallback)(GObject* source_object, - FlValue* message, - gboolean handled, - gpointer user_data); - -/** - * fl_key_event_plugin_new: - * @messenger: an #FlBinaryMessenger. - * @response_callback: the callback to call when a response is received. If not - * given (nullptr), then the default response callback is - * used. Typically used for tests to receive event - * information. If specified, unhandled events will not be - * re-dispatched. - * @text_input_plugin: The #FlTextInputPlugin to send key events to if the - * framework doesn't handle them. - * @channel_name: the name of the channel to send key events to the framework - * on. If not given (nullptr), then the standard key event - * channel name is used. Typically used for tests to send on a - * test channel. - * - * Creates a new plugin that implements SystemChannels.keyEvent from the - * Flutter services library. - * - * Returns: a new #FlKeyEventPlugin. - */ -FlKeyEventPlugin* fl_key_event_plugin_new( - FlBinaryMessenger* messenger, - FlTextInputPlugin* text_input_plugin, - FlKeyEventPluginCallback response_callback = nullptr, - const char* channel_name = nullptr); - -/** - * fl_key_event_plugin_send_key_event: - * @plugin: an #FlKeyEventPlugin. - * @event: a #GdkEventKey. - * @user_data: a pointer to user data to send to the response callback via the - * messenger. - * - * @returns %TRUE if this key event should be considered handled and - * event propagation stopped. - * - * Sends a key event to Flutter. - */ -bool fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin, - GdkEventKey* event, - gpointer user_data = nullptr); - -G_END_DECLS - -#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_ diff --git a/shell/platform/linux/fl_key_event_plugin_private.h b/shell/platform/linux/fl_key_event_plugin_private.h deleted file mode 100644 index 0fb5b3e1d80cf23d58f1bb4a5b0f99f37b06d5fe..0000000000000000000000000000000000000000 --- a/shell/platform/linux/fl_key_event_plugin_private.h +++ /dev/null @@ -1,40 +0,0 @@ -// 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_LINUX_FL_KEY_EVENT_PLUGIN_PRIVATE_H_ -#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_PRIVATE_H_ - -#include "flutter/shell/platform/linux/fl_key_event_plugin.h" - -#include - -G_BEGIN_DECLS - -/** - * fl_key_event_plugin_get_event_id: - * @event: a GDK key event (GdkEventKey). - * - * Calculates an internal key ID for a given event. Used to look up pending - * events using fl_key_event_plugin_find_pending_event. - * - * Returns: a 64-bit ID representing this GDK key event. - */ -uint64_t fl_key_event_plugin_get_event_id(GdkEventKey* event); - -/** - * fl_key_event_plugin_get_event_id: - * @id: a 64-bit id representing a GDK key event (GdkEventKey). Calculated - * using #fl_key_event_plugin_get_event_id. - * - * Looks up an event that is waiting for a response from the framework. - * - * Returns: the GDK key event requested, or nullptr if the event doesn't exist - * in the queue. - */ -GdkEventKey* fl_key_event_plugin_find_pending_event(FlKeyEventPlugin* self, - uint64_t id); - -G_END_DECLS - -#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_PRIVATE_H_ diff --git a/shell/platform/linux/fl_key_event_plugin_test.cc b/shell/platform/linux/fl_key_event_plugin_test.cc deleted file mode 100644 index dc8c112f799643fab8026b1f20d72118560c39d4..0000000000000000000000000000000000000000 --- a/shell/platform/linux/fl_key_event_plugin_test.cc +++ /dev/null @@ -1,364 +0,0 @@ -// 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/linux/fl_key_event_plugin.h" -#include "flutter/shell/platform/linux/fl_key_event_plugin_private.h" - -#include "gtest/gtest.h" - -#include "flutter/shell/platform/linux/fl_binary_messenger_private.h" -#include "flutter/shell/platform/linux/fl_engine_private.h" -#include "flutter/shell/platform/linux/testing/fl_test.h" -#include "flutter/shell/platform/linux/testing/mock_text_input_plugin.h" - -static const char* expected_value = nullptr; -static gboolean expected_handled = FALSE; -static uint64_t expected_id = 0; -static FlKeyEventPlugin* expected_self = nullptr; - -// Called when the message response is received in the send_key_event test. -static void echo_response_cb(GObject* object, - FlValue* message, - gboolean handled, - gpointer user_data) { - EXPECT_NE(message, nullptr); - EXPECT_EQ(fl_value_get_type(message), FL_VALUE_TYPE_MAP); - EXPECT_STREQ(fl_value_to_string(message), expected_value); - EXPECT_EQ(handled, expected_handled); - if (expected_self != nullptr) { - if (handled) { - EXPECT_EQ( - fl_key_event_plugin_find_pending_event(expected_self, expected_id), - nullptr); - } else { - EXPECT_NE( - fl_key_event_plugin_find_pending_event(expected_self, expected_id), - nullptr); - } - } - - g_main_loop_quit(static_cast(user_data)); -} - -static gboolean handle_keypress(FlTextInputPlugin* plugin, GdkEventKey* event) { - return TRUE; -} - -static gboolean ignore_keypress(FlTextInputPlugin* plugin, GdkEventKey* event) { - return FALSE; -} - -// Test sending a letter "A"; -TEST(FlKeyEventPluginTest, SendKeyEvent) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new( - messenger, text_input_plugin, echo_response_cb, "test/echo"); - - char string[] = "A"; - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x0, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - reinterpret_cast(&string[0]), // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = - "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, " - "modifiers: 0, unicodeScalarValues: 65}"; - expected_handled = FALSE; - fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_NE(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); - - key_event = GdkEventKey{ - GDK_KEY_RELEASE, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 23456, // time - 0x0, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - reinterpret_cast(&string[0]), // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = - "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, " - "modifiers: 0, unicodeScalarValues: 65}"; - expected_handled = FALSE; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - EXPECT_TRUE(handled); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_NE(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); -} - -void test_lock_event(guint key_code, - const char* down_expected, - const char* up_expected) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new( - messenger, text_input_plugin, echo_response_cb, "test/echo"); - - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x10, // modifier state - key_code, // key code - 1, // length of string representation - nullptr, // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = down_expected; - expected_handled = FALSE; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - EXPECT_TRUE(handled); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_NE(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); - - key_event.type = GDK_KEY_RELEASE; - key_event.time++; - - expected_value = up_expected; - expected_handled = FALSE; - fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_NE(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); -} - -// Test sending a "NumLock" keypress. -TEST(FlKeyEventPluginTest, SendNumLockKeyEvent) { - test_lock_event(GDK_KEY_Num_Lock, - "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65407, modifiers: 16}", - "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65407, modifiers: 0}"); -} - -// Test sending a "CapsLock" keypress. -TEST(FlKeyEventPluginTest, SendCapsLockKeyEvent) { - test_lock_event(GDK_KEY_Caps_Lock, - "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65509, modifiers: 2}", - "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65509, modifiers: 0}"); -} - -// Test sending a "ShiftLock" keypress. -TEST(FlKeyEventPluginTest, SendShiftLockKeyEvent) { - test_lock_event(GDK_KEY_Shift_Lock, - "{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65510, modifiers: 2}", - "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, " - "keyCode: 65510, modifiers: 0}"); -} - -TEST(FlKeyEventPluginTest, TestKeyEventHandledByFramework) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new( - messenger, text_input_plugin, echo_response_cb, "test/key-event-handled"); - - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x10, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - nullptr, // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = "{handled: true}"; - expected_handled = TRUE; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - // Should always be true, because the event was delayed. - EXPECT_TRUE(handled); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_EQ(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); -} - -TEST(FlKeyEventPluginTest, TestKeyEventHandledByTextInputPlugin) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = - fl_key_event_plugin_new(messenger, text_input_plugin, echo_response_cb, - "test/key-event-not-handled"); - - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x10, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - nullptr, // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = "{handled: false}"; - expected_handled = TRUE; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - // Should always be true, because the event was delayed. - EXPECT_TRUE(handled); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_EQ(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); -} - -TEST(FlKeyEventPluginTest, TestKeyEventNotHandledByTextInputPlugin) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(ignore_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = - fl_key_event_plugin_new(messenger, text_input_plugin, echo_response_cb, - "test/key-event-not-handled"); - - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x10, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - nullptr, // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = "{handled: false}"; - expected_handled = FALSE; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - // Should always be true, because the event was delayed. - EXPECT_TRUE(handled); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - EXPECT_NE(fl_key_event_plugin_find_pending_event( - plugin, fl_key_event_plugin_get_event_id(&key_event)), - nullptr); -} - -TEST(FlKeyEventPluginTest, TestKeyEventResponseOutOfOrderFromFramework) { - g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0); - - g_autoptr(FlEngine) engine = make_mock_engine(); - FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlTextInputPlugin) text_input_plugin = - FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress)); - g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new( - messenger, text_input_plugin, echo_response_cb, "test/key-event-delayed"); - - GdkEventKey key_event = GdkEventKey{ - GDK_KEY_PRESS, // event type - nullptr, // window (not needed) - FALSE, // event was sent explicitly - 12345, // time - 0x10, // modifier state - GDK_KEY_A, // key code - 1, // length of string representation - nullptr, // string representation - 0x04, // scan code - 0, // keyboard group - 0, // is a modifier - }; - - expected_value = "{handled: true}"; - expected_handled = TRUE; - expected_self = plugin; - uint64_t event_id_a = fl_key_event_plugin_get_event_id(&key_event); - expected_id = event_id_a; - bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - // Should always be true, because the event was delayed. - EXPECT_TRUE(handled); - EXPECT_EQ(fl_key_event_plugin_find_pending_event(plugin, event_id_a)->keyval, - static_cast(GDK_KEY_A)); - - // Send a second key event that will be out of order. - key_event.keyval = GDK_KEY_B; - key_event.hardware_keycode = 0x05; - uint64_t event_id_b = fl_key_event_plugin_get_event_id(&key_event); - expected_id = event_id_b; - handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop); - EXPECT_TRUE(handled); - EXPECT_EQ(fl_key_event_plugin_find_pending_event(plugin, event_id_b)->keyval, - static_cast(GDK_KEY_B)); - - // Blocks here until echo_response_cb is called. - g_main_loop_run(loop); - - // Make sure they both were removed - EXPECT_EQ(fl_key_event_plugin_find_pending_event(plugin, event_id_a), - nullptr); - EXPECT_EQ(fl_key_event_plugin_find_pending_event(plugin, event_id_b), - nullptr); - expected_self = nullptr; - expected_id = 0; -} diff --git a/shell/platform/linux/fl_key_responder.cc b/shell/platform/linux/fl_key_responder.cc new file mode 100644 index 0000000000000000000000000000000000000000..c5ac80d290d871497943ef3f1cb4d7d3eebbad5e --- /dev/null +++ b/shell/platform/linux/fl_key_responder.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/linux/fl_key_responder.h" + +G_DEFINE_INTERFACE(FlKeyResponder, fl_key_responder, G_TYPE_OBJECT) + +static void fl_key_responder_default_init(FlKeyResponderInterface* iface) {} + +void fl_key_responder_handle_event(FlKeyResponder* self, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + g_return_if_fail(FL_IS_KEY_RESPONDER(self)); + g_return_if_fail(event != nullptr); + g_return_if_fail(callback != nullptr); + + FL_KEY_RESPONDER_GET_IFACE(self)->handle_event(self, event, callback, + user_data); +} diff --git a/shell/platform/linux/fl_key_responder.h b/shell/platform/linux/fl_key_responder.h new file mode 100644 index 0000000000000000000000000000000000000000..a21620675d00582f90a97b4b96a0523135ada437 --- /dev/null +++ b/shell/platform/linux/fl_key_responder.h @@ -0,0 +1,81 @@ +// 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_LINUX_FL_KEY_RESPONDER_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_RESPONDER_H_ + +#include +#include + +#include "flutter/shell/platform/linux/fl_key_event.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h" + +G_BEGIN_DECLS + +typedef struct _FlKeyboardManager FlKeyboardManager; + +/** + * FlKeyResponderAsyncCallback: + * @event: whether the event has been handled. + * @user_data: the same value as user_data sent by + * #fl_key_responder_handle_event. + * + * The signature for a callback with which a #FlKeyResponder asynchronously + * reports whether the responder handles the event. + **/ +typedef void (*FlKeyResponderAsyncCallback)(bool handled, gpointer user_data); + +#define FL_TYPE_KEY_RESPONDER fl_key_responder_get_type() +G_DECLARE_INTERFACE(FlKeyResponder, + fl_key_responder, + FL, + KEY_RESPONDER, + GObject); + +/** + * FlKeyResponder: + * + * An interface for a responder that can process a key event and decides + * asynchronously whether to handle an event. + * + * To use this class, add it with #fl_keyboard_manager_add_responder. + */ + +struct _FlKeyResponderInterface { + GTypeInterface g_iface; + + /** + * FlKeyResponder::handle_event: + * + * The implementation of #fl_key_responder_handle_event. + */ + void (*handle_event)(FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data); +}; + +/** + * fl_key_responder_handle_event: + * @responder: the #FlKeyResponder self. + * @event: the event to be handled. Must not be null. The object is managed + * by callee and must not be assumed available after this function. + * @callback: the callback to report the result. It should be called exactly + * once. Must not be null. + * @user_data: a value that will be sent back in the callback. Can be null. + * + * Let the responder handle an event, expecting the responder to report + * whether to handle the event. The result will be reported by invoking + * `callback` exactly once, which might happen after + * `fl_key_responder_handle_event` or during it. + */ +void fl_key_responder_handle_event(FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_RESPONDER_H_ diff --git a/shell/platform/linux/fl_keyboard_manager.cc b/shell/platform/linux/fl_keyboard_manager.cc new file mode 100644 index 0000000000000000000000000000000000000000..ad3dc7f6d102c2c90476b59465264f9fb5f4afda --- /dev/null +++ b/shell/platform/linux/fl_keyboard_manager.cc @@ -0,0 +1,405 @@ +// 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/linux/fl_keyboard_manager.h" + +#include + +/* Declare and define FlKeyboardPendingEvent */ + +/** + * FlKeyboardPendingEvent: + * A record for events that have been received by the manager, but + * dispatched to other objects, whose results have yet to return. + * + * This object is used by both the "pending_responds" list and the + * "pending_redispatches" list. + */ +G_DECLARE_FINAL_TYPE(FlKeyboardPendingEvent, + fl_keyboard_pending_event, + FL, + KEYBOARD_PENDING_EVENT, + GObject); + +struct _FlKeyboardPendingEvent { + GObject parent_instance; + + // The target event. + // + // This is freed by #FlKeyboardPendingEvent. + FlKeyEvent* event; + + // Self-incrementing ID attached to an event sent to the framework. + // + // Used to identify pending responds. + uint64_t sequence_id; + // The number of responders that haven't replied. + size_t unreplied; + // Whether any replied responders reported true (handled). + bool any_handled; + + // A value calculated out of critical event information that can be used + // to identify redispatched events. + uint64_t hash; +}; + +G_DEFINE_TYPE(FlKeyboardPendingEvent, fl_keyboard_pending_event, G_TYPE_OBJECT) + +static void fl_keyboard_pending_event_dispose(GObject* object) { + // Redundant, but added so that we don't get a warning about unused function + // for FL_IS_KEYBOARD_PENDING_EVENT. + g_return_if_fail(FL_IS_KEYBOARD_PENDING_EVENT(object)); + + FlKeyboardPendingEvent* self = FL_KEYBOARD_PENDING_EVENT(object); + fl_key_event_dispose(self->event); + G_OBJECT_CLASS(fl_keyboard_pending_event_parent_class)->dispose(object); +} + +static void fl_keyboard_pending_event_class_init( + FlKeyboardPendingEventClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_keyboard_pending_event_dispose; +} + +static void fl_keyboard_pending_event_init(FlKeyboardPendingEvent* self) {} + +// Calculates a unique ID for a given FlKeyEvent object to use for +// identification of responses from the framework. +static uint64_t fl_keyboard_manager_get_event_hash(FlKeyEvent* event) { + // Combine the event timestamp, the type of event, and the hardware keycode + // (scan code) of the event to come up with a unique id for this event that + // can be derived solely from the event data itself, so that we can identify + // whether or not we have seen this event already. + guint64 type = + static_cast(event->is_press ? GDK_KEY_PRESS : GDK_KEY_RELEASE); + guint64 keycode = static_cast(event->keycode); + return (event->time & 0xffffffff) | ((type & 0xffff) << 32) | + ((keycode & 0xffff) << 48); +} + +// Create a new FlKeyboardPendingEvent by providing the target event, +// the sequence ID, and the number of responders that will reply. +// +// This will acquire the ownership of the event. +FlKeyboardPendingEvent* fl_keyboard_pending_event_new(FlKeyEvent* event, + uint64_t sequence_id, + size_t to_reply) { + FlKeyboardPendingEvent* self = FL_KEYBOARD_PENDING_EVENT( + g_object_new(fl_keyboard_pending_event_get_type(), nullptr)); + + self->event = event; + self->sequence_id = sequence_id; + self->unreplied = to_reply; + self->any_handled = false; + self->hash = fl_keyboard_manager_get_event_hash(event); + return self; +} + +/* Declare and define FlKeyboardManagerUserData */ + +/** + * FlKeyEmbedderUserData: + * The user_data used when #FlKeyboardManagerUserData sends event to + * responders. + */ +#define FL_TYPE_KEYBOARD_MANAGER_USER_DATA \ + fl_keyboard_manager_user_data_get_type() +G_DECLARE_FINAL_TYPE(FlKeyboardManagerUserData, + fl_keyboard_manager_user_data, + FL, + KEYBOARD_MANAGER_USER_DATA, + GObject); + +struct _FlKeyboardManagerUserData { + GObject parent_instance; + + // A weak reference to the owner manager. + FlKeyboardManager* manager; + uint64_t sequence_id; +}; + +namespace { + +// Context variables for the foreach call used to dispatch events to responders. +typedef struct { + FlKeyEvent* event; + FlKeyboardManagerUserData* user_data; +} DispatchToResponderLoopContext; + +} // namespace + +G_DEFINE_TYPE(FlKeyboardManagerUserData, + fl_keyboard_manager_user_data, + G_TYPE_OBJECT) + +static void fl_keyboard_manager_user_data_dispose(GObject* object) { + g_return_if_fail(FL_IS_KEYBOARD_MANAGER_USER_DATA(object)); + FlKeyboardManagerUserData* self = FL_KEYBOARD_MANAGER_USER_DATA(object); + if (self->manager != nullptr) { + g_object_remove_weak_pointer(G_OBJECT(self->manager), + reinterpret_cast(&(self->manager))); + self->manager = nullptr; + } +} + +static void fl_keyboard_manager_user_data_class_init( + FlKeyboardManagerUserDataClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_keyboard_manager_user_data_dispose; +} + +static void fl_keyboard_manager_user_data_init( + FlKeyboardManagerUserData* self) {} + +// Creates a new FlKeyboardManagerUserData private class with all information. +FlKeyboardManagerUserData* fl_keyboard_manager_user_data_new( + FlKeyboardManager* manager, + uint64_t sequence_id) { + FlKeyboardManagerUserData* self = FL_KEYBOARD_MANAGER_USER_DATA( + g_object_new(fl_keyboard_manager_user_data_get_type(), nullptr)); + + self->manager = manager; + // Add a weak pointer so we can know if the key event responder disappeared + // while the framework was responding. + g_object_add_weak_pointer(G_OBJECT(manager), + reinterpret_cast(&(self->manager))); + self->sequence_id = sequence_id; + return self; +} + +/* Define FlKeyboardManager */ + +struct _FlKeyboardManager { + GObject parent_instance; + + // The callback that unhandled events should be redispatched through. + FlKeyboardManagerRedispatcher redispatch_callback; + + // A text plugin. + // + // Released by the manager on dispose. + FlTextInputPlugin* text_input_plugin; + + // An array of #FlKeyResponder. Elements are added with + // #fl_keyboard_manager_add_responder immediately after initialization and are + // automatically released on dispose. + GPtrArray* responder_list; + + // An array of #FlKeyboardPendingEvent. FlKeyboardManager must manually + // release the elements unless it is transferring them to + // pending_redispatches. + GPtrArray* pending_responds; + + // An array of #FlKeyboardPendingEvent. FlKeyboardManager must manually + // release the elements. + GPtrArray* pending_redispatches; + + // The last sequence ID used. Increased by 1 by every use. + uint64_t last_sequence_id; +}; + +G_DEFINE_TYPE(FlKeyboardManager, fl_keyboard_manager, G_TYPE_OBJECT); + +static void fl_keyboard_manager_dispose(GObject* object); + +static void fl_keyboard_manager_class_init(FlKeyboardManagerClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_keyboard_manager_dispose; +} + +static void fl_keyboard_manager_init(FlKeyboardManager* self) {} + +static void fl_key_event_destroy_notify(gpointer event); +static void fl_keyboard_manager_dispose(GObject* object) { + FlKeyboardManager* self = FL_KEYBOARD_MANAGER(object); + + if (self->text_input_plugin != nullptr) + g_clear_object(&self->text_input_plugin); + g_ptr_array_free(self->responder_list, TRUE); + g_ptr_array_set_free_func(self->pending_responds, + fl_key_event_destroy_notify); + g_ptr_array_free(self->pending_responds, TRUE); + g_ptr_array_set_free_func(self->pending_redispatches, + fl_key_event_destroy_notify); + g_ptr_array_free(self->pending_redispatches, TRUE); + + G_OBJECT_CLASS(fl_keyboard_manager_parent_class)->dispose(object); +} + +/* Implement FlKeyboardManager */ + +// This is an exact copy of g_ptr_array_find_with_equal_func. Somehow CI +// reports that can not find symbol g_ptr_array_find_with_equal_func, despite +// the fact that it runs well locally. +gboolean g_ptr_array_find_with_equal_func1(GPtrArray* haystack, + gconstpointer needle, + GEqualFunc equal_func, + guint* index_) { + guint i; + g_return_val_if_fail(haystack != NULL, FALSE); + if (equal_func == NULL) + equal_func = g_direct_equal; + for (i = 0; i < haystack->len; i++) { + if (equal_func(g_ptr_array_index(haystack, i), needle)) { + if (index_ != NULL) { + *index_ = i; + } + return TRUE; + } + } + + return FALSE; +} + +// Compare a #FlKeyboardPendingEvent with the given sequence_id. The needle +// should be a pointer to uint64_t sequence_id. +static gboolean compare_pending_by_sequence_id( + gconstpointer pending, + gconstpointer needle_sequence_id) { + uint64_t sequence_id = *reinterpret_cast(needle_sequence_id); + return static_cast(pending)->sequence_id == + sequence_id; +} + +// Compare a #FlKeyboardPendingEvent with the given hash. The #needle should be +// a pointer to uint64_t hash. +static gboolean compare_pending_by_hash(gconstpointer pending, + gconstpointer needle_hash) { + uint64_t hash = *reinterpret_cast(needle_hash); + return static_cast(pending)->hash == hash; +} + +// Try to remove a pending event from `pending_redispatches` with the target +// hash. +// +// Returns true if the event is found and removed. +static bool fl_keyboard_manager_remove_redispatched(FlKeyboardManager* self, + uint64_t hash) { + guint result_index; + gboolean found = g_ptr_array_find_with_equal_func1( + self->pending_redispatches, static_cast(&hash), + compare_pending_by_hash, &result_index); + if (found) { + FlKeyboardPendingEvent* removed = + FL_KEYBOARD_PENDING_EVENT(g_ptr_array_remove_index_fast( + self->pending_redispatches, result_index)); + g_return_val_if_fail(removed != nullptr, TRUE); + g_object_unref(removed); + return TRUE; + } else { + return FALSE; + } +} + +// The callback used by a responder after the event was dispatched. +static void responder_handle_event_callback(bool handled, + gpointer user_data_ptr) { + g_return_if_fail(FL_IS_KEYBOARD_MANAGER_USER_DATA(user_data_ptr)); + FlKeyboardManagerUserData* user_data = + FL_KEYBOARD_MANAGER_USER_DATA(user_data_ptr); + FlKeyboardManager* self = user_data->manager; + + guint result_index = -1; + gboolean found = g_ptr_array_find_with_equal_func1( + self->pending_responds, &user_data->sequence_id, + compare_pending_by_sequence_id, &result_index); + g_return_if_fail(found); + FlKeyboardPendingEvent* pending = FL_KEYBOARD_PENDING_EVENT( + g_ptr_array_index(self->pending_responds, result_index)); + g_return_if_fail(pending != nullptr); + g_return_if_fail(pending->unreplied > 0); + pending->unreplied -= 1; + pending->any_handled = pending->any_handled || handled; + // All responders have replied. + if (pending->unreplied == 0) { + g_object_unref(user_data_ptr); + gpointer removed = + g_ptr_array_remove_index_fast(self->pending_responds, result_index); + g_return_if_fail(removed == pending); + bool should_redispatch = + !pending->any_handled && (self->text_input_plugin == nullptr || + !fl_text_input_plugin_filter_keypress( + self->text_input_plugin, pending->event)); + if (should_redispatch) { + g_ptr_array_add(self->pending_redispatches, pending); + self->redispatch_callback(pending->event->origin); + } else { + g_object_unref(pending); + } + } +} + +FlKeyboardManager* fl_keyboard_manager_new( + FlTextInputPlugin* text_input_plugin, + FlKeyboardManagerRedispatcher redispatch_callback) { + g_return_val_if_fail(text_input_plugin == nullptr || + FL_IS_TEXT_INPUT_PLUGIN(text_input_plugin), + nullptr); + g_return_val_if_fail(redispatch_callback != nullptr, nullptr); + + FlKeyboardManager* self = FL_KEYBOARD_MANAGER( + g_object_new(fl_keyboard_manager_get_type(), nullptr)); + + self->text_input_plugin = text_input_plugin; + self->redispatch_callback = redispatch_callback; + self->responder_list = g_ptr_array_new_with_free_func(g_object_unref); + + self->pending_responds = g_ptr_array_new(); + self->pending_redispatches = g_ptr_array_new(); + + self->last_sequence_id = 1; + + return self; +} + +void fl_keyboard_manager_add_responder(FlKeyboardManager* self, + FlKeyResponder* responder) { + g_return_if_fail(FL_IS_KEYBOARD_MANAGER(self)); + g_return_if_fail(responder != nullptr); + + g_ptr_array_add(self->responder_list, responder); +} + +// The loop body to dispatch an event to a responder. +static void dispatch_to_responder(gpointer responder_data, + gpointer foreach_data_ptr) { + DispatchToResponderLoopContext* context = + reinterpret_cast(foreach_data_ptr); + FlKeyResponder* responder = FL_KEY_RESPONDER(responder_data); + fl_key_responder_handle_event(responder, context->event, + responder_handle_event_callback, + context->user_data); +} + +gboolean fl_keyboard_manager_handle_event(FlKeyboardManager* self, + FlKeyEvent* event) { + g_return_val_if_fail(FL_IS_KEYBOARD_MANAGER(self), FALSE); + g_return_val_if_fail(event != nullptr, FALSE); + + uint64_t incoming_hash = fl_keyboard_manager_get_event_hash(event); + if (fl_keyboard_manager_remove_redispatched(self, incoming_hash)) { + return FALSE; + } + + FlKeyboardPendingEvent* pending = fl_keyboard_pending_event_new( + event, ++self->last_sequence_id, self->responder_list->len); + + g_ptr_array_add(self->pending_responds, pending); + FlKeyboardManagerUserData* user_data = + fl_keyboard_manager_user_data_new(self, pending->sequence_id); + DispatchToResponderLoopContext data{ + .event = event, + .user_data = user_data, + }; + g_ptr_array_foreach(self->responder_list, dispatch_to_responder, &data); + + return TRUE; +} + +gboolean fl_keyboard_manager_is_state_clear(FlKeyboardManager* self) { + g_return_val_if_fail(FL_IS_KEYBOARD_MANAGER(self), FALSE); + return self->pending_responds->len == 0 && + self->pending_redispatches->len == 0; +} + +static void fl_key_event_destroy_notify(gpointer event) { + fl_key_event_dispose(reinterpret_cast(event)); +} diff --git a/shell/platform/linux/fl_keyboard_manager.h b/shell/platform/linux/fl_keyboard_manager.h new file mode 100644 index 0000000000000000000000000000000000000000..23eef64b5eecabae6cf51f0e6da8498c504eb849 --- /dev/null +++ b/shell/platform/linux/fl_keyboard_manager.h @@ -0,0 +1,113 @@ +// 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_LINUX_FL_KEYBOARD_MANAGER_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEYBOARD_MANAGER_H_ + +#include + +#include "flutter/shell/platform/linux/fl_key_responder.h" +#include "flutter/shell/platform/linux/fl_text_input_plugin.h" + +/** + * FlKeyboardManagerRedispatcher: + * @event: the pointer to the event to dispatch. + * + * The signature for a callback with which a #FlKeyboardManager redispatches + * key events that are not handled by anyone. + **/ +typedef void (*FlKeyboardManagerRedispatcher)(gpointer event); + +G_BEGIN_DECLS + +#define FL_TYPE_KEYBOARD_MANAGER fl_keyboard_manager_get_type() +G_DECLARE_FINAL_TYPE(FlKeyboardManager, + fl_keyboard_manager, + FL, + KEYBOARD_MANAGER, + GObject); + +/** + * FlKeyboardManager: + * + * A hub that manages how key events are dispatched to various processing + * objects of Flutter, or possibly back to the system. + * + * This class manage one or more objects of #FlKeyResponder, as well as a + * #TextInputPlugin. + * + * An event that is received by #fl_keyboard_manager_handle_event is first + * dispatched to *all* responders. Each responder responds *ascynchronously* + * with a boolean, indicating whether it handles the event. + * + * An event that is not handled by any responders is then passed to to the + * #TextInputPlugin, which responds *synchronously* with a boolean, indicating + * whether it handles the event. + * + * If no processing objects handle the event, the event is then "redispatched": + * sent back to the system using #redispatch_callback. + * + * Preventing responders from receiving events is not supported, because in + * reality this class will only support 2 hardcoded ones (channel and + * embedder), where the only purpose of supporting two is to support the legacy + * API (channel) during the deprecation window, after which the channel + * responder should be removed. + */ + +/** + * fl_keyboard_manager_new: + * @text_input_plugin: the #FlTextInputPlugin to send key events to if the + * framework doesn't handle them. This object will be managed and freed by + * #FlKeyboardManager. + * @redispatch_callback: how the events should be sent if no processing + * objects handle the event. Typically a function that calls #gdk_event_put + * on #FlKeyEvent::origin. + * + * Create a new #FlKeyboardManager. The text input plugin must be specified + * now, while the responders should be added later with + * #fl_keyboard_manager_add_responder. + * + * Returns: a new #FlKeyboardManager. + */ +FlKeyboardManager* fl_keyboard_manager_new( + FlTextInputPlugin* text_input_plugin, + FlKeyboardManagerRedispatcher redispatch_callback); + +/** + * fl_keyboard_manager_add_responder: + * @manager: the #FlKeyboardManager self. + * @responder: the new responder to be added. + * + * Add a new #FlKeyResponder to the #FlKeyboardManager. Responders added + * earlier will receive events earlier. + */ +void fl_keyboard_manager_add_responder(FlKeyboardManager* manager, + FlKeyResponder* responder); + +/** + * fl_keyboard_manager_handle_event: + * @manager: the #FlKeyboardManager self. + * @event: the event to be dispatched. This event will be managed and + * released by #FlKeyboardManager. + * + * Add a new #FlKeyResponder to the #FlKeyboardManager. Responders added + * earlier will receive events earlier. + */ +gboolean fl_keyboard_manager_handle_event(FlKeyboardManager* manager, + FlKeyEvent* event); + +/** + * fl_keyboard_manager_is_state_clear: + * @manager: the #FlKeyboardManager self. + * + * Whether the manager's various states are cleared, i.e. no pending events + * for redispatching or for responding. This is mostly used in unittests. + * + * Returns: true if the manager's various states are cleared. + */ +gboolean fl_keyboard_manager_is_state_clear(FlKeyboardManager* manager); + +G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEYBOARD_MANAGER_H_ diff --git a/shell/platform/linux/fl_keyboard_manager_test.cc b/shell/platform/linux/fl_keyboard_manager_test.cc new file mode 100644 index 0000000000000000000000000000000000000000..b822f30e52d047d3d55277b00c3387943c33c7dd --- /dev/null +++ b/shell/platform/linux/fl_keyboard_manager_test.cc @@ -0,0 +1,501 @@ +// 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/linux/fl_keyboard_manager.h" + +#include + +#include "flutter/shell/platform/linux/testing/mock_text_input_plugin.h" +#include "gtest/gtest.h" + +#define FL_KEY_EVENT(target) reinterpret_cast(target) + +namespace { +typedef void (*CallbackHandler)(FlKeyResponderAsyncCallback callback, + gpointer user_data); + +constexpr guint16 kKeyCodeKeyA = 0x26u; +constexpr guint16 kKeyCodeKeyB = 0x38u; + +G_DECLARE_FINAL_TYPE(FlKeyboardCallRecord, + fl_keyboard_call_record, + FL, + KEYBOARD_CALL_RECORD, + GObject); + +typedef struct _FlKeyMockResponder FlKeyMockResponder; +struct _FlKeyboardCallRecord { + GObject parent_instance; + + FlKeyMockResponder* responder; + FlKeyEvent* event; + FlKeyResponderAsyncCallback callback; + gpointer user_data; +}; + +#define FL_TYPE_KEY_MOCK_RESPONDER fl_key_mock_responder_get_type() +G_DECLARE_FINAL_TYPE(FlKeyMockResponder, + fl_key_mock_responder, + FL, + KEY_MOCK_RESPONDER, + GObject); + +struct _FlKeyMockResponder { + GObject parent_instance; + + // A weak pointer for a list of FlKeyboardCallRecord. + GPtrArray* call_records; + CallbackHandler callback_handler; + int delegate_id; +}; + +G_DEFINE_TYPE(FlKeyboardCallRecord, fl_keyboard_call_record, G_TYPE_OBJECT) + +static void fl_keyboard_call_record_init(FlKeyboardCallRecord* self) {} + +// Dispose method for FlKeyboardCallRecord. +static void fl_keyboard_call_record_dispose(GObject* object) { + g_return_if_fail(FL_IS_KEYBOARD_CALL_RECORD(object)); + + FlKeyboardCallRecord* self = FL_KEYBOARD_CALL_RECORD(object); + fl_key_event_dispose(self->event); + G_OBJECT_CLASS(fl_keyboard_call_record_parent_class)->dispose(object); +} + +// Class Initialization method for FlKeyboardCallRecord class. +static void fl_keyboard_call_record_class_init( + FlKeyboardCallRecordClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_keyboard_call_record_dispose; +} + +static FlKeyboardCallRecord* fl_keyboard_call_record_new( + FlKeyMockResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + g_return_val_if_fail(FL_IS_KEY_MOCK_RESPONDER(responder), nullptr); + g_return_val_if_fail(event != nullptr, nullptr); + g_return_val_if_fail(callback != nullptr, nullptr); + g_return_val_if_fail(user_data != nullptr, nullptr); + + FlKeyboardCallRecord* self = FL_KEYBOARD_CALL_RECORD( + g_object_new(fl_keyboard_call_record_get_type(), nullptr)); + + self->responder = responder; + self->event = event; + self->callback = callback; + self->user_data = user_data; + + return self; +} + +static void dont_respond(FlKeyResponderAsyncCallback callback, + gpointer user_data) {} +static void respond_true(FlKeyResponderAsyncCallback callback, + gpointer user_data) { + callback(true, user_data); +} +static void respond_false(FlKeyResponderAsyncCallback callback, + gpointer user_data) { + callback(false, user_data); +} + +static gboolean filter_keypress_returns_true(FlTextInputPlugin* self, + FlKeyEvent* event) { + return TRUE; +} + +static gboolean filter_keypress_returns_false(FlTextInputPlugin* self, + FlKeyEvent* event) { + return FALSE; +} + +static void fl_key_mock_responder_iface_init(FlKeyResponderInterface* iface); + +G_DEFINE_TYPE_WITH_CODE(FlKeyMockResponder, + fl_key_mock_responder, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(FL_TYPE_KEY_RESPONDER, + fl_key_mock_responder_iface_init)) + +static void fl_key_mock_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data); + +static void fl_key_mock_responder_iface_init(FlKeyResponderInterface* iface) { + iface->handle_event = fl_key_mock_responder_handle_event; +} + +// Return a newly allocated #FlKeyEvent that is a clone to the given #event +// but with #origin and #dispose set to 0. +static FlKeyEvent* fl_key_event_clone_information_only(FlKeyEvent* event) { + FlKeyEvent* new_event = fl_key_event_clone(event); + new_event->origin = nullptr; + new_event->dispose_origin = nullptr; + return new_event; +} +static void fl_key_mock_responder_handle_event( + FlKeyResponder* responder, + FlKeyEvent* event, + FlKeyResponderAsyncCallback callback, + gpointer user_data) { + FlKeyMockResponder* self = FL_KEY_MOCK_RESPONDER(responder); + g_ptr_array_add(self->call_records, + FL_KEYBOARD_CALL_RECORD(fl_keyboard_call_record_new( + self, fl_key_event_clone_information_only(event), + callback, user_data))); + self->callback_handler(callback, user_data); +} + +static void fl_key_mock_responder_class_init(FlKeyMockResponderClass* klass) {} + +static void fl_key_mock_responder_init(FlKeyMockResponder* self) {} + +static FlKeyMockResponder* fl_key_mock_responder_new(GPtrArray* call_records, + int delegate_id) { + FlKeyMockResponder* self = FL_KEY_MOCK_RESPONDER( + g_object_new(fl_key_mock_responder_get_type(), nullptr)); + + self->call_records = call_records; + self->callback_handler = dont_respond; + self->delegate_id = delegate_id; + + return self; +} + +static void g_ptr_array_clear(GPtrArray* array) { + g_ptr_array_remove_range(array, 0, array->len); +} + +static gpointer g_ptr_array_last(GPtrArray* array) { + return g_ptr_array_index(array, array->len - 1); +} + +static void fl_key_event_free_origin_by_mock(gpointer origin) { + g_free(origin); +} +// Create a new #FlKeyEvent with the given information. +// +// The #origin will be another #FlKeyEvent with the exact information, +// so that it can be used to redispatch, and is freed upon disposal. +static FlKeyEvent* fl_key_event_new_by_mock(bool is_press, + guint keyval, + guint16 keycode, + int state, + gboolean is_modifier) { + FlKeyEvent* event = g_new(FlKeyEvent, 1); + event->is_press = is_press; + event->time = 0; + event->state = state; + event->keyval = keyval; + event->string = nullptr; + event->keycode = keycode; + FlKeyEvent* origin_event = fl_key_event_clone_information_only(event); + event->origin = origin_event; + event->dispose_origin = fl_key_event_free_origin_by_mock; + return event; +} + +namespace { +// A global variable to store redispatched #FlKeyEvent. It is a global variable +// so that it can be used in a function without user_data. +// +// This array does not free elements upon removal. +GPtrArray* _g_redispatched_events; +} // namespace + +static GPtrArray* redispatched_events() { + if (_g_redispatched_events == nullptr) { + _g_redispatched_events = g_ptr_array_new(); + } + return _g_redispatched_events; +} +static void store_redispatched_event(gpointer event) { + FlKeyEvent* new_event = g_new(FlKeyEvent, 1); + *new_event = *reinterpret_cast(event); + g_ptr_array_add(redispatched_events(), new_event); +} + +TEST(FlKeyboardManagerTest, SingleDelegateWithAsyncResponds) { + GPtrArray* call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlKeyboardCallRecord* record; + + gboolean manager_handled = false; + g_autoptr(FlKeyboardManager) manager = + fl_keyboard_manager_new(nullptr, store_redispatched_event); + fl_keyboard_manager_add_responder( + manager, FL_KEY_RESPONDER(fl_key_mock_responder_new(call_records, 1))); + + /// Test 1: One event that is handled by the framework + + // Dispatch a key event + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 1u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x61u); + EXPECT_EQ(record->event->keycode, 0x26u); + + record->callback(true, record->user_data); + EXPECT_EQ(redispatched_events()->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); + + /// Test 2: Two events that are unhandled by the framework + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 1u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x61u); + EXPECT_EQ(record->event->keycode, 0x26u); + + // Dispatch another key event + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_b, kKeyCodeKeyB, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 2u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 1)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x62u); + EXPECT_EQ(record->event->keycode, 0x38u); + + // Resolve the second event first to test out-of-order response + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 1)); + record->callback(false, record->user_data); + EXPECT_EQ(redispatched_events()->len, 1u); + EXPECT_EQ(FL_KEY_EVENT(g_ptr_array_last(redispatched_events()))->keyval, + 0x62u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + record->callback(false, record->user_data); + EXPECT_EQ(redispatched_events()->len, 2u); + EXPECT_EQ(FL_KEY_EVENT(g_ptr_array_last(redispatched_events()))->keyval, + 0x61u); + + g_ptr_array_clear(call_records); + + // Resolve redispatches + manager_handled = fl_keyboard_manager_handle_event( + manager, FL_KEY_EVENT(g_ptr_array_index(redispatched_events(), 0))); + EXPECT_EQ(manager_handled, false); + manager_handled = fl_keyboard_manager_handle_event( + manager, FL_KEY_EVENT(g_ptr_array_index(redispatched_events(), 1))); + EXPECT_EQ(manager_handled, false); + EXPECT_EQ(call_records->len, 0u); + + g_ptr_array_clear(redispatched_events()); + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + + /// Test 3: Dispatch the same event again to ensure that prevention from + /// redispatching only works once. + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 1u); + + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + record->callback(true, record->user_data); + + g_ptr_array_clear(redispatched_events()); + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); +} + +TEST(FlKeyboardManagerTest, SingleDelegateWithSyncResponds) { + GPtrArray* call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlKeyboardCallRecord* record; + + gboolean manager_handled = false; + g_autoptr(FlKeyboardManager) manager = + fl_keyboard_manager_new(nullptr, store_redispatched_event); + FlKeyMockResponder* responder = fl_key_mock_responder_new(call_records, 1); + fl_keyboard_manager_add_responder(manager, FL_KEY_RESPONDER(responder)); + + /// Test 1: One event that is handled by the framework + + // Dispatch a key event + responder->callback_handler = respond_true; + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(call_records->len, 1u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x61u); + EXPECT_EQ(record->event->keycode, 0x26u); + EXPECT_EQ(redispatched_events()->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); + + /// Test 2: An event unhandled by the framework + responder->callback_handler = respond_false; + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(call_records->len, 1u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x61u); + EXPECT_EQ(record->event->keycode, 0x26u); + EXPECT_EQ(redispatched_events()->len, 1u); + + g_ptr_array_clear(call_records); + + // Resolve redispatch + manager_handled = fl_keyboard_manager_handle_event( + manager, FL_KEY_EVENT(g_ptr_array_index(redispatched_events(), 0))); + EXPECT_EQ(manager_handled, false); + EXPECT_EQ(call_records->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(redispatched_events()); + g_ptr_array_clear(call_records); +} + +TEST(FlKeyboardManagerTest, WithTwoAsyncDelegates) { + GPtrArray* call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlKeyboardCallRecord* record; + + gboolean manager_handled = false; + g_autoptr(FlKeyboardManager) manager = + fl_keyboard_manager_new(nullptr, store_redispatched_event); + fl_keyboard_manager_add_responder( + manager, FL_KEY_RESPONDER(fl_key_mock_responder_new(call_records, 1))); + fl_keyboard_manager_add_responder( + manager, FL_KEY_RESPONDER(fl_key_mock_responder_new(call_records, 2))); + + /// Test 1: One delegate responds true, the other false + + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 2u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + EXPECT_EQ(record->responder->delegate_id, 1); + EXPECT_EQ(record->event->keyval, 0x61u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 1)); + EXPECT_EQ(record->responder->delegate_id, 2); + EXPECT_EQ(record->event->keyval, 0x61u); + + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + record->callback(true, record->user_data); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 1)); + record->callback(false, record->user_data); + EXPECT_EQ(redispatched_events()->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); + + /// Test 2: All delegates respond false + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0x10, false)); + + EXPECT_EQ(manager_handled, true); + EXPECT_EQ(redispatched_events()->len, 0u); + EXPECT_EQ(call_records->len, 2u); + + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 0)); + record->callback(false, record->user_data); + EXPECT_EQ(redispatched_events()->len, 0u); + record = FL_KEYBOARD_CALL_RECORD(g_ptr_array_index(call_records, 1)); + record->callback(false, record->user_data); + EXPECT_EQ(redispatched_events()->len, 1u); + + g_ptr_array_clear(call_records); + + // Resolve redispatch + manager_handled = fl_keyboard_manager_handle_event( + manager, FL_KEY_EVENT(g_ptr_array_index(redispatched_events(), 0))); + EXPECT_EQ(manager_handled, false); + EXPECT_EQ(call_records->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); + + g_ptr_array_clear(redispatched_events()); +} + +TEST(FlKeyboardManagerTest, TextInputPluginReturnsFalse) { + GPtrArray* call_records = g_ptr_array_new_with_free_func(g_object_unref); + + gboolean manager_handled = false; + // The text input plugin doesn't handle events. + g_autoptr(FlKeyboardManager) manager = fl_keyboard_manager_new( + FL_TEXT_INPUT_PLUGIN( + fl_mock_text_input_plugin_new(filter_keypress_returns_false)), + store_redispatched_event); + + // The responder never handles events. + FlKeyMockResponder* responder = fl_key_mock_responder_new(call_records, 1); + fl_keyboard_manager_add_responder(manager, FL_KEY_RESPONDER(responder)); + responder->callback_handler = respond_false; + + // Dispatch a key event. + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0, false)); + EXPECT_EQ(manager_handled, true); + // The event was redispatched because no one handles it. + EXPECT_EQ(redispatched_events()->len, 1u); + + // Resolve redispatched event. + manager_handled = fl_keyboard_manager_handle_event( + manager, FL_KEY_EVENT(g_ptr_array_index(redispatched_events(), 0))); + EXPECT_EQ(manager_handled, false); + + g_ptr_array_clear(redispatched_events()); + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); +} + +TEST(FlKeyboardManagerTest, TextInputPluginReturnsTrue) { + GPtrArray* call_records = g_ptr_array_new_with_free_func(g_object_unref); + + gboolean manager_handled = false; + g_autoptr(FlKeyboardManager) manager = fl_keyboard_manager_new( + FL_TEXT_INPUT_PLUGIN( + fl_mock_text_input_plugin_new(filter_keypress_returns_true)), + store_redispatched_event); + + // The responder never handles events. + FlKeyMockResponder* responder = fl_key_mock_responder_new(call_records, 1); + fl_keyboard_manager_add_responder(manager, FL_KEY_RESPONDER(responder)); + responder->callback_handler = respond_false; + + // Dispatch a key event. + manager_handled = fl_keyboard_manager_handle_event( + manager, + fl_key_event_new_by_mock(true, GDK_KEY_a, kKeyCodeKeyA, 0, false)); + EXPECT_EQ(manager_handled, true); + // The event was not redispatched because text input plugin handles it. + EXPECT_EQ(redispatched_events()->len, 0u); + + EXPECT_TRUE(fl_keyboard_manager_is_state_clear(manager)); + g_ptr_array_clear(call_records); +} + +} // namespace diff --git a/shell/platform/linux/fl_settings_plugin.cc b/shell/platform/linux/fl_settings_plugin.cc index db1896108aad19ca46c7c263476e0e5cc24fb98d..c7b31a466bd2d5ca81636744fad952738b3ce4d0 100644 --- a/shell/platform/linux/fl_settings_plugin.cc +++ b/shell/platform/linux/fl_settings_plugin.cc @@ -31,6 +31,8 @@ struct _FlSettingsPlugin { FlBasicMessageChannel* channel; GSettings* interface_settings; + + GArray* connections; }; G_DEFINE_TYPE(FlSettingsPlugin, fl_settings_plugin, G_TYPE_OBJECT) @@ -112,6 +114,11 @@ static void update_settings(FlSettingsPlugin* self) { static void fl_settings_plugin_dispose(GObject* object) { FlSettingsPlugin* self = FL_SETTINGS_PLUGIN(object); + for (guint i = 0; i < self->connections->len; i += 1) { + g_signal_handler_disconnect(self->interface_settings, + g_array_index(self->connections, gulong, i)); + } + g_array_unref(self->connections); g_clear_object(&self->channel); g_clear_object(&self->interface_settings); @@ -133,6 +140,7 @@ FlSettingsPlugin* fl_settings_plugin_new(FlBinaryMessenger* messenger) { g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new(); self->channel = fl_basic_message_channel_new(messenger, kChannelName, FL_MESSAGE_CODEC(codec)); + self->connections = g_array_new(FALSE, FALSE, sizeof(gulong)); return self; } @@ -147,15 +155,19 @@ void fl_settings_plugin_start(FlSettingsPlugin* self) { g_settings_schema_source_lookup(source, kDesktopInterfaceSchema, FALSE); if (schema != nullptr) { self->interface_settings = g_settings_new_full(schema, nullptr, nullptr); - g_signal_connect_object( - self->interface_settings, "changed::text-scaling-factor", - G_CALLBACK(update_settings), self, G_CONNECT_SWAPPED); - g_signal_connect_object(self->interface_settings, "changed::clock-format", - G_CALLBACK(update_settings), self, - G_CONNECT_SWAPPED); - g_signal_connect_object(self->interface_settings, "changed::gtk-theme", - G_CALLBACK(update_settings), self, - G_CONNECT_SWAPPED); + gulong new_connections[] = { + g_signal_connect_object( + self->interface_settings, "changed::text-scaling-factor", + G_CALLBACK(update_settings), self, G_CONNECT_SWAPPED), + g_signal_connect_object( + self->interface_settings, "changed::clock-format", + G_CALLBACK(update_settings), self, G_CONNECT_SWAPPED), + g_signal_connect_object( + self->interface_settings, "changed::gtk-theme", + G_CALLBACK(update_settings), self, G_CONNECT_SWAPPED), + }; + g_array_append_vals(self->connections, new_connections, + sizeof(new_connections) / sizeof(gulong)); } } diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc index 2c4e4debe1a3af2370699374a1bea1784cd9e172..dbf318e5bb7fd3b540976a4c83f7332421822005 100644 --- a/shell/platform/linux/fl_text_input_plugin.cc +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -61,6 +61,9 @@ struct FlTextInputPluginPrivate { // Input method. GtkIMContext* im_context; + // IM filter. + FlTextInputPluginImFilter im_filter; + flutter::TextInputModel* text_model; // The owning Flutter view. @@ -488,7 +491,7 @@ static void fl_text_input_plugin_dispose(GObject* object) { // Implements FlTextInputPlugin::filter_keypress. static gboolean fl_text_input_plugin_filter_keypress_default( FlTextInputPlugin* self, - GdkEventKey* event) { + FlKeyEvent* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), false); FlTextInputPluginPrivate* priv = static_cast( @@ -498,7 +501,7 @@ static gboolean fl_text_input_plugin_filter_keypress_default( return FALSE; } - if (gtk_im_context_filter_keypress(priv->im_context, event)) { + if (priv->im_filter(priv->im_context, event->origin)) { return TRUE; } @@ -506,7 +509,7 @@ static gboolean fl_text_input_plugin_filter_keypress_default( gboolean do_action = FALSE; // Handle navigation keys. gboolean changed = FALSE; - if (event->type == GDK_KEY_PRESS) { + if (event->is_press) { switch (event->keyval) { case GDK_KEY_End: case GDK_KEY_KP_End: @@ -590,9 +593,12 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) { priv->text_model = new flutter::TextInputModel(); } -FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, - FlView* view) { +FlTextInputPlugin* fl_text_input_plugin_new( + FlBinaryMessenger* messenger, + FlView* view, + FlTextInputPluginImFilter im_filter) { g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); + g_return_val_if_fail(im_filter != nullptr, nullptr); FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( g_object_new(fl_text_input_plugin_get_type(), nullptr)); @@ -605,6 +611,7 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, fl_method_channel_set_method_call_handler(priv->channel, method_call_cb, self, nullptr); priv->view = view; + priv->im_filter = im_filter; return self; } @@ -612,7 +619,7 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, // Filters the a keypress given to the plugin through the plugin's // filter_keypress callback. gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, - GdkEventKey* event) { + FlKeyEvent* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); if (FL_TEXT_INPUT_PLUGIN_GET_CLASS(self)->filter_keypress) { return FL_TEXT_INPUT_PLUGIN_GET_CLASS(self)->filter_keypress(self, event); diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h index 0f5d216f7da53dd54e0eeab04489a8ec42cbf730..f8d92b58f24cf93c94f3dae9e4e4d8efef77d2e0 100644 --- a/shell/platform/linux/fl_text_input_plugin.h +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -7,9 +7,23 @@ #include +#include "flutter/shell/platform/linux/fl_key_event.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h" +/** + * FlTextInputPluginImFilter: + * @event: the pointer to the GdkEventKey. + * + * The signature for a callback with which a #FlTextInputPlugin allow an input + * method to internally handle key press and release events. + * + * The #gdk_event is an opaque pointer. It will be GdkEvent* in actual + * applications, or a dummy pointer in unit tests. + **/ +typedef gboolean (*FlTextInputPluginImFilter)(GtkIMContext* im_context, + gpointer gdk_event); + G_BEGIN_DECLS G_DECLARE_DERIVABLE_TYPE(FlTextInputPlugin, @@ -31,33 +45,38 @@ struct _FlTextInputPluginClass { /** * Virtual method called to filter a keypress. */ - gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event); + gboolean (*filter_keypress)(FlTextInputPlugin* self, FlKeyEvent* event); }; /** * fl_text_input_plugin_new: * @messenger: an #FlBinaryMessenger. * @view: the #FlView with which the text input plugin is associated. + * @im_filter: a function used to allow an input method to internally handle + * key press and release events. Typically a wrap of + * #gtk_im_context_filter_keypress. Must not be nullptr. * * Creates a new plugin that implements SystemChannels.textInput from the * Flutter services library. * * Returns: a new #FlTextInputPlugin. */ -FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, - FlView* view); +FlTextInputPlugin* fl_text_input_plugin_new( + FlBinaryMessenger* messenger, + FlView* view, + FlTextInputPluginImFilter im_filter); /** * fl_text_input_plugin_filter_keypress * @plugin: an #FlTextInputPlugin. - * @event: a #GdkEventKey + * @event: a #FlKeyEvent * * Process a Gdk key event. * * Returns: %TRUE if the event was used. */ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* plugin, - GdkEventKey* event); + FlKeyEvent* event); G_END_DECLS diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 8371d1848849fe5e2ee01dd9a8a6266ed3439651..1c593ca7302b136909f1633ae72291d9922bec6f 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -10,12 +10,14 @@ #include "flutter/shell/platform/linux/fl_accessibility_plugin.h" #include "flutter/shell/platform/linux/fl_engine_private.h" -#include "flutter/shell/platform/linux/fl_key_event_plugin.h" +#include "flutter/shell/platform/linux/fl_key_channel_responder.h" +#include "flutter/shell/platform/linux/fl_key_embedder_responder.h" +#include "flutter/shell/platform/linux/fl_key_event.h" +#include "flutter/shell/platform/linux/fl_keyboard_manager.h" #include "flutter/shell/platform/linux/fl_mouse_cursor_plugin.h" #include "flutter/shell/platform/linux/fl_platform_plugin.h" #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" #include "flutter/shell/platform/linux/fl_renderer_gl.h" -#include "flutter/shell/platform/linux/fl_text_input_plugin.h" #include "flutter/shell/platform/linux/fl_view_accessible.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" @@ -39,10 +41,9 @@ struct _FlView { // Flutter system channel handlers. FlAccessibilityPlugin* accessibility_plugin; - FlKeyEventPlugin* key_event_plugin; + FlKeyboardManager* keyboard_manager; FlMouseCursorPlugin* mouse_cursor_plugin; FlPlatformPlugin* platform_plugin; - FlTextInputPlugin* text_input_plugin; GList* gl_area_list; GList* used_area_list; @@ -160,6 +161,11 @@ static void fl_view_plugin_registry_iface_init( iface->get_registrar_for_plugin = fl_view_get_registrar_for_plugin; } +static void redispatch_key_event_by_gtk(gpointer gdk_event); + +static gboolean text_input_im_filter_by_gtk(GtkIMContext* im_context, + gpointer gdk_event); + static gboolean event_box_button_release_event(GtkWidget* widget, GdkEventButton* event, FlView* view); @@ -195,9 +201,16 @@ static void fl_view_constructed(GObject* object) { // Create system channel handlers. FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine); self->accessibility_plugin = fl_accessibility_plugin_new(self); - self->text_input_plugin = fl_text_input_plugin_new(messenger, self); - self->key_event_plugin = - fl_key_event_plugin_new(messenger, self->text_input_plugin); + self->keyboard_manager = fl_keyboard_manager_new( + fl_text_input_plugin_new(messenger, self, text_input_im_filter_by_gtk), + redispatch_key_event_by_gtk); + // The embedder responder must be added before the channel responder. + fl_keyboard_manager_add_responder( + self->keyboard_manager, + FL_KEY_RESPONDER(fl_key_embedder_responder_new(self->engine))); + fl_keyboard_manager_add_responder( + self->keyboard_manager, + FL_KEY_RESPONDER(fl_key_channel_responder_new(messenger))); self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self); self->platform_plugin = fl_platform_plugin_new(messenger); @@ -280,10 +293,9 @@ static void fl_view_dispose(GObject* object) { g_clear_object(&self->renderer); g_clear_object(&self->engine); g_clear_object(&self->accessibility_plugin); - g_clear_object(&self->key_event_plugin); + g_clear_object(&self->keyboard_manager); g_clear_object(&self->mouse_cursor_plugin); g_clear_object(&self->platform_plugin); - g_clear_object(&self->text_input_plugin); g_list_free_full(self->gl_area_list, g_object_unref); self->gl_area_list = nullptr; @@ -571,15 +583,18 @@ static gboolean event_box_leave_notify_event(GtkWidget* widget, static gboolean fl_view_key_press_event(GtkWidget* widget, GdkEventKey* event) { FlView* self = FL_VIEW(widget); - return fl_key_event_plugin_send_key_event(self->key_event_plugin, event); + return fl_keyboard_manager_handle_event( + self->keyboard_manager, fl_key_event_new_from_gdk_event(gdk_event_copy( + reinterpret_cast(event)))); } // Implements GtkWidget::key_release_event. static gboolean fl_view_key_release_event(GtkWidget* widget, GdkEventKey* event) { FlView* self = FL_VIEW(widget); - - return fl_key_event_plugin_send_key_event(self->key_event_plugin, event); + return fl_keyboard_manager_handle_event( + self->keyboard_manager, fl_key_event_new_from_gdk_event(gdk_event_copy( + reinterpret_cast(event)))); } static void fl_view_put(FlView* self, @@ -805,3 +820,18 @@ void fl_view_end_frame(FlView* view) { gtk_widget_queue_draw(GTK_WIDGET(view)); } + +static void redispatch_key_event_by_gtk(gpointer raw_event) { + GdkEvent* gdk_event = reinterpret_cast(raw_event); + GdkEventType type = gdk_event->type; + g_return_if_fail(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE); + gdk_event_put(gdk_event); +} + +static gboolean text_input_im_filter_by_gtk(GtkIMContext* im_context, + gpointer gdk_event) { + GdkEventKey* event = reinterpret_cast(gdk_event); + GdkEventType type = event->type; + g_return_val_if_fail(type == GDK_KEY_PRESS || type == GDK_KEY_RELEASE, false); + return gtk_im_context_filter_keypress(im_context, event); +} diff --git a/shell/platform/linux/key_mapping.cc b/shell/platform/linux/key_mapping.cc new file mode 100644 index 0000000000000000000000000000000000000000..c43249705e7eedd368023869d396cbf7520a3f7b --- /dev/null +++ b/shell/platform/linux/key_mapping.cc @@ -0,0 +1,459 @@ +// 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 "key_mapping.h" + +#include +#include + +#include "flutter/shell/platform/linux/fl_key_embedder_responder_private.h" + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// This file is generated by +// flutter/flutter@dev/tools/gen_keycodes/bin/gen_keycodes.dart and should not +// be edited directly. +// +// Edit the template dev/tools/gen_keycodes/data/gtk_key_mapping_cc.tmpl +// instead. See dev/tools/gen_keycodes/README.md for more information. + +std::map xkb_to_physical_key_map = { + {0x00000009, 0x00070029}, // escape + {0x0000000a, 0x0007001e}, // digit1 + {0x0000000b, 0x0007001f}, // digit2 + {0x0000000c, 0x00070020}, // digit3 + {0x0000000d, 0x00070021}, // digit4 + {0x0000000e, 0x00070022}, // digit5 + {0x0000000f, 0x00070023}, // digit6 + {0x00000010, 0x00070024}, // digit7 + {0x00000011, 0x00070025}, // digit8 + {0x00000012, 0x00070026}, // digit9 + {0x00000013, 0x00070027}, // digit0 + {0x00000014, 0x0007002d}, // minus + {0x00000015, 0x0007002e}, // equal + {0x00000016, 0x0007002a}, // backspace + {0x00000017, 0x0007002b}, // tab + {0x00000018, 0x00070014}, // keyQ + {0x00000019, 0x0007001a}, // keyW + {0x0000001a, 0x00070008}, // keyE + {0x0000001b, 0x00070015}, // keyR + {0x0000001c, 0x00070017}, // keyT + {0x0000001d, 0x0007001c}, // keyY + {0x0000001e, 0x00070018}, // keyU + {0x0000001f, 0x0007000c}, // keyI + {0x00000020, 0x00070012}, // keyO + {0x00000021, 0x00070013}, // keyP + {0x00000022, 0x0007002f}, // bracketLeft + {0x00000023, 0x00070030}, // bracketRight + {0x00000024, 0x00070028}, // enter + {0x00000025, 0x000700e0}, // controlLeft + {0x00000026, 0x00070004}, // keyA + {0x00000027, 0x00070016}, // keyS + {0x00000028, 0x00070007}, // keyD + {0x00000029, 0x00070009}, // keyF + {0x0000002a, 0x0007000a}, // keyG + {0x0000002b, 0x0007000b}, // keyH + {0x0000002c, 0x0007000d}, // keyJ + {0x0000002d, 0x0007000e}, // keyK + {0x0000002e, 0x0007000f}, // keyL + {0x0000002f, 0x00070033}, // semicolon + {0x00000030, 0x00070034}, // quote + {0x00000031, 0x00070035}, // backquote + {0x00000032, 0x000700e1}, // shiftLeft + {0x00000033, 0x00070031}, // backslash + {0x00000034, 0x0007001d}, // keyZ + {0x00000035, 0x0007001b}, // keyX + {0x00000036, 0x00070006}, // keyC + {0x00000037, 0x00070019}, // keyV + {0x00000038, 0x00070005}, // keyB + {0x00000039, 0x00070011}, // keyN + {0x0000003a, 0x00070010}, // keyM + {0x0000003b, 0x00070036}, // comma + {0x0000003c, 0x00070037}, // period + {0x0000003d, 0x00070038}, // slash + {0x0000003e, 0x000700e5}, // shiftRight + {0x0000003f, 0x00070055}, // numpadMultiply + {0x00000040, 0x000700e2}, // altLeft + {0x00000041, 0x0007002c}, // space + {0x00000042, 0x00070039}, // capsLock + {0x00000043, 0x0007003a}, // f1 + {0x00000044, 0x0007003b}, // f2 + {0x00000045, 0x0007003c}, // f3 + {0x00000046, 0x0007003d}, // f4 + {0x00000047, 0x0007003e}, // f5 + {0x00000048, 0x0007003f}, // f6 + {0x00000049, 0x00070040}, // f7 + {0x0000004a, 0x00070041}, // f8 + {0x0000004b, 0x00070042}, // f9 + {0x0000004c, 0x00070043}, // f10 + {0x0000004d, 0x00070053}, // numLock + {0x0000004e, 0x00070047}, // scrollLock + {0x0000004f, 0x0007005f}, // numpad7 + {0x00000050, 0x00070060}, // numpad8 + {0x00000051, 0x00070061}, // numpad9 + {0x00000052, 0x00070056}, // numpadSubtract + {0x00000053, 0x0007005c}, // numpad4 + {0x00000054, 0x0007005d}, // numpad5 + {0x00000055, 0x0007005e}, // numpad6 + {0x00000056, 0x00070057}, // numpadAdd + {0x00000057, 0x00070059}, // numpad1 + {0x00000058, 0x0007005a}, // numpad2 + {0x00000059, 0x0007005b}, // numpad3 + {0x0000005a, 0x00070062}, // numpad0 + {0x0000005b, 0x00070063}, // numpadDecimal + {0x0000005d, 0x00070094}, // lang5 + {0x0000005e, 0x00070064}, // intlBackslash + {0x0000005f, 0x00070044}, // f11 + {0x00000060, 0x00070045}, // f12 + {0x00000061, 0x00070087}, // intlRo + {0x00000062, 0x00070092}, // lang3 + {0x00000063, 0x00070093}, // lang4 + {0x00000064, 0x0007008a}, // convert + {0x00000065, 0x00070088}, // kanaMode + {0x00000066, 0x0007008b}, // nonConvert + {0x00000068, 0x00070058}, // numpadEnter + {0x00000069, 0x000700e4}, // controlRight + {0x0000006a, 0x00070054}, // numpadDivide + {0x0000006b, 0x00070046}, // printScreen + {0x0000006c, 0x000700e6}, // altRight + {0x0000006e, 0x0007004a}, // home + {0x0000006f, 0x00070052}, // arrowUp + {0x00000070, 0x0007004b}, // pageUp + {0x00000071, 0x00070050}, // arrowLeft + {0x00000072, 0x0007004f}, // arrowRight + {0x00000073, 0x0007004d}, // end + {0x00000074, 0x00070051}, // arrowDown + {0x00000075, 0x0007004e}, // pageDown + {0x00000076, 0x00070049}, // insert + {0x00000077, 0x0007004c}, // delete + {0x00000079, 0x0007007f}, // audioVolumeMute + {0x0000007a, 0x00070081}, // audioVolumeDown + {0x0000007b, 0x00070080}, // audioVolumeUp + {0x0000007c, 0x00070066}, // power + {0x0000007d, 0x00070067}, // numpadEqual + {0x0000007e, 0x000700d7}, // numpadSignChange + {0x0000007f, 0x00070048}, // pause + {0x00000080, 0x000c029f}, // showAllWindows + {0x00000081, 0x00070085}, // numpadComma + {0x00000082, 0x00070090}, // lang1 + {0x00000083, 0x00070091}, // lang2 + {0x00000084, 0x00070089}, // intlYen + {0x00000085, 0x000700e3}, // metaLeft + {0x00000086, 0x000700e7}, // metaRight + {0x00000087, 0x00070065}, // contextMenu + {0x00000088, 0x000c0226}, // browserStop + {0x00000089, 0x00070079}, // again + {0x0000008b, 0x0007007a}, // undo + {0x0000008c, 0x00070077}, // select + {0x0000008d, 0x0007007c}, // copy + {0x0000008e, 0x00070074}, // open + {0x0000008f, 0x0007007d}, // paste + {0x00000090, 0x0007007e}, // find + {0x00000091, 0x0007007b}, // cut + {0x00000092, 0x00070075}, // help + {0x00000094, 0x000c0192}, // launchApp2 + {0x00000096, 0x00010082}, // sleep + {0x00000097, 0x00010083}, // wakeUp + {0x00000098, 0x000c0194}, // launchApp1 + {0x0000009e, 0x000c0196}, // launchInternetBrowser + {0x000000a0, 0x000c019e}, // lockScreen + {0x000000a3, 0x000c018a}, // launchMail + {0x000000a4, 0x000c022a}, // browserFavorites + {0x000000a6, 0x000c0224}, // browserBack + {0x000000a7, 0x000c0225}, // browserForward + {0x000000a9, 0x000c00b8}, // eject + {0x000000ab, 0x000c00b5}, // mediaTrackNext + {0x000000ac, 0x000c00cd}, // mediaPlayPause + {0x000000ad, 0x000c00b6}, // mediaTrackPrevious + {0x000000ae, 0x000c00b7}, // mediaStop + {0x000000af, 0x000c00b2}, // mediaRecord + {0x000000b0, 0x000c00b4}, // mediaRewind + {0x000000b1, 0x000c008c}, // launchPhone + {0x000000b3, 0x000c0183}, // mediaSelect + {0x000000b4, 0x000c0223}, // browserHome + {0x000000b5, 0x000c0227}, // browserRefresh + {0x000000b6, 0x000c0094}, // exit + {0x000000bb, 0x000700b6}, // numpadParenLeft + {0x000000bc, 0x000700b7}, // numpadParenRight + {0x000000bd, 0x000c0201}, // newKey + {0x000000be, 0x000c0279}, // redo + {0x000000bf, 0x00070068}, // f13 + {0x000000c0, 0x00070069}, // f14 + {0x000000c1, 0x0007006a}, // f15 + {0x000000c2, 0x0007006b}, // f16 + {0x000000c3, 0x0007006c}, // f17 + {0x000000c4, 0x0007006d}, // f18 + {0x000000c5, 0x0007006e}, // f19 + {0x000000c6, 0x0007006f}, // f20 + {0x000000c7, 0x00070070}, // f21 + {0x000000c8, 0x00070071}, // f22 + {0x000000c9, 0x00070072}, // f23 + {0x000000ca, 0x00070073}, // f24 + {0x000000d1, 0x000c00b1}, // mediaPause + {0x000000d6, 0x000c0203}, // close + {0x000000d7, 0x000c00b0}, // mediaPlay + {0x000000d8, 0x000c00b3}, // mediaFastForward + {0x000000d9, 0x000c00e5}, // bassBoost + {0x000000da, 0x000c0208}, // print + {0x000000e1, 0x000c0221}, // browserSearch + {0x000000e8, 0x000c0070}, // brightnessDown + {0x000000e9, 0x000c006f}, // brightnessUp + {0x000000eb, 0x000100b5}, // displayToggleIntExt + {0x000000ed, 0x000c007a}, // kbdIllumDown + {0x000000ee, 0x000c0079}, // kbdIllumUp + {0x000000ef, 0x000c028c}, // mailSend + {0x000000f0, 0x000c0289}, // mailReply + {0x000000f1, 0x000c028b}, // mailForward + {0x000000f2, 0x000c0207}, // save + {0x000000f3, 0x000c01a7}, // launchDocuments + {0x000000fc, 0x000c0075}, // brightnessAuto + {0x0000016e, 0x000c0060}, // info + {0x00000172, 0x000c008d}, // programGuide + {0x0000017a, 0x000c0061}, // closedCaptionToggle + {0x0000017c, 0x000c0232}, // zoomToggle + {0x0000017e, 0x000c01ae}, // launchKeyboardLayout + {0x00000190, 0x000c01b7}, // launchAudioBrowser + {0x00000195, 0x000c018e}, // launchCalendar + {0x0000019d, 0x000c0083}, // mediaLast + {0x000001a2, 0x000c009c}, // channelUp + {0x000001a3, 0x000c009d}, // channelDown + {0x000001aa, 0x000c022d}, // zoomIn + {0x000001ab, 0x000c022e}, // zoomOut + {0x000001ad, 0x000c0184}, // launchWordProcessor + {0x000001af, 0x000c0186}, // launchSpreadsheet + {0x000001b5, 0x000c018d}, // launchContacts + {0x000001b7, 0x000c0072}, // brightnessToggle + {0x000001b8, 0x000c01ab}, // spellCheck + {0x000001b9, 0x000c019c}, // logOff + {0x0000024b, 0x000c019f}, // launchControlPanel + {0x0000024c, 0x000c01a2}, // selectTask + {0x0000024d, 0x000c01b1}, // launchScreenSaver + {0x0000024e, 0x000c00cf}, // speechInputToggle + {0x0000024f, 0x000c01cb}, // launchAssistant + {0x00000250, 0x000c029d}, // keyboardLayoutSelect + {0x00000258, 0x000c0073}, // brightnessMinimum + {0x00000259, 0x000c0074}, // brightnessMaximum + {0x00000281, 0x00000017}, // privacyScreenToggle +}; + +std::map gtk_keyval_to_logical_key_map = { + {0x000000a5, 0x01100070089}, // yen + {0x0000fd06, 0x01000000405}, // 3270_EraseEOF + {0x0000fd0e, 0x01000000503}, // 3270_Attn + {0x0000fd15, 0x01000000402}, // 3270_Copy + {0x0000fd16, 0x01000000d2f}, // 3270_Play + {0x0000fd1b, 0x01000000406}, // 3270_ExSelect + {0x0000fd1d, 0x01000000608}, // 3270_PrintScreen + {0x0000fd1e, 0x0100000000d}, // 3270_Enter + {0x0000fe03, 0x40000000102}, // ISO_Level3_Shift + {0x0000fe08, 0x01000000709}, // ISO_Next_Group + {0x0000fe0a, 0x0100000070a}, // ISO_Prev_Group + {0x0000fe0c, 0x01000000707}, // ISO_First_Group + {0x0000fe0e, 0x01000000708}, // ISO_Last_Group + {0x0000fe20, 0x01000000009}, // ISO_Left_Tab + {0x0000fe34, 0x0100000000d}, // ISO_Enter + {0x0000ff08, 0x01000000008}, // BackSpace + {0x0000ff09, 0x01000000009}, // Tab + {0x0000ff0b, 0x01000000401}, // Clear + {0x0000ff0d, 0x0100000000d}, // Return + {0x0000ff13, 0x01000000509}, // Pause + {0x0000ff14, 0x0100000010c}, // Scroll_Lock + {0x0000ff1b, 0x0100000001b}, // Escape + {0x0000ff21, 0x01000000719}, // Kanji + {0x0000ff24, 0x0100000071b}, // Romaji + {0x0000ff25, 0x01000000716}, // Hiragana + {0x0000ff26, 0x0100000071a}, // Katakana + {0x0000ff27, 0x01000000717}, // Hiragana_Katakana + {0x0000ff28, 0x0100000071c}, // Zenkaku + {0x0000ff29, 0x01000000715}, // Hankaku + {0x0000ff2a, 0x0100000071d}, // Zenkaku_Hankaku + {0x0000ff2f, 0x01000000714}, // Eisu_Shift + {0x0000ff31, 0x01000000711}, // Hangul + {0x0000ff34, 0x01000000712}, // Hangul_Hanja + {0x0000ff37, 0x01000000703}, // Codeinput + {0x0000ff3c, 0x01000000710}, // SingleCandidate + {0x0000ff3e, 0x0100000070e}, // PreviousCandidate + {0x0000ff50, 0x01000000306}, // Home + {0x0000ff51, 0x01000000302}, // Left + {0x0000ff52, 0x01000000304}, // Up + {0x0000ff53, 0x01000000303}, // Right + {0x0000ff54, 0x01000000301}, // Down + {0x0000ff55, 0x01000000308}, // Page_Up + {0x0000ff56, 0x01000000307}, // Page_Down + {0x0000ff57, 0x01000000305}, // End + {0x0000ff60, 0x0100000050c}, // Select + {0x0000ff61, 0x01000000a0c}, // Print + {0x0000ff62, 0x01000000506}, // Execute + {0x0000ff63, 0x01000000407}, // Insert + {0x0000ff65, 0x0100000040a}, // Undo + {0x0000ff66, 0x01000000409}, // Redo + {0x0000ff67, 0x01000000505}, // Menu + {0x0000ff68, 0x01000000507}, // Find + {0x0000ff69, 0x01000000504}, // Cancel + {0x0000ff6a, 0x01000000508}, // Help + {0x0000ff7e, 0x0100000070b}, // Mode_switch + {0x0000ff7f, 0x0100000010a}, // Num_Lock + {0x0000ff80, 0x00000000020}, // KP_Space + {0x0000ff89, 0x01000000009}, // KP_Tab + {0x0000ff8d, 0x5000000000d}, // KP_Enter + {0x0000ff91, 0x01000000801}, // KP_F1 + {0x0000ff92, 0x01000000802}, // KP_F2 + {0x0000ff93, 0x01000000803}, // KP_F3 + {0x0000ff94, 0x01000000804}, // KP_F4 + {0x0000ff95, 0x50000000037}, // KP_Home + {0x0000ff96, 0x50000000034}, // KP_Left + {0x0000ff97, 0x50000000038}, // KP_Up + {0x0000ff98, 0x50000000036}, // KP_Right + {0x0000ff99, 0x50000000032}, // KP_Down + {0x0000ff9a, 0x50000000039}, // KP_Page_Up + {0x0000ff9b, 0x50000000033}, // KP_Page_Down + {0x0000ff9c, 0x50000000031}, // KP_End + {0x0000ff9e, 0x50000000030}, // KP_Insert + {0x0000ff9f, 0x5000000002e}, // KP_Delete + {0x0000ffaa, 0x5000000002a}, // KP_Multiply + {0x0000ffab, 0x5000000002b}, // KP_Add + {0x0000ffad, 0x5000000002d}, // KP_Subtract + {0x0000ffae, 0x0000000002e}, // KP_Decimal + {0x0000ffaf, 0x5000000002f}, // KP_Divide + {0x0000ffb0, 0x50000000030}, // KP_0 + {0x0000ffb1, 0x50000000031}, // KP_1 + {0x0000ffb2, 0x50000000032}, // KP_2 + {0x0000ffb3, 0x50000000033}, // KP_3 + {0x0000ffb4, 0x50000000034}, // KP_4 + {0x0000ffb5, 0x50000000035}, // KP_5 + {0x0000ffb6, 0x50000000036}, // KP_6 + {0x0000ffb7, 0x50000000037}, // KP_7 + {0x0000ffb8, 0x50000000038}, // KP_8 + {0x0000ffb9, 0x50000000039}, // KP_9 + {0x0000ffbd, 0x5000000003d}, // KP_Equal + {0x0000ffbe, 0x01000000801}, // F1 + {0x0000ffbf, 0x01000000802}, // F2 + {0x0000ffc0, 0x01000000803}, // F3 + {0x0000ffc1, 0x01000000804}, // F4 + {0x0000ffc2, 0x01000000805}, // F5 + {0x0000ffc3, 0x01000000806}, // F6 + {0x0000ffc4, 0x01000000807}, // F7 + {0x0000ffc5, 0x01000000808}, // F8 + {0x0000ffc6, 0x01000000809}, // F9 + {0x0000ffc7, 0x0100000080a}, // F10 + {0x0000ffc8, 0x0100000080b}, // F11 + {0x0000ffc9, 0x0100000080c}, // F12 + {0x0000ffca, 0x0100000080d}, // F13 + {0x0000ffcb, 0x0100000080e}, // F14 + {0x0000ffcc, 0x0100000080f}, // F15 + {0x0000ffcd, 0x01000000810}, // F16 + {0x0000ffce, 0x01000000811}, // F17 + {0x0000ffcf, 0x01000000812}, // F18 + {0x0000ffd0, 0x01000000813}, // F19 + {0x0000ffd1, 0x01000000814}, // F20 + {0x0000ffd2, 0x01000000815}, // F21 + {0x0000ffd3, 0x01000000816}, // F22 + {0x0000ffd4, 0x01000000817}, // F23 + {0x0000ffd5, 0x01000000818}, // F24 + {0x0000ffe1, 0x3000000010d}, // Shift_L + {0x0000ffe2, 0x4000000010d}, // Shift_R + {0x0000ffe3, 0x30000000105}, // Control_L + {0x0000ffe4, 0x40000000105}, // Control_R + {0x0000ffe5, 0x01000000104}, // Caps_Lock + {0x0000ffe7, 0x30000000109}, // Meta_L + {0x0000ffe8, 0x40000000109}, // Meta_R + {0x0000ffe9, 0x30000000102}, // Alt_L + {0x0000ffea, 0x40000000102}, // Alt_R + {0x0000ffeb, 0x0100000010e}, // Super_L + {0x0000ffec, 0x0100000010e}, // Super_R + {0x0000ffed, 0x01000000108}, // Hyper_L + {0x0000ffee, 0x01000000108}, // Hyper_R + {0x0000ffff, 0x0100000007f}, // Delete + {0x1008ff02, 0x01000000602}, // MonBrightnessUp + {0x1008ff03, 0x01000000601}, // MonBrightnessDown + {0x1008ff10, 0x0100000060a}, // Standby + {0x1008ff11, 0x01000000a0f}, // AudioLowerVolume + {0x1008ff12, 0x01000000a11}, // AudioMute + {0x1008ff13, 0x01000000a10}, // AudioRaiseVolume + {0x1008ff14, 0x01000000d2f}, // AudioPlay + {0x1008ff15, 0x01000000a07}, // AudioStop + {0x1008ff16, 0x01000000a09}, // AudioPrev + {0x1008ff17, 0x01000000a08}, // AudioNext + {0x1008ff18, 0x01000000c04}, // HomePage + {0x1008ff19, 0x01000000b03}, // Mail + {0x1008ff1b, 0x01000000c06}, // Search + {0x1008ff1c, 0x01000000d30}, // AudioRecord + {0x1008ff20, 0x01000000b02}, // Calendar + {0x1008ff26, 0x01000000c01}, // Back + {0x1008ff27, 0x01000000c03}, // Forward + {0x1008ff28, 0x01000000c07}, // Stop + {0x1008ff29, 0x01000000c05}, // Refresh + {0x1008ff2a, 0x01000000607}, // PowerOff + {0x1008ff2b, 0x0100000060b}, // WakeUp + {0x1008ff2c, 0x01000000604}, // Eject + {0x1008ff2d, 0x01000000b07}, // ScreenSaver + {0x1008ff2f, 0x01100010082}, // Sleep + {0x1008ff30, 0x01000000c02}, // Favorites + {0x1008ff31, 0x01000000d2e}, // AudioPause + {0x1008ff3e, 0x01000000d31}, // AudioRewind + {0x1008ff56, 0x01000000a01}, // Close + {0x1008ff57, 0x01000000402}, // Copy + {0x1008ff58, 0x01000000404}, // Cut + {0x1008ff61, 0x01000000605}, // LogOff + {0x1008ff68, 0x01000000a0a}, // New + {0x1008ff6b, 0x01000000a0b}, // Open + {0x1008ff6d, 0x01000000408}, // Paste + {0x1008ff6e, 0x01000000b0d}, // Phone + {0x1008ff72, 0x01000000a03}, // Reply + {0x1008ff77, 0x01000000a0d}, // Save + {0x1008ff7b, 0x01000000a04}, // Send + {0x1008ff7c, 0x01000000a0e}, // Spell + {0x1008ff8b, 0x0100000050d}, // ZoomIn + {0x1008ff8c, 0x0100000050e}, // ZoomOut + {0x1008ff90, 0x01000000a02}, // MailForward + {0x1008ff97, 0x01000000d2c}, // AudioForward + {0x1008ffa7, 0x01100000014}, // Suspend +}; + +void initialize_modifier_bit_to_checked_keys(GHashTable* table) { + FlKeyEmbedderCheckedKey* data; + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_SHIFT_MASK), data); + data->is_caps_lock = false; + data->primary_physical_key = 0x0000700e1; // shiftLeft + data->primary_logical_key = 0x3000000010d; // shiftLeft + data->secondary_logical_key = 0x4000000010d; // shiftRight + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_CONTROL_MASK), data); + data->is_caps_lock = false; + data->primary_physical_key = 0x0000700e0; // controlLeft + data->primary_logical_key = 0x30000000105; // controlLeft + data->secondary_logical_key = 0x40000000105; // controlRight + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_MOD1_MASK), data); + data->is_caps_lock = false; + data->primary_physical_key = 0x0000700e2; // altLeft + data->primary_logical_key = 0x30000000102; // altLeft + data->secondary_logical_key = 0x40000000102; // altRight + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_META_MASK), data); + data->is_caps_lock = false; + data->primary_physical_key = 0x0000700e3; // metaLeft + data->primary_logical_key = 0x30000000109; // metaLeft + data->secondary_logical_key = 0x40000000109; // metaRight +} + +void initialize_lock_bit_to_checked_keys(GHashTable* table) { + FlKeyEmbedderCheckedKey* data; + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_LOCK_MASK), data); + data->is_caps_lock = true; + data->primary_physical_key = 0x000070039; // capsLock + data->primary_logical_key = 0x01000000104; // capsLock + + data = g_new(FlKeyEmbedderCheckedKey, 1); + g_hash_table_insert(table, GUINT_TO_POINTER(GDK_MOD2_MASK), data); + data->is_caps_lock = false; + data->primary_physical_key = 0x000070053; // numLock + data->primary_logical_key = 0x0100000010a; // numLock +} diff --git a/shell/platform/linux/key_mapping.h b/shell/platform/linux/key_mapping.h new file mode 100644 index 0000000000000000000000000000000000000000..c537102e04168d2cdea9b3a3ac8a05ea5230e7d8 --- /dev/null +++ b/shell/platform/linux/key_mapping.h @@ -0,0 +1,30 @@ +// 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 KEYBOARD_MAP_H_ +#define KEYBOARD_MAP_H_ + +#include +#include +#include + +inline uint64_t gpointer_to_uint64(gpointer pointer) { + return pointer == nullptr ? 0 : reinterpret_cast(pointer); +} + +inline gpointer uint64_to_gpointer(uint64_t number) { + return reinterpret_cast(number); +} + +// Maps XKB specific key code values to Flutter's physical key code values. +extern std::map xkb_to_physical_key_map; + +// Maps GDK keyval values to Flutter's logical key code values. +extern std::map gtk_keyval_to_logical_key_map; + +void initialize_modifier_bit_to_checked_keys(GHashTable* table); + +void initialize_lock_bit_to_checked_keys(GHashTable* table); + +#endif // KEYBOARD_MAP_H_ diff --git a/shell/platform/linux/testing/mock_engine.cc b/shell/platform/linux/testing/mock_engine.cc index 516adae3c33454863d1639fb01df7fc682d2a346..fc748b69c128b6f373eec41758464ed8d74d787d 100644 --- a/shell/platform/linux/testing/mock_engine.cc +++ b/shell/platform/linux/testing/mock_engine.cc @@ -226,6 +226,14 @@ FlutterEngineResult FlutterEngineSendPointerEvent( return kSuccess; } +FlutterEngineResult FlutterEngineSendKeyEvent(FLUTTER_API_SYMBOL(FlutterEngine) + engine, + const FlutterKeyEvent* event, + FlutterKeyEventCallback callback, + void* user_data) { + return kSuccess; +} + FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendPlatformMessage( FLUTTER_API_SYMBOL(FlutterEngine) engine, @@ -481,6 +489,7 @@ FlutterEngineResult FlutterEngineGetProcAddresses( table->RunInitialized = &FlutterEngineRunInitialized; table->SendWindowMetricsEvent = &FlutterEngineSendWindowMetricsEvent; table->SendPointerEvent = &FlutterEngineSendPointerEvent; + table->SendKeyEvent = &FlutterEngineSendKeyEvent; table->SendPlatformMessage = &FlutterEngineSendPlatformMessage; table->PlatformMessageCreateResponseHandle = &FlutterPlatformMessageCreateResponseHandle; diff --git a/shell/platform/linux/testing/mock_text_input_plugin.cc b/shell/platform/linux/testing/mock_text_input_plugin.cc index 92309fa0e1c7c7f737ae257a68beb4c4d341b681..bb6d7f070ae0b7f669a369e0c2c66e0d101eb393 100644 --- a/shell/platform/linux/testing/mock_text_input_plugin.cc +++ b/shell/platform/linux/testing/mock_text_input_plugin.cc @@ -7,7 +7,7 @@ struct _FlMockTextInputPlugin { FlTextInputPlugin parent_instance; - gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event); + gboolean (*filter_keypress)(FlTextInputPlugin* self, FlKeyEvent* event); }; G_DEFINE_TYPE(FlMockTextInputPlugin, @@ -15,7 +15,7 @@ G_DEFINE_TYPE(FlMockTextInputPlugin, fl_text_input_plugin_get_type()) static gboolean mock_text_input_plugin_filter_keypress(FlTextInputPlugin* self, - GdkEventKey* event) { + FlKeyEvent* event) { FlMockTextInputPlugin* mock_self = FL_MOCK_TEXT_INPUT_PLUGIN(self); if (mock_self->filter_keypress) { return mock_self->filter_keypress(self, event); @@ -33,7 +33,7 @@ static void fl_mock_text_input_plugin_init(FlMockTextInputPlugin* self) {} // Creates a mock text_input_plugin FlMockTextInputPlugin* fl_mock_text_input_plugin_new( - gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event)) { + gboolean (*filter_keypress)(FlTextInputPlugin* self, FlKeyEvent* event)) { FlMockTextInputPlugin* self = FL_MOCK_TEXT_INPUT_PLUGIN( g_object_new(fl_mock_text_input_plugin_get_type(), nullptr)); self->filter_keypress = filter_keypress; diff --git a/shell/platform/linux/testing/mock_text_input_plugin.h b/shell/platform/linux/testing/mock_text_input_plugin.h index 64b079e2c9d08c26cb85724b0583aa4445f90144..7d97f1ad88fb66666f1846749345335647c1144d 100644 --- a/shell/platform/linux/testing/mock_text_input_plugin.h +++ b/shell/platform/linux/testing/mock_text_input_plugin.h @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#ifndef FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_TEXT_INPUT_PLUGIN_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_TEXT_INPUT_PLUGIN_H_ + +#include + #include "flutter/shell/platform/linux/fl_text_input_plugin.h" G_BEGIN_DECLS @@ -13,6 +18,8 @@ G_DECLARE_FINAL_TYPE(FlMockTextInputPlugin, FlTextInputPlugin) FlMockTextInputPlugin* fl_mock_text_input_plugin_new( - gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event)); + gboolean (*filter_keypress)(FlTextInputPlugin* self, FlKeyEvent* event)); G_END_DECLS + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_TEXT_INPUT_PLUGIN_H_ diff --git a/shell/platform/windows/keyboard_key_channel_handler.h b/shell/platform/windows/keyboard_key_channel_handler.h index ff9aa1a8cf4b6b25035cb937b9a0654427c6dbc2..57ade1e4e8513a6e2ddfa8faf3e8e8b9dd52ece5 100644 --- a/shell/platform/windows/keyboard_key_channel_handler.h +++ b/shell/platform/windows/keyboard_key_channel_handler.h @@ -19,7 +19,7 @@ namespace flutter { // A delegate of |KeyboardKeyHandler| that handles events by sending the // raw information through the method channel. // -// This class corresponds to the RawKeyboard API in the framework. +// This class communicates with the RawKeyboard API in the framework. class KeyboardKeyChannelHandler : public KeyboardKeyHandler::KeyboardKeyHandlerDelegate { public: diff --git a/shell/platform/windows/keyboard_key_embedder_handler.cc b/shell/platform/windows/keyboard_key_embedder_handler.cc index ae4fc807c6655b9739b111399bffaad03e4d0d34..dd59473cfa2dddaed9dbc4d4c7560e8c72696f38 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler.cc @@ -39,6 +39,8 @@ constexpr uint64_t kHidPlane = 0x00100000000; constexpr uint64_t kUnicodePlane = 0x00000000000; /** + * The code prefix for keys from Windows which do not have a Unicode + * representation. */ constexpr uint64_t kWindowsKeyIdPlane = 0x00700000000; diff --git a/shell/platform/windows/keyboard_key_embedder_handler.h b/shell/platform/windows/keyboard_key_embedder_handler.h index edc90f207b3c49a0f9bf9df0dcaca2fb68578061..0f557d7b3f89b3354f88f3325626563e0a9bb676 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler.h +++ b/shell/platform/windows/keyboard_key_embedder_handler.h @@ -20,7 +20,7 @@ namespace {} // namespace // A delegate of |KeyboardKeyHandler| that handles events by sending // converted |FlutterKeyEvent|s through the embedder API. // -// This class corresponds to the HardwareKeyboard API in the framework. +// This class communicates with the HardwareKeyboard API in the framework. class KeyboardKeyEmbedderHandler : public KeyboardKeyHandler::KeyboardKeyHandlerDelegate { public: diff --git a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc index 1427dcc4d4a5682af3cdd4fad0671514e7bae91f..75704c9de835474c0e09a7e2bdda736d982b30a8 100644 --- a/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc +++ b/shell/platform/windows/keyboard_key_embedder_handler_unittests.cc @@ -480,7 +480,7 @@ TEST(KeyboardKeyEmbedderHandlerTest, ModifierKeysByVirtualKey) { results.clear(); } -TEST(KeyboardKeyEmbedderHandlerTest, AbruptRepeatIsConvertedtoDown) { +TEST(KeyboardKeyEmbedderHandlerTest, AbruptRepeatIsConvertedToDown) { TestKeystate key_state; std::vector results; TestFlutterKeyEvent* event; diff --git a/shell/platform/windows/keyboard_key_handler.cc b/shell/platform/windows/keyboard_key_handler.cc index 0a7e48772bd63ed8ca017c9bb9bb27b706ac54a5..d5042412fd0f09cf069b8917607575365f6c35d1 100644 --- a/shell/platform/windows/keyboard_key_handler.cc +++ b/shell/platform/windows/keyboard_key_handler.cc @@ -14,20 +14,6 @@ namespace flutter { namespace { -static constexpr char kChannelName[] = "flutter/keyevent"; - -static constexpr char kKeyCodeKey[] = "keyCode"; -static constexpr char kScanCodeKey[] = "scanCode"; -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"; - // The maximum number of pending events to keep before // emitting a warning on the console about unhandled events. static constexpr int kMaxPendingEvents = 1000; @@ -148,7 +134,6 @@ bool KeyboardKeyHandler::RemoveRedispatchedEvent(const PendingEvent& incoming) { } } return false; - ; } void KeyboardKeyHandler::ResolvePendingEvent(uint64_t sequence_id,