1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 2017 Red Hat, Inc. |
3 | * Copyright (C) 2018 Purism SPC |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Lesser General Public |
7 | * License as published by the Free Software Foundation; either |
8 | * version 2 of the License, or (at your option) any later version. |
9 | * |
10 | * This library is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | * Lesser General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Lesser General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | */ |
18 | |
19 | #include "config.h" |
20 | |
21 | #include <string.h> |
22 | #include <wayland-client-protocol.h> |
23 | |
24 | #include "gtk/gtkdragsourceprivate.h" |
25 | #include "gtk/gtkimcontextwayland.h" |
26 | #include "gtk/gtkintl.h" |
27 | #include "gtk/gtkimmoduleprivate.h" |
28 | |
29 | #include "gdk/wayland/gdkwayland.h" |
30 | #include "text-input-unstable-v3-client-protocol.h" |
31 | |
32 | typedef struct _GtkIMContextWaylandGlobal GtkIMContextWaylandGlobal; |
33 | typedef struct _GtkIMContextWayland GtkIMContextWayland; |
34 | typedef struct _GtkIMContextWaylandClass GtkIMContextWaylandClass; |
35 | |
36 | struct _GtkIMContextWaylandGlobal |
37 | { |
38 | struct wl_display *display; |
39 | struct wl_registry *registry; |
40 | uint32_t text_input_manager_wl_id; |
41 | struct zwp_text_input_manager_v3 *text_input_manager; |
42 | struct zwp_text_input_v3 *text_input; |
43 | |
44 | GtkIMContext *current; |
45 | |
46 | /* The input-method.enter event may happen before or after GTK focus-in, |
47 | * so the context may not exist at the time. Same for leave and focus-out. */ |
48 | gboolean focused; |
49 | |
50 | guint serial; |
51 | guint done_serial; |
52 | }; |
53 | |
54 | struct _GtkIMContextWaylandClass |
55 | { |
56 | GtkIMContextSimpleClass parent_class; |
57 | }; |
58 | |
59 | struct preedit { |
60 | char *text; |
61 | int cursor_begin; |
62 | int cursor_end; |
63 | }; |
64 | |
65 | struct surrounding_delete { |
66 | guint before_length; |
67 | guint after_length; |
68 | }; |
69 | |
70 | struct _GtkIMContextWayland |
71 | { |
72 | GtkIMContextSimple parent_instance; |
73 | GtkWidget *widget; |
74 | GtkWidget *controller_widget; |
75 | |
76 | GtkGesture *gesture; |
77 | double press_x; |
78 | double press_y; |
79 | |
80 | struct { |
81 | char *text; |
82 | int cursor_idx; |
83 | int anchor_idx; |
84 | } surrounding; |
85 | |
86 | enum zwp_text_input_v3_change_cause surrounding_change; |
87 | |
88 | struct surrounding_delete pending_surrounding_delete; |
89 | |
90 | struct preedit current_preedit; |
91 | struct preedit pending_preedit; |
92 | |
93 | char *pending_commit; |
94 | |
95 | cairo_rectangle_int_t cursor_rect; |
96 | guint use_preedit : 1; |
97 | }; |
98 | |
99 | static void gtk_im_context_wayland_focus_out (GtkIMContext *context); |
100 | |
101 | static void commit_state (GtkIMContextWayland *context); |
102 | static void notify_surrounding_text (GtkIMContextWayland *context); |
103 | static void notify_cursor_location (GtkIMContextWayland *context); |
104 | static void notify_content_type (GtkIMContextWayland *context); |
105 | |
106 | G_DEFINE_TYPE_WITH_CODE (GtkIMContextWayland, gtk_im_context_wayland, GTK_TYPE_IM_CONTEXT_SIMPLE, |
107 | gtk_im_module_ensure_extension_point (); |
108 | g_io_extension_point_implement (GTK_IM_MODULE_EXTENSION_POINT_NAME, |
109 | g_define_type_id, |
110 | "wayland" , |
111 | 100)); |
112 | |
113 | #define GTK_IM_CONTEXT_WAYLAND(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), gtk_im_context_wayland_get_type (), GtkIMContextWayland)) |
114 | |
115 | static GtkIMContextWaylandGlobal * |
116 | gtk_im_context_wayland_global_get (GdkDisplay *display); |
117 | |
118 | static GtkIMContextWaylandGlobal * |
119 | gtk_im_context_wayland_get_global (GtkIMContextWayland *self) |
120 | { |
121 | GtkIMContextWaylandGlobal *global; |
122 | |
123 | if (self->widget == NULL) |
124 | return NULL; |
125 | |
126 | global = gtk_im_context_wayland_global_get (display: gtk_widget_get_display (widget: self->widget)); |
127 | if (global->current != GTK_IM_CONTEXT (self)) |
128 | return NULL; |
129 | if (global->text_input == NULL) |
130 | return NULL; |
131 | |
132 | return global; |
133 | } |
134 | |
135 | static void |
136 | notify_im_change (GtkIMContextWayland *context, |
137 | enum zwp_text_input_v3_change_cause cause) |
138 | { |
139 | GtkIMContextWaylandGlobal *global; |
140 | gboolean result; |
141 | |
142 | global = gtk_im_context_wayland_get_global (self: context); |
143 | if (global == NULL) |
144 | return; |
145 | |
146 | context->surrounding_change = cause; |
147 | |
148 | g_signal_emit_by_name (instance: global->current, detailed_signal: "retrieve-surrounding" , &result); |
149 | notify_surrounding_text (context); |
150 | notify_content_type (context); |
151 | notify_cursor_location (context); |
152 | commit_state (context); |
153 | } |
154 | |
155 | static void |
156 | text_input_preedit (void *data, |
157 | struct zwp_text_input_v3 *text_input, |
158 | const char *text, |
159 | int cursor_begin, |
160 | int cursor_end) |
161 | { |
162 | GtkIMContextWayland *context; |
163 | GtkIMContextWaylandGlobal *global = data; |
164 | |
165 | if (!global->current) |
166 | return; |
167 | |
168 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
169 | |
170 | g_free (mem: context->pending_preedit.text); |
171 | context->pending_preedit.text = g_strdup (str: text); |
172 | context->pending_preedit.cursor_begin = cursor_begin; |
173 | context->pending_preedit.cursor_end = cursor_end; |
174 | } |
175 | |
176 | static void |
177 | text_input_preedit_apply (GtkIMContextWaylandGlobal *global) |
178 | { |
179 | GtkIMContextWayland *context; |
180 | gboolean state_change; |
181 | struct preedit defaults = {0}; |
182 | |
183 | if (!global->current) |
184 | return; |
185 | |
186 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
187 | if (context->pending_preedit.text == NULL && |
188 | context->current_preedit.text == NULL) |
189 | return; |
190 | |
191 | state_change = ((context->pending_preedit.text == NULL) |
192 | != (context->current_preedit.text == NULL)); |
193 | |
194 | if (state_change && !context->current_preedit.text) |
195 | g_signal_emit_by_name (instance: context, detailed_signal: "preedit-start" ); |
196 | |
197 | g_free (mem: context->current_preedit.text); |
198 | context->current_preedit = context->pending_preedit; |
199 | context->pending_preedit = defaults; |
200 | |
201 | g_signal_emit_by_name (instance: context, detailed_signal: "preedit-changed" ); |
202 | |
203 | if (state_change && !context->current_preedit.text) |
204 | g_signal_emit_by_name (instance: context, detailed_signal: "preedit-end" ); |
205 | } |
206 | |
207 | static void |
208 | text_input_commit (void *data, |
209 | struct zwp_text_input_v3 *text_input, |
210 | const char *text) |
211 | { |
212 | GtkIMContextWaylandGlobal *global = data; |
213 | GtkIMContextWayland *context; |
214 | |
215 | if (!global->current) |
216 | return; |
217 | |
218 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
219 | |
220 | g_free (mem: context->pending_commit); |
221 | context->pending_commit = g_strdup (str: text); |
222 | } |
223 | |
224 | static void |
225 | text_input_commit_apply (GtkIMContextWaylandGlobal *global) |
226 | { |
227 | GtkIMContextWayland *context; |
228 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
229 | if (context->pending_commit) |
230 | g_signal_emit_by_name (instance: global->current, detailed_signal: "commit" , context->pending_commit); |
231 | g_free (mem: context->pending_commit); |
232 | context->pending_commit = NULL; |
233 | } |
234 | |
235 | static void |
236 | text_input_delete_surrounding_text (void *data, |
237 | struct zwp_text_input_v3 *text_input, |
238 | uint32_t before_length, |
239 | uint32_t after_length) |
240 | { |
241 | GtkIMContextWaylandGlobal *global = data; |
242 | GtkIMContextWayland *context; |
243 | |
244 | if (!global->current) |
245 | return; |
246 | |
247 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
248 | |
249 | context->pending_surrounding_delete.before_length = before_length; |
250 | context->pending_surrounding_delete.after_length = after_length; |
251 | } |
252 | |
253 | static void |
254 | text_input_delete_surrounding_text_apply (GtkIMContextWaylandGlobal *global) |
255 | { |
256 | GtkIMContextWayland *context; |
257 | gboolean retval; |
258 | int len; |
259 | struct surrounding_delete defaults = {0}; |
260 | |
261 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
262 | |
263 | len = context->pending_surrounding_delete.after_length |
264 | + context->pending_surrounding_delete.before_length; |
265 | if (len > 0) |
266 | { |
267 | g_signal_emit_by_name (instance: global->current, detailed_signal: "delete-surrounding" , |
268 | -context->pending_surrounding_delete.before_length, |
269 | len, &retval); |
270 | notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), |
271 | cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD); |
272 | } |
273 | |
274 | context->pending_surrounding_delete = defaults; |
275 | } |
276 | |
277 | static void |
278 | text_input_done (void *data, |
279 | struct zwp_text_input_v3 *text_input, |
280 | uint32_t serial) |
281 | { |
282 | GtkIMContextWaylandGlobal *global = data; |
283 | GtkIMContextWayland *context; |
284 | gboolean update_im; |
285 | |
286 | global->done_serial = serial; |
287 | |
288 | if (!global->current) |
289 | return; |
290 | |
291 | context = GTK_IM_CONTEXT_WAYLAND (global->current); |
292 | update_im = (context->pending_commit != NULL || |
293 | g_strcmp0 (str1: context->pending_preedit.text, |
294 | str2: context->current_preedit.text) != 0); |
295 | |
296 | text_input_delete_surrounding_text_apply (global); |
297 | text_input_commit_apply (global); |
298 | text_input_preedit_apply (global); |
299 | |
300 | if (update_im && global->serial == serial) |
301 | notify_im_change (context, cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD); |
302 | } |
303 | |
304 | static void |
305 | notify_surrounding_text (GtkIMContextWayland *context) |
306 | { |
307 | #define MAX_LEN 4000 |
308 | GtkIMContextWaylandGlobal *global; |
309 | const char *start, *end; |
310 | int len, cursor, anchor; |
311 | char *str = NULL; |
312 | |
313 | if (!context->surrounding.text) |
314 | return; |
315 | global = gtk_im_context_wayland_get_global (self: context); |
316 | if (global == NULL) |
317 | return; |
318 | |
319 | len = strlen (s: context->surrounding.text); |
320 | cursor = context->surrounding.cursor_idx; |
321 | anchor = context->surrounding.anchor_idx; |
322 | |
323 | /* The protocol specifies a maximum length of 4KiB on transfers, |
324 | * mangle the surrounding text if it's bigger than that, and relocate |
325 | * cursor/anchor locations as per the string being sent. |
326 | */ |
327 | if (len > MAX_LEN) |
328 | { |
329 | if (context->surrounding.cursor_idx < MAX_LEN && |
330 | context->surrounding.anchor_idx < MAX_LEN) |
331 | { |
332 | start = context->surrounding.text; |
333 | end = &context->surrounding.text[MAX_LEN]; |
334 | } |
335 | else if (context->surrounding.cursor_idx > len - MAX_LEN && |
336 | context->surrounding.anchor_idx > len - MAX_LEN) |
337 | { |
338 | start = &context->surrounding.text[len - MAX_LEN]; |
339 | end = &context->surrounding.text[len]; |
340 | } |
341 | else |
342 | { |
343 | int mid, a, b; |
344 | int cursor_len = ABS (context->surrounding.cursor_idx - |
345 | context->surrounding.anchor_idx); |
346 | |
347 | if (cursor_len > MAX_LEN) |
348 | { |
349 | g_warn_if_reached (); |
350 | return; |
351 | } |
352 | |
353 | mid = MIN (context->surrounding.cursor_idx, |
354 | context->surrounding.anchor_idx) + (cursor_len / 2); |
355 | a = MAX (0, mid - (MAX_LEN / 2)); |
356 | b = MIN (len, mid + (MAX_LEN / 2)); |
357 | |
358 | start = &context->surrounding.text[a]; |
359 | end = &context->surrounding.text[b]; |
360 | } |
361 | |
362 | if (start != context->surrounding.text) |
363 | start = g_utf8_next_char (start); |
364 | if (end != &context->surrounding.text[len]) |
365 | end = g_utf8_find_prev_char (str: context->surrounding.text, p: end); |
366 | |
367 | cursor -= start - context->surrounding.text; |
368 | anchor -= start - context->surrounding.text; |
369 | |
370 | str = g_strndup (str: start, n: end - start); |
371 | } |
372 | |
373 | zwp_text_input_v3_set_surrounding_text (global->text_input, |
374 | str ? str : context->surrounding.text, |
375 | cursor, anchor); |
376 | zwp_text_input_v3_set_text_change_cause (global->text_input, |
377 | context->surrounding_change); |
378 | g_free (mem: str); |
379 | #undef MAX_LEN |
380 | } |
381 | |
382 | static void |
383 | notify_cursor_location (GtkIMContextWayland *context) |
384 | { |
385 | GtkIMContextWaylandGlobal *global; |
386 | cairo_rectangle_int_t rect; |
387 | double x, y, sx, sy; |
388 | |
389 | global = gtk_im_context_wayland_get_global (self: context); |
390 | if (global == NULL) |
391 | return; |
392 | |
393 | rect = context->cursor_rect; |
394 | gtk_widget_translate_coordinates (src_widget: context->widget, |
395 | GTK_WIDGET (gtk_widget_get_root (context->widget)), |
396 | src_x: rect.x, src_y: rect.y, |
397 | dest_x: &x, dest_y: &y); |
398 | |
399 | gtk_native_get_surface_transform (self: gtk_widget_get_native (widget: context->widget), |
400 | x: &sx, y: &sy); |
401 | |
402 | rect.x = x + sx; |
403 | rect.y = y + sy; |
404 | zwp_text_input_v3_set_cursor_rectangle (global->text_input, |
405 | rect.x, rect.y, |
406 | rect.width, rect.height); |
407 | } |
408 | |
409 | static uint32_t |
410 | translate_hints (GtkInputHints input_hints, |
411 | GtkInputPurpose purpose) |
412 | { |
413 | uint32_t hints = 0; |
414 | |
415 | if (input_hints & GTK_INPUT_HINT_SPELLCHECK) |
416 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK; |
417 | if (input_hints & GTK_INPUT_HINT_WORD_COMPLETION) |
418 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION; |
419 | if (input_hints & GTK_INPUT_HINT_LOWERCASE) |
420 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE; |
421 | if (input_hints & GTK_INPUT_HINT_UPPERCASE_CHARS) |
422 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE; |
423 | if (input_hints & GTK_INPUT_HINT_UPPERCASE_WORDS) |
424 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE; |
425 | if (input_hints & GTK_INPUT_HINT_UPPERCASE_SENTENCES) |
426 | hints |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION; |
427 | |
428 | if (purpose == GTK_INPUT_PURPOSE_PIN || |
429 | purpose == GTK_INPUT_PURPOSE_PASSWORD) |
430 | { |
431 | hints |= (ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT | |
432 | ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA); |
433 | } |
434 | |
435 | return hints; |
436 | } |
437 | |
438 | static uint32_t |
439 | translate_purpose (GtkInputPurpose purpose) |
440 | { |
441 | switch (purpose) |
442 | { |
443 | case GTK_INPUT_PURPOSE_FREE_FORM: |
444 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL; |
445 | case GTK_INPUT_PURPOSE_ALPHA: |
446 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA; |
447 | case GTK_INPUT_PURPOSE_DIGITS: |
448 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS; |
449 | case GTK_INPUT_PURPOSE_NUMBER: |
450 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER; |
451 | case GTK_INPUT_PURPOSE_PHONE: |
452 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE; |
453 | case GTK_INPUT_PURPOSE_URL: |
454 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL; |
455 | case GTK_INPUT_PURPOSE_EMAIL: |
456 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL; |
457 | case GTK_INPUT_PURPOSE_NAME: |
458 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME; |
459 | case GTK_INPUT_PURPOSE_PASSWORD: |
460 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD; |
461 | case GTK_INPUT_PURPOSE_PIN: |
462 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN; |
463 | case GTK_INPUT_PURPOSE_TERMINAL: |
464 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL; |
465 | default: |
466 | g_assert_not_reached (); |
467 | } |
468 | |
469 | return ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL; |
470 | } |
471 | |
472 | static void |
473 | notify_content_type (GtkIMContextWayland *context) |
474 | { |
475 | GtkIMContextWaylandGlobal *global; |
476 | GtkInputHints hints; |
477 | GtkInputPurpose purpose; |
478 | |
479 | global = gtk_im_context_wayland_get_global (self: context); |
480 | if (global == NULL) |
481 | return; |
482 | |
483 | g_object_get (object: context, |
484 | first_property_name: "input-hints" , &hints, |
485 | "input-purpose" , &purpose, |
486 | NULL); |
487 | |
488 | zwp_text_input_v3_set_content_type (global->text_input, |
489 | translate_hints (input_hints: hints, purpose), |
490 | translate_purpose (purpose)); |
491 | } |
492 | |
493 | static void |
494 | commit_state (GtkIMContextWayland *context) |
495 | { |
496 | GtkIMContextWaylandGlobal *global; |
497 | |
498 | global = gtk_im_context_wayland_get_global (self: context); |
499 | if (global == NULL) |
500 | return; |
501 | |
502 | global->serial++; |
503 | zwp_text_input_v3_commit (global->text_input); |
504 | context->surrounding_change = ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD; |
505 | } |
506 | |
507 | static void |
508 | gtk_im_context_wayland_finalize (GObject *object) |
509 | { |
510 | GtkIMContextWayland *context = GTK_IM_CONTEXT_WAYLAND (object); |
511 | |
512 | gtk_im_context_wayland_focus_out (GTK_IM_CONTEXT (context)); |
513 | |
514 | g_clear_object (&context->widget); |
515 | g_clear_object (&context->gesture); |
516 | g_free (mem: context->surrounding.text); |
517 | g_free (mem: context->current_preedit.text); |
518 | g_free (mem: context->pending_preedit.text); |
519 | g_free (mem: context->pending_commit); |
520 | |
521 | G_OBJECT_CLASS (gtk_im_context_wayland_parent_class)->finalize (object); |
522 | } |
523 | |
524 | static void |
525 | pressed_cb (GtkGestureClick *gesture, |
526 | int n_press, |
527 | double x, |
528 | double y, |
529 | GtkIMContextWayland *context) |
530 | { |
531 | if (n_press == 1) |
532 | { |
533 | context->press_x = x; |
534 | context->press_y = y; |
535 | } |
536 | } |
537 | |
538 | static void |
539 | released_cb (GtkGestureClick *gesture, |
540 | int n_press, |
541 | double x, |
542 | double y, |
543 | GtkIMContextWayland *context) |
544 | { |
545 | GtkIMContextWaylandGlobal *global; |
546 | GtkInputHints hints; |
547 | |
548 | global = gtk_im_context_wayland_get_global (self: context); |
549 | if (global == NULL) |
550 | return; |
551 | |
552 | g_object_get (object: context, first_property_name: "input-hints" , &hints, NULL); |
553 | |
554 | if (global->focused && |
555 | n_press == 1 && |
556 | (hints & GTK_INPUT_HINT_INHIBIT_OSK) == 0 && |
557 | !gtk_drag_check_threshold_double (widget: context->widget, |
558 | start_x: context->press_x, |
559 | start_y: context->press_y, |
560 | current_x: x, current_y: y)) |
561 | { |
562 | zwp_text_input_v3_enable (global->text_input); |
563 | notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), |
564 | cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); |
565 | } |
566 | } |
567 | |
568 | static void |
569 | gtk_im_context_wayland_set_client_widget (GtkIMContext *context, |
570 | GtkWidget *widget) |
571 | { |
572 | GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
573 | |
574 | if (widget == context_wayland->widget) |
575 | return; |
576 | |
577 | if (context_wayland->widget) |
578 | gtk_im_context_wayland_focus_out (context); |
579 | |
580 | if (context_wayland->controller_widget) |
581 | { |
582 | gtk_widget_remove_controller (widget: context_wayland->controller_widget, |
583 | GTK_EVENT_CONTROLLER (context_wayland->gesture)); |
584 | context_wayland->gesture = NULL; |
585 | g_clear_object (&context_wayland->controller_widget); |
586 | } |
587 | |
588 | g_set_object (&context_wayland->widget, widget); |
589 | |
590 | if (widget) |
591 | { |
592 | GtkWidget *parent; |
593 | GtkGesture *gesture; |
594 | |
595 | gesture = gtk_gesture_click_new (); |
596 | gtk_event_controller_set_name (GTK_EVENT_CONTROLLER (gesture), name: "wayland-im-context-click" ); |
597 | gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), |
598 | phase: GTK_PHASE_CAPTURE); |
599 | g_signal_connect (gesture, "pressed" , |
600 | G_CALLBACK (pressed_cb), context); |
601 | g_signal_connect (gesture, "released" , |
602 | G_CALLBACK (released_cb), context); |
603 | |
604 | parent = gtk_widget_get_parent (widget); |
605 | |
606 | if (parent && |
607 | GTK_IS_EDITABLE (widget) && |
608 | GTK_IS_EDITABLE (parent)) |
609 | g_set_object (&context_wayland->controller_widget, parent); |
610 | else |
611 | g_set_object (&context_wayland->controller_widget, widget); |
612 | |
613 | gtk_widget_add_controller (widget: context_wayland->controller_widget, |
614 | GTK_EVENT_CONTROLLER (gesture)); |
615 | context_wayland->gesture = gesture; |
616 | } |
617 | } |
618 | |
619 | static void |
620 | gtk_im_context_wayland_get_preedit_string (GtkIMContext *context, |
621 | char **str, |
622 | PangoAttrList **attrs, |
623 | int *cursor_pos) |
624 | { |
625 | GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
626 | const char *preedit_str; |
627 | |
628 | if (attrs) |
629 | *attrs = NULL; |
630 | |
631 | GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->get_preedit_string (context, |
632 | str, attrs, |
633 | cursor_pos); |
634 | |
635 | /* If the parent implementation returns a len>0 string, go with it */ |
636 | if (str && *str) |
637 | { |
638 | if (**str) |
639 | return; |
640 | |
641 | g_free (mem: *str); |
642 | } |
643 | |
644 | preedit_str = |
645 | context_wayland->current_preedit.text ? context_wayland->current_preedit.text : "" ; |
646 | |
647 | if (cursor_pos) |
648 | *cursor_pos = g_utf8_strlen (p: preedit_str, |
649 | max: context_wayland->current_preedit.cursor_begin); |
650 | |
651 | if (str) |
652 | *str = g_strdup (str: preedit_str); |
653 | if (attrs) |
654 | { |
655 | PangoAttribute *attr; |
656 | guint len = strlen (s: preedit_str); |
657 | |
658 | if (!*attrs) |
659 | *attrs = pango_attr_list_new (); |
660 | |
661 | attr = pango_attr_underline_new (underline: PANGO_UNDERLINE_SINGLE); |
662 | attr->start_index = 0; |
663 | attr->end_index = len; |
664 | pango_attr_list_insert (list: *attrs, attr); |
665 | |
666 | /* enable fallback, since IBus will send us things like ⎄ */ |
667 | attr = pango_attr_fallback_new (TRUE); |
668 | attr->start_index = 0; |
669 | attr->end_index = len; |
670 | pango_attr_list_insert (list: *attrs, attr); |
671 | |
672 | if (context_wayland->current_preedit.cursor_begin |
673 | != context_wayland->current_preedit.cursor_end) |
674 | { |
675 | /* FIXME: Oh noes, how to highlight while taking into account user preferences? */ |
676 | PangoAttribute *cursor = pango_attr_weight_new (weight: PANGO_WEIGHT_BOLD); |
677 | cursor->start_index = context_wayland->current_preedit.cursor_begin; |
678 | cursor->end_index = context_wayland->current_preedit.cursor_end; |
679 | pango_attr_list_insert (list: *attrs, attr: cursor); |
680 | } |
681 | } |
682 | } |
683 | |
684 | static gboolean |
685 | gtk_im_context_wayland_filter_keypress (GtkIMContext *context, |
686 | GdkEvent *key) |
687 | { |
688 | /* This is done by the compositor */ |
689 | return GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->filter_keypress (context, key); |
690 | } |
691 | |
692 | static void |
693 | enable (GtkIMContextWayland *context_wayland, |
694 | GtkIMContextWaylandGlobal *global) |
695 | { |
696 | zwp_text_input_v3_enable (global->text_input); |
697 | notify_im_change (context: context_wayland, |
698 | cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); |
699 | } |
700 | |
701 | static void |
702 | disable (GtkIMContextWayland *context_wayland, |
703 | GtkIMContextWaylandGlobal *global) |
704 | { |
705 | zwp_text_input_v3_disable (global->text_input); |
706 | commit_state (context: context_wayland); |
707 | |
708 | /* The commit above will still count in the .done event accounting, |
709 | * we should account for it, lest the serial gets out of sync after |
710 | * a future focus_in/enable. |
711 | */ |
712 | global->done_serial++; |
713 | |
714 | /* after disable, incoming state changes won't take effect anyway */ |
715 | if (context_wayland->current_preedit.text) |
716 | { |
717 | text_input_preedit (data: global, text_input: global->text_input, NULL, cursor_begin: 0, cursor_end: 0); |
718 | text_input_preedit_apply (global); |
719 | } |
720 | } |
721 | |
722 | static void |
723 | text_input_enter (void *data, |
724 | struct zwp_text_input_v3 *text_input, |
725 | struct wl_surface *surface) |
726 | { |
727 | GtkIMContextWaylandGlobal *global = data; |
728 | |
729 | global->focused = TRUE; |
730 | |
731 | if (global->current) |
732 | enable (GTK_IM_CONTEXT_WAYLAND (global->current), global); |
733 | } |
734 | |
735 | static void |
736 | text_input_leave (void *data, |
737 | struct zwp_text_input_v3 *text_input, |
738 | struct wl_surface *surface) |
739 | { |
740 | GtkIMContextWaylandGlobal *global = data; |
741 | |
742 | global->focused = FALSE; |
743 | |
744 | if (global->current) |
745 | disable (GTK_IM_CONTEXT_WAYLAND (global->current), global); |
746 | } |
747 | |
748 | |
749 | static const struct zwp_text_input_v3_listener text_input_listener = { |
750 | text_input_enter, |
751 | text_input_leave, |
752 | text_input_preedit, |
753 | text_input_commit, |
754 | text_input_delete_surrounding_text, |
755 | text_input_done, |
756 | }; |
757 | |
758 | static void |
759 | registry_handle_global (void *data, |
760 | struct wl_registry *registry, |
761 | uint32_t id, |
762 | const char *interface, |
763 | uint32_t version) |
764 | { |
765 | GtkIMContextWaylandGlobal *global = data; |
766 | GdkSeat *seat = gdk_display_get_default_seat (display: gdk_display_get_default ()); |
767 | |
768 | if (strcmp (s1: interface, s2: "zwp_text_input_manager_v3" ) == 0) |
769 | { |
770 | global->text_input_manager_wl_id = id; |
771 | global->text_input_manager = |
772 | wl_registry_bind (wl_registry: global->registry, name: global->text_input_manager_wl_id, |
773 | interface: &zwp_text_input_manager_v3_interface, version: 1); |
774 | global->text_input = |
775 | zwp_text_input_manager_v3_get_text_input (global->text_input_manager, |
776 | gdk_wayland_seat_get_wl_seat (seat)); |
777 | global->serial = 0; |
778 | zwp_text_input_v3_add_listener (global->text_input, |
779 | &text_input_listener, global); |
780 | } |
781 | } |
782 | |
783 | static void |
784 | registry_handle_global_remove (void *data, |
785 | struct wl_registry *registry, |
786 | uint32_t id) |
787 | { |
788 | GtkIMContextWaylandGlobal *global = data; |
789 | |
790 | if (id != global->text_input_manager_wl_id) |
791 | return; |
792 | |
793 | g_clear_pointer (&global->text_input, zwp_text_input_v3_destroy); |
794 | g_clear_pointer (&global->text_input_manager, zwp_text_input_manager_v3_destroy); |
795 | } |
796 | |
797 | static const struct wl_registry_listener registry_listener = { |
798 | registry_handle_global, |
799 | registry_handle_global_remove |
800 | }; |
801 | |
802 | static void |
803 | gtk_im_context_wayland_global_free (gpointer data) |
804 | { |
805 | GtkIMContextWaylandGlobal *global = data; |
806 | |
807 | g_free (mem: global); |
808 | } |
809 | |
810 | static GtkIMContextWaylandGlobal * |
811 | gtk_im_context_wayland_global_get (GdkDisplay *display) |
812 | { |
813 | GtkIMContextWaylandGlobal *global; |
814 | |
815 | global = g_object_get_data (G_OBJECT (display), key: "gtk-im-context-wayland-global" ); |
816 | if (global != NULL) |
817 | return global; |
818 | |
819 | global = g_new0 (GtkIMContextWaylandGlobal, 1); |
820 | global->display = gdk_wayland_display_get_wl_display (display); |
821 | global->registry = wl_display_get_registry (wl_display: global->display); |
822 | |
823 | wl_registry_add_listener (wl_registry: global->registry, listener: ®istry_listener, data: global); |
824 | |
825 | g_object_set_data_full (G_OBJECT (display), |
826 | key: "gtk-im-context-wayland-global" , |
827 | data: global, |
828 | destroy: gtk_im_context_wayland_global_free); |
829 | |
830 | return global; |
831 | } |
832 | |
833 | static void |
834 | gtk_im_context_wayland_focus_in (GtkIMContext *context) |
835 | { |
836 | GtkIMContextWayland *self = GTK_IM_CONTEXT_WAYLAND (context); |
837 | GtkIMContextWaylandGlobal *global; |
838 | |
839 | if (self->widget == NULL) |
840 | return; |
841 | |
842 | global = gtk_im_context_wayland_global_get (display: gtk_widget_get_display (widget: self->widget)); |
843 | if (global->current == context) |
844 | return; |
845 | |
846 | global->current = context; |
847 | if (!global->text_input) |
848 | return; |
849 | |
850 | if (self->gesture) |
851 | gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->gesture)); |
852 | |
853 | if (global->focused) |
854 | enable (context_wayland: self, global); |
855 | } |
856 | |
857 | static void |
858 | gtk_im_context_wayland_focus_out (GtkIMContext *context) |
859 | { |
860 | GtkIMContextWayland *self = GTK_IM_CONTEXT_WAYLAND (context); |
861 | GtkIMContextWaylandGlobal *global; |
862 | |
863 | global = gtk_im_context_wayland_get_global (self); |
864 | if (global == NULL) |
865 | return; |
866 | |
867 | if (global->focused) |
868 | disable (context_wayland: self, global); |
869 | |
870 | global->current = NULL; |
871 | } |
872 | |
873 | static void |
874 | gtk_im_context_wayland_reset (GtkIMContext *context) |
875 | { |
876 | notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), |
877 | cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER); |
878 | |
879 | GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->reset (context); |
880 | } |
881 | |
882 | static void |
883 | gtk_im_context_wayland_set_cursor_location (GtkIMContext *context, |
884 | GdkRectangle *rect) |
885 | { |
886 | GtkIMContextWayland *context_wayland; |
887 | int side; |
888 | |
889 | context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
890 | |
891 | if (context_wayland->cursor_rect.x == rect->x && |
892 | context_wayland->cursor_rect.y == rect->y && |
893 | context_wayland->cursor_rect.width == rect->width && |
894 | context_wayland->cursor_rect.height == rect->height) |
895 | return; |
896 | |
897 | /* Reset the gesture if the cursor changes too far (eg. clicking |
898 | * between disjoint positions in the text). |
899 | * |
900 | * Still Allow some jittering (a square almost double the cursor rect height |
901 | * on either side) as clicking on the exact same position between characters |
902 | * is hard. |
903 | */ |
904 | side = context_wayland->cursor_rect.height; |
905 | |
906 | if (context_wayland->gesture && |
907 | (ABS (rect->x - context_wayland->cursor_rect.x) >= side || |
908 | ABS (rect->y - context_wayland->cursor_rect.y) >= side)) |
909 | gtk_event_controller_reset (GTK_EVENT_CONTROLLER (context_wayland->gesture)); |
910 | |
911 | context_wayland->cursor_rect = *rect; |
912 | } |
913 | |
914 | static void |
915 | gtk_im_context_wayland_set_use_preedit (GtkIMContext *context, |
916 | gboolean use_preedit) |
917 | { |
918 | GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
919 | |
920 | context_wayland->use_preedit = !!use_preedit; |
921 | } |
922 | |
923 | static void |
924 | gtk_im_context_wayland_set_surrounding (GtkIMContext *context, |
925 | const char *text, |
926 | int len, |
927 | int cursor_index, |
928 | int selection_bound) |
929 | { |
930 | GtkIMContextWayland *context_wayland; |
931 | |
932 | context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
933 | |
934 | if (context_wayland->surrounding.text && text && |
935 | (len < 0 || len == strlen (s: context_wayland->surrounding.text)) && |
936 | strncmp (s1: context_wayland->surrounding.text, s2: text, n: len) == 0 && |
937 | context_wayland->surrounding.cursor_idx == cursor_index && |
938 | context_wayland->surrounding.anchor_idx == selection_bound) |
939 | return; |
940 | |
941 | g_free (mem: context_wayland->surrounding.text); |
942 | context_wayland->surrounding.text = g_strndup (str: text, n: len); |
943 | context_wayland->surrounding.cursor_idx = cursor_index; |
944 | context_wayland->surrounding.anchor_idx = selection_bound; |
945 | } |
946 | |
947 | static gboolean |
948 | gtk_im_context_wayland_get_surrounding (GtkIMContext *context, |
949 | char **text, |
950 | int *cursor_index, |
951 | int *selection_bound) |
952 | { |
953 | GtkIMContextWayland *context_wayland; |
954 | |
955 | context_wayland = GTK_IM_CONTEXT_WAYLAND (context); |
956 | |
957 | if (!context_wayland->surrounding.text) |
958 | return FALSE; |
959 | |
960 | *text = context_wayland->surrounding.text; |
961 | *cursor_index = context_wayland->surrounding.cursor_idx; |
962 | *selection_bound = context_wayland->surrounding.anchor_idx; |
963 | return TRUE; |
964 | } |
965 | |
966 | static void |
967 | gtk_im_context_wayland_commit (GtkIMContext *context, |
968 | const gchar *str) |
969 | { |
970 | if (GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->commit) |
971 | GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->commit (context, str); |
972 | |
973 | notify_im_change (GTK_IM_CONTEXT_WAYLAND (context), |
974 | cause: ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD); |
975 | } |
976 | |
977 | static void |
978 | gtk_im_context_wayland_class_init (GtkIMContextWaylandClass *klass) |
979 | { |
980 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
981 | GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS (klass); |
982 | |
983 | object_class->finalize = gtk_im_context_wayland_finalize; |
984 | |
985 | im_context_class->set_client_widget = gtk_im_context_wayland_set_client_widget; |
986 | im_context_class->get_preedit_string = gtk_im_context_wayland_get_preedit_string; |
987 | im_context_class->filter_keypress = gtk_im_context_wayland_filter_keypress; |
988 | im_context_class->focus_in = gtk_im_context_wayland_focus_in; |
989 | im_context_class->focus_out = gtk_im_context_wayland_focus_out; |
990 | im_context_class->reset = gtk_im_context_wayland_reset; |
991 | im_context_class->set_cursor_location = gtk_im_context_wayland_set_cursor_location; |
992 | im_context_class->set_use_preedit = gtk_im_context_wayland_set_use_preedit; |
993 | im_context_class->set_surrounding_with_selection = gtk_im_context_wayland_set_surrounding; |
994 | im_context_class->get_surrounding_with_selection = gtk_im_context_wayland_get_surrounding; |
995 | im_context_class->commit = gtk_im_context_wayland_commit; |
996 | } |
997 | |
998 | static void |
999 | on_content_type_changed (GtkIMContextWayland *context) |
1000 | { |
1001 | notify_content_type (context); |
1002 | commit_state (context); |
1003 | } |
1004 | |
1005 | static void |
1006 | gtk_im_context_wayland_init (GtkIMContextWayland *context) |
1007 | { |
1008 | context->use_preedit = TRUE; |
1009 | g_signal_connect_swapped (context, "notify::input-purpose" , |
1010 | G_CALLBACK (on_content_type_changed), context); |
1011 | g_signal_connect_swapped (context, "notify::input-hints" , |
1012 | G_CALLBACK (on_content_type_changed), context); |
1013 | } |
1014 | |