[evolution] Bug 789267 - Add Insert->Emoji into message composer



commit 5a2f74f9e3aed8b6d98792fa4dff37367bcf45c1
Author: Milan Crha <mcrha redhat com>
Date:   Fri Oct 11 13:43:10 2019 +0200

    Bug 789267 - Add Insert->Emoji into message composer
    
    Closes https://bugzilla.gnome.org/show_bug.cgi?id=789267

 po/POTFILES.in                      |   1 +
 src/e-util/CMakeLists.txt           |   2 +
 src/e-util/e-gtkemojichooser.c      | 908 ++++++++++++++++++++++++++++++++++++
 src/e-util/e-gtkemojichooser.h      |  45 ++
 src/e-util/e-html-editor-actions.c  |  48 ++
 src/e-util/e-html-editor-actions.h  |   2 +
 src/e-util/e-html-editor-manager.ui |   1 +
 src/e-util/e-html-editor-private.h  |   2 +
 8 files changed, 1009 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6443ac9378..a9aed0ef59 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -209,6 +209,7 @@ src/e-util/e-filter-option.c
 src/e-util/e-filter-part.c
 src/e-util/e-filter-rule.c
 src/e-util/e-focus-tracker.c
+src/e-util/e-gtkemojichooser.c
 src/e-util/e-html-editor-actions.c
 src/e-util/e-html-editor.c
 src/e-util/e-html-editor-cell-dialog.c
