Skip to content

Commit b63f7dd

Browse files
committed
Add multi-step input method support for Linux
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 flutter#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.
1 parent 44ea967 commit b63f7dd

File tree

3 files changed

+182
-7
lines changed

3 files changed

+182
-7
lines changed

shell/platform/linux/fl_text_input_plugin.cc

Lines changed: 178 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,19 @@ 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;
6078
};
6179

6280
G_DEFINE_TYPE(FlTextInputPlugin, fl_text_input_plugin, G_TYPE_OBJECT)
@@ -99,13 +117,22 @@ static void update_editing_state(FlTextInputPlugin* self) {
99117
fl_value_set_string_take(value, kSelectionExtentKey,
100118
fl_value_new_int(selection.extent()));
101119

120+
int composing_base = self->text_model->composing()
121+
? self->text_model->composing_range().base()
122+
: -1;
123+
int composing_extent = self->text_model->composing()
124+
? self->text_model->composing_range().extent()
125+
: -1;
126+
fl_value_set_string_take(value, kComposingBaseKey,
127+
fl_value_new_int(composing_base));
128+
fl_value_set_string_take(value, kComposingExtentKey,
129+
fl_value_new_int(composing_extent));
130+
102131
// The following keys are not implemented and set to default values.
103132
fl_value_set_string_take(value, kSelectionAffinityKey,
104133
fl_value_new_string(kTextAffinityDownstream));
105134
fl_value_set_string_take(value, kSelectionIsDirectionalKey,
106135
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));
109136

110137
fl_value_append(args, value);
111138

@@ -138,9 +165,42 @@ static void perform_action(FlTextInputPlugin* self) {
138165
nullptr, perform_action_response_cb, self);
139166
}
140167

168+
// Signal handler for GtkIMContext::preedit-start
169+
static void im_preedit_start_cb(FlTextInputPlugin* self) {
170+
self->text_model->BeginComposing();
171+
172+
// Set the top-level window used for system input method windows.
173+
GdkWindow* window =
174+
gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
175+
gtk_im_context_set_client_window(self->im_context, window);
176+
}
177+
178+
// Signal handler for GtkIMContext::preedit-changed
179+
static void im_preedit_changed_cb(FlTextInputPlugin* self) {
180+
gchar* buf = nullptr;
181+
gint cursor_offset = 0;
182+
gtk_im_context_get_preedit_string(self->im_context, &buf, NULL,
183+
&cursor_offset);
184+
cursor_offset += self->text_model->composing_range().base();
185+
self->text_model->UpdateComposingText(buf);
186+
self->text_model->SetSelection(TextRange(cursor_offset, cursor_offset));
187+
188+
update_editing_state(self);
189+
}
190+
141191
// Signal handler for GtkIMContext::commit
142192
static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) {
143-
self->text_model->AddText(text);
193+
if (self->text_model->composing()) {
194+
self->text_model->CommitComposing();
195+
} else {
196+
self->text_model->AddText(text);
197+
}
198+
update_editing_state(self);
199+
}
200+
201+
// Signal handler for GtkIMContext::preedit-end
202+
static void im_preedit_end_cb(FlTextInputPlugin* self) {
203+
self->text_model->EndComposing();
144204
update_editing_state(self);
145205
}
146206

