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