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
37struct _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
55struct _GtkEmojiCompletionClass {
56 GtkPopoverClass parent_class;
57};
58
59static void connect_signals (GtkEmojiCompletion *completion,
60 GtkText *text);
61static void disconnect_signals (GtkEmojiCompletion *completion);
62static int populate_completion (GtkEmojiCompletion *completion,
63 const char *text,
64 guint offset);
65
66#define MAX_ROWS 5
67
68G_DEFINE_TYPE (GtkEmojiCompletion, gtk_emoji_completion, GTK_TYPE_POPOVER)
69
70static void
71gtk_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
83static void
84update_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 {
103next:
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
131static void
132changed_cb (GtkText *text,
133 GtkEmojiCompletion *completion)
134{
135 update_completion (completion);
136}
137
138static void
139emoji_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
158static void
159row_activated (GtkListBox *list,
160 GtkListBoxRow *row,
161 gpointer data)
162{
163 GtkEmojiCompletion *completion = data;
164
165 emoji_activated (GTK_WIDGET (row), completion);
166}
167
168static void
169child_activated (GtkFlowBox *box,
170 GtkFlowBoxChild *child,
171 gpointer data)
172{
173 GtkEmojiCompletion *completion = data;
174
175 emoji_activated (GTK_WIDGET (child), completion);
176}
177
178static void
179move_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
215static void
216activate_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
224static void
225show_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
252static gboolean
253move_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
295static gboolean
296key_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
363static gboolean
364focus_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
373static void
374connect_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
387static void
388disconnect_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
401static gboolean
402has_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
425static void
426get_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
452static void
453add_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
482static void
483add_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
547static int
548populate_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
603static void
604long_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
619static void
620gtk_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
637static void
638gtk_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
652GtkWidget *
653gtk_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

source code of gtk/gtk/gtkemojicompletion.c