Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 77200b5

Browse files
committed
Add Linux multi-step input method support for Linux
1 parent 66d3430 commit 77200b5

File tree

3 files changed

+178
-9
lines changed

3 files changed

+178
-9
lines changed

shell/platform/linux/fl_text_input_plugin.cc

Lines changed: 174 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ static constexpr char kHideMethod[] = "TextInput.hide";
2222
static constexpr char kUpdateEditingStateMethod[] =
2323
"TextInputClient.updateEditingState";
2424
static constexpr char kPerformActionMethod[] = "TextInputClient.performAction";
25+
static constexpr char kSetEditableSizeAndTransform[] =
26+
"TextInput.setEditableSizeAndTransform";
27+
static constexpr char kSetMarkedTextRect[] = "TextInput.setMarkedTextRect";
2528

2629
static constexpr char kInputActionKey[] = "inputAction";
2730
static constexpr char kTextInputTypeKey[] = "inputType";
@@ -34,6 +37,8 @@ static constexpr char kSelectionIsDirectionalKey[] = "selectionIsDirectional";
3437
static constexpr char kComposingBaseKey[] = "composingBase";
3538
static constexpr char kComposingExtentKey[] = "composingExtent";
3639

40+
static constexpr char kTransform[] = "transform";
41+
3742
static constexpr char kTextAffinityDownstream[] = "TextAffinity.downstream";
3843
static constexpr char kMultilineInputType[] = "TextInputType.multiline";
3944

@@ -57,6 +62,23 @@ struct _FlTextInputPlugin {
5762
GtkIMContext* im_context;
5863

5964
flutter::TextInputModel* text_model;
65+
66+
// The owning Flutter view.
67+
FlView* view;
68+
69+
// A 4x4 matrix that maps from `EditableText` local coordinates to the
70+
// coordinate system of `PipelineOwner.rootNode`.
71+
double editabletext_transform[4][4];
72+
73+
// The smallest rect, in local coordinates, of the text in the composing
74+
// range, or of the caret in the case where there is no current composing
75+
// range. This value is updated via `TextInput.setMarkedTextRect` messages
76+
// over the text input channel.
77+
GdkRectangle composing_rect;
78+
79+
// A cached copy of the composing (pre-edit in Gtk terminology) rect in
80+
// window coordinates.
81+
GdkRectangle preedit_rect;
6082
};
6183

6284
G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT)
@@ -100,13 +122,20 @@ static void update_editing_state(FlTextInputPlugin* self) {
100122
value, kSelectionExtentKey,
101123
fl_value_new_int(self->text_model->selection_extent()));
102124

125+
int composing_base =
126+
self->text_model->composing() ? self->text_model->composing_base() : -1;
127+
int composing_extent =
128+
self->text_model->composing() ? self->text_model->composing_extent() : -1;
129+
fl_value_set_string_take(value, kComposingBaseKey,
130+
fl_value_new_int(composing_base));
131+
fl_value_set_string_take(value, kComposingExtentKey,
132+
fl_value_new_int(composing_extent));
133+
103134
// The following keys are not implemented and set to default values.
104135
fl_value_set_string_take(value, kSelectionAffinityKey,
105136
fl_value_new_string(kTextAffinityDownstream));
106137
fl_value_set_string_take(value, kSelectionIsDirectionalKey,
107138
fl_value_new_bool(FALSE));
108-
fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(-1));
109-
fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(-1));
110139

111140
fl_value_append(args, value);
112141

@@ -139,9 +168,50 @@ static void perform_action(FlTextInputPlugin* self) {
139168
nullptr, perform_action_response_cb, self);
140169
}
141170

