From b7bfa342ddf6dfbb820c449b1e2b4bf52ddcb5a0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 21 Jun 2022 19:03:30 +0200 Subject: [PATCH] [Linux] Test text input (flutter/engine#34186) This PR finishes what #33661 and #33111 started - most of `FlTextInputPlugin` is now covered by tests. --- .../src/flutter/shell/platform/linux/BUILD.gn | 1 + .../platform/linux/fl_text_input_plugin.cc | 45 +- .../platform/linux/fl_text_input_plugin.h | 24 +- .../linux/fl_text_input_plugin_test.cc | 910 +++++++++++++++++- .../flutter/shell/platform/linux/fl_view.cc | 22 +- .../platform/linux/testing/mock_im_context.cc | 121 +++ .../platform/linux/testing/mock_im_context.h | 54 ++ 7 files changed, 1103 insertions(+), 74 deletions(-) create mode 100644 engine/src/flutter/shell/platform/linux/testing/mock_im_context.cc create mode 100644 engine/src/flutter/shell/platform/linux/testing/mock_im_context.h diff --git a/engine/src/flutter/shell/platform/linux/BUILD.gn b/engine/src/flutter/shell/platform/linux/BUILD.gn index b722a527d4e..fe0fcf65fa6 100644 --- a/engine/src/flutter/shell/platform/linux/BUILD.gn +++ b/engine/src/flutter/shell/platform/linux/BUILD.gn @@ -226,6 +226,7 @@ executable("flutter_linux_unittests") { "testing/mock_binary_messenger_response_handle.cc", "testing/mock_engine.cc", "testing/mock_epoxy.cc", + "testing/mock_im_context.cc", "testing/mock_plugin_registrar.cc", "testing/mock_renderer.cc", "testing/mock_settings.cc", diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc index 6dff9bc33b9..7fb34602e4b 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.cc @@ -80,14 +80,8 @@ struct FlTextInputPluginPrivate { // Input method. GtkIMContext* im_context; - // IM filter. - FlTextInputPluginImFilter im_filter; - flutter::TextInputModel* text_model; - // The owning native window. - GdkWindow* window; - // A 4x4 matrix that maps from `EditableText` local coordinates to the // coordinate system of `PipelineOwner.rootNode`. double editabletext_transform[4][4]; @@ -216,8 +210,8 @@ static void update_editing_state_with_delta(FlTextInputPlugin* self, g_autoptr(FlValue) deltas = fl_value_new_list(); fl_value_append(deltas, deltaValue); - FlValue* value = fl_value_new_map(); - fl_value_set_string_take(value, "deltas", deltas); + g_autoptr(FlValue) value = fl_value_new_map(); + fl_value_set_string(value, "deltas", deltas); fl_value_append(args, value); @@ -258,9 +252,6 @@ static void im_preedit_start_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->text_model->BeginComposing(); - - // Set the native window used for system input method windows. - gtk_im_context_set_client_window(priv->im_context, priv->window); } // Signal handler for GtkIMContext::preedit-changed @@ -418,9 +409,6 @@ static FlMethodResponse* show(FlTextInputPlugin* self) { return hide(self); } - // Set the native window used for system input method windows. - gtk_im_context_set_client_window(priv->im_context, priv->window); - gtk_im_context_focus_in(priv->im_context); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); @@ -599,7 +587,6 @@ static void fl_text_input_plugin_dispose(GObject* object) { delete priv->text_model; priv->text_model = nullptr; } - priv->window = nullptr; G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object); } @@ -617,7 +604,8 @@ static gboolean fl_text_input_plugin_filter_keypress_default( return FALSE; } - if (priv->im_filter(priv->im_context, event->origin)) { + GdkEventKey* key_event = reinterpret_cast(event->origin); + if (gtk_im_context_filter_keypress(priv->im_context, key_event)) { return TRUE; } @@ -698,12 +686,20 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) { fl_text_input_plugin_get_instance_private(self)); priv->client_id = kClientIdUnset; - priv->im_context = gtk_im_multicontext_new(); + priv->input_type = FL_TEXT_INPUT_TYPE_TEXT; + priv->text_model = new flutter::TextInputModel(); +} + +static void init_im_context(FlTextInputPlugin* self, GtkIMContext* im_context) { + FlTextInputPluginPrivate* priv = static_cast( + fl_text_input_plugin_get_instance_private(self)); + priv->im_context = GTK_IM_CONTEXT(g_object_ref(im_context)); + // On Wayland, this call sets up the input method so it can be enabled // immediately when required. Without it, on-screen keyboard's don't come up // the first time a text field is focused. gtk_im_context_focus_out(priv->im_context); - priv->input_type = FL_TEXT_INPUT_TYPE_TEXT; + g_signal_connect_object(priv->im_context, "preedit-start", G_CALLBACK(im_preedit_start_cb), self, G_CONNECT_SWAPPED); @@ -721,15 +717,12 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) { 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, - GdkWindow* window, - FlTextInputPluginImFilter im_filter) { +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, + GtkIMContext* im_context) { g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); - g_return_val_if_fail(im_filter != nullptr, nullptr); + g_return_val_if_fail(GTK_IS_IM_CONTEXT(im_context), nullptr); FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( g_object_new(fl_text_input_plugin_get_type(), nullptr)); @@ -741,8 +734,8 @@ FlTextInputPlugin* fl_text_input_plugin_new( 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->window = window; - priv->im_filter = im_filter; + + init_im_context(self, im_context); return self; } diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.h b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.h index 9ca2924a07e..d432698ea5f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.h +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin.h @@ -10,19 +10,6 @@ #include "flutter/shell/platform/linux/fl_key_event.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.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, @@ -50,20 +37,15 @@ struct _FlTextInputPluginClass { /** * fl_text_input_plugin_new: * @messenger: an #FlBinaryMessenger. - * @window: the #GdkWindow 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. + * @im_context: (allow-none): a #GtkIMContext. * * 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, - GdkWindow* window, - FlTextInputPluginImFilter im_filter); +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, + GtkIMContext* im_context); /** * fl_text_input_plugin_filter_keypress diff --git a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin_test.cc b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin_test.cc index e5d791d5546..3692e6584c9 100644 --- a/engine/src/flutter/shell/platform/linux/fl_text_input_plugin_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_text_input_plugin_test.cc @@ -10,6 +10,7 @@ #include "flutter/shell/platform/linux/testing/fl_test.h" #include "flutter/shell/platform/linux/testing/mock_binary_messenger.h" #include "flutter/shell/platform/linux/testing/mock_binary_messenger_response_handle.h" +#include "flutter/shell/platform/linux/testing/mock_im_context.h" #include "flutter/testing/testing.h" #include "gmock/gmock.h" @@ -32,6 +33,66 @@ MATCHER_P(SuccessResponse, result, "") { return false; } +MATCHER_P(FlValueEq, value, "equal to " + ::testing::PrintToString(value)) { + return fl_value_equal(arg, value); +} + +class MethodCallMatcher { + public: + using is_gtest_matcher = void; + + explicit MethodCallMatcher(::testing::Matcher name, + ::testing::Matcher args) + : name_(name), args_(args) {} + + bool MatchAndExplain(GBytes* method_call, + ::testing::MatchResultListener* result_listener) const { + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GError) error = nullptr; + g_autofree gchar* name = nullptr; + g_autoptr(FlValue) args = nullptr; + gboolean result = fl_method_codec_decode_method_call( + FL_METHOD_CODEC(codec), method_call, &name, &args, &error); + if (!result) { + *result_listener << ::testing::PrintToString(error->message); + return false; + } + if (!name_.MatchAndExplain(name, result_listener)) { + *result_listener << " where the name doesn't match: \"" << name << "\""; + return false; + } + if (!args_.MatchAndExplain(args, result_listener)) { + *result_listener << " where the args don't match: " + << ::testing::PrintToString(args); + return false; + } + return true; + } + + void DescribeTo(std::ostream* os) const { + *os << "method name "; + name_.DescribeTo(os); + *os << " and args "; + args_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const { + *os << "method name "; + name_.DescribeNegationTo(os); + *os << " or args "; + args_.DescribeNegationTo(os); + } + + private: + ::testing::Matcher name_; + ::testing::Matcher args_; +}; + +::testing::Matcher MethodCall(std::string name, + ::testing::Matcher args) { + return MethodCallMatcher(::testing::StrEq(name), args); +} + static FlValue* build_map(std::map args) { FlValue* value = fl_value_new_map(); for (auto it = args.begin(); it != args.end(); ++it) { @@ -48,29 +109,641 @@ static FlValue* build_list(std::vector args) { return value; } -TEST(FlTextInputPluginTest, SetClient) { - ::testing::NiceMock messenger; - auto filter = - +[](GtkIMContext* im_context, gpointer gdk_event) { return false; }; +struct InputConfig { + int64_t client_id = -1; + const gchar* input_type = "TextInputType.text"; + const gchar* input_action = "TextInputAction.none"; + gboolean enable_delta_model = false; +}; - fl_text_input_plugin_new(messenger, nullptr, - FlTextInputPluginImFilter(filter)); - - EXPECT_TRUE(messenger.HasMessageHandler("flutter/textinput")); - - g_autoptr(FlValue) args = build_list({ - fl_value_new_int(1), // client id +static FlValue* build_input_config(InputConfig config) { + return build_list({ + fl_value_new_int(config.client_id), build_map({ - {"inputAction", fl_value_new_string("")}, + {"inputAction", fl_value_new_string(config.input_action)}, {"inputType", build_map({ - {"name", fl_value_new_string("TextInputType.text")}, + {"name", fl_value_new_string(config.input_type)}, })}, - {"enableDeltaModel", fl_value_new_bool(false)}, + {"enableDeltaModel", fl_value_new_bool(config.enable_delta_model)}, }), }); +} + +struct EditingState { + const gchar* text = ""; + int selection_base = -1; + int selection_extent = -1; + int composing_base = -1; + int composing_extent = -1; +}; + +static FlValue* build_editing_state(EditingState state) { + return build_map({ + {"text", fl_value_new_string(state.text)}, + {"selectionBase", fl_value_new_int(state.selection_base)}, + {"selectionExtent", fl_value_new_int(state.selection_extent)}, + {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")}, + {"selectionIsDirectional", fl_value_new_bool(false)}, + {"composingBase", fl_value_new_int(state.composing_base)}, + {"composingExtent", fl_value_new_int(state.composing_extent)}, + }); +} + +struct EditingDelta { + const gchar* old_text = ""; + const gchar* delta_text = ""; + int delta_start = -1; + int delta_end = -1; + int selection_base = -1; + int selection_extent = -1; + int composing_base = -1; + int composing_extent = -1; +}; + +static FlValue* build_editing_delta(EditingDelta delta) { + return build_map({ + {"oldText", fl_value_new_string(delta.old_text)}, + {"deltaText", fl_value_new_string(delta.delta_text)}, + {"deltaStart", fl_value_new_int(delta.delta_start)}, + {"deltaEnd", fl_value_new_int(delta.delta_end)}, + {"selectionBase", fl_value_new_int(delta.selection_base)}, + {"selectionExtent", fl_value_new_int(delta.selection_extent)}, + {"selectionAffinity", fl_value_new_string("TextAffinity.downstream")}, + {"selectionIsDirectional", fl_value_new_bool(false)}, + {"composingBase", fl_value_new_int(delta.composing_base)}, + {"composingExtent", fl_value_new_int(delta.composing_extent)}, + }); +} + +static void send_key_event(FlTextInputPlugin* plugin, + gint keyval, + gint state = 0) { + GdkEvent* gdk_event = gdk_event_new(GDK_KEY_PRESS); + gdk_event->key.keyval = keyval; + gdk_event->key.state = state; + FlKeyEvent* key_event = fl_key_event_new_from_gdk_event(gdk_event); + fl_text_input_plugin_filter_keypress(plugin, key_event); + fl_key_event_dispose(key_event); +} + +TEST(FlTextInputPluginTest, MessageHandler) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + EXPECT_TRUE(messenger.HasMessageHandler("flutter/textinput")); +} + +TEST(FlTextInputPluginTest, SetClient) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + g_autoptr(FlValue) args = build_input_config({.client_id = 1}); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) message = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", message); +} + +TEST(FlTextInputPluginTest, Show) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + EXPECT_CALL(context, + gtk_im_context_focus_in(::testing::Eq(context))); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); g_autoptr(GBytes) message = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr); + + messenger.ReceiveMessage("flutter/textinput", message); +} + +TEST(FlTextInputPluginTest, Hide) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + EXPECT_CALL(context, + gtk_im_context_focus_out(::testing::Eq(context))); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) message = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.hide", nullptr, nullptr); + + messenger.ReceiveMessage("flutter/textinput", message); +} + +TEST(FlTextInputPluginTest, ClearClient) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) message = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.clearClient", nullptr, nullptr); + + messenger.ReceiveMessage("flutter/textinput", message); +} + +TEST(FlTextInputPluginTest, PerformAction) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set input config + g_autoptr(FlValue) config = build_input_config({ + .client_id = 1, + .input_type = "TextInputType.multiline", + .input_action = "TextInputAction.newline", + }); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // set editing state + g_autoptr(FlValue) state = build_editing_state({ + .text = "Flutter", + .selection_base = 7, + .selection_extent = 7, + }); + g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_state); + + // update editing state + g_autoptr(FlValue) new_state = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutter\n", + .selection_base = 8, + .selection_extent = 8, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(new_state)), + ::testing::_, ::testing::_, ::testing::_)); + + // perform action + g_autoptr(FlValue) action = build_list({ + fl_value_new_int(1), // client_id + fl_value_new_string("TextInputAction.newline"), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.performAction", + FlValueEq(action)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_Return); +} + +TEST(FlTextInputPluginTest, MoveCursor) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set input config + g_autoptr(FlValue) config = build_input_config({.client_id = 1}); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // set editing state + g_autoptr(FlValue) state = build_editing_state({ + .text = "Flutter", + .selection_base = 4, + .selection_extent = 4, + }); + g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_state); + + // move cursor to beginning + g_autoptr(FlValue) beginning = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutter", + .selection_base = 0, + .selection_extent = 0, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(beginning)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_Home); + + // move cursor to end + g_autoptr(FlValue) end = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutter", + .selection_base = 7, + .selection_extent = 7, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(end)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_End); +} + +TEST(FlTextInputPluginTest, Select) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set input config + g_autoptr(FlValue) config = build_input_config({.client_id = 1}); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // set editing state + g_autoptr(FlValue) state = build_editing_state({ + .text = "Flutter", + .selection_base = 4, + .selection_extent = 4, + }); + g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_state); + + // select to end + g_autoptr(FlValue) select_to_end = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutter", + .selection_base = 4, + .selection_extent = 7, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(select_to_end)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_End, GDK_SHIFT_MASK); + + // select to beginning + g_autoptr(FlValue) select_to_beginning = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutter", + .selection_base = 4, + .selection_extent = 0, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(select_to_beginning)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_Home, GDK_SHIFT_MASK); +} + +TEST(FlTextInputPluginTest, Composing) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + g_signal_emit_by_name(context, "preedit-start", nullptr); + + // update + EXPECT_CALL(context, + gtk_im_context_get_preedit_string( + ::testing::Eq(context), + ::testing::A(), ::testing::_, ::testing::A())) + .WillOnce( + ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter")), + ::testing::SetArgPointee<3>(0))); + + g_autoptr(FlValue) state = build_list({ + fl_value_new_int(-1), // client_id + build_editing_state({ + .text = "Flutter", + .selection_base = 0, + .selection_extent = 0, + .composing_base = 0, + .composing_extent = 7, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(state)), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "preedit-changed", nullptr); + + // commit + g_autoptr(FlValue) commit = build_list({ + fl_value_new_int(-1), // client_id + build_editing_state({ + .text = "engine", + .selection_base = 6, + .selection_extent = 6, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(commit)), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "commit", "engine", nullptr); + + // end + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + ::testing::_), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "preedit-end", nullptr); +} + +TEST(FlTextInputPluginTest, SurroundingText) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set input config + g_autoptr(FlValue) config = build_input_config({.client_id = 1}); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", config, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // set editing state + g_autoptr(FlValue) state = build_editing_state({ + .text = "Flutter", + .selection_base = 3, + .selection_extent = 3, + }); + g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_state); + + // retrieve + EXPECT_CALL(context, gtk_im_context_set_surrounding( + ::testing::Eq(context), + ::testing::StrEq("Flutter"), 7, 3)); + + gboolean retrieved = false; + g_signal_emit_by_name(context, "retrieve-surrounding", &retrieved, nullptr); + EXPECT_TRUE(retrieved); + + // delete + g_autoptr(FlValue) update = build_list({ + fl_value_new_int(1), // client_id + build_editing_state({ + .text = "Flutr", + .selection_base = 3, + .selection_extent = 3, + }), + }); + + EXPECT_CALL(messenger, fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingState", + FlValueEq(update)), + ::testing::_, ::testing::_, ::testing::_)); + + gboolean deleted = false; + g_signal_emit_by_name(context, "delete-surrounding", 1, 2, &deleted, nullptr); + EXPECT_TRUE(deleted); +} + +TEST(FlTextInputPluginTest, SetMarkedTextRect) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + g_signal_emit_by_name(context, "preedit-start", nullptr); + + // set editable size and transform + g_autoptr(FlValue) size_and_transform = build_map({ + { + "transform", + build_list({ + fl_value_new_float(1), + fl_value_new_float(2), + fl_value_new_float(3), + fl_value_new_float(4), + fl_value_new_float(5), + fl_value_new_float(6), + fl_value_new_float(7), + fl_value_new_float(8), + fl_value_new_float(9), + fl_value_new_float(10), + fl_value_new_float(11), + fl_value_new_float(12), + fl_value_new_float(13), + fl_value_new_float(14), + fl_value_new_float(15), + fl_value_new_float(16), + }), + }, + }); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_editable_size_and_transform = + fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditableSizeAndTransform", + size_and_transform, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", + set_editable_size_and_transform); + + // set marked text rect + g_autoptr(FlValue) rect = build_map({ + {"x", fl_value_new_float(1)}, + {"y", fl_value_new_float(2)}, + {"width", fl_value_new_float(3)}, + {"height", fl_value_new_float(4)}, + }); + g_autoptr(GBytes) set_marked_text_rect = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setMarkedTextRect", rect, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + EXPECT_CALL(context, gtk_im_context_set_cursor_location( + ::testing::Eq(context), + ::testing::Pointee(::testing::AllOf( + ::testing::Field(&GdkRectangle::x, 27), + ::testing::Field(&GdkRectangle::y, 32), + ::testing::Field(&GdkRectangle::width, 0), + ::testing::Field(&GdkRectangle::height, 0))))); + + messenger.ReceiveMessage("flutter/textinput", set_marked_text_rect); +} + +TEST(FlTextInputPluginTest, TextInputTypeNone) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + g_autoptr(FlValue) args = build_input_config({ + .client_id = 1, + .input_type = "TextInputType.none", + }); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr); g_autoptr(FlValue) null = fl_value_new_null(); @@ -80,5 +753,212 @@ TEST(FlTextInputPluginTest, SetClient) { SuccessResponse(null), ::testing::A())) .WillOnce(::testing::Return(true)); - messenger.ReceiveMessage("flutter/textinput", message); + messenger.ReceiveMessage("flutter/textinput", set_client); + + EXPECT_CALL(context, + gtk_im_context_focus_in(::testing::Eq(context))) + .Times(0); + EXPECT_CALL(context, + gtk_im_context_focus_out(::testing::Eq(context))); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + g_autoptr(GBytes) show = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.show", nullptr, nullptr); + + messenger.ReceiveMessage("flutter/textinput", show); +} + +TEST(FlTextInputPluginTest, TextEditingDelta) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set config + g_autoptr(FlValue) args = build_input_config({ + .client_id = 1, + .enable_delta_model = true, + }); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::A(), + SuccessResponse(null), ::testing::A())) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // set editing state + g_autoptr(FlValue) state = build_editing_state({ + .text = "Flutter", + .selection_base = 7, + .selection_extent = 7, + }); + g_autoptr(GBytes) set_state = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setEditingState", state, nullptr); + + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::_, SuccessResponse(null), ::testing::_)) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_state); + + // update editing state with deltas + g_autoptr(FlValue) deltas = build_list({ + fl_value_new_int(1), // client_id + build_map({{ + "deltas", + build_list({ + build_editing_delta({ + .old_text = "Flutter", + .delta_text = "Flutter", + .delta_start = 7, + .delta_end = 7, + .selection_base = 0, + .selection_extent = 0, + }), + }), + }}), + }); + + EXPECT_CALL(messenger, + fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingStateWithDeltas", + FlValueEq(deltas)), + ::testing::_, ::testing::_, ::testing::_)); + + send_key_event(plugin, GDK_KEY_Home); +} + +TEST(FlTextInputPluginTest, ComposingDelta) { + ::testing::NiceMock messenger; + ::testing::NiceMock context; + + g_autoptr(FlTextInputPlugin) plugin = + fl_text_input_plugin_new(messenger, context); + EXPECT_NE(plugin, nullptr); + + // set config + g_autoptr(FlValue) args = build_input_config({ + .client_id = 1, + .enable_delta_model = true, + }); + g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); + g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call( + FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr); + + g_autoptr(FlValue) null = fl_value_new_null(); + EXPECT_CALL(messenger, fl_binary_messenger_send_response( + ::testing::Eq(messenger), + ::testing::A(), + SuccessResponse(null), ::testing::A())) + .WillOnce(::testing::Return(true)); + + messenger.ReceiveMessage("flutter/textinput", set_client); + + // update + EXPECT_CALL(context, + gtk_im_context_get_preedit_string( + ::testing::Eq(context), + ::testing::A(), ::testing::_, ::testing::A())) + .WillOnce( + ::testing::DoAll(::testing::SetArgPointee<1>(g_strdup("Flutter ")), + ::testing::SetArgPointee<3>(8))); + + g_autoptr(FlValue) update = build_list({ + fl_value_new_int(1), // client_id + build_map({{ + "deltas", + build_list({ + build_editing_delta({ + .old_text = "", + .delta_text = "Flutter ", + .delta_start = 0, + .delta_end = 8, + .selection_base = 8, + .selection_extent = 8, + .composing_base = 0, + .composing_extent = 8, + }), + }), + }}), + }); + + EXPECT_CALL(messenger, + fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingStateWithDeltas", + FlValueEq(update)), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "preedit-changed", nullptr); + + // commit + g_autoptr(FlValue) commit = build_list({ + fl_value_new_int(1), // client_id + build_map({{ + "deltas", + build_list({ + build_editing_delta({ + .old_text = "Flutter ", + .delta_text = "engine", + .delta_start = 8, + .delta_end = 8, + .selection_base = 14, + .selection_extent = 14, + .composing_base = 0, + .composing_extent = 8, + }), + }), + }}), + }); + + EXPECT_CALL(messenger, + fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingStateWithDeltas", + FlValueEq(commit)), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "commit", "engine", nullptr); + + // end + g_autoptr(FlValue) end = build_list({ + fl_value_new_int(1), // client_id + build_map({{ + "deltas", + build_list({ + build_editing_delta({ + .delta_text = "Flutter engine", + .selection_base = 14, + .selection_extent = 14, + }), + }), + }}), + }); + + EXPECT_CALL(messenger, + fl_binary_messenger_send_on_channel( + ::testing::Eq(messenger), + ::testing::StrEq("flutter/textinput"), + MethodCall("TextInputClient.updateEditingStateWithDeltas", + FlValueEq(end)), + ::testing::_, ::testing::_, ::testing::_)); + + g_signal_emit_by_name(context, "preedit-end", nullptr); } diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 2c0d468b4d1..c423c0a000f 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -92,20 +92,16 @@ G_DEFINE_TYPE_WITH_CODE( G_IMPLEMENT_INTERFACE(fl_scrolling_view_delegate_get_type(), fl_view_scrolling_delegate_iface_init)) -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); -} - // Initialize keyboard manager. static void init_keyboard(FlView* self) { FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine); - self->text_input_plugin = fl_text_input_plugin_new( - messenger, gtk_widget_get_window(GTK_WIDGET(self)), - text_input_im_filter_by_gtk); + + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(self)); + g_return_if_fail(GDK_IS_WINDOW(window)); + g_autoptr(GtkIMContext) im_context = gtk_im_multicontext_new(); + gtk_im_context_set_client_window(im_context, window); + + self->text_input_plugin = fl_text_input_plugin_new(messenger, im_context); self->keyboard_manager = fl_keyboard_manager_new(FL_KEYBOARD_VIEW_DELEGATE(self)); } @@ -242,6 +238,7 @@ static void on_pre_engine_restart_cb(FlEngine* engine, gpointer user_data) { FlView* self = FL_VIEW(user_data); g_clear_object(&self->keyboard_manager); + g_clear_object(&self->text_input_plugin); g_clear_object(&self->scrolling_manager); init_keyboard(self); init_scrolling(self); @@ -488,7 +485,6 @@ 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); - init_keyboard(self); init_scrolling(self); self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self); self->platform_plugin = fl_platform_plugin_new(messenger); @@ -622,6 +618,8 @@ static void fl_view_realize(GtkWidget* widget) { gtk_widget_set_window(widget, window); gtk_widget_register_window(widget, window); + init_keyboard(self); + if (!fl_renderer_start(self->renderer, self, &error)) { g_warning("Failed to start Flutter renderer: %s", error->message); return; diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_im_context.cc b/engine/src/flutter/shell/platform/linux/testing/mock_im_context.cc new file mode 100644 index 00000000000..facf66fa397 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/testing/mock_im_context.cc @@ -0,0 +1,121 @@ +// 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_im_context.h" + +using namespace flutter::testing; + +G_DECLARE_FINAL_TYPE(FlMockIMContext, + fl_mock_im_context, + FL, + MOCK_IM_CONTEXT, + GtkIMContext) + +struct _FlMockIMContext { + GtkIMContext parent_instance; + MockIMContext* mock; +}; + +G_DEFINE_TYPE(FlMockIMContext, fl_mock_im_context, GTK_TYPE_IM_CONTEXT) + +static void fl_mock_im_context_set_client_window(GtkIMContext* context, + GdkWindow* window) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_set_client_window(context, window); +} + +static void fl_mock_im_context_get_preedit_string(GtkIMContext* context, + gchar** str, + PangoAttrList** attrs, + gint* cursor_pos) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_get_preedit_string(context, str, attrs, + cursor_pos); +} + +static gboolean fl_mock_im_context_filter_keypress(GtkIMContext* context, + GdkEventKey* event) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + return self->mock->gtk_im_context_filter_keypress(context, event); +} + +static void fl_mock_im_context_focus_in(GtkIMContext* context) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_focus_in(context); +} + +static void fl_mock_im_context_focus_out(GtkIMContext* context) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_focus_out(context); +} + +static void fl_mock_im_context_reset(GtkIMContext* context) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_reset(context); +} + +static void fl_mock_im_context_set_cursor_location(GtkIMContext* context, + GdkRectangle* area) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_set_cursor_location(context, area); +} + +static void fl_mock_im_context_set_use_preedit(GtkIMContext* context, + gboolean use_preedit) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_set_use_preedit(context, use_preedit); +} + +static void fl_mock_im_context_set_surrounding(GtkIMContext* context, + const gchar* text, + gint len, + gint cursor_index) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + self->mock->gtk_im_context_set_surrounding(context, text, len, cursor_index); +} + +static gboolean fl_mock_im_context_get_surrounding(GtkIMContext* context, + gchar** text, + gint* cursor_index) { + FlMockIMContext* self = FL_MOCK_IM_CONTEXT(context); + return self->mock->gtk_im_context_get_surrounding(context, text, + cursor_index); +} + +static void fl_mock_im_context_class_init(FlMockIMContextClass* klass) { + GtkIMContextClass* im_context_class = GTK_IM_CONTEXT_CLASS(klass); + im_context_class->set_client_window = fl_mock_im_context_set_client_window; + im_context_class->get_preedit_string = fl_mock_im_context_get_preedit_string; + im_context_class->filter_keypress = fl_mock_im_context_filter_keypress; + im_context_class->focus_in = fl_mock_im_context_focus_in; + im_context_class->focus_out = fl_mock_im_context_focus_out; + im_context_class->reset = fl_mock_im_context_reset; + im_context_class->set_cursor_location = + fl_mock_im_context_set_cursor_location; + im_context_class->set_use_preedit = fl_mock_im_context_set_use_preedit; + im_context_class->set_surrounding = fl_mock_im_context_set_surrounding; + im_context_class->get_surrounding = fl_mock_im_context_get_surrounding; +} + +static void fl_mock_im_context_init(FlMockIMContext* self) {} + +static GtkIMContext* fl_mock_im_context_new(MockIMContext* mock) { + FlMockIMContext* self = + FL_MOCK_IM_CONTEXT(g_object_new(fl_mock_im_context_get_type(), nullptr)); + self->mock = mock; + return GTK_IM_CONTEXT(self); +} + +MockIMContext::~MockIMContext() { + if (FL_IS_MOCK_IM_CONTEXT(instance_)) { + g_clear_object(&instance_); + } +} + +MockIMContext::operator GtkIMContext*() { + if (instance_ == nullptr) { + instance_ = fl_mock_im_context_new(this); + } + return instance_; +} diff --git a/engine/src/flutter/shell/platform/linux/testing/mock_im_context.h b/engine/src/flutter/shell/platform/linux/testing/mock_im_context.h new file mode 100644 index 00000000000..40dd0128ad6 --- /dev/null +++ b/engine/src/flutter/shell/platform/linux/testing/mock_im_context.h @@ -0,0 +1,54 @@ +// 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_TESTING_MOCK_IM_CONTEXT_H_ +#define FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_IM_CONTEXT_H_ + +#include + +#include "gmock/gmock.h" + +namespace flutter { +namespace testing { + +class MockIMContext { + public: + ~MockIMContext(); + + operator GtkIMContext*(); + + MOCK_METHOD2(gtk_im_context_set_client_window, + void(GtkIMContext* context, GdkWindow* window)); + MOCK_METHOD4(gtk_im_context_get_preedit_string, + void(GtkIMContext* context, + gchar** str, + PangoAttrList** attrs, + gint* cursor_pos)); + MOCK_METHOD2(gtk_im_context_filter_keypress, + gboolean(GtkIMContext* context, GdkEventKey* event)); + MOCK_METHOD1(gtk_im_context_focus_in, gboolean(GtkIMContext* context)); + MOCK_METHOD1(gtk_im_context_focus_out, void(GtkIMContext* context)); + MOCK_METHOD1(gtk_im_context_reset, void(GtkIMContext* context)); + MOCK_METHOD2(gtk_im_context_set_cursor_location, + void(GtkIMContext* context, GdkRectangle* area)); + MOCK_METHOD2(gtk_im_context_set_use_preedit, + void(GtkIMContext* context, gboolean use_preedit)); + MOCK_METHOD4(gtk_im_context_set_surrounding, + void(GtkIMContext* context, + const gchar* text, + gint len, + gint cursor_index)); + MOCK_METHOD3(gtk_im_context_get_surrounding, + gboolean(GtkIMContext* context, + gchar** text, + gint* cursor_index)); + + private: + GtkIMContext* instance_ = nullptr; +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_LINUX_TESTING_MOCK_IM_CONTEXT_H_