// 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 #include "flutter/shell/platform/common/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" static constexpr char kChannelName[] = "flutter/textinput"; static constexpr char kBadArgumentsError[] = "Bad Arguments"; 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 kSetEditableSizeAndTransform[] = "TextInput.setEditableSizeAndTransform"; static constexpr char kSetMarkedTextRect[] = "TextInput.setMarkedTextRect"; static constexpr char kInputActionKey[] = "inputAction"; static constexpr char kTextInputTypeKey[] = "inputType"; static constexpr char kTextInputTypeNameKey[] = "name"; 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 kTransform[] = "transform"; static constexpr char kTextAffinityDownstream[] = "TextAffinity.downstream"; static constexpr char kMultilineInputType[] = "TextInputType.multiline"; static constexpr int64_t kClientIdUnset = -1; struct FlTextInputPluginPrivate { 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; // Send newline when multi-line and enter is pressed. gboolean input_multiline; // Input method. GtkIMContext* im_context; flutter::TextInputModel* text_model; // The owning Flutter view. FlView* view; // A 4x4 matrix that maps from `EditableText` local coordinates to the // coordinate system of `PipelineOwner.rootNode`. double editabletext_transform[4][4]; // The smallest rect, in local coordinates, of the text in the composing // range, or of the caret in the case where there is no current composing // range. This value is updated via `TextInput.setMarkedTextRect` messages // over the text input channel. GdkRectangle composing_rect; }; G_DEFINE_TYPE_WITH_PRIVATE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT) // Completes method call and returns TRUE if the call was successful. static gboolean finish_method(GObject* object, 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) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); g_autoptr(FlValue) args = fl_value_new_list(); fl_value_append_take(args, fl_value_new_int(priv->client_id)); g_autoptr(FlValue) value = fl_value_new_map(); TextRange selection = priv->text_model->selection(); fl_value_set_string_take( value, kTextKey, fl_value_new_string(priv->text_model->GetText().c_str())); fl_value_set_string_take(value, kSelectionBaseKey, fl_value_new_int(selection.base())); fl_value_set_string_take(value, kSelectionExtentKey, fl_value_new_int(selection.extent())); int composing_base = priv->text_model->composing() ? priv->text_model->composing_range().base() : -1; int composing_extent = priv->text_model->composing() ? priv->text_model->composing_range().extent() : -1; fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(composing_base)); fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(composing_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_append(args, value); fl_method_channel_invoke_method(priv->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) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); g_return_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self)); g_return_if_fail(priv->client_id != 0); g_return_if_fail(priv->input_action != nullptr); g_autoptr(FlValue) args = fl_value_new_list(); fl_value_append_take(args, fl_value_new_int(priv->client_id)); fl_value_append_take(args, fl_value_new_string(priv->input_action)); fl_method_channel_invoke_method(priv->channel, kPerformActionMethod, args, nullptr, perform_action_response_cb, self); } // Signal handler for GtkIMContext::preedit-start static void im_preedit_start_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->text_model->BeginComposing(); // Set the top-level window used for system input method windows. GdkWindow* window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(priv->view))); gtk_im_context_set_client_window(priv->im_context, window); } // Signal handler for GtkIMContext::preedit-changed static void im_preedit_changed_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); g_autofree gchar* buf = nullptr; gint cursor_offset = 0; gtk_im_context_get_preedit_string(priv->im_context, &buf, nullptr, &cursor_offset); cursor_offset += priv->text_model->composing_range().base(); priv->text_model->UpdateComposingText(buf); priv->text_model->SetSelection(TextRange(cursor_offset, cursor_offset)); update_editing_state(self); } // Signal handler for GtkIMContext::commit static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->text_model->AddText(text); if (priv->text_model->composing()) { priv->text_model->CommitComposing(); } update_editing_state(self); } // Signal handler for GtkIMContext::preedit-end static void im_preedit_end_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->text_model->EndComposing(); update_editing_state(self); } // Signal handler for GtkIMContext::retrieve-surrounding static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); auto text = priv->text_model->GetText(); size_t cursor_offset = priv->text_model->GetCursorOffset(); gtk_im_context_set_surrounding(priv->im_context, text.c_str(), -1, cursor_offset); return TRUE; } // Signal handler for GtkIMContext::delete-surrounding static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self, gint offset, gint n_chars) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); if (priv->text_model->DeleteSurrounding(offset, n_chars)) { update_editing_state(self); } return TRUE; } // Called when the input method client is set up. static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) { if (fl_value_get_type(args) != FL_VALUE_TYPE_LIST || fl_value_get_length(args) < 2) { return FL_METHOD_RESPONSE(fl_method_error_response_new( kBadArgumentsError, "Expected 2-element list", nullptr)); } FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->client_id = fl_value_get_int(fl_value_get_list_value(args, 0)); FlValue* config_value = fl_value_get_list_value(args, 1); g_free(priv->input_action); FlValue* input_action_value = fl_value_lookup_string(config_value, kInputActionKey); if (fl_value_get_type(input_action_value) == FL_VALUE_TYPE_STRING) { priv->input_action = g_strdup(fl_value_get_string(input_action_value)); } // Clear the multiline flag, then set it only if the field is multiline. priv->input_multiline = FALSE; FlValue* input_type_value = fl_value_lookup_string(config_value, kTextInputTypeKey); if (fl_value_get_type(input_type_value) == FL_VALUE_TYPE_MAP) { FlValue* input_type_name = fl_value_lookup_string(input_type_value, kTextInputTypeNameKey); if (fl_value_get_type(input_type_name) == FL_VALUE_TYPE_STRING && g_strcmp0(fl_value_get_string(input_type_name), kMultilineInputType) == 0) { priv->input_multiline = TRUE; } } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Shows the input method. static FlMethodResponse* show(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); // Set the top-level window used for system input method windows. GdkWindow* window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(priv->view))); gtk_im_context_set_client_window(priv->im_context, window); gtk_im_context_focus_in(priv->im_context); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Updates the editing state from Flutter. static FlMethodResponse* set_editing_state(FlTextInputPlugin* self, FlValue* args) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); const gchar* text = fl_value_get_string(fl_value_lookup_string(args, kTextKey)); priv->text_model->SetText(text); 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)); // Flutter uses -1/-1 for invalid; translate that to 0/0 for the model. if (selection_base == -1 && selection_extent == -1) { selection_base = selection_extent = 0; } priv->text_model->SetText(text); priv->text_model->SetSelection(TextRange(selection_base, selection_extent)); int64_t composing_base = fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey)); int64_t composing_extent = fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey)); if (composing_base == -1 && composing_extent == -1) { priv->text_model->EndComposing(); } else { size_t composing_start = std::min(composing_base, composing_extent); size_t cursor_offset = selection_base - composing_start; priv->text_model->SetComposingRange( TextRange(composing_base, composing_extent), cursor_offset); } return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Called when the input method client is complete. static FlMethodResponse* clear_client(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->client_id = kClientIdUnset; return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Hides the input method. static FlMethodResponse* hide(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); gtk_im_context_focus_out(priv->im_context); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Update the IM cursor position. // // As text is input by the user, the framework sends two streams of updates // over the text input channel: updates to the composing rect (cursor rect when // not in IME composing mode) and updates to the matrix transform from local // coordinates to Flutter root coordinates. This function is called after each // of these updates. It transforms the composing rect to GTK window coordinates // and notifies GTK of the updated cursor position. static void update_im_cursor_position(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); // Skip update if not composing to avoid setting to position 0. if (!priv->text_model->composing()) { return; } // Transform the x, y positions of the cursor from local coordinates to // Flutter view coordinates. gint x = priv->composing_rect.x * priv->editabletext_transform[0][0] + priv->composing_rect.y * priv->editabletext_transform[1][0] + priv->editabletext_transform[3][0] + priv->composing_rect.width; gint y = priv->composing_rect.x * priv->editabletext_transform[0][1] + priv->composing_rect.y * priv->editabletext_transform[1][1] + priv->editabletext_transform[3][1] + priv->composing_rect.height; // Transform from Flutter view coordinates to GTK window coordinates. GdkRectangle preedit_rect; gtk_widget_translate_coordinates( GTK_WIDGET(priv->view), gtk_widget_get_toplevel(GTK_WIDGET(priv->view)), x, y, &preedit_rect.x, &preedit_rect.y); // Set the cursor location in window coordinates so that GTK can position any // system input method windows. gtk_im_context_set_cursor_location(priv->im_context, &preedit_rect); } // Handles updates to the EditableText size and position from the framework. // // On changes to the size or position of the RenderObject underlying the // EditableText, this update may be triggered. It provides an updated size and // transform from the local coordinate system of the EditableText to root // Flutter coordinate system. static FlMethodResponse* set_editable_size_and_transform( FlTextInputPlugin* self, FlValue* args) { FlValue* transform = fl_value_lookup_string(args, kTransform); size_t transform_len = fl_value_get_length(transform); g_warn_if_fail(transform_len == 16); for (size_t i = 0; i < transform_len; ++i) { double val = fl_value_get_float(fl_value_get_list_value(transform, i)); FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->editabletext_transform[i / 4][i % 4] = val; } update_im_cursor_position(self); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // Handles updates to the composing rect from the framework. // // On changes to the state of the EditableText in the framework, this update // may be triggered. It provides an updated rect for the composing region in // local coordinates of the EditableText. In the case where there is no // composing region, the cursor rect is sent. static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self, FlValue* args) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->composing_rect.x = fl_value_get_float(fl_value_lookup_string(args, "x")); priv->composing_rect.y = fl_value_get_float(fl_value_lookup_string(args, "y")); priv->composing_rect.width = fl_value_get_float(fl_value_lookup_string(args, "width")); priv->composing_rect.height = fl_value_get_float(fl_value_lookup_string(args, "height")); update_im_cursor_position(self); return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } // 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); g_autoptr(FlMethodResponse) response = nullptr; if (strcmp(method, kSetClientMethod) == 0) { response = set_client(self, args); } else if (strcmp(method, kShowMethod) == 0) { response = show(self); } else if (strcmp(method, kSetEditingStateMethod) == 0) { response = set_editing_state(self, args); } else if (strcmp(method, kClearClientMethod) == 0) { response = clear_client(self); } else if (strcmp(method, kHideMethod) == 0) { response = hide(self); } else if (strcmp(method, kSetEditableSizeAndTransform) == 0) { response = set_editable_size_and_transform(self, args); } else if (strcmp(method, kSetMarkedTextRect) == 0) { response = set_marked_text_rect(self, args); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } g_autoptr(GError) error = nullptr; if (!fl_method_call_respond(method_call, response, &error)) { g_warning("Failed to send method call response: %s", error->message); } } // Disposes of an FlTextInputPlugin. static void fl_text_input_plugin_dispose(GObject* object) { FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object); FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); g_clear_object(&priv->channel); g_clear_pointer(&priv->input_action, g_free); g_clear_object(&priv->im_context); if (priv->text_model != nullptr) { delete priv->text_model; priv->text_model = nullptr; } priv->view = nullptr; G_OBJECT_CLASS(fl_text_input_plugin_parent_class)->dispose(object); } // Implements FlTextInputPlugin::filter_keypress. static gboolean fl_text_input_plugin_filter_keypress_default( FlTextInputPlugin* self, GdkEventKey* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), false); FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); if (priv->client_id == kClientIdUnset) { return FALSE; } if (gtk_im_context_filter_keypress(priv->im_context, event)) { return TRUE; } // Handle the enter/return key. gboolean do_action = FALSE; // Handle navigation keys. gboolean changed = FALSE; if (event->type == GDK_KEY_PRESS) { switch (event->keyval) { case GDK_KEY_End: case GDK_KEY_KP_End: changed = priv->text_model->MoveCursorToEnd(); break; case GDK_KEY_Return: case GDK_KEY_KP_Enter: case GDK_KEY_ISO_Enter: if (priv->input_multiline == TRUE) { priv->text_model->AddCodePoint('\n'); changed = TRUE; } do_action = TRUE; break; case GDK_KEY_Home: case GDK_KEY_KP_Home: changed = priv->text_model->MoveCursorToBeginning(); break; case GDK_KEY_BackSpace: case GDK_KEY_Delete: case GDK_KEY_KP_Delete: case GDK_KEY_Left: case GDK_KEY_KP_Left: case GDK_KEY_Right: case GDK_KEY_KP_Right: // Already handled inside the framework in RenderEditable. break; } } if (changed) { update_editing_state(self); } if (do_action) { perform_action(self); } return changed; } // Initializes the FlTextInputPlugin class. static void fl_text_input_plugin_class_init(FlTextInputPluginClass* klass) { G_OBJECT_CLASS(klass)->dispose = fl_text_input_plugin_dispose; FL_TEXT_INPUT_PLUGIN_CLASS(klass)->filter_keypress = fl_text_input_plugin_filter_keypress_default; } // Initializes an instance of the FlTextInputPlugin class. static void fl_text_input_plugin_init(FlTextInputPlugin* self) { FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->client_id = kClientIdUnset; priv->im_context = gtk_im_multicontext_new(); priv->input_multiline = FALSE; g_signal_connect_object(priv->im_context, "preedit-start", G_CALLBACK(im_preedit_start_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(priv->im_context, "preedit-end", G_CALLBACK(im_preedit_end_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(priv->im_context, "preedit-changed", G_CALLBACK(im_preedit_changed_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(priv->im_context, "commit", G_CALLBACK(im_commit_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(priv->im_context, "retrieve-surrounding", G_CALLBACK(im_retrieve_surrounding_cb), self, G_CONNECT_SWAPPED); g_signal_connect_object(priv->im_context, "delete-surrounding", G_CALLBACK(im_delete_surrounding_cb), self, G_CONNECT_SWAPPED); priv->text_model = new flutter::TextInputModel(); } FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger, FlView* view) { g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr); FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN( g_object_new(fl_text_input_plugin_get_type(), nullptr)); g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new(); FlTextInputPluginPrivate* priv = static_cast( fl_text_input_plugin_get_instance_private(self)); priv->channel = fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler(priv->channel, method_call_cb, self, nullptr); priv->view = view; return self; } // Filters the a keypress given to the plugin through the plugin's // filter_keypress callback. gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self, GdkEventKey* event) { g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(self), FALSE); if (FL_TEXT_INPUT_PLUGIN_GET_CLASS(self)->filter_keypress) { return FL_TEXT_INPUT_PLUGIN_GET_CLASS(self)->filter_keypress(self, event); } return FALSE; }