diff --git a/src/e-util/CMakeLists.txt b/src/e-util/CMakeLists.txt
index eb9523d37a..b8de2be054 100644
--- a/src/e-util/CMakeLists.txt
+++ b/src/e-util/CMakeLists.txt
@@ -133,6 +133,8 @@ set(SOURCES
        e-filter-part.c
        e-filter-rule.c
        e-focus-tracker.c
+       e-gtkemojichooser.h
+       e-gtkemojichooser.c
        e-html-editor-actions.c
        e-html-editor-cell-dialog.c
        e-html-editor-dialog.c
diff --git a/src/e-util/e-gtkemojichooser.c b/src/e-util/e-gtkemojichooser.c
new file mode 100644
index 0000000000..7839e4db29
--- /dev/null
+++ b/src/e-util/e-gtkemojichooser.c
@@ -0,0 +1,908 @@
+/*
+ * Copyright 2017, Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-config.h"
+
+/* Copied and adapted a bit from gtk+'s gtkemojichooser.h,
+   waiting for it to be made public:
+   https://gitlab.gnome.org/GNOME/gtk/issues/86
+*/
+
+#include <gtk/gtk.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-gtkemojichooser.h"
+
+#define BOX_SPACE 6
+
+typedef struct {
+  GtkWidget *box;
+  GtkWidget *heading;
+  GtkWidget *button;
+  const char *first;
+  gunichar label;
+  gboolean empty;
+} EmojiSection;
+
+struct _EGtkEmojiChooser
+{
+  GtkPopover parent_instance;
+
+  GtkWidget *search_entry;
+  GtkWidget *stack;
+  GtkWidget *scrolled_window;
+
+  int emoji_max_width;
+
+  EmojiSection recent;
+  EmojiSection people;
+  EmojiSection body;
+  EmojiSection nature;
+  EmojiSection food;
+  EmojiSection travel;
+  EmojiSection activities;
+  EmojiSection objects;
+  EmojiSection symbols;
+  EmojiSection flags;
+
+  GtkGesture *recent_long_press;
+  GtkGesture *recent_multi_press;
+  GtkGesture *people_long_press;
+  GtkGesture *people_multi_press;
+  GtkGesture *body_long_press;
+  GtkGesture *body_multi_press;
+
+  GVariant *data;
+  GtkWidget *box;
+  GVariantIter *iter;
+  guint populate_idle;
+
+  GSettings *settings;
+};
+
+struct _EGtkEmojiChooserClass {
+  GtkPopoverClass parent_class;
+};
+
+enum {
+  EMOJI_PICKED,
+  LAST_SIGNAL
+};
+
+static int signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE (EGtkEmojiChooser, e_gtk_emoji_chooser, GTK_TYPE_POPOVER)
+
+static void
+e_gtk_emoji_chooser_finalize (GObject *object)
+{
+  EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (object);
+
+  if (chooser->populate_idle)
+    g_source_remove (chooser->populate_idle);
+
+  g_variant_unref (chooser->data);
+  g_object_unref (chooser->settings);
+
+  g_clear_object (&chooser->recent_long_press);
+  g_clear_object (&chooser->recent_multi_press);
+  g_clear_object (&chooser->people_long_press);
+  g_clear_object (&chooser->people_multi_press);
+  g_clear_object (&chooser->body_long_press);
+  g_clear_object (&chooser->body_multi_press);
+
+  G_OBJECT_CLASS (e_gtk_emoji_chooser_parent_class)->finalize (object);
+}
+
+static void
+scroll_to_section (GtkButton *button,
+                   gpointer   data)
+{
+  EmojiSection *section = data;
+  EGtkEmojiChooser *chooser;
+  GtkAdjustment *adj;
+  GtkAllocation alloc = { 0, 0, 0, 0 };
+
+  chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), E_GTK_TYPE_EMOJI_CHOOSER));
+
+  adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
+
+  if (section->heading)
+    gtk_widget_get_allocation (section->heading, &alloc);
+
+  gtk_adjustment_set_value (adj, alloc.y - BOX_SPACE);
+}
+
+static void
+add_emoji (GtkWidget    *box,
+           gboolean      prepend,
+           GVariant     *item,
+           gunichar      modifier,
+           EGtkEmojiChooser *chooser);
+
+#define MAX_RECENT (7*3)
+
+static void
+populate_recent_section (EGtkEmojiChooser *chooser)
+{
+  GVariant *variant;
+  GVariant *item;
+  GVariantIter iter;
+  gboolean empty = FALSE;
+
+  variant = g_settings_get_value (chooser->settings, "recent-emoji");
+  g_variant_iter_init (&iter, variant);
+  while ((item = g_variant_iter_next_value (&iter)))
+    {
+      GVariant *emoji_data;
+      gunichar modifier;
+
+      emoji_data = g_variant_get_child_value (item, 0);
+      g_variant_get_child (item, 1, "u", &modifier);
+      add_emoji (chooser->recent.box, FALSE, emoji_data, modifier, chooser);
+      g_variant_unref (emoji_data);
+      g_variant_unref (item);
+      empty = FALSE;
+    }
+
+  if (!empty)
+    {
+      gtk_widget_show (chooser->recent.box);
+      gtk_widget_set_sensitive (chooser->recent.button, TRUE);
+    }
+  g_variant_unref (variant);
+}
+
+static void
+add_recent_item (EGtkEmojiChooser *chooser,
+                 GVariant        *item,
+                 gunichar         modifier)
+{
+  GList *children, *l;
+  int i;
+  GVariantBuilder builder;
+
+  g_variant_ref (item);
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a((auss)u)"));
+  g_variant_builder_add (&builder, "(@(auss)u)", item, modifier);
+
+  children = gtk_container_get_children (GTK_CONTAINER (chooser->recent.box));
+  for (l = children, i = 1; l; l = l->next, i++)
+    {
+      GVariant *item2 = g_object_get_data (G_OBJECT (l->data), "emoji-data");
+      gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier"));
+
+      if (modifier == modifier2 && g_variant_equal (item, item2))
+        {
+          gtk_widget_destroy (GTK_WIDGET (l->data));
+          i--;
+          continue;
+        }
+      if (i >= MAX_RECENT)
+        {
+          gtk_widget_destroy (GTK_WIDGET (l->data));
+          continue;
+        }
+
+      g_variant_builder_add (&builder, "(@(auss)u)", item2, modifier2);
+    }
+  g_list_free (children);
+
+  add_emoji (chooser->recent.box, TRUE, item, modifier, chooser);
+
+  /* Enable recent */
+  gtk_widget_show (chooser->recent.box);
+  gtk_widget_set_sensitive (chooser->recent.button, TRUE);
+
+  g_settings_set_value (chooser->settings, "recent-emoji", g_variant_builder_end (&builder));
+
+  g_variant_unref (item);
+}
+
+static void
+emoji_activated (GtkFlowBox      *box,
+                 GtkFlowBoxChild *child,
+                 gpointer         data)
+{
+  EGtkEmojiChooser *chooser = data;
+  char *text;
+  GtkWidget *ebox;
+  GtkWidget *label;
+  GVariant *item;
+  gunichar modifier;
+
+  gtk_popover_popdown (GTK_POPOVER (chooser));
+
+  ebox = gtk_bin_get_child (GTK_BIN (child));
+  label = gtk_bin_get_child (GTK_BIN (ebox));
+  text = g_strdup (gtk_label_get_label (GTK_LABEL (label)));
+
+  item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
+  modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier"));
+  add_recent_item (chooser, item, modifier);
+
+  g_signal_emit (data, signals[EMOJI_PICKED], 0, text);
+  g_free (text);
+}
+
+static gboolean
+has_variations (GVariant *emoji_data)
+{
+  GVariant *codes;
+  int i;
+  gboolean has_variations;
+
+  has_variations = FALSE;
+  codes = g_variant_get_child_value (emoji_data, 0);
+  for (i = 0; i < g_variant_n_children (codes); i++)
+    {
+      gunichar code;
+      g_variant_get_child (codes, i, "u", &code);
+      if (code == 0)
+        {
+          has_variations = TRUE;
+          break;
+        }
+    }
+  g_variant_unref (codes);
+
+  return has_variations;
+}
+
+static void
+show_variations (EGtkEmojiChooser *chooser,
+                 GtkWidget       *child)
+{
+  GtkWidget *popover;
+  GtkWidget *view;
+  GtkWidget *box;
+  GVariant *emoji_data;
+  GtkWidget *parent_popover;
+  gunichar modifier;
+
+  if (!child)
+    return;
+
+  emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
+  if (!emoji_data)
+    return;
+
+  if (!has_variations (emoji_data))
+    return;
+
+  parent_popover = gtk_widget_get_ancestor (child, GTK_TYPE_POPOVER);
+  popover = gtk_popover_new (child);
+  view = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+  gtk_style_context_add_class (gtk_widget_get_style_context (view), "view");
+  box = gtk_flow_box_new ();
+  gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (box), TRUE);
+  gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (box), 6);
+  gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), 6);
+  gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (box), TRUE);
+  gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (box), GTK_SELECTION_NONE);
+  gtk_container_add (GTK_CONTAINER (popover), view);
+  gtk_container_add (GTK_CONTAINER (view), box);
+
+  g_signal_connect (box, "child-activated", G_CALLBACK (emoji_activated), parent_popover);
+
+  add_emoji (box, FALSE, emoji_data, 0, chooser);
+  for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++)
+    add_emoji (box, FALSE, emoji_data, modifier, chooser);
+
+  gtk_widget_show_all (view);
+  gtk_popover_popup (GTK_POPOVER (popover));
+}
+
+static void
+update_hover (GtkWidget *widget,
+              GdkEvent  *event,
+              gpointer   data)
+{
+  if (event->type == GDK_ENTER_NOTIFY)
+    gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
+  else
+    gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_PRELIGHT);
+}
+
+static void
+long_pressed_cb (GtkGesture *gesture,
+                 double      x,
+                 double      y,
+                 gpointer    data)
+{
+  EGtkEmojiChooser *chooser = data;
+  GtkWidget *box;
+  GtkWidget *child;
+
+  box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
+  child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y));
+  show_variations (chooser, child);
+}
+
+static void
+pressed_cb (GtkGesture *gesture,
+            int         n_press,
+            double      x,
+            double      y,
+            gpointer    data)
+{
+  EGtkEmojiChooser *chooser = data;
+  GtkWidget *box;
+  GtkWidget *child;
+
+  box = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
+  child = GTK_WIDGET (gtk_flow_box_get_child_at_pos (GTK_FLOW_BOX (box), x, y));
+  show_variations (chooser, child);
+}
+
+static gboolean
+popup_menu (GtkWidget *widget,
+            gpointer   data)
+{
+  EGtkEmojiChooser *chooser = data;
+
+  show_variations (chooser, widget);
+  return TRUE;
+}
+
+static void
+add_emoji (GtkWidget    *box,
+           gboolean      prepend,
+           GVariant     *item,
+           gunichar      modifier,
+           EGtkEmojiChooser *chooser)
+{
+  GtkWidget *child;
+  GtkWidget *ebox;
+  GtkWidget *label;
+  PangoAttrList *attrs;
+  GVariant *codes;
+  char text[64];
+  char *p = text;
+  int i;
+  PangoLayout *layout;
+  PangoRectangle rect;
+
+  codes = g_variant_get_child_value (item, 0);
+  for (i = 0; i < g_variant_n_children (codes); i++)
+    {
+      gunichar code;
+
+      g_variant_get_child (codes, i, "u", &code);
+      if (code == 0)
+        code = modifier;
+      if (code != 0)
+        p += g_unichar_to_utf8 (code, p);
+    }
+  g_variant_unref (codes);
+  p += g_unichar_to_utf8 (0xFE0F, p); /* U+FE0F is the Emoji variation selector */
+  p[0] = 0;
+
+  label = gtk_label_new (text);
+  attrs = pango_attr_list_new ();
+  pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE));
+  gtk_label_set_attributes (GTK_LABEL (label), attrs);
+  pango_attr_list_unref (attrs);
+
+  layout = gtk_label_get_layout (GTK_LABEL (label));
+  pango_layout_get_extents (layout, &rect, NULL);
+
+  /* Check for fallback rendering that generates too wide items */
+  if (pango_layout_get_unknown_glyphs_count (layout) > 0 ||
+      rect.width >= 1.5 * chooser->emoji_max_width)
+    {
+      gtk_widget_destroy (label);
+      return;
+    }
+
+  child = gtk_flow_box_child_new ();
+  gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji");
+  g_object_set_data_full (G_OBJECT (child), "emoji-data",
+                          g_variant_ref (item),
+                          (GDestroyNotify)g_variant_unref);
+  if (modifier != 0)
+    g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier));
+
+  ebox = gtk_event_box_new ();
+  gtk_widget_add_events (ebox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+  g_signal_connect (ebox, "enter-notify-event", G_CALLBACK (update_hover), FALSE);
+  g_signal_connect (ebox, "leave-notify-event", G_CALLBACK (update_hover), FALSE);
+  gtk_container_add (GTK_CONTAINER (child), ebox);
+  gtk_container_add (GTK_CONTAINER (ebox), label);
+  gtk_widget_show_all (child);
+
+  if (chooser)
+    g_signal_connect (child, "popup-menu", G_CALLBACK (popup_menu), chooser);
+
+  gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1);
+}
+
+static gboolean
+populate_emoji_chooser (gpointer data)
+{
+  EGtkEmojiChooser *chooser = data;
+  GBytes *bytes = NULL;
+  GVariant *item;
+  guint64 start, now;
+
+  start = g_get_monotonic_time ();
+
+  if (!chooser->data)
+    {
+      bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/emoji.data", 0, NULL);
+      chooser->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, 
TRUE));
+    }
+
+  if (!chooser->iter)
+    {
+      chooser->iter = g_variant_iter_new (chooser->data);
+      chooser->box = chooser->people.box;
+    }
+  while ((item = g_variant_iter_next_value (chooser->iter)))
+    {
+      const char *name;
+
+      g_variant_get_child (item, 1, "&s", &name);
+
+      if (strcmp (name, chooser->body.first) == 0)
+        chooser->box = chooser->body.box;
+      else if (strcmp (name, chooser->nature.first) == 0)
+        chooser->box = chooser->nature.box;
+      else if (strcmp (name, chooser->food.first) == 0)
+        chooser->box = chooser->food.box;
+      else if (strcmp (name, chooser->travel.first) == 0)
+        chooser->box = chooser->travel.box;
+      else if (strcmp (name, chooser->activities.first) == 0)
+        chooser->box = chooser->activities.box;
+      else if (strcmp (name, chooser->objects.first) == 0)
+        chooser->box = chooser->objects.box;
+      else if (strcmp (name, chooser->symbols.first) == 0)
+        chooser->box = chooser->symbols.box;
+      else if (strcmp (name, chooser->flags.first) == 0)
+        chooser->box = chooser->flags.box;
+
+      add_emoji (chooser->box, FALSE, item, 0, chooser);
+      g_variant_unref (item);
+
+      now = g_get_monotonic_time ();
+      if (now > start + 8000)
+        return G_SOURCE_CONTINUE;
+    }
+
+  /* We scroll to the top on show, so check the right button for the 1st time */
+  gtk_widget_set_state_flags (chooser->recent.button, GTK_STATE_FLAG_CHECKED, FALSE);
+
+  g_variant_iter_free (chooser->iter);
+  chooser->iter = NULL;
+  chooser->box = NULL;
+  chooser->populate_idle = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+adj_value_changed (GtkAdjustment *adj,
+                   gpointer       data)
+{
+  EGtkEmojiChooser *chooser = data;
+  double value = gtk_adjustment_get_value (adj);
+  EmojiSection const *sections[] = {
+    &chooser->recent,
+    &chooser->people,
+    &chooser->body,
+    &chooser->nature,
+    &chooser->food,
+    &chooser->travel,
+    &chooser->activities,
+    &chooser->objects,
+    &chooser->symbols,
+    &chooser->flags,
+  };
+  EmojiSection const *select_section = sections[0];
+  gsize i;
+
+  /* Figure out which section the current scroll position is within */
+  for (i = 0; i < G_N_ELEMENTS (sections); ++i)
+    {
+      EmojiSection const *section = sections[i];
+      GtkAllocation alloc;
+
+      if (section->heading)
+        gtk_widget_get_allocation (section->heading, &alloc);
+      else
+        gtk_widget_get_allocation (section->box, &alloc);
+
+      if (value < alloc.y - BOX_SPACE)
+        break;
+
+      select_section = section;
+    }
+
+  /* Un/Check the section buttons accordingly */
+  for (i = 0; i < G_N_ELEMENTS (sections); ++i)
+    {
+      EmojiSection const *section = sections[i];
+
+      if (section == select_section)
+        gtk_widget_set_state_flags (section->button, GTK_STATE_FLAG_CHECKED, FALSE);
+      else
+        gtk_widget_unset_state_flags (section->button, GTK_STATE_FLAG_CHECKED);
+    }
+}
+
+static gboolean
+filter_func (GtkFlowBoxChild *child,
+             gpointer         data)
+{
+  EmojiSection *section = data;
+  EGtkEmojiChooser *chooser;
+  GVariant *emoji_data;
+  const char *text;
+  const char *name;
+  gboolean res;
+
+  res = TRUE;
+
+  chooser = E_GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), E_GTK_TYPE_EMOJI_CHOOSER));
+  text = gtk_entry_get_text (GTK_ENTRY (chooser->search_entry));
+  emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), "emoji-data");
+
+  if (text[0] == 0)
+    goto out;
+
+  if (!emoji_data)
+    goto out;
+
+  g_variant_get_child (emoji_data, 1, "&s", &name);
+  res = g_str_match_string (text, name, TRUE);
+
+out:
+  if (res)
+    section->empty = FALSE;
+
+  return res;
+}
+
+static void
+invalidate_section (EmojiSection *section)
+{
+  section->empty = TRUE;
+  gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box));
+}
+
+static void
+update_headings (EGtkEmojiChooser *chooser)
+{
+  gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty);
+  gtk_widget_set_visible (chooser->people.box, !chooser->people.empty);
+  gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty);
+  gtk_widget_set_visible (chooser->body.box, !chooser->body.empty);
+  gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty);
+  gtk_widget_set_visible (chooser->nature.box, !chooser->nature.empty);
+  gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty);
+  gtk_widget_set_visible (chooser->food.box, !chooser->food.empty);
+  gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty);
+  gtk_widget_set_visible (chooser->travel.box, !chooser->travel.empty);
+  gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty);
+  gtk_widget_set_visible (chooser->activities.box, !chooser->activities.empty);
+  gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty);
+  gtk_widget_set_visible (chooser->objects.box, !chooser->objects.empty);
+  gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty);
+  gtk_widget_set_visible (chooser->symbols.box, !chooser->symbols.empty);
+  gtk_widget_set_visible (chooser->flags.heading, !chooser->flags.empty);
+  gtk_widget_set_visible (chooser->flags.box, !chooser->flags.empty);
+
+  if (chooser->recent.empty && chooser->people.empty &&
+      chooser->body.empty && chooser->nature.empty &&
+      chooser->food.empty && chooser->travel.empty &&
+      chooser->activities.empty && chooser->objects.empty &&
+      chooser->symbols.empty && chooser->flags.empty)
+    gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "empty");
+  else
+    gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "list");
+}
+
+static void
+search_changed (GtkEntry *entry,
+                gpointer  data)
+{
+  EGtkEmojiChooser *chooser = data;
+
+  invalidate_section (&chooser->recent);
+  invalidate_section (&chooser->people);
+  invalidate_section (&chooser->body);
+  invalidate_section (&chooser->nature);
+  invalidate_section (&chooser->food);
+  invalidate_section (&chooser->travel);
+  invalidate_section (&chooser->activities);
+  invalidate_section (&chooser->objects);
+  invalidate_section (&chooser->symbols);
+  invalidate_section (&chooser->flags);
+
+  update_headings (chooser);
+}
+
+static void
+setup_section (EGtkEmojiChooser *chooser,
+               EmojiSection   *section,
+               const char     *first,
+               const char     *icon)
+{
+  GtkAdjustment *adj;
+  GtkWidget *image;
+
+  section->first = first;
+
+  image = gtk_bin_get_child (GTK_BIN (section->button));
+  gtk_image_set_from_icon_name (GTK_IMAGE (image), icon, GTK_ICON_SIZE_BUTTON);
+
+  adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
+
+  gtk_container_set_focus_vadjustment (GTK_CONTAINER (section->box), adj);
+  gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, section, NULL);
+  g_signal_connect (section->button, "clicked", G_CALLBACK (scroll_to_section), section);
+}
+
+static void
+e_gtk_emoji_chooser_add_group (EGtkEmojiChooser *chooser,
+                              GtkBox *emoji_box,
+                              GtkBox *button_box,
+                              const gchar *label,
+                              EmojiSection *section)
+{
+       section->heading = gtk_label_new (label);
+       g_object_set (G_OBJECT (section->heading),
+               "xalign", 0.0,
+               NULL);
+       gtk_box_pack_start (emoji_box, GTK_WIDGET (section->heading), FALSE, FALSE, 0);
+
+       section->box = gtk_flow_box_new ();
+       g_object_set (G_OBJECT (section->box),
+               "homogeneous", TRUE,
+               "selection-mode", GTK_SELECTION_NONE,
+               NULL);
+       gtk_box_pack_start (emoji_box, GTK_WIDGET (section->box), FALSE, FALSE, 0);
+       g_signal_connect (section->box, "child-activated",
+               G_CALLBACK (emoji_activated), chooser);
+
+       section->button = gtk_button_new ();
+       gtk_container_add (GTK_CONTAINER (section->button), gtk_image_new ());
+       gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (section->button)), 
"emoji-section");
+       g_object_set (G_OBJECT (section->button),
+               "relief", GTK_RELIEF_NONE,
+               "tooltip-text", label,
+               NULL);
+       gtk_box_pack_start (button_box, GTK_WIDGET (section->button), FALSE, FALSE, 0);
+}
+
+static void
+e_gtk_emoji_chooser_construct_without_template (EGtkEmojiChooser *chooser)
+{
+       GtkBox *box, *emoji_box, *button_box;
+       GtkGrid *grid;
+       GtkWidget *widget;
+       PangoAttrList *attrs;
+
+       gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser)), "emoji-picker");
+
+       box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
+       gtk_container_add (GTK_CONTAINER (chooser), GTK_WIDGET (box));
+
+       chooser->search_entry = gtk_search_entry_new ();
+       gtk_box_pack_start (box, chooser->search_entry, FALSE, FALSE, 0);
+
+       g_signal_connect (chooser->search_entry, "search-changed", G_CALLBACK (search_changed), chooser);
+
+       chooser->stack = gtk_stack_new ();
+       gtk_box_pack_start (box, chooser->stack, FALSE, FALSE, 0);
+
+       gtk_widget_show_all (GTK_WIDGET (box));
+
+       box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
+       gtk_widget_set_name (GTK_WIDGET (box), "list");
+       gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (box));
+
+       chooser->scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+       g_object_set (G_OBJECT (chooser->scrolled_window),
+               "vexpand", TRUE,
+               "hscrollbar-policy", GTK_POLICY_NEVER,
+               "min-content-height", 250,
+               NULL);
+       gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->scrolled_window)), 
"view");
+       gtk_box_pack_start (box, chooser->scrolled_window, FALSE, FALSE, 0);
+
+       emoji_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 0));
+       g_object_set (G_OBJECT (emoji_box),
+               "margin", 6,
+               "spacing", 6,
+               NULL);
+       gtk_container_add (GTK_CONTAINER (chooser->scrolled_window), GTK_WIDGET (emoji_box));
+
+       button_box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0));
+       gtk_box_pack_start (box, GTK_WIDGET (button_box), FALSE, FALSE, 0);
+
+       chooser->recent.box = gtk_flow_box_new ();
+       g_object_set (G_OBJECT (chooser->recent.box),
+               "homogeneous", TRUE,
+               "selection-mode", GTK_SELECTION_NONE,
+               NULL);
+       gtk_box_pack_start (emoji_box, GTK_WIDGET (chooser->recent.box), FALSE, FALSE, 0);
+       g_signal_connect (chooser->recent.box, "child-activated",
+               G_CALLBACK (emoji_activated), chooser);
+
+       chooser->recent.button = gtk_button_new ();
+       gtk_container_add (GTK_CONTAINER (chooser->recent.button), gtk_image_new ());
+       gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (chooser->recent.button)), 
"emoji-section");
+       g_object_set (G_OBJECT (chooser->recent.button),
+               "relief", GTK_RELIEF_NONE,
+               "tooltip-text", _("Recent"),
+               NULL);
+       gtk_box_pack_start (button_box, GTK_WIDGET (chooser->recent.button), FALSE, FALSE, 0);
+
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Smileys & People"), 
&chooser->people);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Body & Clothing"), &chooser->body);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Animals & Nature"), 
&chooser->nature);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Food & Drink"), &chooser->food);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Travel & Places"), 
&chooser->travel);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Activities"), &chooser->activities);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, C_("emoji category", "Objects"), 
&chooser->objects);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Symbols"), &chooser->symbols);
+       e_gtk_emoji_chooser_add_group (chooser, emoji_box, button_box, _("Flags"), &chooser->flags);
+
+       grid = GTK_GRID (gtk_grid_new ());
+       g_object_set (G_OBJECT (grid),
+               "name", "empty",
+               "row-spacing", 12,
+               "halign", GTK_ALIGN_CENTER,
+               "valign", GTK_ALIGN_CENTER,
+               NULL);
+       gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (grid)), "dim-label");
+       gtk_container_add (GTK_CONTAINER (chooser->stack), GTK_WIDGET (grid));
+
+       widget = gtk_image_new ();
+       g_object_set (G_OBJECT (widget),
+               "icon-name", "edit-find-symbolic",
+               "pixel-size", 72,
+               NULL);
+       gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label");
+       gtk_grid_attach (grid, widget, 0, 0, 1, 1);
+
+       attrs = pango_attr_list_new ();
+       pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+       pango_attr_list_insert (attrs, pango_attr_scale_new (1.44));
+
+       widget = gtk_label_new (_("No Results Found"));
+       g_object_set (G_OBJECT (widget),
+               "attributes", attrs,
+               NULL);
+       gtk_grid_attach (grid, widget, 0, 1, 1, 1);
+
+       pango_attr_list_unref (attrs);
+
+       widget = gtk_label_new (_("Try a different search"));
+       gtk_style_context_add_class (gtk_widget_get_style_context (widget), "dim-label");
+       gtk_grid_attach (grid, widget, 0, 1, 1, 1);
+
+       gtk_widget_show_all (GTK_WIDGET (chooser->stack));
+}
+
+static void
+e_gtk_emoji_chooser_init (EGtkEmojiChooser *chooser)
+{
+  GtkAdjustment *adj;
+
+  chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser");
+
+  e_gtk_emoji_chooser_construct_without_template (chooser);
+
+  /* Get a reasonable maximum width for an emoji. We do this to
+   * skip overly wide fallback rendering for certain emojis the
+   * font does not contain and therefore end up being rendered
+   * as multiply glyphs.
+   */
+  {
+    PangoLayout *layout = gtk_widget_create_pango_layout (GTK_WIDGET (chooser), "🙂");
+    PangoAttrList *attrs;
+    PangoRectangle rect;
+
+    attrs = pango_attr_list_new ();
+    pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE));
+    pango_layout_set_attributes (layout, attrs);
+    pango_attr_list_unref (attrs);
+
+    pango_layout_get_extents (layout, &rect, NULL);
+    chooser->emoji_max_width = rect.width;
+
+    g_object_unref (layout);
+  }
+
+  chooser->recent_long_press = gtk_gesture_long_press_new (chooser->recent.box);
+  g_signal_connect (chooser->recent_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
+  chooser->recent_multi_press = gtk_gesture_multi_press_new (chooser->recent.box);
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->recent_multi_press), GDK_BUTTON_SECONDARY);
+  g_signal_connect (chooser->recent_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
+
+  chooser->people_long_press = gtk_gesture_long_press_new (chooser->people.box);
+  g_signal_connect (chooser->people_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
+  chooser->people_multi_press = gtk_gesture_multi_press_new (chooser->people.box);
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->people_multi_press), GDK_BUTTON_SECONDARY);
+  g_signal_connect (chooser->people_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
+
+  chooser->body_long_press = gtk_gesture_long_press_new (chooser->body.box);
+  g_signal_connect (chooser->body_long_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
+  chooser->body_multi_press = gtk_gesture_multi_press_new (chooser->body.box);
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (chooser->body_multi_press), GDK_BUTTON_SECONDARY);
+  g_signal_connect (chooser->body_multi_press, "pressed", G_CALLBACK (pressed_cb), chooser);
+
+  adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
+  g_signal_connect (adj, "value-changed", G_CALLBACK (adj_value_changed), chooser);
+
+  setup_section (chooser, &chooser->recent, NULL, "emoji-recent-symbolic");
+  setup_section (chooser, &chooser->people, "grinning face", "emoji-people-symbolic");
+  setup_section (chooser, &chooser->body, "selfie", "emoji-body-symbolic");
+  setup_section (chooser, &chooser->nature, "monkey face", "emoji-nature-symbolic");
+  setup_section (chooser, &chooser->food, "grapes", "emoji-food-symbolic");
+  setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", "emoji-travel-symbolic");
+  setup_section (chooser, &chooser->activities, "jack-o-lantern", "emoji-activities-symbolic");
+  setup_section (chooser, &chooser->objects, "muted speaker", "emoji-objects-symbolic");
+  setup_section (chooser, &chooser->symbols, "ATM sign", "emoji-symbols-symbolic");
+  setup_section (chooser, &chooser->flags, "chequered flag", "emoji-flags-symbolic");
+
+  populate_recent_section (chooser);
+
+  chooser->populate_idle = g_idle_add (populate_emoji_chooser, chooser);
+  g_source_set_name_by_id (chooser->populate_idle, "[gtk] populate_emoji_chooser");
+}
+
+static void
+e_gtk_emoji_chooser_show (GtkWidget *widget)
+{
+  EGtkEmojiChooser *chooser = E_GTK_EMOJI_CHOOSER (widget);
+  GtkAdjustment *adj;
+
+  GTK_WIDGET_CLASS (e_gtk_emoji_chooser_parent_class)->show (widget);
+
+  adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
+  gtk_adjustment_set_value (adj, 0);
+
+  gtk_entry_set_text (GTK_ENTRY (chooser->search_entry), "");
+}
+
+static void
+e_gtk_emoji_chooser_class_init (EGtkEmojiChooserClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = e_gtk_emoji_chooser_finalize;
+  widget_class->show = e_gtk_emoji_chooser_show;
+
+  signals[EMOJI_PICKED] = g_signal_new ("emoji-picked",
+                                        G_OBJECT_CLASS_TYPE (object_class),
+                                        G_SIGNAL_RUN_LAST,
+                                        0,
+                                        NULL, NULL,
+                                        NULL,
+                                        G_TYPE_NONE, 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+GtkWidget *
+e_gtk_emoji_chooser_new (void)
+{
+  return GTK_WIDGET (g_object_new (E_GTK_TYPE_EMOJI_CHOOSER, NULL));
+}
diff --git a/src/e-util/e-gtkemojichooser.h b/src/e-util/e-gtkemojichooser.h
new file mode 100644
index 0000000000..c7ca4dc70c
--- /dev/null
+++ b/src/e-util/e-gtkemojichooser.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017, Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Copied and adapted a bit from gtk+'s gtkemojichooser.h,
+   waiting for it to be made public:
+   https://gitlab.gnome.org/GNOME/gtk/issues/86
+*/
+
+#ifndef E_GTKEMOJICHOOSER_H
+#define E_GTKEMOJICHOOSER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define E_GTK_TYPE_EMOJI_CHOOSER                 (e_gtk_emoji_chooser_get_type ())
+#define E_GTK_EMOJI_CHOOSER(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooser))
+#define E_GTK_EMOJI_CHOOSER_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), 
E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooserClass))
+#define E_GTK_IS_EMOJI_CHOOSER(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
E_GTK_TYPE_EMOJI_CHOOSER))
+#define E_GTK_IS_EMOJI_CHOOSER_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), 
E_GTK_TYPE_EMOJI_CHOOSER))
+#define E_GTK_EMOJI_CHOOSER_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), 
E_GTK_TYPE_EMOJI_CHOOSER, EGtkEmojiChooserClass))
+
+typedef struct _EGtkEmojiChooser      EGtkEmojiChooser;
+typedef struct _EGtkEmojiChooserClass EGtkEmojiChooserClass;
+
+GType          e_gtk_emoji_chooser_get_type    (void) G_GNUC_CONST;
+GtkWidget *    e_gtk_emoji_chooser_new         (void);
+
+G_END_DECLS
+
+#endif /* E_GTKEMOJICHOOSER_H */
diff --git a/src/e-util/e-html-editor-actions.c b/src/e-util/e-html-editor-actions.c
index b65e22fbbe..0c151a72a4 100644
--- a/src/e-util/e-html-editor-actions.c
+++ b/src/e-util/e-html-editor-actions.c
@@ -29,6 +29,7 @@
 #include "e-html-editor-actions.h"
 #include "e-emoticon-action.h"
 #include "e-emoticon-chooser.h"
