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:
Greg Spencer 2020-12-03 15:00:37 -08:00 committed by GitHub
parent 66f44c6c39
commit bdadaad20d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 734 additions and 179 deletions

View File

@ -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" ]

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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) {

View File

@ -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;

View 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;
}

View 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