@@ -208,6 +268,8 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
208268
FlValue* args) {
209269
const gchar* text =
210270
fl_value_get_string(fl_value_lookup_string(args, kTextKey));
271+
self->text_model->SetText(text);
272+
211273
int64_t selection_base =
212274
fl_value_get_int(fl_value_lookup_string(args, kSelectionBaseKey));
213275
int64_t selection_extent =
@@ -220,6 +282,19 @@ static FlMethodResponse* set_editing_state(FlTextInputPlugin* self,
220282
self->text_model->SetText(text);
221283
self->text_model->SetSelection(TextRange(selection_base, selection_extent));
222284

285+
int64_t composing_base =
286+
fl_value_get_int(fl_value_lookup_string(args, kComposingBaseKey));
287+
int64_t composing_extent =
288+
fl_value_get_int(fl_value_lookup_string(args, kComposingExtentKey));
289+
if (composing_base == -1 && composing_extent == -1) {
290+
self->text_model->EndComposing();
291+
} else {
292+
size_t composing_start = std::min(composing_base, composing_extent);
293+
size_t cursor_offset = selection_base - composing_start;
294+
self->text_model->SetComposingRange(
295+
TextRange(composing_base, composing_extent), cursor_offset);
296+
}
297+
223298
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
224299
}
225300

@@ -237,6 +312,84 @@ static FlMethodResponse* hide(FlTextInputPlugin* self) {
237312
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
238313
}
239314

315+
// Update the IM cursor position.
316+
//
317+
// As text is input by the user, the framework sends two streams of updates
318+
// over the text input channel: updates to the composing rect (cursor rect when
319+
// not in IME composing mode) and updates to the matrix transform from local
320+
// coordinates to Flutter root coordinates. This function is called after each
321+
// of these updates. It transforms the composing rect to Gtk window coordinates
322+
// and notifies Gtk of the updated cursor position.
323+
static void update_im_cursor_position(FlTextInputPlugin* self) {
324+
// Skip update if not composing to avoid setting to position 0.
325+
if (!self->text_model->composing()) {
326+
return;
327+
}
328+
329+
// Transform the x, y positions of the cursor from local coordinates to
330+
// Flutter view coordinates.
331+
gint x = self->composing_rect.x * self->editabletext_transform[0][0] +
332+
self->composing_rect.y * self->editabletext_transform[1][0] +
333+
self->editabletext_transform[3][0] + self->composing_rect.width;
334+
gint y = self->composing_rect.x * self->editabletext_transform[0][1] +
335+
self->composing_rect.y * self->editabletext_transform[1][1] +
336+
self->editabletext_transform[3][1] + self->composing_rect.height;
337+
338+
339+
// Transform from Flutter view coordinates to Gtk window coordinates.
340+
GdkRectangle preedit_rect;
341+
gtk_widget_translate_coordinates(
342+
GTK_WIDGET(self->view), gtk_widget_get_toplevel(GTK_WIDGET(self->view)),
343+
x, y, &preedit_rect.x, &preedit_rect.y);
344+
345+
// Set the cursor location in window coordinates so that Gtk can position any
346+
// system input method windows.
347+
gtk_im_context_set_cursor_location(self->im_context, &preedit_rect);
348+
}
349+
350+
// Handles updates to the EditableText size and position from the framework.
351+
//
352+
// On changes to the size or position of the RenderObject underlying the
353+
// EditableText, this update may be triggered. It provides an updated size and
354+
// transform from the local coordinate system of the EditableText to root
355+
// Flutter coordinate system.
356+
static FlMethodResponse* set_editable_size_and_transform(
357+
FlTextInputPlugin* self,
358+
FlValue* args) {
359+
FlValue* transform = fl_value_lookup_string(args, kTransform);
360+
size_t transform_len = fl_value_get_length(transform);
361+
g_warn_if_fail(transform_len == 16);
362+
363+
for (size_t i = 0; i < transform_len; ++i) {
364+
double val = fl_value_get_float(fl_value_get_list_value(transform, i));
365+
self->editabletext_transform[i / 4][i % 4] = val;
366+
}
367+
update_im_cursor_position(self);
368+
369+
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
370+
}
371+
372+
// Handles updates to the composing rect from the framework.
373+
//
374+
// On changes to the state of the EditableText in the framework, this update
375+
// may be triggered. It provides an updated rect for the composing region in
376+
// local coordinates of the EditableText. In the case where there is no
377+
// composing region, the cursor rect is sent.
378+
static FlMethodResponse* set_marked_text_rect(FlTextInputPlugin* self,
379+
FlValue* args) {
380+
self->composing_rect.x =
381+
fl_value_get_float(fl_value_lookup_string(args, "x"));
382+
self->composing_rect.y =
383+
fl_value_get_float(fl_value_lookup_string(args, "y"));
384+
self->composing_rect.width =
385+
fl_value_get_float(fl_value_lookup_string(args, "width"));
386+
self->composing_rect.height =
387+
fl_value_get_float(fl_value_lookup_string(args, "height"));
388+
update_im_cursor_position(self);
389+
390+
return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
391+
}
392+
240393
// Called when a method call is received from Flutter.
241394
static void method_call_cb(FlMethodChannel* channel,
242395
FlMethodCall* method_call,
@@ -257,6 +410,10 @@ static void method_call_cb(FlMethodChannel* channel,
257410
response = clear_client(self);
258411
} else if (strcmp(method, kHideMethod) == 0) {
259412
response = hide(self);
413+
} else if (strcmp(method, kSetEditableSizeAndTransform) == 0) {
414+
response = set_editable_size_and_transform(self, args);
415+
} else if (strcmp(method, kSetMarkedTextRect) == 0) {
416+
response = set_marked_text_rect(self, args);
260417
} else {
261418
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
262419
}
@@ -267,6 +424,11 @@ static void method_call_cb(FlMethodChannel* channel,
267424
}
268425
}
269426

427+
static void view_weak_notify_cb(gpointer user_data, GObject* object) {
428+
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
429+
self->view = nullptr;
430+
}
431+
270432
static void fl_text_input_plugin_dispose(GObject* object) {
271433
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(object);
272434

@@ -289,6 +451,15 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
289451
self->client_id = kClientIdUnset;
290452
self->im_context = gtk_im_multicontext_new();
291453
self->input_multiline = FALSE;
454+
g_signal_connect_object(self->im_context, "preedit-start",
455+
G_CALLBACK(im_preedit_start_cb), self,
456+
G_CONNECT_SWAPPED);
457+
g_signal_connect_object(self->im_context, "preedit-end",
458+
G_CALLBACK(im_preedit_end_cb), self,
459+
G_CONNECT_SWAPPED);
460+
g_signal_connect_object(self->im_context, "preedit-changed",
461+
G_CALLBACK(im_preedit_changed_cb), self,
462+
G_CONNECT_SWAPPED);
292463
g_signal_connect_object(self->im_context, "commit", G_CALLBACK(im_commit_cb),
293464
self, G_CONNECT_SWAPPED);
294465
g_signal_connect_object(self->im_context, "retrieve-surrounding",
@@ -300,7 +471,8 @@ static void fl_text_input_plugin_init(FlTextInputPlugin* self) {
300471
self->text_model = new flutter::TextInputModel();
301472
}
302473

303-
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
474+
FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger,
475+
FlView* view) {
304476
g_return_val_if_fail(FL_IS_BINARY_MESSENGER(messenger), nullptr);
305477

306478
FlTextInputPlugin* self = FL_TEXT_INPUT_PLUGIN(
@@ -311,7 +483,8 @@ FlTextInputPlugin* fl_text_input_plugin_new(FlBinaryMessenger* messenger) {
311483
fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
312484
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
313485
nullptr);
314-
486+
self->view = view;
487+
g_object_weak_ref(G_OBJECT(view), view_weak_notify_cb, self);
315488
return self;
316489
}
317490

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)