diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index be2534491f587..1a29894da2fd9 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1229,6 +1229,8 @@ FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_standard_method_codec_test.cc FILE: ../../../flutter/shell/platform/linux/fl_string_codec.cc FILE: ../../../flutter/shell/platform/linux/fl_string_codec_test.cc +FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.cc +FILE: ../../../flutter/shell/platform/linux/fl_text_input_plugin.h FILE: ../../../flutter/shell/platform/linux/fl_value.cc FILE: ../../../flutter/shell/platform/linux/fl_value_test.cc FILE: ../../../flutter/shell/platform/linux/fl_view.cc diff --git a/shell/platform/common/cpp/BUILD.gn b/shell/platform/common/cpp/BUILD.gn index a58bdbe46265a..9f1ad8c6121fe 100644 --- a/shell/platform/common/cpp/BUILD.gn +++ b/shell/platform/common/cpp/BUILD.gn @@ -30,12 +30,28 @@ source_set("common_cpp_library_headers") { configs += [ ":desktop_library_implementation" ] } +source_set("common_cpp_input") { + public = [ + "text_input_model.h", + ] + + sources = [ + "text_input_model.cc", + ] + + configs += [ ":desktop_library_implementation" ] + + if (is_win) { + # For wstring_conversion. See issue #50053. + defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ] + } +} + source_set("common_cpp") { public = [ "incoming_message_dispatcher.h", "json_message_codec.h", "json_method_codec.h", - "text_input_model.h", ] # TODO: Refactor flutter_glfw.cc to move the implementations corresponding @@ -44,7 +60,6 @@ source_set("common_cpp") { "incoming_message_dispatcher.cc", "json_message_codec.cc", "json_method_codec.cc", - "text_input_model.cc", ] configs += [ ":desktop_library_implementation" ] @@ -59,11 +74,6 @@ source_set("common_cpp") { ":common_cpp_core", "//third_party/rapidjson", ] - - if (is_win) { - # For wstring_conversion. See issue #50053. - defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ] - } } # The portion of common_cpp that has no dependencies on the public/ @@ -117,6 +127,7 @@ executable("common_cpp_unittests") { deps = [ ":common_cpp", ":common_cpp_fixtures", + ":common_cpp_input", "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper", "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper_library_stubs", "//flutter/testing", diff --git a/shell/platform/common/cpp/text_input_model.cc b/shell/platform/common/cpp/text_input_model.cc index 49c11ed4ba8c4..67f04559b645e 100644 --- a/shell/platform/common/cpp/text_input_model.cc +++ b/shell/platform/common/cpp/text_input_model.cc @@ -85,6 +85,12 @@ void TextInputModel::AddText(const std::u16string& text) { selection_base_ = selection_extent_; } +void TextInputModel::AddText(const std::string& text) { + std::wstring_convert, char16_t> + utf16_converter; + AddText(utf16_converter.from_bytes(text)); +} + bool TextInputModel::Backspace() { if (selection_base_ != selection_extent_) { DeleteSelected(); @@ -113,6 +119,46 @@ bool TextInputModel::Delete() { return false; } +bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) { + auto start = selection_extent_; + if (offset_from_cursor < 0) { + for (int i = 0; i < -offset_from_cursor; i++) { + // If requested start is before the available text then reduce the + // number of characters to delete. + if (start == text_.begin()) { + count = i; + break; + } + start -= IsTrailingSurrogate(*(start - 1)) ? 2 : 1; + } + } else { + for (int i = 0; i < offset_from_cursor && start != text_.end(); i++) { + start += IsLeadingSurrogate(*start) ? 2 : 1; + } + } + + auto end = start; + for (int i = 0; i < count && end != text_.end(); i++) { + end += IsLeadingSurrogate(*start) ? 2 : 1; + } + + if (start == end) { + return false; + } + + auto new_base = text_.erase(start, end); + + // Cursor moves only if deleted area is before it. + if (offset_from_cursor <= 0) { + selection_base_ = new_base; + } + + // Clear selection. + selection_extent_ = selection_base_; + + return true; +} + bool TextInputModel::MoveCursorToBeginning() { if (selection_base_ == text_.begin() && selection_extent_ == text_.begin()) return false; @@ -172,4 +218,13 @@ std::string TextInputModel::GetText() const { return utf8_converter.to_bytes(text_); } +int TextInputModel::GetCursorOffset() const { + // Measure the length of the current text up to the cursor. + // There is probably a much more efficient way of doing this. + auto leading_text = text_.substr(0, selection_extent_ - text_.begin()); + std::wstring_convert, char16_t> + utf8_converter; + return utf8_converter.to_bytes(leading_text).size(); +} + } // namespace flutter diff --git a/shell/platform/common/cpp/text_input_model.h b/shell/platform/common/cpp/text_input_model.h index 5444283bd1da9..4494dca4c3456 100644 --- a/shell/platform/common/cpp/text_input_model.h +++ b/shell/platform/common/cpp/text_input_model.h @@ -33,12 +33,18 @@ class TextInputModel { // code point. void AddCodePoint(char32_t c); - // Adds a UTF-16 text. + // Adds UTF-16 text. // // Either appends after the cursor (when selection base and extent are the // same), or deletes the selected text, replacing it with the given text. void AddText(const std::u16string& text); + // Adds UTF-8 text. + // + // Either appends after the cursor (when selection base and extent are the + // same), or deletes the selected text, replacing it with the given text. + void AddText(const std::string& text); + // Deletes either the selection, or one character ahead of the cursor. // // Deleting one character ahead of the cursor occurs when the selection base @@ -47,6 +53,17 @@ class TextInputModel { // Returns true if any deletion actually occurred. bool Delete(); + // Deletes text near the cursor. + // + // A section is made starting at @offset code points past the cursor (negative + // values go before the cursor). @count code points are removed. The selection + // may go outside the bounds of the text and will result in only the part + // selection that covers the available text being deleted. The existing + // selection is ignored and removed after this operation. + // + // Returns true if any deletion actually occurred. + bool DeleteSurrounding(int offset_from_cursor, int count); + // Deletes either the selection, or one character behind the cursor. // // Deleting one character behind the cursor occurs when the selection base @@ -77,9 +94,13 @@ class TextInputModel { // Returns true if the cursor could be moved. bool MoveCursorToEnd(); - // Get the current text + // Gets the current text as UTF-8. std::string GetText() const; + // Gets the cursor position as a byte offset in UTF-8 string returned from + // GetText(). + int GetCursorOffset() const; + // The position of the cursor int selection_base() const { return static_cast(selection_base_ - text_.begin()); diff --git a/shell/platform/common/cpp/text_input_model_unittests.cc b/shell/platform/common/cpp/text_input_model_unittests.cc index 7853015dfd360..09eac75e7eafa 100644 --- a/shell/platform/common/cpp/text_input_model_unittests.cc +++ b/shell/platform/common/cpp/text_input_model_unittests.cc @@ -103,8 +103,8 @@ TEST(TextInputModel, AddCodePointSelectionWideCharacter) { TEST(TextInputModel, AddText) { auto model = std::make_unique("", ""); model->AddText(u"ABCDE"); - model->AddText(u"😄"); - model->AddText(u"FGHIJ"); + model->AddText("😄"); + model->AddText("FGHIJ"); EXPECT_EQ(model->selection_base(), 12); EXPECT_EQ(model->selection_extent(), 12); EXPECT_STREQ(model->GetText().c_str(), "ABCDE😄FGHIJ"); @@ -113,7 +113,7 @@ TEST(TextInputModel, AddText) { TEST(TextInputModel, AddTextSelection) { auto model = std::make_unique("", ""); EXPECT_TRUE(model->SetEditingState(1, 4, "ABCDE")); - model->AddText(u"xy"); + model->AddText("xy"); EXPECT_EQ(model->selection_base(), 3); EXPECT_EQ(model->selection_extent(), 3); EXPECT_STREQ(model->GetText().c_str(), "AxyE"); @@ -173,6 +173,96 @@ TEST(TextInputModel, DeleteSelection) { EXPECT_STREQ(model->GetText().c_str(), "AE"); } +TEST(TextInputModel, DeleteSurroundingAtCursor) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(0, 1)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "ABDE"); +} + +TEST(TextInputModel, DeleteSurroundingAtCursorAll) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(0, 3)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "AB"); +} + +TEST(TextInputModel, DeleteSurroundingAtCursorGreedy) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(0, 4)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "AB"); +} + +TEST(TextInputModel, DeleteSurroundingBeforeCursor) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(-1, 1)); + EXPECT_EQ(model->selection_base(), 1); + EXPECT_EQ(model->selection_extent(), 1); + EXPECT_STREQ(model->GetText().c_str(), "ACDE"); +} + +TEST(TextInputModel, DeleteSurroundingBeforeCursorAll) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(-2, 2)); + EXPECT_EQ(model->selection_base(), 0); + EXPECT_EQ(model->selection_extent(), 0); + EXPECT_STREQ(model->GetText().c_str(), "CDE"); +} + +TEST(TextInputModel, DeleteSurroundingBeforeCursorGreedy) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(-3, 3)); + EXPECT_EQ(model->selection_base(), 0); + EXPECT_EQ(model->selection_extent(), 0); + EXPECT_STREQ(model->GetText().c_str(), "CDE"); +} + +TEST(TextInputModel, DeleteSurroundingAfterCursor) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(1, 1)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "ABCE"); +} + +TEST(TextInputModel, DeleteSurroundingAfterCursorAll) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(1, 2)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "ABC"); +} + +TEST(TextInputModel, DeleteSurroundingAfterCursorGreedy) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 2, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(1, 3)); + EXPECT_EQ(model->selection_base(), 2); + EXPECT_EQ(model->selection_extent(), 2); + EXPECT_STREQ(model->GetText().c_str(), "ABC"); +} + +TEST(TextInputModel, DeleteSurroundingSelection) { + auto model = std::make_unique("", ""); + model->SetEditingState(2, 3, "ABCDE"); + EXPECT_TRUE(model->DeleteSurrounding(0, 1)); + EXPECT_EQ(model->selection_base(), 3); + EXPECT_EQ(model->selection_extent(), 3); + EXPECT_STREQ(model->GetText().c_str(), "ABCE"); +} + TEST(TextInputModel, BackspaceStart) { auto model = std::make_unique("", ""); EXPECT_TRUE(model->SetEditingState(0, 0, "ABCDE")); @@ -380,4 +470,25 @@ TEST(TextInputModel, MoveCursorToEndSelection) { EXPECT_STREQ(model->GetText().c_str(), "ABCDE"); } +TEST(TextInputModel, GetCursorOffset) { + auto model = std::make_unique("", ""); + // These characters take 1, 2, 3 and 4 bytes in UTF-8. + model->SetEditingState(0, 0, "$¢€𐍈"); + EXPECT_EQ(model->GetCursorOffset(), 0); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->GetCursorOffset(), 1); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->GetCursorOffset(), 3); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->GetCursorOffset(), 6); + EXPECT_TRUE(model->MoveCursorForward()); + EXPECT_EQ(model->GetCursorOffset(), 10); +} + +TEST(TextInputModel, GetCursorOffsetSelection) { + auto model = std::make_unique("", ""); + model->SetEditingState(1, 4, "ABCDE"); + EXPECT_EQ(model->GetCursorOffset(), 4); +} + } // namespace flutter diff --git a/shell/platform/glfw/BUILD.gn b/shell/platform/glfw/BUILD.gn index c00df52a3156e..7771c67a9ca59 100644 --- a/shell/platform/glfw/BUILD.gn +++ b/shell/platform/glfw/BUILD.gn @@ -53,6 +53,7 @@ source_set("flutter_glfw") { ":flutter_glfw_headers", "//build/secondary/third_party/glfw", "//flutter/shell/platform/common/cpp:common_cpp", + "//flutter/shell/platform/common/cpp:common_cpp_input", "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper", "//flutter/shell/platform/embedder:embedder_with_symbol_prefix", "//flutter/shell/platform/glfw/client_wrapper:client_wrapper_glfw", diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn index d425088023dee..809d5a25d849a 100644 --- a/shell/platform/linux/BUILD.gn +++ b/shell/platform/linux/BUILD.gn @@ -94,6 +94,7 @@ source_set("flutter_linux") { "fl_standard_message_codec.cc", "fl_standard_method_codec.cc", "fl_string_codec.cc", + "fl_text_input_plugin.cc", "fl_value.cc", "fl_view.cc", ] @@ -108,6 +109,7 @@ source_set("flutter_linux") { defines = [ "FLUTTER_LINUX_COMPILATION" ] deps = [ + "//flutter/shell/platform/common/cpp:common_cpp_input", "//flutter/shell/platform/embedder:embedder_with_symbol_prefix", "//third_party/rapidjson", ] diff --git a/shell/platform/linux/fl_text_input_plugin.cc b/shell/platform/linux/fl_text_input_plugin.cc new file mode 100644 index 0000000000000..51127bc69a6e2 --- /dev/null +++ b/shell/platform/linux/fl_text_input_plugin.cc @@ -0,0 +1,290 @@ +// 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" + +#include "flutter/shell/platform/common/cpp/text_input_model.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_method_codec.h" +#include "flutter/shell/platform/linux/public/flutter_linux/fl_method_channel.h" + +#include + +static constexpr char kChannelName[] = "flutter/textinput"; + +static constexpr char kSetClientMethod[] = "TextInput.setClient"; +static constexpr char kShowMethod[] = "TextInput.show"; +static constexpr char kSetEditingStateMethod[] = "TextInput.setEditingState"; +static constexpr char kClearClientMethod[] = "TextInput.clearClient"; +static constexpr char kHideMethod[] = "TextInput.hide"; +static constexpr char kUpdateEditingStateMethod[] = + "TextInputClient.updateEditingState"; +static constexpr char kPerformActionMethod[] = "TextInputClient.performAction"; + +static constexpr char kInputActionKey[] = "inputAction"; +static constexpr char kTextKey[] = "text"; +static constexpr char kSelectionBaseKey[] = "selectionBase"; +static constexpr char kSelectionExtentKey[] = "selectionExtent"; +static constexpr char kSelectionAffinityKey[] = "selectionAffinity"; +static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional"; +static constexpr char kComposingBaseKey[] = "composingBase"; +static constexpr char kComposingExtentKey[] = "composingExtent"; + +static constexpr char kTextAffinityDownstream[] = "TextAffinity.downstream"; + +static constexpr int64_t kClientIdUnset = -1; + +struct _FlTextInputPlugin { + GObject parent_instance; + + FlMethodChannel* channel; + + // Client ID provided by Flutter to report events with. + int64_t client_id; + + // Input action to perform when enter pressed. + gchar* input_action; + + // Input method. + GtkIMContext* im_context; + + flutter::TextInputModel* text_model; +}; + +G_DEFINE_TYPE(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, + GAsyncResult* result, + GError** error) { + g_autoptr(FlMethodResponse) response = fl_method_channel_invoke_method_finish( + FL_METHOD_CHANNEL(object), result, error); + if (response == nullptr) + return FALSE; + return fl_method_response_get_result(response, error) != nullptr; +} + +// Called when a response is received from TextInputClient.updateEditingState() +static void update_editing_state_response_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + if (!finish_method(object, result, &error)) { + g_warning("Failed to call %s: %s", kUpdateEditingStateMethod, + error->message); + } +} + +// Informs Flutter of text input changes. +static void update_editing_state(FlTextInputPlugin* self) { + g_autoptr(FlValue) args = fl_value_new_list(); + fl_value_append_take(args, fl_value_new_int(self->client_id)); + g_autoptr(FlValue) value = fl_value_new_map(); + + fl_value_set_string_take( + value, kTextKey, + fl_value_new_string(self->text_model->GetText().c_str())); + fl_value_set_string_take( + value, kSelectionBaseKey, + fl_value_new_int(self->text_model->selection_base())); + fl_value_set_string_take( + value, kSelectionExtentKey, + fl_value_new_int(self->text_model->selection_extent())); + + // The following keys are not implemented and set to default values. + fl_value_set_string_take(value, kSelectionAffinityKey, + fl_value_new_string(kTextAffinityDownstream)); + fl_value_set_string_take(value, kSelectionIsDirectionalKey, + fl_value_new_bool(FALSE)); + fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(-1)); + fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(-1)); + + fl_value_append(args, value); + + fl_method_channel_invoke_method(self->channel, kUpdateEditingStateMethod, + args, nullptr, + update_editing_state_response_cb, self); +} + +// Called when a response is received from TextInputClient.performAction() +static void perform_action_response_cb(GObject* object, + GAsyncResult* result, + gpointer user_data) { + g_autoptr(GError) error = nullptr; + if (!finish_method(object, result, &error)) + g_warning("Failed to call %s: %s", kPerformActionMethod, error->message); +} + +// Inform Flutter that the input has been activated. +static void perform_action(FlTextInputPlugin* 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_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_method_channel_invoke_method(self->channel, kPerformActionMethod, args, + nullptr, perform_action_response_cb, self); +} + +// Signal handler for GtkIMContext::commit +static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) { + self->text_model->AddText(text); + 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, + cursor_offset); + return TRUE; +} + +// Signal handler for GtkIMContext::delete-surrounding +static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, + gint offset, + gint n_chars) { + if (self->text_model->DeleteSurrounding(offset, n_chars)) + update_editing_state(self); + return TRUE; +} + +// Called when a method call is received from Flutter. +static void method_call_cb(FlMethodChannel* channel, + FlMethodCall* method_call, + gpointer user_data) { + FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(user_data); + + const gchar* method = fl_method_call_get_name(method_call); + FlValue* args = fl_method_call_get_args(method_call); + + if (strcmp(method, kSetClientMethod) == 0) { + self->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); + 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)); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, kShowMethod) == 0) { + gtk_im_context_focus_in(self->im_context); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, kSetEditingStateMethod) == 0) { + const gchar* text = + fl_value_get_string(fl_value_lookup_string(args, kTextKey)); + int64_t selection_base = + fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey)); + int64_t selection_extent = + fl_value_get_int(fl_value_lookup_string(args, kSelectionExtentKey)); + + self->text_model->SetEditingState(selection_base, selection_extent, text); + + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, kClearClientMethod) == 0) { + self->client_id = kClientIdUnset; + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else if (strcmp(method, kHideMethod) == 0) { + gtk_im_context_focus_out(self->im_context); + fl_method_call_respond_success(method_call, nullptr, nullptr); + } else + fl_method_call_respond_not_implemented(method_call, nullptr); +} + +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; + } + + 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(); + 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) { + 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); + + return self; +} + +gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, + GdkEventKey* event) { + g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); + if (gtk_im_context_filter_keypress(self->im_context, event)) + return TRUE; + + // Handle navigation keys. + 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(); + break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_ISO_Enter: + perform_action(self); + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + changed = self->text_model->MoveCursorToBeginning(); + break; + 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. + break; + } + } + + if (changed) + update_editing_state(self); + + return FALSE; +} diff --git a/shell/platform/linux/fl_text_input_plugin.h b/shell/platform/linux/fl_text_input_plugin.h new file mode 100644 index 0000000000000..270c46ce57ba2 --- /dev/null +++ b/shell/platform/linux/fl_text_input_plugin.h @@ -0,0 +1,52 @@ +// 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_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_ +#define FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_ + +#include + +#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h" + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(FlTextInputPlugin, + fl_text_input_plugin, + FL, + TEXT_INPUT_PLUGIN, + GObject); + +/** + * FlTextInputPlugin: + * + * #FlTextInputPlugin is a text_input channel that implements the shell side + * of TextInputPlugins.textInput from the Flutter services library. + */ + +/** + * fl_text_input_plugin_new: + * @messenger: an #FlBinaryMessenger. + * + * Creates a new plugin that implements TextInputPlugins.textInput from the + * Flutter services library. + * + * Returns: a new #FlTextInputPlugin. + */ +FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger); + +/** + * fl_text_input_plugin_filter_keypress + * @self: an #FlTextInputPlugin. + * @event: a #GdkEventKey + * + * Process a Gdk key event. + * + * Returns: %TRUE if the event was used. + */ +gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, + GdkEventKey* event); + +G_END_DECLS + +#endif // FLUTTER_SHELL_TEXT_INPUT_LINUX_FL_TEXT_INPUT_PLUGIN_H_ diff --git a/shell/platform/linux/fl_view.cc b/shell/platform/linux/fl_view.cc index b3d3bb61290e1..85e3517848007 100644 --- a/shell/platform/linux/fl_view.cc +++ b/shell/platform/linux/fl_view.cc @@ -8,6 +8,7 @@ #include "flutter/shell/platform/linux/fl_key_event_plugin.h" #include "flutter/shell/platform/linux/fl_plugin_registrar_private.h" #include "flutter/shell/platform/linux/fl_renderer_x11.h" +#include "flutter/shell/platform/linux/fl_text_input_plugin.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h" #include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h" @@ -32,6 +33,7 @@ struct _FlView { // Flutter system channel handlers. FlKeyEventPlugin* key_event_plugin; + FlTextInputPlugin* text_input_plugin; }; enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST }; @@ -114,6 +116,7 @@ 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); } static void fl_view_set_property(GObject* object, @@ -156,6 +159,7 @@ static void fl_view_dispose(GObject* object) { g_clear_object(&self->renderer); g_clear_object(&self->engine); g_clear_object(&self->key_event_plugin); + g_clear_object(&self->text_input_plugin); G_OBJECT_CLASS(fl_view_parent_class)->dispose(object); } @@ -256,6 +260,9 @@ 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); + if (fl_text_input_plugin_filter_keypress(self->text_input_plugin, event)) + return TRUE; + fl_key_event_plugin_send_key_event(self->key_event_plugin, event); return TRUE; @@ -266,6 +273,9 @@ static gboolean fl_view_key_release_event(GtkWidget* widget, GdkEventKey* event) { FlView* self = FL_VIEW(widget); + if (fl_text_input_plugin_filter_keypress(self->text_input_plugin, event)) + return TRUE; + fl_key_event_plugin_send_key_event(self->key_event_plugin, event); return TRUE; diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index ce8c6c0ec7fe1..53d335c02fe8c 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -72,6 +72,7 @@ source_set("flutter_windows_source") { deps = [ ":flutter_windows_headers", "//flutter/shell/platform/common/cpp:common_cpp", + "//flutter/shell/platform/common/cpp:common_cpp_input", "//flutter/shell/platform/common/cpp/client_wrapper:client_wrapper", "//flutter/shell/platform/embedder:embedder_with_symbol_prefix", "//flutter/shell/platform/windows/client_wrapper:client_wrapper_windows",