mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add delayed event delivery for Linux. (#22577)
This changes the text handling so that keyboard events are sent to the framework first for handling, and then passed to the text input plugin, so that the framework has a chance to handle keys before they get given to the text field. This is complicated by the async nature of the interaction with the framework, since GTK wants a synchronous response. So, in this change, I always tell GTK that the event was handled, and if it wasn't, then I re-dispatch the event once we know one way or the other.
This commit is contained in:
parent
66f44c6c39
commit
bdadaad20d
@ -177,6 +177,7 @@ executable("flutter_linux_unittests") {
|
||||
"testing/mock_egl.cc",
|
||||
"testing/mock_engine.cc",
|
||||
"testing/mock_renderer.cc",
|
||||
"testing/mock_text_input_plugin.cc",
|
||||
]
|
||||
|
||||
public_configs = [ "//flutter:config" ]
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h"
|
||||
|
||||
@ -20,33 +24,272 @@ static constexpr char kUnicodeScalarValuesKey[] = "unicodeScalarValues";
|
||||
static constexpr char kGtkToolkit[] = "gtk";
|
||||
static constexpr char kLinuxKeymap[] = "linux";
|
||||
|
||||
static constexpr uint64_t kMaxPendingEvents = 1000;
|
||||
|
||||
// Definition of the FlKeyEventPlugin GObject class.
|
||||
|
||||
struct _FlKeyEventPlugin {
|
||||
GObject parent_instance;
|
||||
|
||||
FlBasicMessageChannel* channel = nullptr;
|
||||
GAsyncReadyCallback response_callback = nullptr;
|
||||
FlTextInputPlugin* text_input_plugin = nullptr;
|
||||
FlKeyEventPluginCallback response_callback = nullptr;
|
||||
GPtrArray* pending_events;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FlKeyEventPlugin, fl_key_event_plugin, G_TYPE_OBJECT)
|
||||
|
||||
// Declare and define a private pair object to bind the id and the event
|
||||
// together.
|
||||
|
||||
G_DECLARE_FINAL_TYPE(FlKeyEventPair,
|
||||
fl_key_event_pair,
|
||||
FL,
|
||||
KEY_EVENT_PAIR,
|
||||
GObject);
|
||||
|
||||
struct _FlKeyEventPair {
|
||||
GObject parent_instance;
|
||||
|
||||
uint64_t id;
|
||||
GdkEventKey* event;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FlKeyEventPair, fl_key_event_pair, G_TYPE_OBJECT)
|
||||
|
||||
// Dispose method for FlKeyEventPair.
|
||||
static void fl_key_event_pair_dispose(GObject* object) {
|
||||
// Redundant, but added so that we don't get a warning about unused function
|
||||
// for FL_IS_KEY_EVENT_PAIR.
|
||||
g_return_if_fail(FL_IS_KEY_EVENT_PAIR(object));
|
||||
|
||||
FlKeyEventPair* self = FL_KEY_EVENT_PAIR(object);
|
||||
g_clear_pointer(&self->event, gdk_event_free);
|
||||
G_OBJECT_CLASS(fl_key_event_pair_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
// Class Initialization method for FlKeyEventPair class.
|
||||
static void fl_key_event_pair_class_init(FlKeyEventPairClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = fl_key_event_pair_dispose;
|
||||
}
|
||||
|
||||
// Initialization for FlKeyEventPair instances.
|
||||
static void fl_key_event_pair_init(FlKeyEventPair* self) {}
|
||||
|
||||
// Creates a new FlKeyEventPair instance, given a unique ID, and an event struct
|
||||
// to keep.
|
||||
FlKeyEventPair* fl_key_event_pair_new(uint64_t id, GdkEventKey* event) {
|
||||
FlKeyEventPair* self =
|
||||
FL_KEY_EVENT_PAIR(g_object_new(fl_key_event_pair_get_type(), nullptr));
|
||||
|
||||
// Copy the event to preserve refcounts for referenced values (mainly the
|
||||
// window).
|
||||
GdkEventKey* event_copy = reinterpret_cast<GdkEventKey*>(
|
||||
gdk_event_copy(reinterpret_cast<GdkEvent*>(event)));
|
||||
self->id = id;
|
||||
self->event = event_copy;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Declare and define a private class to hold response data from the framework.
|
||||
G_DECLARE_FINAL_TYPE(FlKeyEventResponseData,
|
||||
fl_key_event_response_data,
|
||||
FL,
|
||||
KEY_EVENT_RESPONSE_DATA,
|
||||
GObject);
|
||||
|
||||
struct _FlKeyEventResponseData {
|
||||
GObject parent_instance;
|
||||
|
||||
FlKeyEventPlugin* plugin;
|
||||
uint64_t id;
|
||||
gpointer user_data;
|
||||
};
|
||||
|
||||
// Definition for FlKeyEventResponseData private class.
|
||||
G_DEFINE_TYPE(FlKeyEventResponseData, fl_key_event_response_data, G_TYPE_OBJECT)
|
||||
|
||||
// Dispose method for FlKeyEventResponseData private class.
|
||||
static void fl_key_event_response_data_dispose(GObject* object) {
|
||||
g_return_if_fail(FL_IS_KEY_EVENT_RESPONSE_DATA(object));
|
||||
FlKeyEventResponseData* self = FL_KEY_EVENT_RESPONSE_DATA(object);
|
||||
// Don't need to weak pointer anymore.
|
||||
g_object_remove_weak_pointer(G_OBJECT(self->plugin),
|
||||
reinterpret_cast<gpointer*>(&(self->plugin)));
|
||||
}
|
||||
|
||||
// Class initialization method for FlKeyEventResponseData private class.
|
||||
static void fl_key_event_response_data_class_init(
|
||||
FlKeyEventResponseDataClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = fl_key_event_response_data_dispose;
|
||||
}
|
||||
|
||||
// Instance initialization method for FlKeyEventResponseData private class.
|
||||
static void fl_key_event_response_data_init(FlKeyEventResponseData* self) {}
|
||||
|
||||
// Creates a new FlKeyEventResponseData private class with a plugin that created
|
||||
// the request, a unique ID for tracking, and optional user data.
|
||||
// Will keep a weak pointer to the plugin.
|
||||
FlKeyEventResponseData* fl_key_event_response_data_new(FlKeyEventPlugin* plugin,
|
||||
uint64_t id,
|
||||
gpointer user_data) {
|
||||
FlKeyEventResponseData* self = FL_KEY_EVENT_RESPONSE_DATA(
|
||||
g_object_new(fl_key_event_response_data_get_type(), nullptr));
|
||||
|
||||
self->plugin = plugin;
|
||||
// Add a weak pointer so we can know if the key event plugin disappeared
|
||||
// while the framework was responding.
|
||||
g_object_add_weak_pointer(G_OBJECT(plugin),
|
||||
reinterpret_cast<gpointer*>(&(self->plugin)));
|
||||
self->id = id;
|
||||
self->user_data = user_data;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Calculates a unique ID for a given GdkEventKey object to use for
|
||||
// identification of responses from the framework.
|
||||
static uint64_t get_event_id(GdkEventKey* event) {
|
||||
// Combine the event timestamp, the type of event, and the hardware keycode
|
||||
// (scan code) of the event to come up with a unique id for this event that
|
||||
// can be derived solely from the event data itself, so that we can identify
|
||||
// whether or not we have seen this event already.
|
||||
return (event->time & 0xffffffff) |
|
||||
(static_cast<uint64_t>(event->type) & 0xffff) << 32 |
|
||||
(static_cast<uint64_t>(event->hardware_keycode) & 0xffff) << 48;
|
||||
}
|
||||
|
||||
// Finds an event in the event queue that was sent to the framework by its ID.
|
||||
static GdkEventKey* find_pending_event(FlKeyEventPlugin* self, uint64_t id) {
|
||||
if (self->pending_events->len == 0 ||
|
||||
FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id != id) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->event;
|
||||
}
|
||||
|
||||
// Removes an event from the pending event queue.
|
||||
static void remove_pending_event(FlKeyEventPlugin* self, uint64_t id) {
|
||||
if (self->pending_events->len == 0 ||
|
||||
FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id != id) {
|
||||
g_warning(
|
||||
"Tried to remove pending event with id %ld, but the event was out of "
|
||||
"order, or is unknown.",
|
||||
id);
|
||||
return;
|
||||
}
|
||||
g_ptr_array_remove_index(self->pending_events, 0);
|
||||
}
|
||||
|
||||
// Adds an GdkEventKey to the pending event queue, with a unique ID, and the
|
||||
// plugin that added it.
|
||||
static void add_pending_event(FlKeyEventPlugin* self,
|
||||
uint64_t id,
|
||||
GdkEventKey* event) {
|
||||
if (self->pending_events->len > kMaxPendingEvents) {
|
||||
g_warning(
|
||||
"There are %d keyboard events that have not yet received a "
|
||||
"response from the framework. Are responses being sent?",
|
||||
self->pending_events->len);
|
||||
}
|
||||
g_ptr_array_add(self->pending_events, fl_key_event_pair_new(id, event));
|
||||
}
|
||||
|
||||
// Handles a response from the framework to a key event sent to the framework
|
||||
// earlier.
|
||||
static void handle_response(GObject* object,
|
||||
GAsyncResult* result,
|
||||
gpointer user_data) {
|
||||
g_autoptr(FlKeyEventResponseData) data =
|
||||
FL_KEY_EVENT_RESPONSE_DATA(user_data);
|
||||
|
||||
// Will also return if the weak pointer has been destroyed.
|
||||
if (data->plugin == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlKeyEventPlugin* self = data->plugin;
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
FlBasicMessageChannel* messageChannel = FL_BASIC_MESSAGE_CHANNEL(object);
|
||||
FlValue* message =
|
||||
fl_basic_message_channel_send_finish(messageChannel, result, &error);
|
||||
if (error != nullptr) {
|
||||
g_warning("Unable to retrieve framework response: %s", error->message);
|
||||
return;
|
||||
}
|
||||
g_autoptr(FlValue) handled_value = fl_value_lookup_string(message, "handled");
|
||||
bool handled = FALSE;
|
||||
if (handled_value != nullptr) {
|
||||
GdkEventKey* event = find_pending_event(self, data->id);
|
||||
if (event == nullptr) {
|
||||
g_warning(
|
||||
"Event response for event id %ld received, but event was received "
|
||||
"out of order, or is unknown.",
|
||||
data->id);
|
||||
} else {
|
||||
handled = fl_value_get_bool(handled_value);
|
||||
if (!handled) {
|
||||
if (self->text_input_plugin != nullptr) {
|
||||
// Propagate the event to the text input plugin.
|
||||
handled = fl_text_input_plugin_filter_keypress(
|
||||
self->text_input_plugin, event);
|
||||
}
|
||||
// Dispatch the event to other GTK windows if the text input plugin
|
||||
// didn't handle it. We keep track of the event id so we can recognize
|
||||
// the event when our window receives it again and not respond to it. If
|
||||
// the response callback is set, then use that instead.
|
||||
if (!handled && self->response_callback == nullptr) {
|
||||
gdk_event_put(reinterpret_cast<GdkEvent*>(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// Because the event was handled, we no longer need to track it. Unhandled
|
||||
// events will be removed when the event is re-dispatched to the window.
|
||||
remove_pending_event(self, data->id);
|
||||
}
|
||||
|
||||
if (self->response_callback != nullptr) {
|
||||
self->response_callback(object, message, handled, data->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Disposes of an FlKeyEventPlugin instance.
|
||||
static void fl_key_event_plugin_dispose(GObject* object) {
|
||||
FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(object);
|
||||
|
||||
g_clear_object(&self->channel);
|
||||
g_object_remove_weak_pointer(
|
||||
G_OBJECT(self->text_input_plugin),
|
||||
reinterpret_cast<gpointer*>(&(self->text_input_plugin)));
|
||||
g_ptr_array_free(self->pending_events, TRUE);
|
||||
|
||||
G_OBJECT_CLASS(fl_key_event_plugin_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
// Initializes the FlKeyEventPlugin class methods.
|
||||
static void fl_key_event_plugin_class_init(FlKeyEventPluginClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = fl_key_event_plugin_dispose;
|
||||
}
|
||||
|
||||
// Initializes an FlKeyEventPlugin instance.
|
||||
static void fl_key_event_plugin_init(FlKeyEventPlugin* self) {}
|
||||
|
||||
FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger,
|
||||
GAsyncReadyCallback response_callback,
|
||||
const char* channel_name) {
|
||||
// Creates a new FlKeyEventPlugin instance, with a messenger used to send
|
||||
// messages to the framework, an FlTextInputPlugin used to handle key events
|
||||
// that the framework doesn't handle. Mainly for testing purposes, it also takes
|
||||
// an optional callback to call when a response is received, and an optional
|
||||
// channel name to use when sending messages.
|
||||
FlKeyEventPlugin* fl_key_event_plugin_new(
|
||||
FlBinaryMessenger* messenger,
|
||||
FlTextInputPlugin* text_input_plugin,
|
||||
FlKeyEventPluginCallback response_callback,
|
||||
const char* channel_name) {
|
||||
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
|
||||
g_return_val_if_fail(FL_IS_TEXT_INPUT_PLUGIN(text_input_plugin), nullptr);
|
||||
|
||||
FlKeyEventPlugin* self = FL_KEY_EVENT_PLUGIN(
|
||||
g_object_new(fl_key_event_plugin_get_type(), nullptr));
|
||||
@ -56,15 +299,37 @@ FlKeyEventPlugin* fl_key_event_plugin_new(FlBinaryMessenger* messenger,
|
||||
messenger, channel_name == nullptr ? kChannelName : channel_name,
|
||||
FL_MESSAGE_CODEC(codec));
|
||||
self->response_callback = response_callback;
|
||||
// Add a weak pointer so we know if the text input plugin goes away.
|
||||
g_object_add_weak_pointer(
|
||||
G_OBJECT(text_input_plugin),
|
||||
reinterpret_cast<gpointer*>(&(self->text_input_plugin)));
|
||||
self->text_input_plugin = text_input_plugin;
|
||||
|
||||
self->pending_events = g_ptr_array_new_with_free_func(g_object_unref);
|
||||
return self;
|
||||
}
|
||||
|
||||
void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
|
||||
// Sends a key event to the framework.
|
||||
bool fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
|
||||
GdkEventKey* event,
|
||||
gpointer user_data) {
|
||||
g_return_if_fail(FL_IS_KEY_EVENT_PLUGIN(self));
|
||||
g_return_if_fail(event != nullptr);
|
||||
g_return_val_if_fail(FL_IS_KEY_EVENT_PLUGIN(self), FALSE);
|
||||
g_return_val_if_fail(event != nullptr, FALSE);
|
||||
|
||||
// Get an ID for the event, so we can match them up when we get a response
|
||||
// from the framework. Use the event time, type, and hardware keycode as a
|
||||
// unique ID, since they are part of the event structure that we can look up
|
||||
// when we receive a random event that may or may not have been
|
||||
// tracked/produced by this code.
|
||||
uint64_t id = get_event_id(event);
|
||||
if (self->pending_events->len != 0 &&
|
||||
FL_KEY_EVENT_PAIR(g_ptr_array_index(self->pending_events, 0))->id == id) {
|
||||
// If the event is at the head of the queue of pending events we've seen,
|
||||
// and has the same id, then we know that this is a re-dispatched event, and
|
||||
// we shouldn't respond to it, but we should remove it from tracking.
|
||||
remove_pending_event(self, id);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const gchar* type;
|
||||
switch (event->type) {
|
||||
@ -75,7 +340,7 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
|
||||
type = kTypeValueUp;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int64_t scan_code = event->hardware_keycode;
|
||||
@ -109,9 +374,9 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
|
||||
// Remove lock states from state mask.
|
||||
guint state = event->state & ~(GDK_LOCK_MASK | GDK_MOD2_MASK);
|
||||
|
||||
static bool shift_lock_pressed = false;
|
||||
static bool caps_lock_pressed = false;
|
||||
static bool num_lock_pressed = false;
|
||||
static bool shift_lock_pressed = FALSE;
|
||||
static bool caps_lock_pressed = FALSE;
|
||||
static bool num_lock_pressed = FALSE;
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_Num_Lock:
|
||||
num_lock_pressed = event->type == GDK_KEY_PRESS;
|
||||
@ -144,6 +409,14 @@ void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* self,
|
||||
fl_value_new_int(unicodeScalarValues));
|
||||
}
|
||||
|
||||
// Track the event as pending a response from the framework.
|
||||
add_pending_event(self, id, event);
|
||||
FlKeyEventResponseData* data =
|
||||
fl_key_event_response_data_new(self, id, user_data);
|
||||
// Send the message off to the framework for handling (or not).
|
||||
fl_basic_message_channel_send(self->channel, message, nullptr,
|
||||
self->response_callback, user_data);
|
||||
handle_response, data);
|
||||
// Return true before we know what the framework will do, because if it
|
||||
// doesn't handle the key, we'll re-dispatch it later.
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_
|
||||
#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEY_EVENT_PLUGIN_H_
|
||||
|
||||
#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"
|
||||
|
||||
#include <gdk/gdk.h>
|
||||
|
||||
@ -24,15 +26,36 @@ G_DECLARE_FINAL_TYPE(FlKeyEventPlugin,
|
||||
* of SystemChannels.keyEvent from the Flutter services library.
|
||||
*/
|
||||
|
||||
/**
|
||||
* FlKeyEventPluginCallback:
|
||||
* @source_object: (nullable): the object the key event was started with.
|
||||
* @message: the message returned from the framework.
|
||||
* @handled: a boolean indicating whether the key event was handled in the
|
||||
* framework.
|
||||
* @user_data: user data passed to the callback.
|
||||
*
|
||||
* Type definition for a function that will be called when a key event is
|
||||
* received from the engine.
|
||||
**/
|
||||
typedef void (*FlKeyEventPluginCallback)(GObject* source_object,
|
||||
FlValue* message,
|
||||
bool handled,
|
||||
gpointer user_data);
|
||||
|
||||
/**
|
||||
* fl_key_event_plugin_new:
|
||||
* @messenger: an #FlBinaryMessenger.
|
||||
* @response_callback: the callback to call when a response is received. If not
|
||||
* given (nullptr), then the default response callback is
|
||||
* used.
|
||||
* @channel_name: the name of the channel to send key events on. If not given
|
||||
* (nullptr), then the standard key event channel name is used.
|
||||
* Typically used for tests to send on a test channel.
|
||||
* used. Typically used for tests to receive event
|
||||
* information. If specified, unhandled events will not be
|
||||
* re-dispatched.
|
||||
* @text_input_plugin: The #FlTextInputPlugin to send key events to if the
|
||||
* framework doesn't handle them.
|
||||
* @channel_name: the name of the channel to send key events to the framework
|
||||
* on. If not given (nullptr), then the standard key event
|
||||
* channel name is used. Typically used for tests to send on a
|
||||
* test channel.
|
||||
*
|
||||
* Creates a new plugin that implements SystemChannels.keyEvent from the
|
||||
* Flutter services library.
|
||||
@ -41,7 +64,8 @@ G_DECLARE_FINAL_TYPE(FlKeyEventPlugin,
|
||||
*/
|
||||
FlKeyEventPlugin* fl_key_event_plugin_new(
|
||||
FlBinaryMessenger* messenger,
|
||||
GAsyncReadyCallback response_callback = nullptr,
|
||||
FlTextInputPlugin* text_input_plugin,
|
||||
FlKeyEventPluginCallback response_callback = nullptr,
|
||||
const char* channel_name = nullptr);
|
||||
|
||||
/**
|
||||
@ -51,9 +75,12 @@ FlKeyEventPlugin* fl_key_event_plugin_new(
|
||||
* @user_data: a pointer to user data to send to the response callback via the
|
||||
* messenger.
|
||||
*
|
||||
* @returns %TRUE if this key event should be considered handled and
|
||||
* event propagation stopped.
|
||||
*
|
||||
* Sends a key event to Flutter.
|
||||
*/
|
||||
void fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin,
|
||||
bool fl_key_event_plugin_send_key_event(FlKeyEventPlugin* plugin,
|
||||
GdkEventKey* event,
|
||||
gpointer user_data = nullptr);
|
||||
|
||||
|
||||
@ -4,41 +4,47 @@
|
||||
|
||||
#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
|
||||
|
||||
#include <iostream>
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "flutter/shell/platform/linux/fl_binary_messenger_private.h"
|
||||
#include "flutter/shell/platform/linux/fl_engine_private.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_basic_message_channel.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h"
|
||||
#include "flutter/shell/platform/linux/testing/fl_test.h"
|
||||
#include "flutter/shell/platform/linux/testing/mock_renderer.h"
|
||||
#include "flutter/shell/platform/linux/testing/mock_text_input_plugin.h"
|
||||
|
||||
const char* expected_value = nullptr;
|
||||
gboolean expected_handled = FALSE;
|
||||
|
||||
// Called when the message response is received in the send_key_event test.
|
||||
static void echo_response_cb(GObject* object,
|
||||
GAsyncResult* result,
|
||||
FlValue* message,
|
||||
bool handled,
|
||||
gpointer user_data) {
|
||||
g_autoptr(GError) error = nullptr;
|
||||
g_autoptr(FlValue) message = fl_basic_message_channel_send_finish(
|
||||
FL_BASIC_MESSAGE_CHANNEL(object), result, &error);
|
||||
EXPECT_NE(message, nullptr);
|
||||
EXPECT_EQ(error, nullptr);
|
||||
|
||||
EXPECT_EQ(fl_value_get_type(message), FL_VALUE_TYPE_MAP);
|
||||
EXPECT_STREQ(fl_value_to_string(message), expected_value);
|
||||
EXPECT_EQ(handled, expected_handled);
|
||||
|
||||
g_main_loop_quit(static_cast<GMainLoop*>(user_data));
|
||||
}
|
||||
|
||||
static gboolean handle_keypress(FlTextInputPlugin* plugin, GdkEventKey* event) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean ignore_keypress(FlTextInputPlugin* plugin, GdkEventKey* event) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Test sending a letter "A";
|
||||
TEST(FlKeyEventPluginTest, SendKeyEvent) {
|
||||
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
|
||||
|
||||
g_autoptr(FlEngine) engine = make_mock_engine();
|
||||
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
|
||||
g_autoptr(FlKeyEventPlugin) plugin =
|
||||
fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo");
|
||||
g_autoptr(FlTextInputPlugin) text_input_plugin =
|
||||
FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress));
|
||||
g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new(
|
||||
messenger, text_input_plugin, echo_response_cb, "test/echo");
|
||||
|
||||
char string[] = "A";
|
||||
GdkEventKey key_event = GdkEventKey{
|
||||
@ -58,6 +64,7 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) {
|
||||
expected_value =
|
||||
"{type: keydown, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, "
|
||||
"modifiers: 0, unicodeScalarValues: 65}";
|
||||
expected_handled = FALSE;
|
||||
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
@ -67,7 +74,7 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) {
|
||||
GDK_KEY_RELEASE, // event type
|
||||
nullptr, // window (not needed)
|
||||
FALSE, // event was sent explicitly
|
||||
12345, // time
|
||||
23456, // time
|
||||
0x0, // modifier state
|
||||
GDK_KEY_A, // key code
|
||||
1, // length of string representation
|
||||
@ -80,7 +87,9 @@ TEST(FlKeyEventPluginTest, SendKeyEvent) {
|
||||
expected_value =
|
||||
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, keyCode: 65, "
|
||||
"modifiers: 0, unicodeScalarValues: 65}";
|
||||
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
expected_handled = FALSE;
|
||||
bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
EXPECT_TRUE(handled);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
g_main_loop_run(loop);
|
||||
@ -93,8 +102,10 @@ void test_lock_event(guint key_code,
|
||||
|
||||
g_autoptr(FlEngine) engine = make_mock_engine();
|
||||
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
|
||||
g_autoptr(FlKeyEventPlugin) plugin =
|
||||
fl_key_event_plugin_new(messenger, echo_response_cb, "test/echo");
|
||||
g_autoptr(FlTextInputPlugin) text_input_plugin =
|
||||
FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress));
|
||||
g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new(
|
||||
messenger, text_input_plugin, echo_response_cb, "test/echo");
|
||||
|
||||
GdkEventKey key_event = GdkEventKey{
|
||||
GDK_KEY_PRESS, // event type
|
||||
@ -111,14 +122,18 @@ void test_lock_event(guint key_code,
|
||||
};
|
||||
|
||||
expected_value = down_expected;
|
||||
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
expected_handled = FALSE;
|
||||
bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
EXPECT_TRUE(handled);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
g_main_loop_run(loop);
|
||||
|
||||
key_event.type = GDK_KEY_RELEASE;
|
||||
key_event.time++;
|
||||
|
||||
expected_value = up_expected;
|
||||
expected_handled = FALSE;
|
||||
fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
@ -151,3 +166,107 @@ TEST(FlKeyEventPluginTest, SendShiftLockKeyEvent) {
|
||||
"{type: keyup, keymap: linux, scanCode: 4, toolkit: gtk, "
|
||||
"keyCode: 65510, modifiers: 0}");
|
||||
}
|
||||
|
||||
TEST(FlKeyEventPluginTest, TestKeyEventHandledByFramework) {
|
||||
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
|
||||
|
||||
g_autoptr(FlEngine) engine = make_mock_engine();
|
||||
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
|
||||
g_autoptr(FlTextInputPlugin) text_input_plugin =
|
||||
FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress));
|
||||
g_autoptr(FlKeyEventPlugin) plugin = fl_key_event_plugin_new(
|
||||
messenger, text_input_plugin, echo_response_cb, "test/key-event-handled");
|
||||
|
||||
GdkEventKey key_event = GdkEventKey{
|
||||
GDK_KEY_PRESS, // event type
|
||||
nullptr, // window (not needed)
|
||||
FALSE, // event was sent explicitly
|
||||
12345, // time
|
||||
0x10, // modifier state
|
||||
GDK_KEY_A, // key code
|
||||
1, // length of string representation
|
||||
nullptr, // string representation
|
||||
0x04, // scan code
|
||||
0, // keyboard group
|
||||
0, // is a modifier
|
||||
};
|
||||
|
||||
expected_value = "{handled: true}";
|
||||
expected_handled = TRUE;
|
||||
bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
// Should always be true, because the event was delayed.
|
||||
EXPECT_TRUE(handled);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
g_main_loop_run(loop);
|
||||
}
|
||||
|
||||
TEST(FlKeyEventPluginTest, TestKeyEventHandledByTextInputPlugin) {
|
||||
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
|
||||
|
||||
g_autoptr(FlEngine) engine = make_mock_engine();
|
||||
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
|
||||
g_autoptr(FlTextInputPlugin) text_input_plugin =
|
||||
FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(handle_keypress));
|
||||
g_autoptr(FlKeyEventPlugin) plugin =
|
||||
fl_key_event_plugin_new(messenger, text_input_plugin, echo_response_cb,
|
||||
"test/key-event-not-handled");
|
||||
|
||||
GdkEventKey key_event = GdkEventKey{
|
||||
GDK_KEY_PRESS, // event type
|
||||
nullptr, // window (not needed)
|
||||
FALSE, // event was sent explicitly
|
||||
12345, // time
|
||||
0x10, // modifier state
|
||||
GDK_KEY_A, // key code
|
||||
1, // length of string representation
|
||||
nullptr, // string representation
|
||||
0x04, // scan code
|
||||
0, // keyboard group
|
||||
0, // is a modifier
|
||||
};
|
||||
|
||||
expected_value = "{handled: false}";
|
||||
expected_handled = TRUE;
|
||||
bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
// Should always be true, because the event was delayed.
|
||||
EXPECT_TRUE(handled);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
g_main_loop_run(loop);
|
||||
}
|
||||
|
||||
TEST(FlKeyEventPluginTest, TestKeyEventNotHandledByTextInputPlugin) {
|
||||
g_autoptr(GMainLoop) loop = g_main_loop_new(nullptr, 0);
|
||||
|
||||
g_autoptr(FlEngine) engine = make_mock_engine();
|
||||
FlBinaryMessenger* messenger = fl_binary_messenger_new(engine);
|
||||
g_autoptr(FlTextInputPlugin) text_input_plugin =
|
||||
FL_TEXT_INPUT_PLUGIN(fl_mock_text_input_plugin_new(ignore_keypress));
|
||||
g_autoptr(FlKeyEventPlugin) plugin =
|
||||
fl_key_event_plugin_new(messenger, text_input_plugin, echo_response_cb,
|
||||
"test/key-event-not-handled");
|
||||
|
||||
GdkEventKey key_event = GdkEventKey{
|
||||
GDK_KEY_PRESS, // event type
|
||||
nullptr, // window (not needed)
|
||||
FALSE, // event was sent explicitly
|
||||
12345, // time
|
||||
0x10, // modifier state
|
||||
GDK_KEY_A, // key code
|
||||
1, // length of string representation
|
||||
nullptr, // string representation
|
||||
0x04, // scan code
|
||||
0, // keyboard group
|
||||
0, // is a modifier
|
||||
};
|
||||
|
||||
expected_value = "{handled: false}";
|
||||
expected_handled = FALSE;
|
||||
bool handled = fl_key_event_plugin_send_key_event(plugin, &key_event, loop);
|
||||
// Should always be true, because the event was delayed.
|
||||
EXPECT_TRUE(handled);
|
||||
|
||||
// Blocks here until echo_response_cb is called.
|
||||
g_main_loop_run(loop);
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ static constexpr char kMultilineInputType[] = "TextInputType.multiline";
|
||||
|
||||
static constexpr int64_t kClientIdUnset = -1;
|
||||
|
||||
struct _FlTextInputPlugin {
|
||||
struct FlTextInputPluginPrivate {
|
||||
GObject parent_instance;
|
||||
|
||||
FlMethodChannel* channel;
|
||||
@ -77,7 +77,9 @@ struct _FlTextInputPlugin {
|
||||
GdkRectangle composing_rect;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT)
|
||||
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,
|
||||
@ -104,24 +106,27 @@ static void update_editing_state_response_cb(GObject* object,
|
||||
|
||||
// Informs Flutter of text input changes.
|
||||
static void update_editing_state(FlTextInputPlugin* self) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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(self->client_id));
|
||||
fl_value_append_take(args, fl_value_new_int(priv->client_id));
|
||||
g_autoptr(FlValue) value = fl_value_new_map();
|
||||
|
||||
TextRange selection = self->text_model->selection();
|
||||
TextRange selection = priv->text_model->selection();
|
||||
fl_value_set_string_take(
|
||||
value, kTextKey,
|
||||
fl_value_new_string(self->text_model->GetText().c_str()));
|
||||
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 = self->text_model->composing()
|
||||
? self->text_model->composing_range().base()
|
||||
int composing_base = priv->text_model->composing()
|
||||
? priv->text_model->composing_range().base()
|
||||
: -1;
|
||||
int composing_extent = self->text_model->composing()
|
||||
? self->text_model->composing_range().extent()
|
||||
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));
|
||||
@ -136,7 +141,7 @@ static void update_editing_state(FlTextInputPlugin* self) {
|
||||
|
||||
fl_value_append(args, value);
|
||||
|
||||
fl_method_channel_invoke_method(self->channel, kUpdateEditingStateMethod,
|
||||
fl_method_channel_invoke_method(priv->channel, kUpdateEditingStateMethod,
|
||||
args, nullptr,
|
||||
update_editing_state_response_cb, self);
|
||||
}
|
||||
@ -153,61 +158,74 @@ static void perform_action_response_cb(GObject* object,
|
||||
|
||||
// Inform Flutter that the input has been activated.
|
||||
static void perform_action(FlTextInputPlugin* self) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(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_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(self->client_id));
|
||||
fl_value_append_take(args, fl_value_new_string(self->input_action));
|
||||
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(self->channel, kPerformActionMethod, args,
|
||||
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) {
|
||||
self->text_model->BeginComposing();
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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(self->view)));
|
||||
gtk_im_context_set_client_window(self->im_context, 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<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
g_autofree gchar* buf = nullptr;
|
||||
gint cursor_offset = 0;
|
||||
gtk_im_context_get_preedit_string(self->im_context, &buf, nullptr,
|
||||
gtk_im_context_get_preedit_string(priv->im_context, &buf, nullptr,
|
||||
&cursor_offset);
|
||||
cursor_offset += self->text_model->composing_range().base();
|
||||
self->text_model->UpdateComposingText(buf);
|
||||
self->text_model->SetSelection(TextRange(cursor_offset, 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) {
|
||||
self->text_model->AddText(text);
|
||||
if (self->text_model->composing()) {
|
||||
self->text_model->CommitComposing();
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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) {
|
||||
self->text_model->EndComposing();
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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) {
|
||||
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,
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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;
|
||||
}
|
||||
@ -216,7 +234,9 @@ static gboolean im_retrieve_surrounding_cb(FlTextInputPlugin* self) {
|
||||
static gboolean im_delete_surrounding_cb(FlTextInputPlugin* self,
|
||||
gint offset,
|
||||
gint n_chars) {
|
||||
if (self->text_model->DeleteSurrounding(offset, n_chars)) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
if (priv->text_model->DeleteSurrounding(offset, n_chars)) {
|
||||
update_editing_state(self);
|
||||
}
|
||||
return TRUE;
|
||||
@ -229,18 +249,20 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) {
|
||||
return FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
kBadArgumentsError, "Expected 2-element list", nullptr));
|
||||
}
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
|
||||
self->client_id = fl_value_get_int(fl_value_get_list_value(args, 0));
|
||||
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(self->input_action);
|
||||
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) {
|
||||
self->input_action = g_strdup(fl_value_get_string(input_action_value));
|
||||
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.
|
||||
self->input_multiline = FALSE;
|
||||
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) {
|
||||
@ -249,7 +271,7 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) {
|
||||
if (fl_value_get_type(input_type_name) == FL_VALUE_TYPE_STRING &&
|
||||
g_strcmp0(fl_value_get_string(input_type_name), kMultilineInputType) ==
|
||||
0) {
|
||||
self->input_multiline = TRUE;
|
||||
priv->input_multiline = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,12 +280,14 @@ static FlMethodResponse* set_client(FlTextInputPlugin* self, FlValue* args) {
|
||||
|
||||
// Shows the input method.
|
||||
static FlMethodResponse* show(FlTextInputPlugin* self) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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(self->view)));
|
||||
gtk_im_context_set_client_window(self->im_context, 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(self->im_context);
|
||||
gtk_im_context_focus_in(priv->im_context);
|
||||
|
||||
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
}
|
||||
@ -271,9 +295,11 @@ static FlMethodResponse* show(FlTextInputPlugin* self) {
|
||||
// Updates the editing state from Flutter.
|
||||
static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
|
||||
FlValue* args) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
const gchar* text =
|
||||
fl_value_get_string(fl_value_lookup_string(args, kTextKey));
|
||||
self->text_model->SetText(text);
|
||||
priv->text_model->SetText(text);
|
||||
|
||||
int64_t selection_base =
|
||||
fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey));
|
||||
@ -284,19 +310,19 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
|
||||
selection_base = selection_extent = 0;
|
||||
}
|
||||
|
||||
self->text_model->SetText(text);
|
||||
self->text_model->SetSelection(TextRange(selection_base, selection_extent));
|
||||
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) {
|
||||
self->text_model->EndComposing();
|
||||
priv->text_model->EndComposing();
|
||||
} else {
|
||||
size_t composing_start = std::min(composing_base, composing_extent);
|
||||
size_t cursor_offset = selection_base - composing_start;
|
||||
self->text_model->SetComposingRange(
|
||||
priv->text_model->SetComposingRange(
|
||||
TextRange(composing_base, composing_extent), cursor_offset);
|
||||
}
|
||||
|
||||
@ -305,14 +331,18 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
|
||||
|
||||
// Called when the input method client is complete.
|
||||
static FlMethodResponse* clear_client(FlTextInputPlugin* self) {
|
||||
self->client_id = kClientIdUnset;
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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) {
|
||||
gtk_im_context_focus_out(self->im_context);
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
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));
|
||||
}
|
||||
@ -326,29 +356,32 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) {
|
||||
// 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<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
|
||||
// Skip update if not composing to avoid setting to position 0.
|
||||
if (!self->text_model->composing()) {
|
||||
if (!priv->text_model->composing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform the x, y positions of the cursor from local coordinates to
|
||||
// Flutter view coordinates.
|
||||
gint x = self->composing_rect.x * self->editabletext_transform[0][0] +
|
||||
self->composing_rect.y * self->editabletext_transform[1][0] +
|
||||
self->editabletext_transform[3][0] + self->composing_rect.width;
|
||||
gint y = self->composing_rect.x * self->editabletext_transform[0][1] +
|
||||
self->composing_rect.y * self->editabletext_transform[1][1] +
|
||||
self->editabletext_transform[3][1] + self->composing_rect.height;
|
||||
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(self->view), gtk_widget_get_toplevel(GTK_WIDGET(self->view)),
|
||||
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(self->im_context, &preedit_rect);
|
||||
gtk_im_context_set_cursor_location(priv->im_context, &preedit_rect);
|
||||
}
|
||||
|
||||
// Handles updates to the EditableText size and position from the framework.
|
||||
@ -366,7 +399,9 @@ static FlMethodResponse* set_editable_size_and_transform(
|
||||
|
||||
for (size_t i = 0; i < transform_len; ++i) {
|
||||
double val = fl_value_get_float(fl_value_get_list_value(transform, i));
|
||||
self->editabletext_transform[i / 4][i % 4] = val;
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
priv->editabletext_transform[i / 4][i % 4] = val;
|
||||
}
|
||||
update_im_cursor_position(self);
|
||||
|
||||
@ -381,13 +416,15 @@ static FlMethodResponse* set_editable_size_and_transform(
|
||||
// composing region, the cursor rect is sent.
|
||||
static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self,
|
||||
FlValue* args) {
|
||||
self->composing_rect.x =
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
priv->composing_rect.x =
|
||||
fl_value_get_float(fl_value_lookup_string(args, "x"));
|
||||
self->composing_rect.y =
|
||||
priv->composing_rect.y =
|
||||
fl_value_get_float(fl_value_lookup_string(args, "y"));
|
||||
self->composing_rect.width =
|
||||
priv->composing_rect.width =
|
||||
fl_value_get_float(fl_value_lookup_string(args, "width"));
|
||||
self->composing_rect.height =
|
||||
priv->composing_rect.height =
|
||||
fl_value_get_float(fl_value_lookup_string(args, "height"));
|
||||
update_im_cursor_position(self);
|
||||
|
||||
@ -428,75 +465,38 @@ static void method_call_cb(FlMethodChannel* channel,
|
||||
}
|
||||
}
|
||||
|
||||
// Disposes of an FlTextInputPlugin.
|
||||
static void fl_text_input_plugin_dispose(GObject* object) {
|
||||
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
|
||||
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_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;
|
||||
}
|
||||
self->view = nullptr;
|
||||
priv->view = 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;
|
||||
}
|
||||
// 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);
|
||||
|
||||
static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
|
||||
self->client_id = kClientIdUnset;
|
||||
self->im_context = gtk_im_multicontext_new();
|
||||
self->input_multiline = FALSE;
|
||||
g_signal_connect_object(self->im_context, "preedit-start",
|
||||
G_CALLBACK(im_preedit_start_cb), self,
|
||||
G_CONNECT_SWAPPED);
|
||||
g_signal_connect_object(self->im_context, "preedit-end",
|
||||
G_CALLBACK(im_preedit_end_cb), self,
|
||||
G_CONNECT_SWAPPED);
|
||||
g_signal_connect_object(self->im_context, "preedit-changed",
|
||||
G_CALLBACK(im_preedit_changed_cb), self,
|
||||
G_CONNECT_SWAPPED);
|
||||
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();
|
||||
}
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
|
||||
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();
|
||||
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);
|
||||
self->view = view;
|
||||
|
||||
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 (self->client_id == kClientIdUnset) {
|
||||
if (priv->client_id == kClientIdUnset) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (gtk_im_context_filter_keypress(self->im_context, event)) {
|
||||
if (gtk_im_context_filter_keypress(priv->im_context, event)) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@ -506,37 +506,31 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self,
|
||||
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();
|
||||
changed = priv->text_model->MoveCursorToEnd();
|
||||
break;
|
||||
case GDK_KEY_Return:
|
||||
case GDK_KEY_KP_Enter:
|
||||
case GDK_KEY_ISO_Enter:
|
||||
if (self->input_multiline == TRUE) {
|
||||
self->text_model->AddCodePoint('\n');
|
||||
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 = self->text_model->MoveCursorToBeginning();
|
||||
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:
|
||||
// Already handled inside Flutter.
|
||||
break;
|
||||
case GDK_KEY_Right:
|
||||
case GDK_KEY_KP_Right:
|
||||
// Already handled inside Flutter.
|
||||
// Already handled inside the framework in RenderEditable.
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -548,5 +542,70 @@ gboolean fl_text_input_plugin_filter_keypress(FlTextInputPlugin* self,
|
||||
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<FlTextInputPluginPrivate*>(
|
||||
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<FlTextInputPluginPrivate*>(
|
||||
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;
|
||||
}
|
||||
|
||||
@ -12,11 +12,11 @@
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
G_DECLARE_FINAL_TYPE(FlTextInputPlugin,
|
||||
fl_text_input_plugin,
|
||||
FL,
|
||||
TEXT_INPUT_PLUGIN,
|
||||
GObject);
|
||||
G_DECLARE_DERIVABLE_TYPE(FlTextInputPlugin,
|
||||
fl_text_input_plugin,
|
||||
FL,
|
||||
TEXT_INPUT_PLUGIN,
|
||||
GObject);
|
||||
|
||||
/**
|
||||
* FlTextInputPlugin:
|
||||
@ -25,6 +25,15 @@ G_DECLARE_FINAL_TYPE(FlTextInputPlugin,
|
||||
* of SystemChannels.textInput from the Flutter services library.
|
||||
*/
|
||||
|
||||
struct _FlTextInputPluginClass {
|
||||
GObjectClass parent_class;
|
||||
|
||||
/**
|
||||
* Virtual method called to filter a keypress.
|
||||
*/
|
||||
gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event);
|
||||
};
|
||||
|
||||
/**
|
||||
* fl_text_input_plugin_new:
|
||||
* @messenger: an #FlBinaryMessenger.
|
||||
|
||||
@ -158,10 +158,11 @@ 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, self);
|
||||
self->key_event_plugin =
|
||||
fl_key_event_plugin_new(messenger, self->text_input_plugin);
|
||||
self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self);
|
||||
self->platform_plugin = fl_platform_plugin_new(messenger);
|
||||
self->text_input_plugin = fl_text_input_plugin_new(messenger, self);
|
||||
}
|
||||
|
||||
static void fl_view_set_property(GObject* object,
|
||||
@ -348,10 +349,7 @@ 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);
|
||||
|
||||
fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
|
||||
fl_text_input_plugin_filter_keypress(self->text_input_plugin, event);
|
||||
|
||||
return TRUE;
|
||||
return fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
|
||||
}
|
||||
|
||||
// Implements GtkWidget::key_release_event.
|
||||
@ -359,10 +357,7 @@ static gboolean fl_view_key_release_event(GtkWidget* widget,
|
||||
GdkEventKey* event) {
|
||||
FlView* self = FL_VIEW(widget);
|
||||
|
||||
fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
|
||||
fl_text_input_plugin_filter_keypress(self->text_input_plugin, event);
|
||||
|
||||
return TRUE;
|
||||
return fl_key_event_plugin_send_key_event(self->key_event_plugin, event);
|
||||
}
|
||||
|
||||
static void fl_view_class_init(FlViewClass* klass) {
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
#include "flutter/shell/platform/embedder/embedder.h"
|
||||
#include "flutter/shell/platform/linux/fl_method_codec_private.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_json_message_codec.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_method_response.h"
|
||||
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_method_codec.h"
|
||||
#include "gtest/gtest.h"
|
||||
@ -310,6 +311,18 @@ FlutterEngineResult FlutterEngineSendPlatformMessage(
|
||||
} else if (strcmp(message->channel, "test/failure") == 0) {
|
||||
// Generates an internal error.
|
||||
return kInternalInconsistency;
|
||||
} else if (strcmp(message->channel, "test/key-event-handled") == 0 ||
|
||||
strcmp(message->channel, "test/key-event-not-handled") == 0) {
|
||||
bool value = strcmp(message->channel, "test/key-event-handled") == 0;
|
||||
g_autoptr(FlJsonMessageCodec) codec = fl_json_message_codec_new();
|
||||
g_autoptr(FlValue) handledValue = fl_value_new_map();
|
||||
fl_value_set_string_take(handledValue, "handled", fl_value_new_bool(value));
|
||||
g_autoptr(GBytes) response = fl_message_codec_encode_message(
|
||||
FL_MESSAGE_CODEC(codec), handledValue, nullptr);
|
||||
send_response(
|
||||
engine, message->channel, message->response_handle,
|
||||
static_cast<const uint8_t*>(g_bytes_get_data(response, nullptr)),
|
||||
g_bytes_get_size(response));
|
||||
}
|
||||
|
||||
return kSuccess;
|
||||
|
||||
41
shell/platform/linux/testing/mock_text_input_plugin.cc
Normal file
41
shell/platform/linux/testing/mock_text_input_plugin.cc
Normal file
@ -0,0 +1,41 @@
|
||||
// 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_text_input_plugin.h"
|
||||
|
||||
struct _FlMockTextInputPlugin {
|
||||
FlTextInputPlugin parent_instance;
|
||||
|
||||
gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event);
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FlMockTextInputPlugin,
|
||||
fl_mock_text_input_plugin,
|
||||
fl_text_input_plugin_get_type())
|
||||
|
||||
static gboolean mock_text_input_plugin_filter_keypress(FlTextInputPlugin* self,
|
||||
GdkEventKey* event) {
|
||||
FlMockTextInputPlugin* mock_self = FL_MOCK_TEXT_INPUT_PLUGIN(self);
|
||||
if (mock_self->filter_keypress) {
|
||||
return mock_self->filter_keypress(self, event);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void fl_mock_text_input_plugin_class_init(
|
||||
FlMockTextInputPluginClass* klass) {
|
||||
FL_TEXT_INPUT_PLUGIN_CLASS(klass)->filter_keypress =
|
||||
mock_text_input_plugin_filter_keypress;
|
||||
}
|
||||
|
||||
static void fl_mock_text_input_plugin_init(FlMockTextInputPlugin* self) {}
|
||||
|
||||
// Creates a mock text_input_plugin
|
||||
FlMockTextInputPlugin* fl_mock_text_input_plugin_new(
|
||||
gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event)) {
|
||||
FlMockTextInputPlugin* self = FL_MOCK_TEXT_INPUT_PLUGIN(
|
||||
g_object_new(fl_mock_text_input_plugin_get_type(), nullptr));
|
||||
self->filter_keypress = filter_keypress;
|
||||
return self;
|
||||
}
|
||||
18
shell/platform/linux/testing/mock_text_input_plugin.h
Normal file
18
shell/platform/linux/testing/mock_text_input_plugin.h
Normal file
@ -0,0 +1,18 @@
|
||||
// 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"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
G_DECLARE_FINAL_TYPE(FlMockTextInputPlugin,
|
||||
fl_mock_text_input_plugin,
|
||||
FL,
|
||||
MOCK_TEXT_INPUT_PLUGIN,
|
||||
FlTextInputPlugin)
|
||||
|
||||
FlMockTextInputPlugin* fl_mock_text_input_plugin_new(
|
||||
gboolean (*filter_keypress)(FlTextInputPlugin* self, GdkEventKey* event));
|
||||
|
||||
G_END_DECLS
|
||||
Loading…
x
Reference in New Issue
Block a user