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

Commit c4e72bd

Browse files
committed
Add Linux multi-step input method support for Linux
1 parent df5568d commit c4e72bd

File tree

3 files changed

+180
-7
lines changed

3 files changed

+180
-7
lines changed

shell/platform/linux/fl_text_input_plugin.cc

Lines changed: 176 additions & 5 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)
@@ -99,13 +121,22 @@ static void update_editing_state(FlTextInputPlugin* self) {
99121
fl_value_set_string_take(value, kSelectionExtentKey,
100122
fl_value_new_int(selection.extent()));
101123

124+
int composing_base = self->text_model->composing()
125+
? self->text_model->composing_range().base()
126+
: -1;
127+
int composing_extent = self->text_model->composing()
128+
? self->text_model->composing_range().extent()
129+
: -1;
130+
fl_value_set_string_take(value, kComposingBaseKey,
131+
fl_value_new_int(composing_base));
132+
fl_value_set_string_take(value, kComposingExtentKey,
133+
fl_value_new_int(composing_extent));
134+
102135
// The following keys are not implemented and set to default values.
103136
fl_value_set_string_take(value, kSelectionAffinityKey,
104137
fl_value_new_string(kTextAffinityDownstream));
105138
fl_value_set_string_take(value, kSelectionIsDirectionalKey,
106139
fl_value_new_bool(FALSE));
107-
fl_value_set_string_take(value, kComposingBaseKey, fl_value_new_int(-1));
108-
fl_value_set_string_take(value, kComposingExtentKey, fl_value_new_int(-1));
109140

110141
fl_value_append(args, value);
111142

@@ -138,9 +169,50 @@ static void perform_action(FlTextInputPlugin* self) {
138169
nullptr, perform_action_response_cb, self);
139170
}
140171

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

@@ -208,6 +280,8 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
208280
FlValue* args) {
209281
const gchar* text =
210282
fl_value_get_string(fl_value_lookup_string(args, kTextKey));
283+
self->text_model->SetText(text);
284+
211285
int64_t selection_base =
212286
fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey));
213287
int64_t selection_extent =
@@ -220,6 +294,19 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
220294
self->text_model->SetText(text);
221295
self->text_model->SetSelection(TextRange(selection_base, selection_extent));
222296

297+
int64_t composing_base =
298+
fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey));
299+
int64_t composing_extent =
300+
fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey));
301+
if (composing_base == -1 && composing_extent == -1) {
302+
self->text_model->EndComposing();
303+
} else {
304+
size_t composing_start = std::min(composing_base, composing_extent);
305+
size_t cursor_offset = selection_base - composing_start;
306+
self->text_model->SetComposingRange(
307+
TextRange(composing_base, composing_extent), cursor_offset);
308+
}
309+
223310
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
224311
}
225312

@@ -237,6 +324,70 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) {
237324
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
238325
}
239326

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

425+
static void view_weak_notify_cb(gpointer user_data, GObject* object) {
426+
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
427+
self->view = nullptr;
428+
}
429+
270430
static void fl_text_input_plugin_dispose(GObject* object) {
271431
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
272432

@@ -289,6 +449,15 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
289449
self->client_id = kClientIdUnset;
290450
self->im_context = gtk_im_multicontext_new();
291451
self->input_multiline = FALSE;
452+
g_signal_connect_object(self->im_context, "preedit-start",
453+
G_CALLBACK(im_preedit_start_cb), self,
454+
G_CONNECT_SWAPPED);
455+
g_signal_connect_object(self->im_context, "preedit-end",
456+
G_CALLBACK(im_preedit_end_cb), self,
457+
G_CONNECT_SWAPPED);
458+
g_signal_connect_object(self->im_context, "preedit-changed",
459+
G_CALLBACK(im_preedit_changed_cb), self,
460+
G_CONNECT_SWAPPED);
292461
g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb),
293462
self, G_CONNECT_SWAPPED);
294463
g_signal_connect_object(self->im_context, "retrieve-surrounding",
@@ -300,7 +469,8 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
300469
self->text_model = new flutter::TextInputModel();
301470
}
302471

303-
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
472+
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
473+
FlView* view) {
304474
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
305475

306476
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(
@@ -311,7 +481,8 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
311481
fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
312482
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
313483
nullptr);
314-
484+
self->view = view;
485+
g_object_weak_ref(G_OBJECT(view), view_weak_notify_cb, self);
315486
return self;
316487
}
317488

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)