+#include "e-gtkemojichooser.h"
 #include "e-image-chooser-dialog.h"
 #include "e-spell-checker.h"
 #include "e-misc-utils.h"
@@ -274,6 +275,43 @@ action_indent_cb (GtkAction *action,
                e_content_editor_selection_indent (cnt_editor);
 }
 
+static void
+emoji_chooser_emoji_picked_cb (EHTMLEditor *editor,
+                              const gchar *emoji_text)
+{
+       if (emoji_text) {
+               EContentEditor *cnt_editor;
+
+               cnt_editor = e_html_editor_get_content_editor (editor);
+
+               e_content_editor_insert_content (cnt_editor, emoji_text,
+                       E_CONTENT_EDITOR_INSERT_CONVERT |
+                       E_CONTENT_EDITOR_INSERT_TEXT_PLAIN);
+       }
+}
+
+static void
+action_insert_emoji_cb (GtkAction *action,
+                       EHTMLEditor *editor)
+{
+       if (!editor->priv->emoji_chooser) {
+               GtkWidget *popover;
+
+               popover = e_gtk_emoji_chooser_new ();
+
+               gtk_popover_set_relative_to (GTK_POPOVER (popover), GTK_WIDGET (editor));
+               gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM);
+               gtk_popover_set_modal (GTK_POPOVER (popover), TRUE);
+
+               g_signal_connect_object (popover, "emoji-picked",
+                       G_CALLBACK (emoji_chooser_emoji_picked_cb), editor, G_CONNECT_SWAPPED);
+
+               editor->priv->emoji_chooser = popover;
+       }
+
+       gtk_popover_popup (GTK_POPOVER (editor->priv->emoji_chooser));
+}
+
 static void
 action_insert_emoticon_cb (GtkAction *action,
                            EHTMLEditor *editor)
