1 | /* gtkemojicompletion.c: An Emoji picker widget |
2 | * Copyright 2017, Red Hat, Inc. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "config.h" |
19 | |
20 | #include "gtkemojicompletion.h" |
21 | |
22 | #include "gtktextprivate.h" |
23 | #include "gtkeditable.h" |
24 | #include "gtkbox.h" |
25 | #include "gtkcssprovider.h" |
26 | #include "gtklistbox.h" |
27 | #include "gtklabel.h" |
28 | #include "gtkpopover.h" |
29 | #include "gtkintl.h" |
30 | #include "gtkprivate.h" |
31 | #include "gtkgesturelongpress.h" |
32 | #include "gtkeventcontrollerkey.h" |
33 | #include "gtkflowbox.h" |
34 | #include "gtkstack.h" |
35 | #include "gtkstylecontext.h" |
36 | |
37 | struct _GtkEmojiCompletion |
38 | { |
39 | GtkPopover parent_instance; |
40 | |
41 | GtkText *entry; |
42 | char *text; |
43 | guint length; |
44 | guint offset; |
45 | gulong changed_id; |
46 | guint n_matches; |
47 | |
48 | GtkWidget *list; |
49 | GtkWidget *active; |
50 | GtkWidget *active_variation; |
51 | |
52 | GVariant *data; |
53 | }; |
54 | |
55 | struct _GtkEmojiCompletionClass { |
56 | GtkPopoverClass parent_class; |
57 | }; |
58 | |
59 | static void connect_signals (GtkEmojiCompletion *completion, |
60 | GtkText *text); |
61 | static void disconnect_signals (GtkEmojiCompletion *completion); |
62 | static int populate_completion (GtkEmojiCompletion *completion, |
63 | const char *text, |
64 | guint offset); |
65 | |
66 | #define MAX_ROWS 5 |
67 | |
68 | G_DEFINE_TYPE (GtkEmojiCompletion, gtk_emoji_completion, GTK_TYPE_POPOVER) |
69 | |
70 | static void |
71 | gtk_emoji_completion_finalize (GObject *object) |
72 | { |
73 | GtkEmojiCompletion *completion = GTK_EMOJI_COMPLETION (object); |
74 | |
75 | disconnect_signals (completion); |
76 | |
77 | g_free (mem: completion->text); |
78 | g_variant_unref (value: completion->data); |
79 | |
80 | G_OBJECT_CLASS (gtk_emoji_completion_parent_class)->finalize (object); |
81 | } |
82 | |
83 | static void |
84 | update_completion (GtkEmojiCompletion *completion) |
85 | { |
86 | const char *text; |
87 | guint length; |
88 | guint n_matches; |
89 | |
90 | n_matches = 0; |
91 | |
92 | text = gtk_editable_get_text (GTK_EDITABLE (completion->entry)); |
93 | length = strlen (s: text); |
94 | |
95 | if (length > 0) |
96 | { |
97 | gboolean found_candidate = FALSE; |
98 | const char *p; |
99 | |
100 | p = text + length; |
101 | do |
102 | { |
103 | next: |
104 | p = g_utf8_prev_char (p); |
105 | if (*p == ':') |
106 | { |
107 | if (p + 1 == text + length) |
108 | goto next; |
109 | |
110 | if (p == text || !g_unichar_isalnum (c: g_utf8_get_char (p: p - 1))) |
111 | { |
112 | found_candidate = TRUE; |
113 | } |
114 | |
115 | break; |
116 | } |
117 | } |
118 | while (p > text && |
119 | (g_unichar_isalnum (c: g_utf8_get_char (p)) || *p == '_' || *p == ' ')); |
120 | |
121 | if (found_candidate) |
122 | n_matches = populate_completion (completion, text: p, offset: 0); |
123 | } |
124 | |
125 | if (n_matches > 0) |
126 | gtk_popover_popup (GTK_POPOVER (completion)); |
127 | else |
128 | gtk_popover_popdown (GTK_POPOVER (completion)); |
129 | } |
130 | |
131 | static void |
132 | changed_cb (GtkText *text, |
133 | GtkEmojiCompletion *completion) |
134 | { |
135 | update_completion (completion); |
136 | } |
137 | |
138 | static void |
139 | emoji_activated (GtkWidget *row, |
140 | GtkEmojiCompletion *completion) |
141 | { |
142 | const char *emoji; |
143 | guint length; |
144 | |
145 | gtk_popover_popdown (GTK_POPOVER (completion)); |
146 | |
147 | emoji = (const char *)g_object_get_data (G_OBJECT (row), key: "text" ); |
148 | |
149 | g_signal_handler_block (instance: completion->entry, handler_id: completion->changed_id); |
150 | |
151 | length = g_utf8_strlen (p: gtk_editable_get_text (GTK_EDITABLE (completion->entry)), max: -1); |
152 | gtk_editable_select_region (GTK_EDITABLE (completion->entry), start_pos: length - completion->length, end_pos: length); |
153 | gtk_text_enter_text (entry: completion->entry, text: emoji); |
154 | |
155 | g_signal_handler_unblock (instance: completion->entry, handler_id: completion->changed_id); |
156 | } |
157 | |
158 | static void |
159 | row_activated (GtkListBox *list, |
160 | GtkListBoxRow *row, |
161 | gpointer data) |
162 | { |
163 | GtkEmojiCompletion *completion = data; |
164 | |
165 | emoji_activated (GTK_WIDGET (row), completion); |
166 | } |
167 | |
168 | static void |
169 | child_activated (GtkFlowBox *box, |
170 | GtkFlowBoxChild *child, |
171 | gpointer data) |
172 | { |
173 | GtkEmojiCompletion *completion = data; |
174 | |
175 | emoji_activated (GTK_WIDGET (child), completion); |
176 | } |
177 | |
178 | static void |
179 | move_active_row (GtkEmojiCompletion *completion, |
180 | int direction) |
181 | { |
182 | GtkWidget *child; |
183 | |
184 | for (child = gtk_widget_get_first_child (widget: completion->list); |
185 | child != NULL; |
186 | child = gtk_widget_get_next_sibling (widget: child)) |
187 | gtk_widget_unset_state_flags (widget: child, flags: GTK_STATE_FLAG_FOCUSED); |
188 | |
189 | if (completion->active != NULL) |
190 | { |
191 | if (direction == 1) |
192 | completion->active = gtk_widget_get_next_sibling (widget: completion->active); |
193 | else |
194 | completion->active = gtk_widget_get_prev_sibling (widget: completion->active); |
195 | } |
196 | |
197 | if (completion->active == NULL) |
198 | { |
199 | if (direction == 1) |
200 | completion->active = gtk_widget_get_first_child (widget: completion->list); |
201 | else |
202 | completion->active = gtk_widget_get_last_child (widget: completion->list); |
203 | } |
204 | |
205 | if (completion->active != NULL) |
206 | gtk_widget_set_state_flags (widget: completion->active, flags: GTK_STATE_FLAG_FOCUSED, FALSE); |
207 | |
208 | if (completion->active_variation) |
209 | { |
210 | gtk_widget_unset_state_flags (widget: completion->active_variation, flags: GTK_STATE_FLAG_FOCUSED); |
211 | completion->active_variation = NULL; |
212 | } |
213 | } |
214 | |
215 | static void |
216 | activate_active_row (GtkEmojiCompletion *completion) |
217 | { |
218 | if (GTK_IS_FLOW_BOX_CHILD (completion->active_variation)) |
219 | emoji_activated (row: completion->active_variation, completion); |
220 | else if (completion->active != NULL) |
221 | emoji_activated (row: completion->active, completion); |
222 | } |
223 | |
224 | static void |
225 | show_variations (GtkEmojiCompletion *completion, |
226 | GtkWidget *row, |
227 | gboolean visible) |
228 | { |
229 | GtkWidget *stack; |
230 | GtkWidget *box; |
231 | GtkWidget *child; |
232 | gboolean is_visible; |
233 | |
234 | if (!row) |
235 | return; |
236 | |
237 | stack = GTK_WIDGET (g_object_get_data (G_OBJECT (row), "stack" )); |
238 | box = gtk_stack_get_child_by_name (GTK_STACK (stack), name: "variations" ); |
239 | if (!box) |
240 | return; |
241 | |
242 | is_visible = gtk_stack_get_visible_child (GTK_STACK (stack)) == box; |
243 | if (is_visible == visible) |
244 | return; |
245 | |
246 | gtk_stack_set_visible_child_name (GTK_STACK (stack), name: visible ? "variations" : "text" ); |
247 | for (child = gtk_widget_get_first_child (widget: box); child; child = gtk_widget_get_next_sibling (widget: child)) |
248 | gtk_widget_unset_state_flags (widget: child, flags: GTK_STATE_FLAG_FOCUSED); |
249 | completion->active_variation = NULL; |
250 | } |
251 | |
252 | static gboolean |
253 | move_active_variation (GtkEmojiCompletion *completion, |
254 | int direction) |
255 | { |
256 | GtkWidget *base; |
257 | GtkWidget *stack; |
258 | GtkWidget *box; |
259 | GtkWidget *next; |
260 | |
261 | if (!completion->active) |
262 | return FALSE; |
263 | |
264 | base = GTK_WIDGET (g_object_get_data (G_OBJECT (completion->active), "base" )); |
265 | stack = GTK_WIDGET (g_object_get_data (G_OBJECT (completion->active), "stack" )); |
266 | box = gtk_stack_get_child_by_name (GTK_STACK (stack), name: "variations" ); |
267 | |
268 | if (gtk_stack_get_visible_child (GTK_STACK (stack)) != box) |
269 | return FALSE; |
270 | |
271 | next = NULL; |
272 | |
273 | if (!completion->active_variation) |
274 | next = base; |
275 | else if (completion->active_variation == base && direction == 1) |
276 | next = gtk_widget_get_first_child (widget: box); |
277 | else if (completion->active_variation == gtk_widget_get_first_child (widget: box) && direction == -1) |
278 | next = base; |
279 | else if (direction == 1) |
280 | next = gtk_widget_get_next_sibling (widget: completion->active_variation); |
281 | else if (direction == -1) |
282 | next = gtk_widget_get_prev_sibling (widget: completion->active_variation); |
283 | |
284 | if (next) |
285 | { |
286 | if (completion->active_variation) |
287 | gtk_widget_unset_state_flags (widget: completion->active_variation, flags: GTK_STATE_FLAG_FOCUSED); |
288 | completion->active_variation = next; |
289 | gtk_widget_set_state_flags (widget: completion->active_variation, flags: GTK_STATE_FLAG_FOCUSED, FALSE); |
290 | } |
291 | |
292 | return next != NULL; |
293 | } |
294 | |
295 | static gboolean |
296 | key_press_cb (GtkEventControllerKey *key, |
297 | guint keyval, |
298 | guint keycode, |
299 | GdkModifierType modifiers, |
300 | GtkEmojiCompletion *completion) |
301 | { |
302 | if (!gtk_widget_get_visible (GTK_WIDGET (completion))) |
303 | return FALSE; |
304 | |
305 | if (keyval == GDK_KEY_Escape) |
306 | { |
307 | gtk_popover_popdown (GTK_POPOVER (completion)); |
308 | return TRUE; |
309 | } |
310 | |
311 | if (keyval == GDK_KEY_Tab) |
312 | { |
313 | show_variations (completion, row: completion->active, FALSE); |
314 | |
315 | guint offset = completion->offset + MAX_ROWS; |
316 | if (offset >= completion->n_matches) |
317 | offset = 0; |
318 | populate_completion (completion, text: completion->text, offset); |
319 | return TRUE; |
320 | } |
321 | |
322 | if (keyval == GDK_KEY_Up) |
323 | { |
324 | show_variations (completion, row: completion->active, FALSE); |
325 | |
326 | move_active_row (completion, direction: -1); |
327 | return TRUE; |
328 | } |
329 | |
330 | if (keyval == GDK_KEY_Down) |
331 | { |
332 | show_variations (completion, row: completion->active, FALSE); |
333 | |
334 | move_active_row (completion, direction: 1); |
335 | return TRUE; |
336 | } |
337 | |
338 | if (keyval == GDK_KEY_Return || |
339 | keyval == GDK_KEY_KP_Enter || |
340 | keyval == GDK_KEY_ISO_Enter) |
341 | { |
342 | activate_active_row (completion); |
343 | return TRUE; |
344 | } |
345 | |
346 | if (keyval == GDK_KEY_Right) |
347 | { |
348 | show_variations (completion, row: completion->active, TRUE); |
349 | move_active_variation (completion, direction: 1); |
350 | return TRUE; |
351 | } |
352 | |
353 | if (keyval == GDK_KEY_Left) |
354 | { |
355 | if (!move_active_variation (completion, direction: -1)) |
356 | show_variations (completion, row: completion->active, FALSE); |
357 | return TRUE; |
358 | } |
359 | |
360 | return FALSE; |
361 | } |
362 | |
363 | static gboolean |
364 | focus_out_cb (GtkWidget *text, |
365 | GParamSpec *pspec, |
366 | GtkEmojiCompletion *completion) |
367 | { |
368 | if (!gtk_widget_has_focus (widget: text)) |
369 | gtk_popover_popdown (GTK_POPOVER (completion)); |
370 | return FALSE; |
371 | } |
372 | |
373 | static void |
374 | connect_signals (GtkEmojiCompletion *completion, |
375 | GtkText *entry) |
376 | { |
377 | GtkEventController *key_controller; |
378 | |
379 | completion->entry = g_object_ref (entry); |
380 | key_controller = gtk_text_get_key_controller (entry); |
381 | |
382 | g_signal_connect (key_controller, "key-pressed" , G_CALLBACK (key_press_cb), completion); |
383 | completion->changed_id = g_signal_connect (entry, "changed" , G_CALLBACK (changed_cb), completion); |
384 | g_signal_connect (entry, "notify::has-focus" , G_CALLBACK (focus_out_cb), completion); |
385 | } |
386 | |
387 | static void |
388 | disconnect_signals (GtkEmojiCompletion *completion) |
389 | { |
390 | GtkEventController *key_controller; |
391 | |
392 | key_controller = gtk_text_get_key_controller (entry: completion->entry); |
393 | |
394 | g_signal_handlers_disconnect_by_func (completion->entry, changed_cb, completion); |
395 | g_signal_handlers_disconnect_by_func (key_controller, key_press_cb, completion); |
396 | g_signal_handlers_disconnect_by_func (completion->entry, focus_out_cb, completion); |
397 | |
398 | g_clear_object (&completion->entry); |
399 | } |
400 | |
401 | static gboolean |
402 | has_variations (GVariant *emoji_data) |
403 | { |
404 | GVariant *codes; |
405 | gsize i; |
406 | gboolean has_variations; |
407 | |
408 | has_variations = FALSE; |
409 | codes = g_variant_get_child_value (value: emoji_data, index_: 0); |
410 | for (i = 0; i < g_variant_n_children (value: codes); i++) |
411 | { |
412 | gunichar code; |
413 | g_variant_get_child (value: codes, index_: i, format_string: "u" , &code); |
414 | if (code == 0) |
415 | { |
416 | has_variations = TRUE; |
417 | break; |
418 | } |
419 | } |
420 | g_variant_unref (value: codes); |
421 | |
422 | return has_variations; |
423 | } |
424 | |
425 | static void |
426 | get_text (GVariant *emoji_data, |
427 | gunichar modifier, |
428 | char *text, |
429 | gsize length) |
430 | { |
431 | GVariant *codes; |
432 | gsize i; |
433 | char *p; |
434 | |
435 | p = text; |
436 | codes = g_variant_get_child_value (value: emoji_data, index_: 0); |
437 | for (i = 0; i < g_variant_n_children (value: codes); i++) |
438 | { |
439 | gunichar code; |
440 | |
441 | g_variant_get_child (value: codes, index_: i, format_string: "u" , &code); |
442 | if (code == 0) |
443 | code = modifier; |
444 | if (code != 0) |
445 | p += g_unichar_to_utf8 (c: code, outbuf: p); |
446 | } |
447 | g_variant_unref (value: codes); |
448 | p += g_unichar_to_utf8 (c: 0xFE0F, outbuf: p); /* U+FE0F is the Emoji variation selector */ |
449 | p[0] = 0; |
450 | } |
451 | |
452 | static void |
453 | add_emoji_variation (GtkWidget *box, |
454 | GVariant *emoji_data, |
455 | gunichar modifier) |
456 | { |
457 | GtkWidget *child; |
458 | GtkWidget *label; |
459 | PangoAttrList *attrs; |
460 | char text[64]; |
461 | |
462 | get_text (emoji_data, modifier, text, length: 64); |
463 | |
464 | label = gtk_label_new (str: text); |
465 | attrs = pango_attr_list_new (); |
466 | pango_attr_list_insert (list: attrs, attr: pango_attr_scale_new (PANGO_SCALE_X_LARGE)); |
467 | gtk_label_set_attributes (GTK_LABEL (label), attrs); |
468 | pango_attr_list_unref (list: attrs); |
469 | |
470 | child = g_object_new (GTK_TYPE_FLOW_BOX_CHILD, first_property_name: "css-name" , "emoji" , NULL); |
471 | g_object_set_data_full (G_OBJECT (child), key: "text" , data: g_strdup (str: text), destroy: g_free); |
472 | g_object_set_data_full (G_OBJECT (child), key: "emoji-data" , |
473 | data: g_variant_ref (value: emoji_data), |
474 | destroy: (GDestroyNotify)g_variant_unref); |
475 | if (modifier != 0) |
476 | g_object_set_data (G_OBJECT (child), key: "modifier" , GUINT_TO_POINTER (modifier)); |
477 | |
478 | gtk_flow_box_child_set_child (GTK_FLOW_BOX_CHILD (child), child: label); |
479 | gtk_flow_box_insert (GTK_FLOW_BOX (box), widget: child, position: -1); |
480 | } |
481 | |
482 | static void |
483 | add_emoji (GtkWidget *list, |
484 | GVariant *emoji_data, |
485 | GtkEmojiCompletion *completion) |
486 | { |
487 | GtkWidget *child; |
488 | GtkWidget *label; |
489 | GtkWidget *box; |
490 | PangoAttrList *attrs; |
491 | char text[64]; |
492 | const char *name; |
493 | GtkWidget *stack; |
494 | gunichar modifier; |
495 | |
496 | get_text (emoji_data, modifier: 0, text, length: 64); |
497 | |
498 | label = gtk_label_new (str: text); |
499 | attrs = pango_attr_list_new (); |
500 | pango_attr_list_insert (list: attrs, attr: pango_attr_scale_new (PANGO_SCALE_X_LARGE)); |
501 | gtk_label_set_attributes (GTK_LABEL (label), attrs); |
502 | pango_attr_list_unref (list: attrs); |
503 | gtk_widget_add_css_class (widget: label, css_class: "emoji" ); |
504 | |
505 | child = g_object_new (GTK_TYPE_LIST_BOX_ROW, first_property_name: "css-name" , "emoji-completion-row" , NULL); |
506 | gtk_widget_set_focus_on_click (widget: child, FALSE); |
507 | box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0); |
508 | gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (child), child: box); |
509 | gtk_box_append (GTK_BOX (box), child: label); |
510 | g_object_set_data (G_OBJECT (child), key: "base" , data: label); |
511 | |
512 | stack = gtk_stack_new (); |
513 | gtk_stack_set_hhomogeneous (GTK_STACK (stack), TRUE); |
514 | gtk_stack_set_vhomogeneous (GTK_STACK (stack), TRUE); |
515 | gtk_stack_set_transition_type (GTK_STACK (stack), transition: GTK_STACK_TRANSITION_TYPE_OVER_RIGHT_LEFT); |
516 | gtk_box_append (GTK_BOX (box), child: stack); |
517 | g_object_set_data (G_OBJECT (child), key: "stack" , data: stack); |
518 | |
519 | g_variant_get_child (value: emoji_data, index_: 1, format_string: "&s" , &name); |
520 | label = gtk_label_new (str: name); |
521 | gtk_label_set_xalign (GTK_LABEL (label), xalign: 0); |
522 | |
523 | gtk_stack_add_named (GTK_STACK (stack), child: label, name: "text" ); |
524 | |
525 | if (has_variations (emoji_data)) |
526 | { |
527 | box = gtk_flow_box_new (); |
528 | gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE); |
529 | gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), n_children: 5); |
530 | gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), n_children: 5); |
531 | gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE); |
532 | gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), mode: GTK_SELECTION_NONE); |
533 | g_signal_connect (box, "child-activated" , G_CALLBACK (child_activated), completion); |
534 | for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++) |
535 | add_emoji_variation (box, emoji_data, modifier); |
536 | |
537 | gtk_stack_add_named (GTK_STACK (stack), child: box, name: "variations" ); |
538 | } |
539 | |
540 | g_object_set_data_full (G_OBJECT (child), key: "text" , data: g_strdup (str: text), destroy: g_free); |
541 | g_object_set_data_full (G_OBJECT (child), key: "emoji-data" , |
542 | data: g_variant_ref (value: emoji_data), destroy: (GDestroyNotify)g_variant_unref); |
543 | |
544 | gtk_list_box_insert (GTK_LIST_BOX (list), child, position: -1); |
545 | } |
546 | |
547 | static int |
548 | populate_completion (GtkEmojiCompletion *completion, |
549 | const char *text, |
550 | guint offset) |
551 | { |
552 | guint n_matches; |
553 | guint n_added; |
554 | GVariantIter iter; |
555 | GVariant *item; |
556 | GtkWidget *child; |
557 | |
558 | if (completion->text != text) |
559 | { |
560 | g_free (mem: completion->text); |
561 | completion->text = g_strdup (str: text); |
562 | completion->length = g_utf8_strlen (p: text, max: -1); |
563 | } |
564 | completion->offset = offset; |
565 | |
566 | while ((child = gtk_widget_get_first_child (widget: completion->list))) |
567 | gtk_list_box_remove (GTK_LIST_BOX (completion->list), child); |
568 | |
569 | completion->active = NULL; |
570 | |
571 | n_matches = 0; |
572 | n_added = 0; |
573 | g_variant_iter_init (iter: &iter, value: completion->data); |
574 | while ((item = g_variant_iter_next_value (iter: &iter))) |
575 | { |
576 | const char *name; |
577 | |
578 | g_variant_get_child (value: item, index_: 1, format_string: "&s" , &name); |
579 | |
580 | if (g_str_has_prefix (str: name, prefix: text + 1)) |
581 | { |
582 | n_matches++; |
583 | |
584 | if (n_matches > offset && n_added < MAX_ROWS) |
585 | { |
586 | add_emoji (list: completion->list, emoji_data: item, completion); |
587 | n_added++; |
588 | } |
589 | } |
590 | } |
591 | |
592 | completion->n_matches = n_matches; |
593 | |
594 | if (n_added > 0) |
595 | { |
596 | completion->active = gtk_widget_get_first_child (widget: completion->list); |
597 | gtk_widget_set_state_flags (widget: completion->active, flags: GTK_STATE_FLAG_FOCUSED, FALSE); |
598 | } |
599 | |
600 | return n_added; |
601 | } |
602 | |
603 | static void |
604 | long_pressed_cb (GtkGesture *gesture, |
605 | double x, |
606 | double y, |
607 | gpointer data) |
608 | { |
609 | GtkEmojiCompletion *completion = data; |
610 | GtkWidget *row; |
611 | |
612 | row = GTK_WIDGET (gtk_list_box_get_row_at_y (GTK_LIST_BOX (completion->list), y)); |
613 | if (!row) |
614 | return; |
615 | |
616 | show_variations (completion, row, TRUE); |
617 | } |
618 | |
619 | static void |
620 | gtk_emoji_completion_init (GtkEmojiCompletion *completion) |
621 | { |
622 | GBytes *bytes = NULL; |
623 | GtkGesture *long_press; |
624 | |
625 | gtk_widget_init_template (GTK_WIDGET (completion)); |
626 | |
627 | bytes = get_emoji_data (); |
628 | completion->data = g_variant_ref_sink (value: g_variant_new_from_bytes (G_VARIANT_TYPE ("a(ausasu)" ), bytes, TRUE)); |
629 | |
630 | g_bytes_unref (bytes); |
631 | |
632 | long_press = gtk_gesture_long_press_new (); |
633 | g_signal_connect (long_press, "pressed" , G_CALLBACK (long_pressed_cb), completion); |
634 | gtk_widget_add_controller (widget: completion->list, GTK_EVENT_CONTROLLER (long_press)); |
635 | } |
636 | |
637 | static void |
638 | gtk_emoji_completion_class_init (GtkEmojiCompletionClass *klass) |
639 | { |
640 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
641 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
642 | |
643 | object_class->finalize = gtk_emoji_completion_finalize; |
644 | |
645 | gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/org/gtk/libgtk/ui/gtkemojicompletion.ui" ); |
646 | |
647 | gtk_widget_class_bind_template_child (widget_class, GtkEmojiCompletion, list); |
648 | |
649 | gtk_widget_class_bind_template_callback (widget_class, row_activated); |
650 | } |
651 | |
652 | GtkWidget * |
653 | gtk_emoji_completion_new (GtkText *text) |
654 | { |
655 | GtkEmojiCompletion *completion; |
656 | |
657 | completion = GTK_EMOJI_COMPLETION (g_object_new (GTK_TYPE_EMOJI_COMPLETION, NULL)); |
658 | gtk_widget_set_parent (GTK_WIDGET (completion), GTK_WIDGET (text)); |
659 | |
660 | connect_signals (completion, entry: text); |
661 | |
662 | return GTK_WIDGET (completion); |
663 | } |
664 | |