Skip to content

Commit 547638f

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 now recompute and store a cached copy of the preedit rect in the Gtk window co-ordinates. This rect is used to inform Gtk of the cursor position, 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 547638f

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)