mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This implements the Gtk hooks required to support multi-step input methods on Linux. This builds on the support for composing regions (preedit region in Gtk terminology) added to TextInputModel in https://github.com/flutter/engine/pull/21682. Specifically, the following changes are included: 1. Add handler for TextInput.setMarkedTextRegion framework messages: On any change to the EditableText in the framework, this message is sent which provides an updated rect (in the local co-ordinates of the EditableText) for the composing region. If not in composing mode, the cursor rect is sent. 2. Add handler for TextInput.setEditableSizeAndTransform framework messages: On any change to the RenderObject underlying the EditableText, an updated size for the full EditableText widget, as well as an affine transform matrix from local co-ordinates to Flutter root co-ordinates is sent. 3. On either of the above messages, we use the transformed composing rect to compute the cursor position in Gtk window co-ordinates and inform Gtk, so that it can position any system IM composing window correctly for on-the-spot composing, such as is used when inputting Japanese text. 4. Adds handlers for preedit-start, preedit-changed, and preedit-end signals from Gtk. These are passed on to the TextInputModel. 5. Updates the preedit-commit handler to commit the composing region to the text or, if not composing, insert new text at the cursor. 6. Updates the handler for TextInput.setEditingState framework messages to extract the composing range base and extent and pass these on to TextInputModel. 7. Updates update_editing_state function to set composing base and extent on text input state updates sent to the framework.
395 lines
13 KiB
C++
395 lines
13 KiB
C++
// 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/public/flutter_linux/fl_view.h"
|
|
|
|
#include <gdk/gdkwayland.h>
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <gdk/gdkx.h>
|
|
#endif
|
|
#include <cstring>
|
|
|
|
#include "flutter/shell/platform/linux/fl_engine_private.h"
|
|
#include "flutter/shell/platform/linux/fl_key_event_plugin.h"
|
|
#include "flutter/shell/platform/linux/fl_mouse_cursor_plugin.h"
|
|
#include "flutter/shell/platform/linux/fl_platform_plugin.h"
|
|
#include "flutter/shell/platform/linux/fl_plugin_registrar_private.h"
|
|
#include "flutter/shell/platform/linux/fl_renderer_wayland.h"
|
|
#include "flutter/shell/platform/linux/fl_renderer_x11.h"
|
|
#include "flutter/shell/platform/linux/fl_text_input_plugin.h"
|
|
#include "flutter/shell/platform/linux/public/flutter_linux/fl_engine.h"
|
|
#include "flutter/shell/platform/linux/public/flutter_linux/fl_plugin_registry.h"
|
|
|
|
static constexpr int kMicrosecondsPerMillisecond = 1000;
|
|
|
|
struct _FlView {
|
|
GtkWidget parent_instance;
|
|
|
|
// Project being run.
|
|
FlDartProject* project;
|
|
|
|
// Rendering output.
|
|
FlRenderer* renderer;
|
|
|
|
// Engine running @project.
|
|
FlEngine* engine;
|
|
|
|
// Pointer button state recorded for sending status updates.
|
|
int64_t button_state;
|
|
|
|
// Flutter system channel handlers.
|
|
FlKeyEventPlugin* key_event_plugin;
|
|
FlMouseCursorPlugin* mouse_cursor_plugin;
|
|
FlPlatformPlugin* platform_plugin;
|
|
FlTextInputPlugin* text_input_plugin;
|
|
};
|
|
|
|
enum { PROP_FLUTTER_PROJECT = 1, PROP_LAST };
|
|
|
|
static void fl_view_plugin_registry_iface_init(
|
|
FlPluginRegistryInterface* iface);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE(
|
|
FlView,
|
|
fl_view,
|
|
GTK_TYPE_WIDGET,
|
|
G_IMPLEMENT_INTERFACE(fl_plugin_registry_get_type(),
|
|
fl_view_plugin_registry_iface_init))
|
|
|
|
// Converts a GDK button event into a Flutter event and sends it to the engine.
|
|
static gboolean fl_view_send_pointer_button_event(FlView* self,
|
|
GdkEventButton* event) {
|
|
int64_t button;
|
|
switch (event->button) {
|
|
case 1:
|
|
button = kFlutterPointerButtonMousePrimary;
|
|
break;
|
|
case 2:
|
|
button = kFlutterPointerButtonMouseMiddle;
|
|
break;
|
|
case 3:
|
|
button = kFlutterPointerButtonMouseSecondary;
|
|
break;
|
|
default:
|
|
return FALSE;
|
|
}
|
|
int old_button_state = self->button_state;
|
|
FlutterPointerPhase phase = kMove;
|
|
if (event->type == GDK_BUTTON_PRESS) {
|
|
// Drop the event if Flutter already thinks the button is down.
|
|
if ((self->button_state & button) != 0) {
|
|
return FALSE;
|
|
}
|
|
self->button_state ^= button;
|
|
|
|
phase = old_button_state == 0 ? kDown : kMove;
|
|
} else if (event->type == GDK_BUTTON_RELEASE) {
|
|
// Drop the event if Flutter already thinks the button is up.
|
|
if ((self->button_state & button) == 0) {
|
|
return FALSE;
|
|
}
|
|
self->button_state ^= button;
|
|
|
|
phase = self->button_state == 0 ? kUp : kMove;
|
|
}
|
|
|
|
if (self->engine == nullptr) {
|
|
return FALSE;
|
|
}
|
|
|
|
gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
|
|
fl_engine_send_mouse_pointer_event(
|
|
self->engine, phase, event->time * kMicrosecondsPerMillisecond,
|
|
event->x * scale_factor, event->y * scale_factor, 0, 0,
|
|
self->button_state);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Updates the engine with the current window metrics.
|
|
static void fl_view_geometry_changed(FlView* self) {
|
|
GtkAllocation allocation;
|
|
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
|
|
gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
|
|
fl_engine_send_window_metrics_event(
|
|
self->engine, allocation.width * scale_factor,
|
|
allocation.height * scale_factor, scale_factor);
|
|
fl_renderer_set_geometry(self->renderer, &allocation, scale_factor);
|
|
}
|
|
|
|
// Implements FlPluginRegistry::get_registrar_for_plugin.
|
|
static FlPluginRegistrar* fl_view_get_registrar_for_plugin(
|
|
FlPluginRegistry* registry,
|
|
const gchar* name) {
|
|
FlView* self = FL_VIEW(registry);
|
|
|
|
return fl_plugin_registrar_new(self,
|
|
fl_engine_get_binary_messenger(self->engine));
|
|
}
|
|
|
|
static void fl_view_plugin_registry_iface_init(
|
|
FlPluginRegistryInterface* iface) {
|
|
iface->get_registrar_for_plugin = fl_view_get_registrar_for_plugin;
|
|
}
|
|
|
|
static FlRenderer* fl_view_get_renderer_for_display(GdkDisplay* display) {
|
|
#ifdef GDK_WINDOWING_X11
|
|
if (GDK_IS_X11_DISPLAY(display)) {
|
|
return FL_RENDERER(fl_renderer_x11_new());
|
|
}
|
|
#endif
|
|
|
|
if (GDK_IS_WAYLAND_DISPLAY(display)) {
|
|
return FL_RENDERER(fl_renderer_wayland_new());
|
|
}
|
|
|
|
g_error("Unsupported GDK backend");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
static void fl_view_constructed(GObject* object) {
|
|
FlView* self = FL_VIEW(object);
|
|
|
|
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(self));
|
|
self->renderer = fl_view_get_renderer_for_display(display);
|
|
self->engine = fl_engine_new(self->project, self->renderer);
|
|
|
|
// Create system channel handlers.
|
|
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(self->engine);
|
|
self->key_event_plugin = fl_key_event_plugin_new(messenger);
|
|
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,
|
|
guint prop_id,
|
|
const GValue* value,
|
|
GParamSpec* pspec) {
|
|
FlView* self = FL_VIEW(object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_FLUTTER_PROJECT:
|
|
g_set_object(&self->project,
|
|
static_cast<FlDartProject*>(g_value_get_object(value)));
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fl_view_get_property(GObject* object,
|
|
guint prop_id,
|
|
GValue* value,
|
|
GParamSpec* pspec) {
|
|
FlView* self = FL_VIEW(object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_FLUTTER_PROJECT:
|
|
g_value_set_object(value, self->project);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fl_view_notify(GObject* object, GParamSpec* pspec) {
|
|
FlView* self = FL_VIEW(object);
|
|
|
|
if (strcmp(pspec->name, "scale-factor") == 0) {
|
|
fl_view_geometry_changed(self);
|
|
}
|
|
|
|
if (G_OBJECT_CLASS(fl_view_parent_class)->notify != nullptr) {
|
|
G_OBJECT_CLASS(fl_view_parent_class)->notify(object, pspec);
|
|
}
|
|
}
|
|
|
|
static void fl_view_dispose(GObject* object) {
|
|
FlView* self = FL_VIEW(object);
|
|
|
|
g_clear_object(&self->project);
|
|
g_clear_object(&self->renderer);
|
|
g_clear_object(&self->engine);
|
|
g_clear_object(&self->key_event_plugin);
|
|
g_clear_object(&self->mouse_cursor_plugin);
|
|
g_clear_object(&self->platform_plugin);
|
|
g_clear_object(&self->text_input_plugin);
|
|
|
|
G_OBJECT_CLASS(fl_view_parent_class)->dispose(object);
|
|
}
|
|
|
|
// Implements GtkWidget::realize.
|
|
static void fl_view_realize(GtkWidget* widget) {
|
|
FlView* self = FL_VIEW(widget);
|
|
g_autoptr(GError) error = nullptr;
|
|
|
|
gtk_widget_set_realized(widget, TRUE);
|
|
|
|
if (!fl_renderer_start(self->renderer, widget, &error)) {
|
|
g_warning("Failed to start Flutter renderer: %s", error->message);
|
|
return;
|
|
}
|
|
|
|
if (!fl_engine_start(self->engine, &error)) {
|
|
g_warning("Failed to start Flutter engine: %s", error->message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Implements GtkWidget::size-allocate.
|
|
static void fl_view_size_allocate(GtkWidget* widget,
|
|
GtkAllocation* allocation) {
|
|
FlView* self = FL_VIEW(widget);
|
|
|
|
gtk_widget_set_allocation(widget, allocation);
|
|
|
|
if (gtk_widget_get_realized(widget) && gtk_widget_get_has_window(widget)) {
|
|
gdk_window_move_resize(gtk_widget_get_window(widget), allocation->x,
|
|
allocation->y, allocation->width,
|
|
allocation->height);
|
|
}
|
|
|
|
fl_view_geometry_changed(self);
|
|
}
|
|
|
|
// Implements GtkWidget::button_press_event.
|
|
static gboolean fl_view_button_press_event(GtkWidget* widget,
|
|
GdkEventButton* event) {
|
|
FlView* self = FL_VIEW(widget);
|
|
|
|
// Flutter doesn't handle double and triple click events.
|
|
if (event->type == GDK_DOUBLE_BUTTON_PRESS ||
|
|
event->type == GDK_TRIPLE_BUTTON_PRESS) {
|
|
return FALSE;
|
|
}
|
|
|
|
return fl_view_send_pointer_button_event(self, event);
|
|
}
|
|
|
|
// Implements GtkWidget::button_release_event.
|
|
static gboolean fl_view_button_release_event(GtkWidget* widget,
|
|
GdkEventButton* event) {
|
|
FlView* self = FL_VIEW(widget);
|
|
|
|
return fl_view_send_pointer_button_event(self, event);
|
|
}
|
|
|
|
// Implements GtkWidget::scroll_event.
|
|
static gboolean fl_view_scroll_event(GtkWidget* widget, GdkEventScroll* event) {
|
|
FlView* self = FL_VIEW(widget);
|
|
|
|
// TODO(robert-ancell): Update to use GtkEventControllerScroll when we can
|
|
// depend on GTK 3.24.
|
|
|
|
gdouble scroll_delta_x = 0.0, scroll_delta_y = 0.0;
|
|
if (event->direction == GDK_SCROLL_SMOOTH) {
|
|
scroll_delta_x = event->delta_x;
|
|
scroll_delta_y = event->delta_y;
|
|
} else if (event->direction == GDK_SCROLL_UP) {
|
|
scroll_delta_y = -1;
|
|
} else if (event->direction == GDK_SCROLL_DOWN) {
|
|
scroll_delta_y = 1;
|
|
} else if (event->direction == GDK_SCROLL_LEFT) {
|
|
scroll_delta_x = -1;
|
|
} else if (event->direction == GDK_SCROLL_RIGHT) {
|
|
scroll_delta_x = 1;
|
|
}
|
|
|
|
// TODO(robert-ancell): See if this can be queried from the OS; this value is
|
|
// chosen arbitrarily to get something that feels reasonable.
|
|
const int kScrollOffsetMultiplier = 20;
|
|
scroll_delta_x *= kScrollOffsetMultiplier;
|
|
scroll_delta_y *= kScrollOffsetMultiplier;
|
|
|
|
gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
|
|
fl_engine_send_mouse_pointer_event(
|
|
self->engine, self->button_state != 0 ? kMove : kHover,
|
|
event->time * kMicrosecondsPerMillisecond, event->x * scale_factor,
|
|
event->y * scale_factor, scroll_delta_x, scroll_delta_y,
|
|
self->button_state);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Implements GtkWidget::motion_notify_event.
|
|
static gboolean fl_view_motion_notify_event(GtkWidget* widget,
|
|
GdkEventMotion* event) {
|
|
FlView* self = FL_VIEW(widget);
|
|
|
|
if (self->engine == nullptr) {
|
|
return FALSE;
|
|
}
|
|
|
|
gint scale_factor = gtk_widget_get_scale_factor(GTK_WIDGET(self));
|
|
fl_engine_send_mouse_pointer_event(
|
|
self->engine, self->button_state != 0 ? kMove : kHover,
|
|
event->time * kMicrosecondsPerMillisecond, event->x * scale_factor,
|
|
event->y * scale_factor, 0, 0, self->button_state);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
// Implements GtkWidget::key_press_event.
|
|
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;
|
|
}
|
|
|
|
// Implements GtkWidget::key_release_event.
|
|
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;
|
|
}
|
|
|
|
static void fl_view_class_init(FlViewClass* klass) {
|
|
G_OBJECT_CLASS(klass)->constructed = fl_view_constructed;
|
|
G_OBJECT_CLASS(klass)->set_property = fl_view_set_property;
|
|
G_OBJECT_CLASS(klass)->get_property = fl_view_get_property;
|
|
G_OBJECT_CLASS(klass)->notify = fl_view_notify;
|
|
G_OBJECT_CLASS(klass)->dispose = fl_view_dispose;
|
|
GTK_WIDGET_CLASS(klass)->realize = fl_view_realize;
|
|
GTK_WIDGET_CLASS(klass)->size_allocate = fl_view_size_allocate;
|
|
GTK_WIDGET_CLASS(klass)->button_press_event = fl_view_button_press_event;
|
|
GTK_WIDGET_CLASS(klass)->button_release_event = fl_view_button_release_event;
|
|
GTK_WIDGET_CLASS(klass)->scroll_event = fl_view_scroll_event;
|
|
GTK_WIDGET_CLASS(klass)->motion_notify_event = fl_view_motion_notify_event;
|
|
GTK_WIDGET_CLASS(klass)->key_press_event = fl_view_key_press_event;
|
|
GTK_WIDGET_CLASS(klass)->key_release_event = fl_view_key_release_event;
|
|
|
|
g_object_class_install_property(
|
|
G_OBJECT_CLASS(klass), PROP_FLUTTER_PROJECT,
|
|
g_param_spec_object(
|
|
"flutter-project", "flutter-project", "Flutter project in use",
|
|
fl_dart_project_get_type(),
|
|
static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
|
|
G_PARAM_STATIC_STRINGS)));
|
|
}
|
|
|
|
static void fl_view_init(FlView* self) {
|
|
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
|
|
}
|
|
|
|
G_MODULE_EXPORT FlView* fl_view_new(FlDartProject* project) {
|
|
return static_cast<FlView*>(
|
|
g_object_new(fl_view_get_type(), "flutter-project", project, nullptr));
|
|
}
|
|
|
|
G_MODULE_EXPORT FlEngine* fl_view_get_engine(FlView* view) {
|
|
g_return_val_if_fail(FL_IS_VIEW(view), nullptr);
|
|
return view->engine;
|
|
}
|