1 /*
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 "evolution-config.h"
19
20 /* Copied and adapted a bit from gtk+'s gtkemojichooser.h,
21 waiting for it to be made public:
22 https://gitlab.gnome.org/GNOME/gtk/issues/86
23 */
24
25 #include <gtk/gtk.h>
26 #include <glib/gi18n-lib.h>
27
28 #include "e-gtkemojichooser.h"
29
30 #define BOX_SPACE 6
31
32 typedef struct {
33 GtkWidget *box;
34 GtkWidget *heading;
35 GtkWidget *button;
36 const char *first;
37 gunichar label;
38 gboolean empty;
39 } EmojiSection;
40
41 struct _EGtkEmojiChooser
42 {
43 GtkPopover parent_instance;
44
45 GtkWidget *search_entry;
46 GtkWidget *stack;
47 GtkWidget *scrolled_window;
48
49 int emoji_max_width;
50
51 EmojiSection recent;
52 EmojiSection people;
53 EmojiSection body;
54 EmojiSection nature;
55 EmojiSection food;
56 EmojiSection travel;
57 EmojiSection activities;
58 EmojiSection objects;
59 EmojiSection symbols;
60 EmojiSection flags;
61
62 GtkGesture *recent_long_press;
63 GtkGesture *recent_multi_press;
64 GtkGesture *people_long_press;
65 GtkGesture *people_multi_press;
66 GtkGesture *body_long_press;
67 GtkGesture *body_multi_press;
68
69 GVariant *data;
70 GtkWidget *box;
71 GVariantIter *iter;
72 guint populate_idle;
73
74 GSettings *settings;
75 };
76
77 struct _EGtkEmojiChooserClass {
78 GtkPopoverClass parent_class;
79 };
80
81 enum {
82 EMOJI_PICKED,
83 LAST_SIGNAL
84 };
85
86 static int signals[LAST_SIGNAL];
87
G_DEFINE_TYPE(EGtkEmojiChooser,e_gtk_emoji_chooser,GTK_TYPE_POPOVER)88 G_DEFINE_TYPE (EGtkEmojiChooser, e_gtk_emoji_chooser, GTK_TYPE_POPOVER)
89
90 static void
91 e_gtk_emoji_chooser_finalize (GObject *object)
92 {
93 EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (object);
94
95 if (chooser->populate_idle)
96 g_source_remove (chooser->populate_idle);
97
98 g_variant_unref (chooser->data);
99 g_object_unref (chooser->settings);
100
101 g_clear_object (&chooser->recent_long_press);
102 g_clear_object (&chooser->recent_multi_press);
103 g_clear_object (&chooser->people_long_press);
104 g_clear_object (&chooser->people_multi_press);
105 g_clear_object (&chooser->body_long_press);
106 g_clear_object (&chooser->body_multi_press);
107
108 G_OBJECT_CLASS (e_gtk_emoji_chooser_parent_class)->finalize (object);
109 }
110
111 static void
scroll_to_section(GtkButton * button,gpointer data)112 scroll_to_section (GtkButton *button,
113 gpointer data)
114 {
115 EmojiSection *section = data;
116 EGtkEmojiChooser *chooser;
117 GtkAdjustment *adj;
118 GtkAllocation alloc = { 0, 0, 0, 0 };
119
120 chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), E_GTK_TYPE_EMOJI_CHOOSER));
121
122 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
123
124 if (section->heading)
125 gtk_widget_get_allocation (section->heading, &alloc);
126
127 gtk_adjustment_set_value (adj, alloc.y - BOX_SPACE);
128 }
129
130 static void
131 add_emoji (GtkWidget *box,
132 gboolean prepend,
133 GVariant *item,
134 gunichar modifier,
135 EGtkEmojiChooser *chooser);
136
137 #define MAX_RECENT (7*3)
138
139 static void
populate_recent_section(EGtkEmojiChooser * chooser)140 populate_recent_section (EGtkEmojiChooser *chooser)
141 {
142 GVariant *variant;
143 GVariant *item;
144 GVariantIter iter;
145 gboolean empty = FALSE;
146
147 variant = g_settings_get_value (chooser->settings, "recent-emoji");
148 g_variant_iter_init (&iter, variant);
149 while ((item = g_variant_iter_next_value (&iter)))
150 {
151 GVariant *emoji_data;
152 gunichar modifier;
153
154 emoji_data = g_variant_get_child_value (item, 0);
155 g_variant_get_child (item, 1, "u", &modifier);
156 add_emoji (chooser->recent.box, FALSE, emoji_data, modifier, chooser);
157 g_variant_unref (emoji_data);
158 g_variant_unref (item);
159 empty = FALSE;
160 }
161
162 if (!empty)
163 {
164 gtk_widget_show (chooser->recent.box);
165 gtk_widget_set_sensitive (chooser->recent.button, TRUE);
166 }
167 g_variant_unref (variant);
168 }
169
170 static void
add_recent_item(EGtkEmojiChooser * chooser,GVariant * item,gunichar modifier)171 add_recent_item (EGtkEmojiChooser *chooser,
172 GVariant *item,
173 gunichar modifier)
174 {
175 GList *children, *l;
176 int i;
177 GVariantBuilder builder;
178
179 g_variant_ref (item);
180
181 g_variant_builder_init (&builder, G_VARIANT_TYPE ("a((auss)u)"));
182 g_variant_builder_add (&builder, "(@(auss)u)", item, modifier);
183
184 children = gtk_container_get_children (GTK_CONTAINER (chooser->recent.box));
185 for (l = children, i = 1; l; l = l->next, i++)
186 {
187 GVariant *item2 = g_object_get_data (G_OBJECT (l->data), "emoji-data");
188 gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier"));
189
190 if (modifier == modifier2 && g_variant_equal (item, item2))
191 {
192 gtk_widget_destroy (GTK_WIDGET (l->data));
193 i--;
194 continue;
195 }
196 if (i >= MAX_RECENT)
197 {
198 gtk_widget_destroy (GTK_WIDGET (l->data));
199 continue;
200 }
201
202 g_variant_builder_add (&builder, "(@(auss)u)", item2, modifier2);
203 }
204 g_list_free (children);
205
206 add_emoji (chooser->recent.box, TRUE, item, modifier, chooser);
207
208 /* Enable recent */
209 gtk_widget_show (chooser->recent.box);
210 gtk_widget_set_sensitive (chooser->recent.button, TRUE);
211
212 g_settings_set_value (chooser->settings, "recent-emoji", g_variant_builder_end (&builder));
213
214 g_variant_unref (item);
215 }
216
217 static void
emoji_activated(GtkFlowBox * box,GtkFlowBoxChild * child,gpointer data)218 emoji_activated (GtkFlowBox *box,
219 GtkFlowBoxChild *child,
220 gpointer data)
221 {
222 EGtkEmojiChooser *chooser = data;
223 char *text;
224 GtkWidget *ebox;
225 GtkWidget *label;
226 GVariant *item;
227 gunichar modifier;
228
229 gtk_popover_popdown (GTK_POPOVER (chooser));
230
231 ebox = gtk_bin_get_child (GTK_BIN (child));
232 label = gtk_bin_get_child (GTK_BIN (ebox));
233 text = g_strdup (gtk_label_get_label (GTK_LABEL (label)));
234
235 item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
236 modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier"));
237 add_recent_item (chooser, item, modifier);
238
239 g_signal_emit (data, signals[EMOJI_PICKED], 0, text);
240 g_free (text);
241 }
242
243 static gboolean
has_variations(GVariant * emoji_data)244 has_variations (GVariant *emoji_data)
245 {
246 GVariant *codes;
247 int i;
248 gboolean has_variations;
249
250 has_variations = FALSE;
251 codes = g_variant_get_child_value (emoji_data, 0);
252 for (i = 0; i < g_variant_n_children (codes); i++)
253 {
254 gunichar code;
255 g_variant_get_child (codes, i, "u", &code);
256 if (code == 0)
257 {
258 has_variations = TRUE;
259 break;
260 }
261 }
262 g_variant_unref (codes);
263
264 return has_variations;
265 }
266
267 static void
show_variations(EGtkEmojiChooser * chooser,GtkWidget * child)268 show_variations (EGtkEmojiChooser *chooser,
269 GtkWidget *child)
270 {
271 GtkWidget *popover;
272 GtkWidget *view;
273 GtkWidget *box;
274 GVariant *emoji_data;
275 GtkWidget *parent_popover;
276 gunichar modifier;
277
278 if (!child)
279 return;
280
281 emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
282 if (!emoji_data)
283 return;
284
285 if (!has_variations (emoji_data))
286 return;
287
288 parent_popover = gtk_widget_get_ancestor (child, GTK_TYPE_POPOVER);
289 popover = gtk_popover_new (child);
290 view = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
291 gtk_style_context_add_class (gtk_widget_get_style_context (view), "view");
292 box = gtk_flow_box_new ();
293 gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE);
294 gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), 6);
295 gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), 6);
296 gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE);
297 gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), GTK_SELECTION_NONE);
298 gtk_container_add (GTK_CONTAINER (popover), view);
299 gtk_container_add (GTK_CONTAINER (view), box);
300
301 g_signal_connect (box, "child-activated", G_CALLBACK (emoji_activated), parent_popover);
302
303 add_emoji (box, FALSE, emoji_data, 0, chooser);
304 for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++)
305 add_emoji (box, FALSE, emoji_data, modifier, chooser);
306
307 gtk_widget_show_all (view);
308 gtk_popover_popup (GTK_POPOVER (popover));
309 }
310
311 static void
update_hover(GtkWidget * widget,GdkEvent * event,gpointer data)312 update_hover (GtkWidget *widget,
313 GdkEvent *event,
314 gpointer data)
315 {
316 if (event->type == GDK_ENTER_NOTIFY)
317 gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
318 else
319 gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_PRELIGHT);
320 }
321
322 static void
long_pressed_cb(GtkGesture * gesture,double x,double y,gpointer data)323 long_pressed_cb (GtkGesture *gesture,
324 double x,
325 double y,
326 gpointer data)
327 {
328 EGtkEmojiChooser *chooser = data;
329 GtkWidget *box;
330 GtkWidget *child;
331
332 box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
333 child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y));
334 show_variations (chooser, child);
335 }
336
337 static void
pressed_cb(GtkGesture * gesture,int n_press,double x,double y,gpointer data)338 pressed_cb (GtkGesture *gesture,
339 int n_press,
340 double x,
341 double y,
342 gpointer data)
343 {
344 EGtkEmojiChooser *chooser = data;
345 GtkWidget *box;
346 GtkWidget *child;
347
348 box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
349 child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y));
350 show_variations (chooser, child);
351 }
352
353 static gboolean
popup_menu(GtkWidget * widget,gpointer data)354 popup_menu (GtkWidget *widget,
355 gpointer data)
356 {
357 EGtkEmojiChooser *chooser = data;
358
359 show_variations (chooser, widget);
360 return TRUE;
361 }
362
363 static void
add_emoji(GtkWidget * box,gboolean prepend,GVariant * item,gunichar modifier,EGtkEmojiChooser * chooser)364 add_emoji (GtkWidget *box,
365 gboolean prepend,
366 GVariant *item,
367 gunichar modifier,
368 EGtkEmojiChooser *chooser)
369 {
370 GtkWidget *child;
371 GtkWidget *ebox;
372 GtkWidget *label;
373 PangoAttrList *attrs;
374 GVariant *codes;
375 char text[64];
376 char *p = text;
377 int i;
378 PangoLayout *layout;
379 PangoRectangle rect;
380
381 codes = g_variant_get_child_value (item, 0);
382 for (i = 0; i < g_variant_n_children (codes); i++)
383 {
384 gunichar code;
385
386 g_variant_get_child (codes, i, "u", &code);
387 if (code == 0)
388 code = modifier;
389 if (code != 0)
390 p += g_unichar_to_utf8 (code, p);
391 }
392 g_variant_unref (codes);
393 p += g_unichar_to_utf8 (0xFE0F, p); /* U+FE0F is the Emoji variation selector */
394 p[0] = 0;
395
396 label = gtk_label_new (text);
397 attrs = pango_attr_list_new ();
398 pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE));
399 gtk_label_set_attributes (GTK_LABEL (label), attrs);
400 pango_attr_list_unref (attrs);
401
402 layout = gtk_label_get_layout (GTK_LABEL (label));
403 pango_layout_get_extents (layout, &rect, NULL);
404
405 /* Check for fallback rendering that generates too wide items */
406 if (pango_layout_get_unknown_glyphs_count (layout) > 0 ||
407 rect.width >= 1.5 * chooser->emoji_max_width)
408 {
409 gtk_widget_destroy (label);
410 return;
411 }
412
413 child = gtk_flow_box_child_new ();
414 gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji");
415 g_object_set_data_full (G_OBJECT (child), "emoji-data",
416 g_variant_ref (item),
417 (GDestroyNotify)g_variant_unref);
418 if (modifier != 0)
419 g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier));
420
421 ebox = gtk_event_box_new ();
422 gtk_widget_add_events (ebox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
423 g_signal_connect (ebox, "enter-notify-event", G_CALLBACK (update_hover), FALSE);
424 g_signal_connect (ebox, "leave-notify-event", G_CALLBACK (update_hover), FALSE);
425 gtk_container_add (GTK_CONTAINER (child), ebox);
426 gtk_container_add (GTK_CONTAINER (ebox), label);
427 gtk_widget_show_all (child);
428
429 if (chooser)
430 g_signal_connect (child, "popup-menu", G_CALLBACK (popup_menu), chooser);
431
432 gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1);
433 }
434
435 static gboolean
populate_emoji_chooser(gpointer data)436 populate_emoji_chooser (gpointer data)
437 {
438 EGtkEmojiChooser *chooser = data;
439 GBytes *bytes = NULL;
440 GVariant *item;
441 guint64 start, now;
442
443 start = g_get_monotonic_time ();
444
445 if (!chooser->data)
446 {
447 bytes = g_resources_lookup_data ("/org.gnome.Evolution/emoji.data", 0, NULL);
448 chooser->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, TRUE));
449 }
450
451 if (!chooser->iter)
452 {
453 chooser->iter = g_variant_iter_new (chooser->data);
454 chooser->box = chooser->people.box;
455 }
456 while ((item = g_variant_iter_next_value (chooser->iter)))
457 {
458 const char *name;
459
460 g_variant_get_child (item, 1, "&s", &name);
461
462 if (strcmp (name, chooser->body.first) == 0)
463 chooser->box = chooser->body.box;
464 else if (strcmp (name, chooser->nature.first) == 0)
465 chooser->box = chooser->nature.box;
466 else if (strcmp (name, chooser->food.first) == 0)
467 chooser->box = chooser->food.box;
468 else if (strcmp (name, chooser->travel.first) == 0)
469 chooser->box = chooser->travel.box;
470 else if (strcmp (name, chooser->activities.first) == 0)
471 chooser->box = chooser->activities.box;
472 else if (strcmp (name, chooser->objects.first) == 0)
473 chooser->box = chooser->objects.box;
474 else if (strcmp (name, chooser->symbols.first) == 0)
475 chooser->box = chooser->symbols.box;
476 else if (strcmp (name, chooser->flags.first) == 0)
477 chooser->box = chooser->flags.box;
478
479 add_emoji (chooser->box, FALSE, item, 0, chooser);
480 g_variant_unref (item);
481
482 now = g_get_monotonic_time ();
483 if (now > start + 8000)
484 return G_SOURCE_CONTINUE;
485 }
486
487 /* We scroll to the top on show, so check the right button for the 1st time */
488 gtk_widget_set_state_flags (chooser->recent.button, GTK_STATE_FLAG_CHECKED, FALSE);
489
490 g_variant_iter_free (chooser->iter);
491 chooser->iter = NULL;
492 chooser->box = NULL;
493 chooser->populate_idle = 0;
494
495 return G_SOURCE_REMOVE;
496 }
497
498 static void
adj_value_changed(GtkAdjustment * adj,gpointer data)499 adj_value_changed (GtkAdjustment *adj,
500 gpointer data)
501 {
502 EGtkEmojiChooser *chooser = data;
503 double value = gtk_adjustment_get_value (adj);
504 EmojiSection const *sections[] = {
505 &chooser->recent,
506 &chooser->people,
507 &chooser->body,
508 &chooser->nature,
509 &chooser->food,
510 &chooser->travel,
511 &chooser->activities,
512 &chooser->objects,
513 &chooser->symbols,
514 &chooser->flags,
515 };
516 EmojiSection const *select_section = sections[0];
517 gsize i;
518
519 /* Figure out which section the current scroll position is within */
520 for (i = 0; i < G_N_ELEMENTS (sections); ++i)
521 {
522 EmojiSection const *section = sections[i];
523 GtkAllocation alloc;
524
525 if (section->heading)
526 gtk_widget_get_allocation (section->heading, &alloc);
527 else
528 gtk_widget_get_allocation (section->box, &alloc);
529
530 if (value < alloc.y - BOX_SPACE)
531 break;
532
533 select_section = section;
534 }
535
536 /* Un/Check the section buttons accordingly */
537 for (i = 0; i < G_N_ELEMENTS (sections); ++i)
538 {
539 EmojiSection const *section = sections[i];
540
541 if (section == select_section)
542 gtk_widget_set_state_flags (section->button, GTK_STATE_FLAG_CHECKED, FALSE);
543 else
544 gtk_widget_unset_state_flags (section->button, GTK_STATE_FLAG_CHECKED);
545 }
546 }
547
548 static gboolean
filter_func(GtkFlowBoxChild * child,gpointer data)549 filter_func (GtkFlowBoxChild *child,
550 gpointer data)
551 {
552 EmojiSection *section = data;
553 EGtkEmojiChooser *chooser;
554 GVariant *emoji_data;
555 const char *text;
556 const char *name;
557 gboolean res;
558
559 res = TRUE;
560
561 chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), E_GTK_TYPE_EMOJI_CHOOSER));
562 text = gtk_entry_get_text (GTK_ENTRY (chooser->search_entry));
563 emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), "emoji-data");
564
565 if (text[0] == 0)
566 goto out;
567
568 if (!emoji_data)
569 goto out;
570
571 g_variant_get_child (emoji_data, 1, "&s", &name);
572 res = g_str_match_string (text, name, TRUE);
573
574 out:
575 if (res)
576 section->empty = FALSE;
577
578 return res;
579 }
580
581 static void
invalidate_section(EmojiSection * section)582 invalidate_section (EmojiSection *section)
583 {
584 section->empty = TRUE;
585 gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box));
586 }
587
588 static void
update_headings(EGtkEmojiChooser * chooser)589 update_headings (EGtkEmojiChooser *chooser)
590 {
591 gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty);
592 gtk_widget_set_visible (chooser->people.box, !chooser->people.empty);
593 gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty);
594 gtk_widget_set_visible (chooser->body.box, !chooser->body.empty);
595 gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty);
596 gtk_widget_set_visible (chooser->nature.box, !chooser->nature.empty);
597 gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty);
598 gtk_widget_set_visible (chooser->food.box, !chooser->food.empty);
599 gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty);
600 gtk_widget_set_visible (chooser->travel.box, !chooser->travel.empty);
601 gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty);
602 gtk_widget_set_visible (chooser->activities.box, !chooser->activities.empty);
603 gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty);
604 gtk_widget_set_visible (chooser->objects.box, !chooser->objects.empty);
605 gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty);
606 gtk_widget_set_visible (chooser->symbols.box, !chooser->symbols.empty);
607 gtk_widget_set_visible (chooser->flags.heading, !chooser->flags.empty);
608 gtk_widget_set_visible (chooser->flags.box, !chooser->flags.empty);
609
610 if (chooser->recent.empty && chooser->people.empty &&
611 chooser->body.empty && chooser->nature.empty &&
612 chooser->food.empty && chooser->travel.empty &&
613 chooser->activities.empty && chooser->objects.empty &&
614 chooser->symbols.empty && chooser->flags.empty)
615 gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "empty");
616 else
617 gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "list");
618 }
619
620 static void
search_changed(GtkEntry * entry,gpointer data)621 search_changed (GtkEntry *entry,
622 gpointer data)
623 {
624 EGtkEmojiChooser *chooser = data;
625
626 invalidate_section (&chooser->recent);
627 invalidate_section (&chooser->people);
628 invalidate_section (&chooser->body);
629 invalidate_section (&chooser->nature);
630 invalidate_section (&chooser->food);
631 invalidate_section (&chooser->travel);
632 invalidate_section (&chooser->activities);
633 invalidate_section (&chooser->objects);
634 invalidate_section (&chooser->symbols);
635 invalidate_section (&chooser->flags);
636
637 update_headings (chooser);
638 }
639
640 static void
setup_section(EGtkEmojiChooser * chooser,EmojiSection * section,const char * first,const char * icon)641 setup_section (EGtkEmojiChooser *chooser,
642 EmojiSection *section,
643 const char *first,
644 const char *icon)
645 {
646 GtkAdjustment *adj;
647 GtkWidget *image;
648
649 section->first = first;
650
651 image = gtk_bin_get_child (GTK_BIN (section->button));
652 gtk_image_set_from_icon_name (GTK_IMAGE (image), icon, GTK_ICON_SIZE_BUTTON);
653
654 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
655
656 gtk_container_set_focus_vadjustment (GTK_CONTAINER (section->box), adj);
657 gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, section, NULL);
658 g_signal_connect (section->button, "clicked", G_CALLBACK (scroll_to_section), section);
659 }
660
661 static void
e_gtk_emoji_chooser_add_group(EGtkEmojiChooser * chooser,GtkBox * emoji_box,GtkBox * button_box,const gchar * label,EmojiSection * section)662 e_gtk_emoji_chooser_add_group (EGtkEmojiChooser *chooser,
663 GtkBox *emoji_box,
664 GtkBox *button_box,
665 const gchar *label,
666 EmojiSection *section)
667 {
668 section->heading = gtk_label_new (label);
669 g_object_set (G_OBJECT (section->heading),
670 "xalign", 0.0,
671 NULL);
672 gtk_box_pack_start (emoji_box, GTK_WIDGET (section->heading), FALSE, FALSE, 0);
673
674 section->box = gtk_flow_box_new ();
675 g_object_set (G_OBJECT (section->box),
676 "homogeneous", TRUE,
677 "selection-mode", GTK_SELECTION_NONE,
678 NULL);
679 gtk_box_pack_start (emoji_box, GTK_WIDGET (section->box), FALSE, FALSE, 0);
680 g_signal_connect (section->box, "child-activated",
681 G_CALLBACK (emoji_activated), chooser);
682
683 section->button = gtk_button_new ();
684 gtk_container_add (GTK_CONTAINER (section->button), gtk_image_new ());
685 gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (section->button)), "emoji-section");
686 g_object_set (G_OBJECT (section->button),
687 "relief", GTK_RELIEF_NONE,
688 "tooltip-text", label,
689 NULL);
690 gtk_box_pack_start (button_box, GTK_WIDGET (section->button), FALSE, FALSE, 0);
691 }
692
693 static void
e_gtk_emoji_chooser_construct_without_template(EGtkEmojiChooser * chooser)694 e_gtk_emoji_chooser_construct_without_template (EGtkEmojiChooser *chooser)
695 {
696 GtkBox *box, *emoji_box, *button_box;
697 GtkGrid *grid;
698 GtkWidget *widget;
699 PangoAttrList *attrs;
700
701 gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser)), "emoji-picker");
702
703 box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
704 gtk_container_add (GTK_CONTAINER (chooser), GTK_WIDGET (box));
705
706 chooser->search_entry = gtk_search_entry_new ();
707 gtk_box_pack_start (box, chooser->search_entry, FALSE, FALSE, 0);
708
709 g_signal_connect (chooser->search_entry, "search-changed", G_CALLBACK (search_changed), chooser);
710
711 chooser->stack = gtk_stack_new ();
712 gtk_box_pack_start (box, chooser->stack, FALSE, FALSE, 0);
713
714 gtk_widget_show_all (GTK_WIDGET (box));
715
716 box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
717 gtk_widget_set_name (GTK_WIDGET (box), "list");
718 gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (box));
719
720 chooser->scrolled_window = gtk_scrolled_window_new (NULL, NULL);
721 g_object_set (G_OBJECT (chooser->scrolled_window),
722 "vexpand", TRUE,
723 "hscrollbar-policy", GTK_POLICY_NEVER,
724 "min-content-height", 250,
725 NULL);
726 gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->scrolled_window)), "view");
727 gtk_box_pack_start (box, chooser->scrolled_window, FALSE, FALSE, 0);
728
729 emoji_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
730 g_object_set (G_OBJECT (emoji_box),
731 "margin", 6,
732 "spacing", 6,
733 NULL);
734 gtk_container_add (GTK_CONTAINER (chooser->scrolled_window), GTK_WIDGET (emoji_box));
735
736 button_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0));
737 gtk_box_pack_start (box, GTK_WIDGET (button_box), FALSE, FALSE, 0);
738
739 chooser->recent.box = gtk_flow_box_new ();
740 g_object_set (G_OBJECT (chooser->recent.box),
741 "homogeneous", TRUE,
742 "selection-mode", GTK_SELECTION_NONE,
743 NULL);
744 gtk_box_pack_start (emoji_box, GTK_WIDGET (chooser->recent.box), FALSE, FALSE, 0);
745 g_signal_connect (chooser->recent.box, "child-activated",
746 G_CALLBACK (emoji_activated), chooser);
747
748 chooser->recent.button = gtk_button_new ();
749 gtk_container_add (GTK_CONTAINER (chooser->recent.button), gtk_image_new ());
750 gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->recent.button)), "emoji-section");
751 g_object_set (G_OBJECT (chooser->recent.button),
752 "relief", GTK_RELIEF_NONE,
753 "tooltip-text", C_("EmojiChooser", "Recent"),
754 NULL);
755 gtk_box_pack_start (button_box, GTK_WIDGET (chooser->recent.button), FALSE, FALSE, 0);
756
757 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Smileys & People"), &chooser->people);
758 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Body & Clothing"), &chooser->body);
759 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Animals & Nature"), &chooser->nature);
760 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Food & Drink"), &chooser->food);
761 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Travel & Places"), &chooser->travel);
762 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Activities"), &chooser->activities);
763 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Objects"), &chooser->objects);
764 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Symbols"), &chooser->symbols);
765 e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("EmojiChooser", "Flags"), &chooser->flags);
766
767 grid = GTK_GRID (gtk_grid_new ());
768 g_object_set (G_OBJECT (grid),
769 "name", "empty",
770 "row-spacing", 12,
771 "halign", GTK_ALIGN_CENTER,
772 "valign", GTK_ALIGN_CENTER,
773 NULL);
774 gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (grid)), "dim-label");
775 gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (grid));
776
777 widget = gtk_image_new ();
778 g_object_set (G_OBJECT (widget),
779 "icon-name", "edit-find-symbolic",
780 "pixel-size", 72,
781 NULL);
782 gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label");
783 gtk_grid_attach (grid, widget, 0, 0, 1, 1);
784
785 attrs = pango_attr_list_new ();
786 pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
787 pango_attr_list_insert (attrs, pango_attr_scale_new (1.44));
788
789 widget = gtk_label_new (C_("EmojiChooser", "No Results Found"));
790 g_object_set (G_OBJECT (widget),
791 "attributes", attrs,
792 NULL);
793 gtk_grid_attach (grid, widget, 0, 1, 1, 1);
794
795 pango_attr_list_unref (attrs);
796
797 widget = gtk_label_new (C_("EmojiChooser", "Try a different search"));
798 gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label");
799 gtk_grid_attach (grid, widget, 0, 1, 1, 1);
800
801 gtk_widget_show_all (GTK_WIDGET (chooser->stack));
802 }
803
804 static void
e_gtk_emoji_chooser_init(EGtkEmojiChooser * chooser)805 e_gtk_emoji_chooser_init (EGtkEmojiChooser *chooser)
806 {
807 GtkAdjustment *adj;
808
809 chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser");
810
811 e_gtk_emoji_chooser_construct_without_template (chooser);
812
813 /* Get a reasonable maximum width for an emoji. We do this to
814 * skip overly wide fallback rendering for certain emojis the
815 * font does not contain and therefore end up being rendered
816 * as multiply glyphs.
817 */
818 {
819 PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET (chooser), "");
820 PangoAttrList *attrs;
821 PangoRectangle rect;
822
823 attrs = pango_attr_list_new ();
824 pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE));
825 pango_layout_set_attributes (layout, attrs);
826 pango_attr_list_unref (attrs);
827
828 pango_layout_get_extents (layout, &rect, NULL);
829 chooser->emoji_max_width = rect.width;
830
831 g_object_unref (layout);
832 }
833
834 chooser->recent_long_press = gtk_gesture_long_press_new (chooser->recent.box);
835 g_signal_connect (chooser->recent_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
836 chooser->recent_multi_press = gtk_gesture_multi_press_new (chooser->recent.box);
837 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->recent_multi_press), GDK_BUTTON_SECONDARY);
838 g_signal_connect (chooser->recent_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
839
840 chooser->people_long_press = gtk_gesture_long_press_new (chooser->people.box);
841 g_signal_connect (chooser->people_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
842 chooser->people_multi_press = gtk_gesture_multi_press_new (chooser->people.box);
843 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->people_multi_press), GDK_BUTTON_SECONDARY);
844 g_signal_connect (chooser->people_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
845
846 chooser->body_long_press = gtk_gesture_long_press_new (chooser->body.box);
847 g_signal_connect (chooser->body_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
848 chooser->body_multi_press = gtk_gesture_multi_press_new (chooser->body.box);
849 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->body_multi_press), GDK_BUTTON_SECONDARY);
850 g_signal_connect (chooser->body_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
851
852 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
853 g_signal_connect (adj, "value-changed", G_CALLBACK (adj_value_changed), chooser);
854
855 setup_section (chooser, &chooser->recent, NULL, "emoji-recent-symbolic");
856 setup_section (chooser, &chooser->people, "grinning face", "emoji-people-symbolic");
857 setup_section (chooser, &chooser->body, "selfie", "emoji-body-symbolic");
858 setup_section (chooser, &chooser->nature, "monkey face", "emoji-nature-symbolic");
859 setup_section (chooser, &chooser->food, "grapes", "emoji-food-symbolic");
860 setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", "emoji-travel-symbolic");
861 setup_section (chooser, &chooser->activities, "jack-o-lantern", "emoji-activities-symbolic");
862 setup_section (chooser, &chooser->objects, "muted speaker", "emoji-objects-symbolic");
863 setup_section (chooser, &chooser->symbols, "ATM sign", "emoji-symbols-symbolic");
864 setup_section (chooser, &chooser->flags, "chequered flag", "emoji-flags-symbolic");
865
866 populate_recent_section (chooser);
867
868 chooser->populate_idle = g_idle_add (populate_emoji_chooser, chooser);
869 g_source_set_name_by_id (chooser->populate_idle, "[gtk] populate_emoji_chooser");
870 }
871
872 static void
e_gtk_emoji_chooser_show(GtkWidget * widget)873 e_gtk_emoji_chooser_show (GtkWidget *widget)
874 {
875 EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (widget);
876 GtkAdjustment *adj;
877
878 GTK_WIDGET_CLASS (e_gtk_emoji_chooser_parent_class)->show (widget);
879
880 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
881 gtk_adjustment_set_value (adj, 0);
882
883 gtk_entry_set_text (GTK_ENTRY (chooser->search_entry), "");
884 }
885
886 static void
e_gtk_emoji_chooser_class_init(EGtkEmojiChooserClass * klass)887 e_gtk_emoji_chooser_class_init (EGtkEmojiChooserClass *klass)
888 {
889 GObjectClass *object_class = G_OBJECT_CLASS (klass);
890 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
891
892 object_class->finalize = e_gtk_emoji_chooser_finalize;
893 widget_class->show = e_gtk_emoji_chooser_show;
894
895 signals[EMOJI_PICKED] = g_signal_new ("emoji-picked",
896 G_OBJECT_CLASS_TYPE (object_class),
897 G_SIGNAL_RUN_LAST,
898 0,
899 NULL, NULL,
900 NULL,
901 G_TYPE_NONE, 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE);
902 }
903
904 GtkWidget *
e_gtk_emoji_chooser_new(void)905 e_gtk_emoji_chooser_new (void)
906 {
907 return GTK_WIDGET (g_object_new (E_GTK_TYPE_EMOJI_CHOOSER, NULL));
908 }
909