diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index 6dcc1779f1930..58e5fdfa7f174 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -177,6 +177,7 @@ executable("flutter_linux_unittests") { "testing/mock_egl.cc", "testing/mock_engine.cc", "testing/mock_renderer.cc", + "testing/mock_text_input_plugin.cc", ] public_configs = [ "//flutter:config" ] diff --git a/shell/platform/linux/fl_key_event_plugin.cc b/shell/platform/linux/fl_key_event_plugin.cc index fe5628b55f007..432ff5c1853f6 100644 --- a/shell/platform/linux/fl_key_event_plugin.cc +++ b/shell/platform/linux/fl_key_event_plugin.cc @@ -3,6 +3,10 @@ // found in the LICENSE file. #include "flutter/shell/platform/linux/fl_key_event_plugin.h" + +#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" @@ -20,33 +24,272 @@ 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; - GAsyncReadyCallback response_callback = 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); + // Don't need to weak pointer anymore. + g_object_remove_weak_pointer(G_OBJECT(self->plugin), + reinterpret_cast(&(self->plugin))); +} + +// 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. +static uint64_t 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. +static GdkEventKey* find_pending_event(FlKeyEventPlugin* self, uint64_t id) { + if (self->pending_events->len == 0 || + FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id != id) { + return nullptr; + } + + return FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->event; +} + +// Removes an event from the pending event queue. +static void remove_pending_event(FlKeyEventPlugin* self, uint64_t id) { + if (self->pending_events->len == 0 || + FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id != id) { + g_warning( + "Tried to remove pending event with id %ld, but the event was out of " + "order, or is unknown.", + id); + return; + } + g_ptr_array_remove_index(self->pending_events, 0); +} + +// 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 = find_pending_event(self, data->id); + if (event == nullptr) { + g_warning( + "Event response for event id %ld received, but event was received " + "out of order, or is unknown.", + 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); + g_object_remove_weak_pointer( + G_OBJECT(self->text_input_plugin), + reinterpret_cast(&(self->text_input_plugin))); + 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) {} -FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger, - GAsyncReadyCallback response_callback, - const char* channel_name) { +// 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)); @@ -56,15 +299,37 @@ FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger, 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; } -void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* 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_if_fail(FL_IS_KEY_EVENT_PLUGIN(self)); - g_return_if_fail(event != nullptr); + 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 = get_event_id(event); + if (self->pending_events->len != 0 && + FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id == id) { + // If the event is at the head of the queue of pending events we've seen, + // and has the same id, 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) { @@ -75,7 +340,7 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self, type = kTypeValueUp; break; default: - return; + return FALSE; } int64_t scan_code = event->hardware_keycode; @@ -109,9 +374,9 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self, // 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; + 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; @@ -144,6 +409,14 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self, 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, - self->response_callback, user_data); + 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 index 4b6b2ae822399..687045285a897 100644 --- a/shell/platform/linux/fl_key_event_plugin.h +++ b/shell/platform/linux/fl_key_event_plugin.h @@ -5,7 +5,9 @@ #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 @@ -24,15 +26,36 @@ G_DECLARE_FINAL_TYPE(FlKeyEventPlugin, * 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, + bool 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. - * @channel_name: the name of the channel to send key events on. If not given - * (nullptr), then the standard key event channel name is used. - * Typically used for tests to send on a test channel. + * 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. @@ -41,7 +64,8 @@ G_DECLARE_FINAL_TYPE(FlKeyEventPlugin, */ FlKeyEventPlugin* fl_key_event_plugin_new( FlBinaryMessenger* messenger, - GAsyncReadyCallback response_callback = nullptr, + FlTextInputPlugin* text_input_plugin, + FlKeyEventPluginCallback response_callback = nullptr, const char* channel_name = nullptr); /** @@ -51,9 +75,12 @@ FlKeyEventPlugin* fl_key_event_plugin_new( * @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. */ -void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin, +bool fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin, GdkEventKey* event, gpointer user_data = nullptr); diff --git a/shell/platform/linux/fl_key_event_plugin_test.cc b/shell/platform/linux/fl_key_event_plugin_test.cc index 4f3f99a39de39..f39b0b54ff221 100644 --- a/shell/platform/linux/fl_key_event_plugin_test.cc +++ b/shell/platform/linux/fl_key_event_plugin_test.cc @@ -4,41 +4,47 @@ #include "flutter/shell/platform/linux/fl_key_event_plugin.h" -#include #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/public/flutter_linux/fl_basic_message_channel.h" -#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h" #include "flutter/shell/platform/linux/testing/fl_test.h" -#include "flutter/shell/platform/linux/testing/mock_renderer.h" +#include "flutter/shell/platform/linux/testing/mock_text_input_plugin.h" const char* expected_value = nullptr; +gboolean expected_handled = FALSE; // Called when the message response is received in the send_key_event test. static void echo_response_cb(GObject* object, - GAsyncResult* result, + FlValue* message, + bool handled, gpointer user_data) { - g_autoptr(GError) error = nullptr; - g_autoptr(FlValue) message = fl_basic_message_channel_send_finish( - FL_BASIC_MESSAGE_CHANNEL(object), result, &error); EXPECT_NE(message, nullptr); - EXPECT_EQ(error, 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); + 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(FlKeyEventPlugin) plugin = - fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo"); + 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{ @@ -58,6 +64,7 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) { 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. @@ -67,7 +74,7 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) { GDK_KEY_RELEASE, // event type nullptr, // window (not needed) FALSE, // event was sent explicitly - 12345, // time + 23456, // time 0x0, // modifier state GDK_KEY_A, // key code 1, // length of string representation @@ -80,7 +87,9 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) { expected_value = "{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, " "modifiers: 0, unicodeScalarValues: 65}"; - fl_key_event_plugin_send_key_event(plugin, &key_event, loop); + 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); @@ -93,8 +102,10 @@ void test_lock_event(guint key_code, g_autoptr(FlEngine) engine = make_mock_engine(); FlBinaryMessenger* messenger = fl_binary_messenger_new(engine); - g_autoptr(FlKeyEventPlugin) plugin = - fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo"); + 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 @@ -111,14 +122,18 @@ void test_lock_event(guint key_code, }; expected_value = down_expected; - fl_key_event_plugin_send_key_event(plugin, &key_event, loop); + 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); 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. @@ -151,3 +166,107 @@ TEST(FlKeyEventPluginTest, SendShiftLockKeyEvent) { "{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); +} + +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); +} + +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); +} diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc index 2841db1e79ca7..eb7841f723788 100644 --- a/shell/platform/linux/fl_text_input_plugin.cc +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -44,7 +44,7 @@ static constexpr char kMultilineInputType[] = "TextInputType.multiline"; static constexpr int64_t kClientIdUnset = -1; -struct _FlTextInputPlugin { +struct FlTextInputPluginPrivate { GObject parent_instance; FlMethodChannel* channel; @@ -77,7 +77,9 @@ struct _FlTextInputPlugin { GdkRectangle composing_rect; }; -G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT) +G_DEFINE_TYPE_WITH_PRIVATE(FlTextInputPlugin, + fl_text_input_plugin, + G_TYPE_OBJECT) // Completes method call and returns TRUE if the call was successful. static gboolean finish_method(GObject* object, @@ -104,24 +106,27 @@ static void update_editing_state_response_cb(GObject* object, // Informs Flutter of text input changes. static void update_editing_state(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + g_autoptr(FlValue) args = fl_value_new_list(); - fl_value_append_take(args, fl_value_new_int(self->client_id)); + fl_value_append_take(args, fl_value_new_int(priv->client_id)); g_autoptr(FlValue) value = fl_value_new_map(); - TextRange selection = self->text_model->selection(); + TextRange selection = priv->text_model->selection(); fl_value_set_string_take( value, kTextKey, - fl_value_new_string(self->text_model->GetText().c_str())); + fl_value_new_string(priv->text_model->GetText().c_str())); fl_value_set_string_take(value, kSelectionBaseKey, fl_value_new_int(selection.base())); fl_value_set_string_take(value, kSelectionExtentKey, fl_value_new_int(selection.extent())); - int composing_base = self->text_model->composing() - ? self->text_model->composing_range().base() + int composing_base = priv->text_model->composing() + ? priv->text_model->composing_range().base() : -1; - int composing_extent = self->text_model->composing() - ? self->text_model->composing_range().extent() + int composing_extent = priv->text_model->composing() + ? priv->text_model->composing_range().extent() : -1; fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(composing_base)); @@ -136,7 +141,7 @@ static void update_editing_state(FlTextInputPlugin* self) { fl_value_append(args, value); - fl_method_channel_invoke_method(self->channel, kUpdateEditingStateMethod, + fl_method_channel_invoke_method(priv->channel, kUpdateEditingStateMethod, args, nullptr, update_editing_state_response_cb, self); } @@ -153,61 +158,74 @@ static void perform_action_response_cb(GObject* object, // Inform Flutter that the input has been activated. static void perform_action(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + g_return_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self)); - g_return_if_fail(self->client_id != 0); - g_return_if_fail(self->input_action != nullptr); + g_return_if_fail(priv->client_id != 0); + g_return_if_fail(priv->input_action != nullptr); g_autoptr(FlValue) args = fl_value_new_list(); - fl_value_append_take(args, fl_value_new_int(self->client_id)); - fl_value_append_take(args, fl_value_new_string(self->input_action)); + fl_value_append_take(args, fl_value_new_int(priv->client_id)); + fl_value_append_take(args, fl_value_new_string(priv->input_action)); - fl_method_channel_invoke_method(self->channel, kPerformActionMethod, args, + fl_method_channel_invoke_method(priv->channel, kPerformActionMethod, args, nullptr, perform_action_response_cb, self); } // Signal handler for GtkIMContext::preedit-start static void im_preedit_start_cb(FlTextInputPlugin* self) { - self->text_model->BeginComposing(); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->text_model->BeginComposing(); // Set the top-level window used for system input method windows. GdkWindow* window = - gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self->view))); - gtk_im_context_set_client_window(self->im_context, window); + gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(priv->view))); + gtk_im_context_set_client_window(priv->im_context, window); } // Signal handler for GtkIMContext::preedit-changed static void im_preedit_changed_cb(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); g_autofree gchar* buf = nullptr; gint cursor_offset = 0; - gtk_im_context_get_preedit_string(self->im_context, &buf, nullptr, + gtk_im_context_get_preedit_string(priv->im_context, &buf, nullptr, &cursor_offset); - cursor_offset += self->text_model->composing_range().base(); - self->text_model->UpdateComposingText(buf); - self->text_model->SetSelection(TextRange(cursor_offset, cursor_offset)); + cursor_offset += priv->text_model->composing_range().base(); + priv->text_model->UpdateComposingText(buf); + priv->text_model->SetSelection(TextRange(cursor_offset, cursor_offset)); update_editing_state(self); } // Signal handler for GtkIMContext::commit static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) { - self->text_model->AddText(text); - if (self->text_model->composing()) { - self->text_model->CommitComposing(); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->text_model->AddText(text); + if (priv->text_model->composing()) { + priv->text_model->CommitComposing(); } update_editing_state(self); } // Signal handler for GtkIMContext::preedit-end static void im_preedit_end_cb(FlTextInputPlugin* self) { - self->text_model->EndComposing(); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->text_model->EndComposing(); update_editing_state(self); } // Signal handler for GtkIMContext::retrieve-surrounding static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) { - auto text = self->text_model->GetText(); - size_t cursor_offset = self->text_model->GetCursorOffset(); - gtk_im_context_set_surrounding(self->im_context, text.c_str(), -1, + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + auto text = priv->text_model->GetText(); + size_t cursor_offset = priv->text_model->GetCursorOffset(); + gtk_im_context_set_surrounding(priv->im_context, text.c_str(), -1, cursor_offset); return TRUE; } @@ -216,7 +234,9 @@ static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) { static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, gint offset, gint n_chars) { - if (self->text_model->DeleteSurrounding(offset, n_chars)) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + if (priv->text_model->DeleteSurrounding(offset, n_chars)) { update_editing_state(self); } return TRUE; @@ -229,18 +249,20 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { return FL_METHOD_RESPONSE(fl_method_error_response_new( kBadArgumentsError, "Expected 2-element list", nullptr)); } + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); - self->client_id = fl_value_get_int(fl_value_get_list_value(args, 0)); + priv->client_id = fl_value_get_int(fl_value_get_list_value(args, 0)); FlValue* config_value = fl_value_get_list_value(args, 1); - g_free(self->input_action); + g_free(priv->input_action); FlValue* input_action_value = fl_value_lookup_string(config_value, kInputActionKey); if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) { - self->input_action = g_strdup(fl_value_get_string(input_action_value)); + priv->input_action = g_strdup(fl_value_get_string(input_action_value)); } // Clear the multiline flag, then set it only if the field is multiline. - self->input_multiline = FALSE; + priv->input_multiline = FALSE; FlValue* input_type_value = fl_value_lookup_string(config_value, kTextInputTypeKey); if (fl_value_get_type(input_type_value) == FL_VALUE_TYPE_MAP) { @@ -249,7 +271,7 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { if (fl_value_get_type(input_type_name) == FL_VALUE_TYPE_STRING && g_strcmp0(fl_value_get_string(input_type_name), kMultilineInputType) == 0) { - self->input_multiline = TRUE; + priv->input_multiline = TRUE; } } @@ -258,12 +280,14 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { // Shows the input method. static FlMethodResponse* show(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); // Set the top-level window used for system input method windows. GdkWindow* window = - gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self->view))); - gtk_im_context_set_client_window(self->im_context, window); + gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(priv->view))); + gtk_im_context_set_client_window(priv->im_context, window); - gtk_im_context_focus_in(self->im_context); + gtk_im_context_focus_in(priv->im_context); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -271,9 +295,11 @@ static FlMethodResponse* show(FlTextInputPlugin* self) { // Updates the editing state from Flutter. static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, FlValue* args) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); const gchar* text = fl_value_get_string(fl_value_lookup_string(args, kTextKey)); - self->text_model->SetText(text); + priv->text_model->SetText(text); int64_t selection_base = fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey)); @@ -284,19 +310,19 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, selection_base = selection_extent = 0; } - self->text_model->SetText(text); - self->text_model->SetSelection(TextRange(selection_base, selection_extent)); + priv->text_model->SetText(text); + priv->text_model->SetSelection(TextRange(selection_base, selection_extent)); int64_t composing_base = fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey)); int64_t composing_extent = fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey)); if (composing_base == -1 && composing_extent == -1) { - self->text_model->EndComposing(); + priv->text_model->EndComposing(); } else { size_t composing_start = std::min(composing_base, composing_extent); size_t cursor_offset = selection_base - composing_start; - self->text_model->SetComposingRange( + priv->text_model->SetComposingRange( TextRange(composing_base, composing_extent), cursor_offset); } @@ -305,14 +331,18 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, // Called when the input method client is complete. static FlMethodResponse* clear_client(FlTextInputPlugin* self) { - self->client_id = kClientIdUnset; + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->client_id = kClientIdUnset; return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Hides the input method. static FlMethodResponse* hide(FlTextInputPlugin* self) { - gtk_im_context_focus_out(self->im_context); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + gtk_im_context_focus_out(priv->im_context); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } @@ -326,29 +356,32 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) { // of these updates. It transforms the composing rect to GTK window coordinates // and notifies GTK of the updated cursor position. static void update_im_cursor_position(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + // Skip update if not composing to avoid setting to position 0. - if (!self->text_model->composing()) { + if (!priv->text_model->composing()) { return; } // Transform the x, y positions of the cursor from local coordinates to // Flutter view coordinates. - gint x = self->composing_rect.x * self->editabletext_transform[0][0] + - self->composing_rect.y * self->editabletext_transform[1][0] + - self->editabletext_transform[3][0] + self->composing_rect.width; - gint y = self->composing_rect.x * self->editabletext_transform[0][1] + - self->composing_rect.y * self->editabletext_transform[1][1] + - self->editabletext_transform[3][1] + self->composing_rect.height; + gint x = priv->composing_rect.x * priv->editabletext_transform[0][0] + + priv->composing_rect.y * priv->editabletext_transform[1][0] + + priv->editabletext_transform[3][0] + priv->composing_rect.width; + gint y = priv->composing_rect.x * priv->editabletext_transform[0][1] + + priv->composing_rect.y * priv->editabletext_transform[1][1] + + priv->editabletext_transform[3][1] + priv->composing_rect.height; // Transform from Flutter view coordinates to GTK window coordinates. GdkRectangle preedit_rect; gtk_widget_translate_coordinates( - GTK_WIDGET(self->view), gtk_widget_get_toplevel(GTK_WIDGET(self->view)), + GTK_WIDGET(priv->view), gtk_widget_get_toplevel(GTK_WIDGET(priv->view)), x, y, &preedit_rect.x, &preedit_rect.y); // Set the cursor location in window coordinates so that GTK can position any // system input method windows. - gtk_im_context_set_cursor_location(self->im_context, &preedit_rect); + gtk_im_context_set_cursor_location(priv->im_context, &preedit_rect); } // Handles updates to the EditableText size and position from the framework. @@ -366,7 +399,9 @@ static FlMethodResponse* set_editable_size_and_transform( for (size_t i = 0; i < transform_len; ++i) { double val = fl_value_get_float(fl_value_get_list_value(transform, i)); - self->editabletext_transform[i / 4][i % 4] = val; + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->editabletext_transform[i / 4][i % 4] = val; } update_im_cursor_position(self); @@ -381,13 +416,15 @@ static FlMethodResponse* set_editable_size_and_transform( // composing region, the cursor rect is sent. static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self, FlValue* args) { - self->composing_rect.x = + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->composing_rect.x = fl_value_get_float(fl_value_lookup_string(args, "x")); - self->composing_rect.y = + priv->composing_rect.y = fl_value_get_float(fl_value_lookup_string(args, "y")); - self->composing_rect.width = + priv->composing_rect.width = fl_value_get_float(fl_value_lookup_string(args, "width")); - self->composing_rect.height = + priv->composing_rect.height = fl_value_get_float(fl_value_lookup_string(args, "height")); update_im_cursor_position(self); @@ -428,75 +465,38 @@ static void method_call_cb(FlMethodChannel* channel, } } +// Disposes of an FlTextInputPlugin. static void fl_text_input_plugin_dispose(GObject* object) { FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object); - - g_clear_object(&self->channel); - g_clear_pointer(&self->input_action, g_free); - g_clear_object(&self->im_context); - if (self->text_model != nullptr) { - delete self->text_model; - self->text_model = nullptr; + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + + g_clear_object(&priv->channel); + g_clear_pointer(&priv->input_action, g_free); + g_clear_object(&priv->im_context); + if (priv->text_model != nullptr) { + delete priv->text_model; + priv->text_model = nullptr; } - self->view = nullptr; + priv->view = nullptr; G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object); } -static void fl_text_input_plugin_class_init(FlTextInputPluginClass* klass) { - G_OBJECT_CLASS(klass)->dispose = fl_text_input_plugin_dispose; -} - -static void fl_text_input_plugin_init(FlTextInputPlugin* self) { - self->client_id = kClientIdUnset; - self->im_context = gtk_im_multicontext_new(); - self->input_multiline = FALSE; - g_signal_connect_object(self->im_context, "preedit-start", - G_CALLBACK(im_preedit_start_cb), self, - G_CONNECT_SWAPPED); - g_signal_connect_object(self->im_context, "preedit-end", - G_CALLBACK(im_preedit_end_cb), self, - G_CONNECT_SWAPPED); - g_signal_connect_object(self->im_context, "preedit-changed", - G_CALLBACK(im_preedit_changed_cb), self, - G_CONNECT_SWAPPED); - g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb), - self, G_CONNECT_SWAPPED); - g_signal_connect_object(self->im_context, "retrieve-surrounding", - G_CALLBACK(im_retrieve_surrounding_cb), self, - G_CONNECT_SWAPPED); - g_signal_connect_object(self->im_context, "delete-surrounding", - G_CALLBACK(im_delete_surrounding_cb), self, - G_CONNECT_SWAPPED); - self->text_model = new flutter::TextInputModel(); -} - -FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, - FlView* view) { - g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); - - FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( - g_object_new(fl_text_input_plugin_get_type(), nullptr)); - - g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); - self->channel = - fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); - fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self, - nullptr); - self->view = view; - - return self; -} +// Implements FlTextInputPlugin::filter_keypress. +static gboolean fl_text_input_plugin_filter_keypress_default( + FlTextInputPlugin* self, + GdkEventKey* event) { + g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), false); -gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, - GdkEventKey* event) { - g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); - if (self->client_id == kClientIdUnset) { + if (priv->client_id == kClientIdUnset) { return FALSE; } - if (gtk_im_context_filter_keypress(self->im_context, event)) { + if (gtk_im_context_filter_keypress(priv->im_context, event)) { return TRUE; } @@ -506,37 +506,31 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, gboolean changed = FALSE; if (event->type == GDK_KEY_PRESS) { switch (event->keyval) { - case GDK_KEY_BackSpace: - changed = self->text_model->Backspace(); - break; - case GDK_KEY_Delete: - case GDK_KEY_KP_Delete: - // Already handled inside Flutter. - break; case GDK_KEY_End: case GDK_KEY_KP_End: - changed = self->text_model->MoveCursorToEnd(); + changed = priv->text_model->MoveCursorToEnd(); break; case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_ISO_Enter: - if (self->input_multiline == TRUE) { - self->text_model->AddCodePoint('\n'); + if (priv->input_multiline == TRUE) { + priv->text_model->AddCodePoint('\n'); changed = TRUE; } do_action = TRUE; break; case GDK_KEY_Home: case GDK_KEY_KP_Home: - changed = self->text_model->MoveCursorToBeginning(); + changed = priv->text_model->MoveCursorToBeginning(); break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: case GDK_KEY_Left: case GDK_KEY_KP_Left: - // Already handled inside Flutter. - break; case GDK_KEY_Right: case GDK_KEY_KP_Right: - // Already handled inside Flutter. + // Already handled inside the framework in RenderEditable. break; } } @@ -548,5 +542,70 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, perform_action(self); } + return changed; +} + +// Initializes the FlTextInputPlugin class. +static void fl_text_input_plugin_class_init(FlTextInputPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_text_input_plugin_dispose; + FL_TEXT_INPUT_PLUGIN_CLASS(klass)->filter_keypress = + fl_text_input_plugin_filter_keypress_default; +} + +// Initializes an instance of the FlTextInputPlugin class. +static void fl_text_input_plugin_init(FlTextInputPlugin* self) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + + priv->client_id = kClientIdUnset; + priv->im_context = gtk_im_multicontext_new(); + priv->input_multiline = FALSE; + g_signal_connect_object(priv->im_context, "preedit-start", + G_CALLBACK(im_preedit_start_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(priv->im_context, "preedit-end", + G_CALLBACK(im_preedit_end_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(priv->im_context, "preedit-changed", + G_CALLBACK(im_preedit_changed_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(priv->im_context, "commit", G_CALLBACK(im_commit_cb), + self, G_CONNECT_SWAPPED); + g_signal_connect_object(priv->im_context, "retrieve-surrounding", + G_CALLBACK(im_retrieve_surrounding_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object(priv->im_context, "delete-surrounding", + G_CALLBACK(im_delete_surrounding_cb), self, + G_CONNECT_SWAPPED); + priv->text_model = new flutter::TextInputModel(); +} + +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, + FlView* view) { + g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); + + FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( + g_object_new(fl_text_input_plugin_get_type(), nullptr)); + + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->channel = + fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(priv->channel, method_call_cb, self, + nullptr); + priv->view = view; + + return self; +} + +// 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) { + 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); + } return FALSE; } diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h index d68f5903c17a6..0f5d216f7da53 100644 --- a/shell/platform/linux/fl_text_input_plugin.h +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -12,11 +12,11 @@ G_BEGIN_DECLS -G_DECLARE_FINAL_TYPE(FlTextInputPlugin, - fl_text_input_plugin, - FL, - TEXT_INPUT_PLUGIN, - GObject); +G_DECLARE_DERIVABLE_TYPE(FlTextInputPlugin, + fl_text_input_plugin, + FL, + TEXT_INPUT_PLUGIN, + GObject); /** * FlTextInputPlugin: @@ -25,6 +25,15 @@ G_DECLARE_FINAL_TYPE(FlTextInputPlugin, * of SystemChannels.textInput from the Flutter services library. */ +struct _FlTextInputPluginClass { + GObjectClass parent_class; + + /** + * Virtual method called to filter a keypress. + */ + gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event); +}; + /** * fl_text_input_plugin_new: * @messenger: an #FlBinaryMessenger. diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index 87decdce97c88..6f1565e9bcf0c 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -158,10 +158,11 @@ static void fl_view_constructed(GObject* object) { // Create system channel handlers. FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine); - self->key_event_plugin = fl_key_event_plugin_new(messenger); + 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->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self); self->platform_plugin = fl_platform_plugin_new(messenger); - self->text_input_plugin = fl_text_input_plugin_new(messenger, self); } static void fl_view_set_property(GObject* object, @@ -348,10 +349,7 @@ static gboolean fl_view_motion_notify_event(GtkWidget* widget, static gboolean fl_view_key_press_event(GtkWidget* widget, GdkEventKey* event) { FlView* self = FL_VIEW(widget); - fl_key_event_plugin_send_key_event(self->key_event_plugin, event); - fl_text_input_plugin_filter_keypress(self->text_input_plugin, event); - - return TRUE; + return fl_key_event_plugin_send_key_event(self->key_event_plugin, event); } // Implements GtkWidget::key_release_event. @@ -359,10 +357,7 @@ static gboolean fl_view_key_release_event(GtkWidget* widget, GdkEventKey* event) { FlView* self = FL_VIEW(widget); - fl_key_event_plugin_send_key_event(self->key_event_plugin, event); - fl_text_input_plugin_filter_keypress(self->text_input_plugin, event); - - return TRUE; + return fl_key_event_plugin_send_key_event(self->key_event_plugin, event); } static void fl_view_class_init(FlViewClass* klass) { diff --git a/shell/platform/linux/testing/mock_engine.cc b/shell/platform/linux/testing/mock_engine.cc index c498e92f955b2..98d3267cc3248 100644 --- a/shell/platform/linux/testing/mock_engine.cc +++ b/shell/platform/linux/testing/mock_engine.cc @@ -13,6 +13,7 @@ #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/linux/fl_method_codec_private.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_method_response.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_method_codec.h" #include "gtest/gtest.h" @@ -310,6 +311,18 @@ FlutterEngineResult FlutterEngineSendPlatformMessage( } else if (strcmp(message->channel, "test/failure") == 0) { // Generates an internal error. return kInternalInconsistency; + } else if (strcmp(message->channel, "test/key-event-handled") == 0 || + strcmp(message->channel, "test/key-event-not-handled") == 0) { + bool value = strcmp(message->channel, "test/key-event-handled") == 0; + g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new(); + g_autoptr(FlValue) handledValue = fl_value_new_map(); + fl_value_set_string_take(handledValue, "handled", fl_value_new_bool(value)); + g_autoptr(GBytes) response = fl_message_codec_encode_message( + FL_MESSAGE_CODEC(codec), handledValue, nullptr); + send_response( + engine, message->channel, message->response_handle, + static_cast(g_bytes_get_data(response, nullptr)), + g_bytes_get_size(response)); } return kSuccess; diff --git a/shell/platform/linux/testing/mock_text_input_plugin.cc b/shell/platform/linux/testing/mock_text_input_plugin.cc new file mode 100644 index 0000000000000..92309fa0e1c7c --- /dev/null +++ b/shell/platform/linux/testing/mock_text_input_plugin.cc @@ -0,0 +1,41 @@ +// 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/testing/mock_text_input_plugin.h" + +struct _FlMockTextInputPlugin { + FlTextInputPlugin parent_instance; + + gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event); +}; + +G_DEFINE_TYPE(FlMockTextInputPlugin, + fl_mock_text_input_plugin, + fl_text_input_plugin_get_type()) + +static gboolean mock_text_input_plugin_filter_keypress(FlTextInputPlugin* self, + GdkEventKey* event) { + FlMockTextInputPlugin* mock_self = FL_MOCK_TEXT_INPUT_PLUGIN(self); + if (mock_self->filter_keypress) { + return mock_self->filter_keypress(self, event); + } + return FALSE; +} + +static void fl_mock_text_input_plugin_class_init( + FlMockTextInputPluginClass* klass) { + FL_TEXT_INPUT_PLUGIN_CLASS(klass)->filter_keypress = + mock_text_input_plugin_filter_keypress; +} + +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)) { + FlMockTextInputPlugin* self = FL_MOCK_TEXT_INPUT_PLUGIN( + g_object_new(fl_mock_text_input_plugin_get_type(), nullptr)); + self->filter_keypress = filter_keypress; + return self; +} diff --git a/shell/platform/linux/testing/mock_text_input_plugin.h b/shell/platform/linux/testing/mock_text_input_plugin.h new file mode 100644 index 0000000000000..64b079e2c9d08 --- /dev/null +++ b/shell/platform/linux/testing/mock_text_input_plugin.h @@ -0,0 +1,18 @@ +// 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_text_input_plugin.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlMockTextInputPlugin, + fl_mock_text_input_plugin, + FL, + MOCK_TEXT_INPUT_PLUGIN, + FlTextInputPlugin) + +FlMockTextInputPlugin* fl_mock_text_input_plugin_new( + gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event)); + +G_END_DECLS