@@ -1015,6 +1053,13 @@ static GtkActionEntry core_editor_entries[] = {
          N_("Increase Indent"),
          G_CALLBACK (action_indent_cb) },
 
+       { "insert-emoji",
+         NULL,
+         N_("E_moji"),
+         NULL,
+         N_("Insert Emoji"),
+         G_CALLBACK (action_insert_emoji_cb) },
+
        { "insert-html-file",
          NULL,
          N_("_HTML File…"),
@@ -2016,6 +2061,9 @@ editor_actions_init (EHTMLEditor *editor)
        g_object_set (
                G_OBJECT (ACTION (SHOW_REPLACE)),
                "short-label", _("Re_place"), NULL);
+       g_object_set (
+               G_OBJECT (ACTION (INSERT_EMOJI)),
+               "short-label", _("E_moji"), NULL);
        g_object_set (
                G_OBJECT (ACTION (INSERT_IMAGE)),
                "short-label", _("_Image"), NULL);
diff --git a/src/e-util/e-html-editor-actions.h b/src/e-util/e-html-editor-actions.h
index f724db0be2..74b2773eee 100644
--- a/src/e-util/e-html-editor-actions.h
+++ b/src/e-util/e-html-editor-actions.h
@@ -85,6 +85,8 @@
        E_HTML_EDITOR_ACTION ((editor), "format-text")
 #define E_HTML_EDITOR_ACTION_INSERT_EMOTICON(editor) \
        E_HTML_EDITOR_ACTION ((editor), "insert-emoticon")
+#define E_HTML_EDITOR_ACTION_INSERT_EMOJI(editor) \
+       E_HTML_EDITOR_ACTION ((editor), "insert-emoji")
 #define E_HTML_EDITOR_ACTION_INSERT_IMAGE(editor) \
        E_HTML_EDITOR_ACTION ((editor), "insert-image")
 #define E_HTML_EDITOR_ACTION_INSERT_LINK(editor) \
diff --git a/src/e-util/e-html-editor-manager.ui b/src/e-util/e-html-editor-manager.ui
index 487dce6be0..e6af175d34 100644
--- a/src/e-util/e-html-editor-manager.ui
+++ b/src/e-util/e-html-editor-manager.ui
@@ -44,6 +44,7 @@
       <menuitem action='insert-text-file'/>
       <menuitem action='insert-html-file'/>
       <menuitem action='insert-emoticon'/>
+      <menuitem action='insert-emoji'/>
     </menu>
     <placeholder name='pre-format-menu'/>
     <menu action='format-menu'>
diff --git a/src/e-util/e-html-editor-private.h b/src/e-util/e-html-editor-private.h
index ec068f4ae6..5d66c203b2 100644
--- a/src/e-util/e-html-editor-private.h
+++ b/src/e-util/e-html-editor-private.h
@@ -83,6 +83,8 @@ struct _EHTMLEditorPrivate {
        GtkWidget *style_combo_box;
        GtkWidget *scrolled_window;
 
+       GtkWidget *emoji_chooser;
+
        GHashTable *content_editors;
        EContentEditor *use_content_editor;
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]