171+
// Signal handler for GtkIMContext::preedit-start
172+
static void im_preedit_start_cb(FlTextInputPlugin* self) {
173+
self->text_model->BeginComposing();
174+
175+
// Set the top-level window used for system input method windows.
176+
GdkWindow* window =
177+
gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
178+
gtk_im_context_set_client_window(self->im_context, window);
179+
180+
// Set the cursor location in window co-ordinates so that Gtk can position
181+
// any system input method windows.
182+
gtk_im_context_set_cursor_location(self->im_context, &self->preedit_rect);
183+
}
184+
185+
// Signal handler for GtkIMContext::preedit-changed
186+
static void im_preedit_changed_cb(FlTextInputPlugin* self) {
187+
// Set the cursor location in window co-ordinates so that Gtk can position
188+
// any system input method windows.
189+
gtk_im_context_set_cursor_location(self->im_context, &self->preedit_rect);
190+
191+
gchar* buf = nullptr;
192+
gint cursor_offset = 0;
193+
gtk_im_context_get_preedit_string(self->im_context, &buf, NULL,
194+
&cursor_offset);
195+
cursor_offset += self->text_model->composing_base();
196+
self->text_model->UpdateComposingText(buf);
197+
self->text_model->SetSelection(cursor_offset, cursor_offset);
198+
199+
update_editing_state(self);
200+
}
201+
142202
// Signal handler for GtkIMContext::commit
143203
static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) {
144-
self->text_model->AddText(text);
204+
if (self->text_model->composing()) {
205+
self->text_model->CommitComposing();
206+
} else {
207+
self->text_model->AddText(text);
208+
}
209+
update_editing_state(self);
210+
}
211+
212+
// Signal handler for GtkIMContext::preedit-end
213+
static void im_preedit_end_cb(FlTextInputPlugin* self) {
214+
self->text_model->EndComposing();
145215
update_editing_state(self);
146216
}
147217

@@ -209,6 +279,8 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
209279
FlValue* args) {
210280
const gchar* text =
211281
fl_value_get_string(fl_value_lookup_string(args, kTextKey));
282+
self->text_model->SetText(text);
283+
212284
int64_t selection_base =
213285
fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey));
214286
int64_t selection_extent =
@@ -217,10 +289,21 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
217289
if (selection_base == -1 && selection_extent == -1) {
218290
selection_base = selection_extent = 0;
219291
}
220-
221-
self->text_model->SetText(text);
222292
self->text_model->SetSelection(selection_base, selection_extent);
223293

294+
int64_t composing_base =
295+
fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey));
296+
int64_t composing_extent =
297+
fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey));
298+
if (composing_base == -1 && composing_extent == -1) {
299+
self->text_model->EndComposing();
300+
} else {
301+
size_t composing_start = std::min(composing_base, composing_extent);
302+
size_t cursor_offset = selection_base - composing_start;
303+
self->text_model->SetComposingRange(composing_base, composing_extent,
304+
cursor_offset);
305+
}
306+
224307
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
225308
}
226309

@@ -238,6 +321,70 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) {
238321
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
239322
}
240323

324+
// Update the local copy of the preedit (composing) rect.
325+
//
326+
// As text is input by the user, the framework sends two streams of updates
327+
// over the text input channel: updates to the composing rect (cursor rect when
328+
// not in IME composing mode) and updates to the matrix transform from local
329+
// coordinates to Flutter root coordinates. This function is called after each
330+
// of these updates.
331+
static void update_preedit_rect(FlTextInputPlugin* self) {
332+
// Skip update if not composing to avoid setting to position 0.
333+
if (!self->text_model->composing()) {
334+
return;
335+
}
336+
337+
// Transform the x, y positions of the cursor from local coordinates to
338+
// Flutter view coordinates.
339+
gint x = self->composing_rect.x * self->editabletext_transform[0][0] +
340+
self->composing_rect.y * self->editabletext_transform[1][0] +
341+
self->editabletext_transform[3][0] + self->composing_rect.width;
342+
gint y = self->composing_rect.x * self->editabletext_transform[0][1] +
343+
self->composing_rect.y * self->editabletext_transform[1][1] +
344+
self->editabletext_transform[3][1] + self->composing_rect.height;
345+
346+
// Transform from Flutter view coordinates to Gtk window coordinates and
347+
// write them to preedit_rect.
348+
gtk_widget_translate_coordinates(
349+
GTK_WIDGET(self->view), gtk_widget_get_toplevel(GTK_WIDGET(self->view)),
350+
x, y, &self->preedit_rect.x, &self->preedit_rect.y);
351+
}
352+
353+
// Handles updates from the framework to the cached coordinate transform of the
354+
// `EditableText` relative to the root coordinate system.
355+
static FlMethodResponse* set_editable_size_and_transform(
356+
FlTextInputPlugin* self,
357+
FlValue* args) {
358+
FlValue* transform = fl_value_lookup_string(args, kTransform);
359+
size_t transform_len = fl_value_get_length(transform);
360+
g_warn_if_fail(transform_len == 16);
361+
362+
for (size_t i = 0; i < transform_len; ++i) {
363+
double val = fl_value_get_float(fl_value_get_list_value(transform, i));
364+
self->editabletext_transform[i / 4][i % 4] = val;
365+
}
366+
update_preedit_rect(self);
367+
368+
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
369+
}
370+
371+
// Handles updates from the framework to the cached composing rect in local
372+
// coordinates.
373+
static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self,
374+
FlValue* args) {
375+
self->composing_rect.x =
376+
fl_value_get_float(fl_value_lookup_string(args, "x"));
377+
self->composing_rect.y =
378+
fl_value_get_float(fl_value_lookup_string(args, "y"));
379+
self->composing_rect.width =
380+
fl_value_get_float(fl_value_lookup_string(args, "width"));
381+
self->composing_rect.height =
382+
fl_value_get_float(fl_value_lookup_string(args, "height"));
383+
update_preedit_rect(self);
384+
385+
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
386+
}
387+
241388
// Called when a method call is received from Flutter.
242389
static void method_call_cb(FlMethodChannel* channel,
243390
FlMethodCall* method_call,
@@ -258,6 +405,10 @@ static void method_call_cb(FlMethodChannel* channel,
258405
response = clear_client(self);
259406
} else if (strcmp(method, kHideMethod) == 0) {
260407
response = hide(self);
408+
} else if (strcmp(method, kSetEditableSizeAndTransform) == 0) {
409+
response = set_editable_size_and_transform(self, args);
410+
} else if (strcmp(method, kSetMarkedTextRect) == 0) {
411+
response = set_marked_text_rect(self, args);
261412
} else {
262413
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
263414
}
@@ -268,6 +419,11 @@ static void method_call_cb(FlMethodChannel* channel,
268419
}
269420
}
270421

