Add FlTextInputPlugin (#18314)
* Add FlTextInputPlugin
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index fff517b..ba413f5 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1230,6 +1230,8 @@
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 a58bdbe..9f1ad8c 100644
--- a/shell/platform/common/cpp/BUILD.gn
+++ b/shell/platform/common/cpp/BUILD.gn
@@ -30,12 +30,28 @@
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 @@
"incoming_message_dispatcher.cc",
"json_message_codec.cc",
"json_method_codec.cc",
- "text_input_model.cc",
]
configs += [ ":desktop_library_implementation" ]
@@ -59,11 +74,6 @@
":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 @@
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 49c11ed..67f0455 100644
--- a/shell/platform/common/cpp/text_input_model.cc
+++ b/shell/platform/common/cpp/text_input_model.cc
@@ -85,6 +85,12 @@
selection_base_ = selection_extent_;
}
+void TextInputModel::AddText(const std::string& text) {
+ std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
+ utf16_converter;
+ AddText(utf16_converter.from_bytes(text));
+}
+
bool TextInputModel::Backspace() {
if (selection_base_ != selection_extent_) {
DeleteSelected();
@@ -113,6 +119,46 @@
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 @@
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<std::codecvt_utf8_utf16<char16_t>, 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 5444283..4494dca 100644
--- a/shell/platform/common/cpp/text_input_model.h
+++ b/shell/platform/common/cpp/text_input_model.h
@@ -33,12 +33,18 @@
// 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 @@
// 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 @@
// 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<int>(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 7853015..09eac75 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, AddText) {
auto model = std::make_unique<TextInputModel>("", "");
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, AddTextSelection) {
auto model = std::make_unique<TextInputModel>("", "");
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 @@
EXPECT_STREQ(model->GetText().c_str(), "AE");
}
+TEST(TextInputModel, DeleteSurroundingAtCursor) {
+ auto model = std::make_unique<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
+ 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<TextInputModel>("", "");
EXPECT_TRUE(model->SetEditingState(0, 0, "ABCDE"));
@@ -380,4 +470,25 @@
EXPECT_STREQ(model->GetText().c_str(), "ABCDE");
}
+TEST(TextInputModel, GetCursorOffset) {
+ auto model = std::make_unique<TextInputModel>("", "");
+ // 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<TextInputModel>("", "");
+ 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 c00df52..7771c67 100644
--- a/shell/platform/glfw/BUILD.gn
+++ b/shell/platform/glfw/BUILD.gn
@@ -53,6 +53,7 @@
":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 d425088..809d5a2 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -94,6 +94,7 @@
"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 @@
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 0000000..51127bc
--- /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 <gtk/gtk.h>
+
+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 0000000..270c46c
--- /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 <gdk/gdk.h>
+
+#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 b3d3bb6..85e3517 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 @@
// Flutter system channel handlers.
FlKeyEventPlugin* key_event_plugin;
+ FlTextInputPlugin* text_input_plugin;
};
enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST };
@@ -114,6 +116,7 @@
// 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 @@
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_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 @@
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 ce8c6c0..53d335c 100644
--- a/shell/platform/windows/BUILD.gn
+++ b/shell/platform/windows/BUILD.gn
@@ -72,6 +72,7 @@
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",