blob: 51127bc69a6e2d18eddf0851171ff5d25711c831 [file] [log] [blame]
// 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;
}