422+
static void view_weak_notify_cb(gpointer user_data, GObject* object) {
423+
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
424+
self->view = nullptr;
425+
}
426+
271427
static void fl_text_input_plugin_dispose(GObject* object) {
272428
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
273429

@@ -290,6 +446,15 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
290446
self->client_id = kClientIdUnset;
291447
self->im_context = gtk_im_multicontext_new();
292448
self->input_multiline = FALSE;
449+
g_signal_connect_object(self->im_context, "preedit-start",
450+
G_CALLBACK(im_preedit_start_cb), self,
451+
G_CONNECT_SWAPPED);
452+
g_signal_connect_object(self->im_context, "preedit-end",
453+
G_CALLBACK(im_preedit_end_cb), self,
454+
G_CONNECT_SWAPPED);
455+
g_signal_connect_object(self->im_context, "preedit-changed",
456+
G_CALLBACK(im_preedit_changed_cb), self,
457+
G_CONNECT_SWAPPED);
293458
g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb),
294459
self, G_CONNECT_SWAPPED);
295460
g_signal_connect_object(self->im_context, "retrieve-surrounding",
@@ -301,7 +466,8 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
301466
self->text_model = new flutter::TextInputModel();
302467
}
303468

304-
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
469+
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
470+
FlView* view) {
305471
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
306472

307473
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(
@@ -312,7 +478,8 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
312478
fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
313479
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
314480
nullptr);
315-
481+
self->view = view;
482+
g_object_weak_ref(G_OBJECT(view), view_weak_notify_cb, self);
316483
return self;
317484
}
318485

shell/platform/linux/fl_text_input_plugin.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <gdk/gdk.h>
99

1010
#include "flutter/shell/platform/linux/public/flutter_linux/fl_binary_messenger.h"
11+
#include "flutter/shell/platform/linux/public/flutter_linux/fl_view.h"
1112

1213
G_BEGIN_DECLS
1314

@@ -33,7 +34,8 @@ G_DECLARE_FINAL_TYPE(FlTextInputPlugin,
3334
*
3435
* Returns: a new #FlTextInputPlugin.
3536
*/
36-
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger);
37+
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
38+
FlView* view);
3739

3840
/**
3941
* fl_text_input_plugin_filter_keypress

shell/platform/linux/fl_view.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ static void fl_view_constructed(GObject* object) {
161161
self->key_event_plugin = fl_key_event_plugin_new(messenger);
162162
self->mouse_cursor_plugin = fl_mouse_cursor_plugin_new(messenger, self);
163163
self->platform_plugin = fl_platform_plugin_new(messenger);
164-
self->text_input_plugin = fl_text_input_plugin_new(messenger);
164+
self->text_input_plugin = fl_text_input_plugin_new(messenger, self);
165165
}
166166

167167
static void fl_view_set_property(GObject* object,

0 commit comments

Comments
 (0)