[gtk+/wip/matthiasc/emoji-picker: 12/16] Add an Emoji chooser widget



commit be3712d40ddd462a51e939a9500e7acf9f3a13e7
Author: Matthias Clasen <mclasen redhat com>
Date:   Fri Aug 11 11:54:15 2017 -0400

    Add an Emoji chooser widget
    
    Add a popover that shows color Emoji, with a search entry.
    The recently-used Emoji are stored in a GSetting.

 gtk/Makefile.am                               |    6 +-
 gtk/gtkemojichooser.c                         |  627 +++++++++++++++++++++++++
 gtk/gtkemojichooser.h                         |   41 ++
 gtk/org.gtk.Settings.EmojiChooser.gschema.xml |   16 +
 gtk/theme/Adwaita/_common.scss                |   49 ++
 gtk/theme/Adwaita/gtk-contained-dark.css      |   26 +
 gtk/theme/Adwaita/gtk-contained.css           |   26 +
 gtk/ui/gtkemojichooser.ui                     |  307 ++++++++++++
 po/POTFILES.in                                |    2 +
 9 files changed, 1099 insertions(+), 1 deletions(-)
---
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index 16fbd18..75889b3 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -451,6 +451,7 @@ gtk_private_h_sources =             \
        gtkdebugupdatesprivate.h        \
        gtkdialogprivate.h      \
        gtkdndprivate.h         \
+       gtkemojichooser.h       \
        gtkentryprivate.h       \
        gtkeventcontrollerprivate.h     \
        gtkfilechooserembed.h   \
@@ -722,6 +723,7 @@ gtk_base_c_sources =                \
        gtkdragsource.c         \
        gtkdrawingarea.c        \
        gtkeditable.c           \
+       gtkemojichooser.c       \
        gtkentry.c              \
        gtkentrybuffer.c        \
        gtkentrycompletion.c    \
@@ -1147,7 +1149,8 @@ templates =                               \
        ui/gtkcolorchooserdialog.ui     \
        ui/gtkcoloreditor.ui            \
        ui/gtkcombobox.ui               \
-       ui/gtkdialog.ui         \
+       ui/gtkdialog.ui                 \
+       ui/gtkemojichooser.ui           \
        ui/gtkfilechooserwidget.ui      \
        ui/gtkfilechooserdialog.ui      \
        ui/gtkfontchooserdialog.ui      \
@@ -1690,6 +1693,7 @@ files:
 gsettings_SCHEMAS = \
        org.gtk.Settings.FileChooser.gschema.xml \
        org.gtk.Settings.ColorChooser.gschema.xml \
+       org.gtk.Settings.EmojiChooser.gschema.xml \
        org.gtk.Settings.Debug.gschema.xml
 
 @GSETTINGS_RULES@
diff --git a/gtk/gtkemojichooser.c b/gtk/gtkemojichooser.c
new file mode 100644
index 0000000..79ecea0
--- /dev/null
+++ b/gtk/gtkemojichooser.c
@@ -0,0 +1,627 @@
+/* gtkemojichooser.c: An Emoji chooser widget
+ * 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 "config.h"
+
+#include "gtkemojichooser.h"
+
+#include "gtkadjustment.h"
+#include "gtkbox.h"
+#include "gtkbutton.h"
+#include "gtkcssprovider.h"
+#include "gtkentry.h"
+#include "gtkflowbox.h"
+#include "gtkstack.h"
+#include "gtklabel.h"
+#include "gtkgesturelongpress.h"
+#include "gtkpopover.h"
+#include "gtkscrolledwindow.h"
+#include "gtkintl.h"
+#include "gtkprivate.h"
+
+typedef struct {
+  GtkWidget *box;
+  GtkWidget *heading;
+  GtkWidget *button;
+  const char *first;
+  gunichar label;
+  gboolean empty;
+} EmojiSection;
+
+struct _GtkEmojiChooser
+{
+  GtkPopover parent_instance;
+
+  GtkWidget *search_entry;
+  GtkWidget *stack;
+  GtkWidget *scrolled_window;
+
+  EmojiSection recent;
+  EmojiSection people;
+  EmojiSection body;
+  EmojiSection nature;
+  EmojiSection food;
+  EmojiSection travel;
+  EmojiSection activities;
+  EmojiSection objects;
+  EmojiSection symbols;
+  EmojiSection flags;
+
+  EmojiSection *scroll_to_section;
+
+  GtkGesture *recent_press;
+  GtkGesture *people_press;
+  GtkGesture *body_press;
+
+  GVariant *data;
+
+  GSettings *settings;
+};
+
+struct _GtkEmojiChooserClass {
+  GtkPopoverClass parent_class;
+};
+
+enum {
+  EMOJI_PICKED,
+  LAST_SIGNAL
+};
+
+static int signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE (GtkEmojiChooser, gtk_emoji_chooser, GTK_TYPE_POPOVER)
+
+static void
+gtk_emoji_chooser_finalize (GObject *object)
+{
+  GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (object);
+
+  g_variant_unref (chooser->data);
+  g_object_unref (chooser->settings);
+
+  G_OBJECT_CLASS (gtk_emoji_chooser_parent_class)->finalize (object);
+}
+
+static gboolean
+scroll_in_idle (gpointer data)
+{
+  GtkEmojiChooser *chooser = data;
+  GtkAdjustment *adj;
+  GtkAllocation alloc = { 0, 0, 0, 0 };
+  double page_increment, value;
+  gboolean dummy;
+
+  adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window));
+  if (chooser->scroll_to_section->heading)
+    gtk_widget_get_allocation (chooser->scroll_to_section->heading, &alloc);
+  page_increment = gtk_adjustment_get_page_increment (adj);
+  value = gtk_adjustment_get_value (adj);
+  gtk_adjustment_set_page_increment (adj, alloc.y - value);
+  g_signal_emit_by_name (chooser->scrolled_window, "scroll-child", GTK_SCROLL_PAGE_FORWARD, FALSE, &dummy);
+  gtk_adjustment_set_page_increment (adj, page_increment);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+scroll_to_section (GtkButton *button,
+                   gpointer   data)
+{
+  EmojiSection *section = data;
+  GtkEmojiChooser *chooser;
+
+  chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_EMOJI_CHOOSER));
+
+  if (chooser->scroll_to_section == section)
+    return;
+
+  chooser->scroll_to_section = section;
+  g_idle_add (scroll_in_idle, chooser);
+}
+
+static void
+add_emoji (GtkWidget    *box,
+           gboolean      prepend,
+           GVariantIter *iter,
+           GVariant     *data);
+
+#define MAX_RECENT (7*3)
+
+static void
+add_recent_item (GtkEmojiChooser *chooser,
+                 GVariant       *item)
+{
+  GList *children, *l;
+  GVariantIter *codes;
+  const char *name;
+  int i;
+  GVariantBuilder builder;
+
+  g_variant_ref (item);
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ausaau)"));
+  g_variant_builder_add_value (&builder, item);
+
+  g_variant_get_child (item, 1, "&s", &name);
+
+  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");
+      const char *name2;
+
+      g_variant_get_child (item2, 1, "&s", &name2);
+      if (strcmp (name, name2) == 0)
+        {
+          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_value (&builder, item2);
+    }
+  g_list_free (children);
+
+  g_variant_get_child (item, 0, "au", &codes);
+  add_emoji (chooser->recent.box, TRUE, codes, item);
+  g_variant_iter_free (codes);
+
+  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)
+{
+  GtkEmojiChooser *chooser = data;
+  char *text;
+  GtkWidget *label;
+  GVariant *item;
+
+  gtk_popover_popdown (GTK_POPOVER (chooser));
+
+  label = gtk_bin_get_child (GTK_BIN (child));
+  text = g_strdup (gtk_label_get_label (GTK_LABEL (label)));
+
+  item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
+  add_recent_item (chooser, item);
+
+  g_signal_emit (data, signals[EMOJI_PICKED], 0, text);
+  g_free (text);
+}
+
+static void
+long_pressed_cb (GtkGesture *gesture,
+                 double      x,
+                 double      y,
+                 gpointer    data)
+{
+  GtkWidget *child;
+  GtkWidget *popover;
+  GtkWidget *view;
+  GtkWidget *box;
+  GVariant *emoji_data;
+  GtkWidget *parent_popover;
+  GVariantIter *iter;
+  GVariantIter *codes;
+
+  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));
+  if (!child)
+    return;
+
+  emoji_data = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data");
+  if (!emoji_data)
+    return;
+
+  g_variant_get_child (emoji_data, 2, "aau", &iter);
+  if (g_variant_iter_n_children (iter) == 0)
+    {
+      g_variant_iter_free (iter);
+      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);
+
+  g_variant_get_child (emoji_data, 0, "au", &codes);
+  add_emoji (box, FALSE, codes, emoji_data);
+  g_variant_iter_free (codes);
+  while (g_variant_iter_next (iter, "au", &codes))
+    {
+      add_emoji (box, FALSE, codes, emoji_data);
+      g_variant_iter_free (codes);
+    }
+
+  gtk_popover_popup (GTK_POPOVER (popover));
+}
+
+static void
+add_emoji (GtkWidget    *box,
+           gboolean      prepend,
+           GVariantIter *iter,
+           GVariant     *data)
+{
+  GtkWidget *child;
+  GtkWidget *label;
+  PangoAttrList *attrs;
+  char text[64];
+  char *p = text;
+  gunichar code;
+
+  while (g_variant_iter_next (iter, "u", &code))
+    p += g_unichar_to_utf8 (code, p);
+  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);
+
+  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 (data),
+                          (GDestroyNotify)g_variant_unref);
+
+  gtk_container_add (GTK_CONTAINER (child), label);
+  gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1);
+}
+
+static void
+populate_emoji_chooser (GtkEmojiChooser *chooser)
+{
+  g_autoptr(GBytes) bytes = NULL;
+  GVariantIter iter;
+  GVariant *item;
+  GtkWidget *box;
+
+  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(ausaau)"), bytes, TRUE));
+
+  g_variant_iter_init (&iter, chooser->data);
+  box = chooser->people.box;
+  while ((item = g_variant_iter_next_value (&iter)))
+    {
+      GVariantIter *codes;
+      const char *name;
+
+      g_variant_get_child (item, 0, "au", &codes);
+      g_variant_get_child (item, 1, "&s", &name);
+
+      if (strcmp (name, chooser->body.first) == 0)
+        box = chooser->body.box;
+      else if (strcmp (name, chooser->nature.first) == 0)
+        box = chooser->nature.box;
+      else if (strcmp (name, chooser->food.first) == 0)
+        box = chooser->food.box;
+      else if (strcmp (name, chooser->travel.first) == 0)
+        box = chooser->travel.box;
+      else if (strcmp (name, chooser->activities.first) == 0)
+        box = chooser->activities.box;
+      else if (strcmp (name, chooser->objects.first) == 0)
+        box = chooser->objects.box;
+      else if (strcmp (name, chooser->symbols.first) == 0)
+        box = chooser->symbols.box;
+      else if (strcmp (name, chooser->flags.first) == 0)
+        box = chooser->flags.box;
+
+      add_emoji (box, FALSE, codes, item);
+      g_variant_iter_free (codes);
+    }
+}
+
+static void
+update_state (EmojiSection *section,
+              double        value)
+{
+  GtkAllocation alloc = { 0, 0, 0, 20 };
+
+  if (section->heading)
+    gtk_widget_get_allocation (section->heading, &alloc);
+
+  if (alloc.y <= value && value < alloc.y + alloc.height)
+    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 void
+adj_value_changed (GtkAdjustment *adj,
+                   gpointer       data)
+{
+  GtkEmojiChooser *chooser = data;
+  double value = gtk_adjustment_get_value (adj);
+
+  update_state (&chooser->recent, value);
+  update_state (&chooser->people, value);
+  update_state (&chooser->body, value);
+  update_state (&chooser->nature, value);
+  update_state (&chooser->food, value);
+  update_state (&chooser->travel, value);
+  update_state (&chooser->activities, value);
+  update_state (&chooser->objects, value);
+  update_state (&chooser->symbols, value);
+  update_state (&chooser->flags, value);
+}
+
+static gboolean
+filter_func (GtkFlowBoxChild *child,
+             gpointer         data)
+{
+  EmojiSection *section = data;
+  GtkEmojiChooser *chooser;
+  GVariant *emoji_data;
+  const char *text;
+  const char *name;
+  gboolean res;
+
+  res = TRUE;
+
+  chooser = GTK_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), 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 || text[1] == 0)
+    goto out;
+
+  if (!emoji_data)
+    goto out;
+
+  g_variant_get_child (emoji_data, 1, "&s", &name);
+  res = strstr (name, text) != NULL;
+
+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 gboolean
+update_headings (gpointer data)
+{
+  GtkEmojiChooser *chooser = data;
+
+  gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty);
+  gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty);
+  gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty);
+  gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty);
+  gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty);
+  gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty);
+  gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty);
+  gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty);
+  gtk_widget_set_visible (chooser->flags.heading, !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");
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+search_changed (GtkEntry *entry,
+                gpointer  data)
+{
+  GtkEmojiChooser *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);
+
+  g_idle_add (update_headings, data);
+}
+
+static void
+setup_section (GtkEmojiChooser *chooser,
+               EmojiSection   *section,
+               const char     *first,
+               gunichar        label)
+{
+  char text[14];
+  char *p;
+  GtkAdjustment *adj;
+
+  section->first = first;
+
+  p = text;
+  p += g_unichar_to_utf8 (label, p);
+  p += g_unichar_to_utf8 (0xfe0e, p);
+  p[0] = 0;
+  gtk_button_set_label (GTK_BUTTON (section->button), text);
+
+  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
+gtk_emoji_chooser_init (GtkEmojiChooser *chooser)
+{
+  GtkAdjustment *adj;
+  GVariant *variant;
+  GVariantIter iter;
+  GVariant *item;
+
+  gtk_widget_init_template (GTK_WIDGET (chooser));
+
+  chooser->recent_press = gtk_gesture_long_press_new (chooser->recent.box);
+  g_signal_connect (chooser->recent_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
+
+  chooser->people_press = gtk_gesture_long_press_new (chooser->people.box);
+  g_signal_connect (chooser->people_press, "pressed", G_CALLBACK (long_pressed_cb), chooser);
+
+  chooser->body_press = gtk_gesture_long_press_new (chooser->body.box);
+  g_signal_connect (chooser->body_press, "pressed", G_CALLBACK (long_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, 0x1f557);
+  setup_section (chooser, &chooser->people, "grinning face", 0x1f642);
+  setup_section (chooser, &chooser->body, "selfie", 0x1f44d);
+  setup_section (chooser, &chooser->nature, "monkey face", 0x1f337);
+  setup_section (chooser, &chooser->food, "grapes", 0x1f374);
+  setup_section (chooser, &chooser->travel, "globe showing Europe-Africa", 0x2708);
+  setup_section (chooser, &chooser->activities, "jack-o-lantern", 0x1f3c3);
+  setup_section (chooser, &chooser->objects, "muted speaker", 0x1f514);
+  setup_section (chooser, &chooser->symbols, "ATM sign", 0x2764);
+  setup_section (chooser, &chooser->flags, "chequered flag", 0x1f3f4);
+
+  populate_emoji_chooser (chooser);
+
+  chooser->settings = g_settings_new ("org.gtk.Settings.EmojiChooser");
+  variant = g_settings_get_value (chooser->settings, "recent-emoji");
+  g_variant_iter_init (&iter, variant);
+  while ((item = g_variant_iter_next_value (&iter)))
+    {
+      GVariantIter *codes;
+
+      g_variant_get_child (item, 0, "au", &codes);
+      add_emoji (chooser->recent.box, FALSE, codes, item);
+      g_variant_iter_free (codes);
+    }
+}
+
+static void
+gtk_emoji_chooser_show (GtkWidget *widget)
+{
+  GtkEmojiChooser *chooser = GTK_EMOJI_CHOOSER (widget);
+  GtkAdjustment *adj;
+
+  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), "");
+
+  GTK_WIDGET_CLASS (gtk_emoji_chooser_parent_class)->show (widget);
+}
+
+static void
+gtk_emoji_chooser_class_init (GtkEmojiChooserClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gtk_emoji_chooser_finalize;
+  widget_class->show = 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);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkemojichooser.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, stack);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, scrolled_window);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, recent.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, people.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, body.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, nature.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, food.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, travel.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, activities.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, objects.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, symbols.button);
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.box);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.heading);
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiChooser, flags.button);
+
+  gtk_widget_class_bind_template_callback (widget_class, emoji_activated);
+  gtk_widget_class_bind_template_callback (widget_class, search_changed);
+}
+
+GtkWidget *
+gtk_emoji_chooser_new (void)
+{
+  return GTK_WIDGET (g_object_new (GTK_TYPE_EMOJI_CHOOSER, NULL));
+}
diff --git a/gtk/gtkemojichooser.h b/gtk/gtkemojichooser.h
new file mode 100644
index 0000000..63670b5
--- /dev/null
+++ b/gtk/gtkemojichooser.h
@@ -0,0 +1,41 @@
+/* gtkemojichooser.h: An Emoji chooser widget
+ * 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/>.
+ */
+
+#pragma once
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#include <gtk/gtkwidget.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_EMOJI_CHOOSER                 (gtk_emoji_chooser_get_type ())
+#define GTK_EMOJI_CHOOSER(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_EMOJI_CHOOSER, 
GtkEmojiChooser))
+#define GTK_EMOJI_CHOOSER_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_EMOJI_CHOOSER, 
GtkEmojiChooserClass))
+#define GTK_IS_EMOJI_CHOOSER(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_EMOJI_CHOOSER))
+#define GTK_IS_EMOJI_CHOOSER_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_EMOJI_CHOOSER))
+#define GTK_EMOJI_CHOOSER_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_EMOJI_CHOOSER, 
GtkEmojiChooserClass))
+
+typedef struct _GtkEmojiChooser      GtkEmojiChooser;
+typedef struct _GtkEmojiChooserClass GtkEmojiChooserClass;
+
+GType      gtk_emoji_chooser_get_type (void) G_GNUC_CONST;
+GtkWidget *gtk_emoji_chooser_new      (void);
+
+G_END_DECLS
diff --git a/gtk/org.gtk.Settings.EmojiChooser.gschema.xml b/gtk/org.gtk.Settings.EmojiChooser.gschema.xml
new file mode 100644
index 0000000..d720275
--- /dev/null
+++ b/gtk/org.gtk.Settings.EmojiChooser.gschema.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schemalist>
+
+  <schema id='org.gtk.Settings.EmojiChooser' path='/org/gtk/settings/emoji-chooser/'>
+    <key name='recent-emoji' type='a(ausaau)'>
+      <default>[]</default>
+      <summary>Recently used Emoji</summary>
+      <description>
+        An array of Emoji definitions to show in the Emoji chooser. Each Emoji is
+        specified as an array of codepoints, a name, and an optional array of
+        nested Emoji.
+      </description>
+    </key>
+  </schema>
+
+</schemalist>
diff --git a/gtk/theme/Adwaita/_common.scss b/gtk/theme/Adwaita/_common.scss
index 539d252..1e84d2f 100644
--- a/gtk/theme/Adwaita/_common.scss
+++ b/gtk/theme/Adwaita/_common.scss
@@ -4376,3 +4376,52 @@ stackswitcher button.text-button.circular { // FIXME aggregate with buttons
   min-height: 32px;
   padding: 0;
 }
+
+popover.emoji-picker { padding-left: 0; padding-right: 0; }
+
+button.emoji-section {
+  border-color: transparent;
+  border-width: 3px;
+  border-style: none none solid;
+  border-radius: 0;
+
+  margin: 2px 4px 2px 4px;
+  padding: 3px 0 0;
+  min-width: 32px;
+  min-height: 28px;
+
+  /* reset props inherited from the button style */
+  background: none;
+  box-shadow: none;
+  text-shadow: none;
+
+  outline-offset: -5px;
+}
+
+button.emoji-section label { padding: 0; }
+
+button.emoji-section:hover { border-color: $borders_color; }
+
+button.emoji-section:checked { border-color: $selected_bg_color; }
+
+button.emoji-section:backdrop:not(:checked) { border-color: transparent; }
+
+button.emoji-section { color: $borders_color; }
+
+button.emoji-section:hover { color: mix($fg_color, $borders_color, 0.5); }
+
+button.emoji-section:checked { color: $fg_color; }
+
+button.emoji-section:backdrop { color: $backdrop_borders_color; }
+
+button.emoji-section:backdrop:checked { color: $backdrop_fg_color; }
+
+.emoji {
+  font-size: x-large;
+  padding: 6px;
+  border-radius: 6px;
+}
+
+.emoji:hover {
+  background: $selected_bg_color;
+}
diff --git a/gtk/theme/Adwaita/gtk-contained-dark.css b/gtk/theme/Adwaita/gtk-contained-dark.css
index c3514bd..26b7911 100644
--- a/gtk/theme/Adwaita/gtk-contained-dark.css
+++ b/gtk/theme/Adwaita/gtk-contained-dark.css
@@ -1893,6 +1893,32 @@ stackswitcher button.text-button { min-width: 100px; }
 
 stackswitcher button.circular, stackswitcher button.text-button.circular { min-width: 32px; min-height: 
32px; padding: 0; }
 
+popover.emoji-picker { padding-left: 0; padding-right: 0; }
+
+button.emoji-section { border-color: transparent; border-width: 3px; border-style: none none solid; 
border-radius: 0; margin: 2px 4px 2px 4px; padding: 3px 0 0; min-width: 32px; min-height: 28px; /* reset 
props inherited from the button style */ background: none; box-shadow: none; text-shadow: none; 
outline-offset: -5px; }
+
+button.emoji-section label { padding: 0; }
+
+button.emoji-section:hover { border-color: #1b1f20; }
+
+button.emoji-section:checked { border-color: #215d9c; }
+
+button.emoji-section:backdrop:not(:checked) { border-color: transparent; }
+
+button.emoji-section { color: #1b1f20; }
+
+button.emoji-section:hover { color: #1c2021; }
+
+button.emoji-section:checked { color: #eeeeec; }
+
+button.emoji-section:backdrop { color: #202425; }
+
+button.emoji-section:backdrop:checked { color: #919494; }
+
+.emoji { font-size: x-large; padding: 6px; border-radius: 6px; }
+
+.emoji:hover { background: #215d9c; }
+
 /* GTK NAMED COLORS ---------------- use responsibly! */
 /*
 widget text/foreground color */
diff --git a/gtk/theme/Adwaita/gtk-contained.css b/gtk/theme/Adwaita/gtk-contained.css
index f1973c0..4f54987 100644
--- a/gtk/theme/Adwaita/gtk-contained.css
+++ b/gtk/theme/Adwaita/gtk-contained.css
@@ -1913,6 +1913,32 @@ stackswitcher button.text-button { min-width: 100px; }
 
 stackswitcher button.circular, stackswitcher button.text-button.circular { min-width: 32px; min-height: 
32px; padding: 0; }
 
+popover.emoji-picker { padding-left: 0; padding-right: 0; }
+
+button.emoji-section { border-color: transparent; border-width: 3px; border-style: none none solid; 
border-radius: 0; margin: 2px 4px 2px 4px; padding: 3px 0 0; min-width: 32px; min-height: 28px; /* reset 
props inherited from the button style */ background: none; box-shadow: none; text-shadow: none; 
outline-offset: -5px; }
+
+button.emoji-section label { padding: 0; }
+
+button.emoji-section:hover { border-color: #b6b6b3; }
+
+button.emoji-section:checked { border-color: #4a90d9; }
+
+button.emoji-section:backdrop:not(:checked) { border-color: transparent; }
+
+button.emoji-section { color: #b6b6b3; }
+
+button.emoji-section:hover { color: #b5b5b2; }
+
+button.emoji-section:checked { color: #2e3436; }
+
+button.emoji-section:backdrop { color: #c0c0bd; }
+
+button.emoji-section:backdrop:checked { color: #8b8e8f; }
+
+.emoji { font-size: x-large; padding: 6px; border-radius: 6px; }
+
+.emoji:hover { background: #4a90d9; }
+
 /* GTK NAMED COLORS ---------------- use responsibly! */
 /*
 widget text/foreground color */
diff --git a/gtk/ui/gtkemojichooser.ui b/gtk/ui/gtkemojichooser.ui
new file mode 100644
index 0000000..5a04da5
--- /dev/null
+++ b/gtk/ui/gtkemojichooser.ui
@@ -0,0 +1,307 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk40">
+  <template class="GtkEmojiChooser" parent="GtkPopover">
+    <property name="modal">1</property>
+    <style>
+      <class name="emoji-picker"/>
+    </style>
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkSearchEntry" id="search_entry">
+            <signal name="search-changed" handler="search_changed"/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkScrolledWindow" id="scrolled_window">
+                    <property name="vexpand">1</property>
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="min-content-height">250</property>
+                    <style>
+                      <class name="view"/>
+                    </style>
+                    <child>
+                      <object class="GtkBox" id="emoji_box">
+                        <property name="orientation">vertical</property>
+                        <property name="margin">6</property>
+                        <property name="spacing">6</property>
+                        <child>
+                          <object class="GtkFlowBox" id="recent.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="people.heading">
+                            <property name="label" translatable="yes">Smileys &amp; People</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="people.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="body.heading">
+                            <property name="label" translatable="yes">Body &amp; Clothing</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="body.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="nature.heading">
+                            <property name="label" translatable="yes">Animals &amp; Nature</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="nature.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="food.heading">
+                            <property name="label" translatable="yes">Food &amp; Drink</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="food.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="travel.heading">
+                            <property name="label" translatable="yes">Travel &amp; Places</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="travel.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="activities.heading">
+                            <property name="label" translatable="yes">Activities</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="activities.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="objects.heading">
+                            <property name="label" translatable="yes">Objects</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="objects.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="symbols.heading">
+                            <property name="label" translatable="yes">Symbols</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="symbols.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="flags.heading">
+                            <property name="label" translatable="yes">Flags</property>
+                            <property name="xalign">0</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkFlowBox" id="flags.box">
+                            <property name="homogeneous">1</property>
+                            <property name="selection-mode">none</property>
+                            <signal name="child-activated" handler="emoji_activated"/>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <child>
+                      <object class="GtkButton" id="recent.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="people.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="body.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="nature.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="food.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="travel.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="activities.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="objects.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="symbols.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="flags.button">
+                        <property name="relief">none</property>
+                        <style>
+                          <class name="emoji-section"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">list</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkGrid">
+                <property name="row-spacing">12</property>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">edit-find-symbolic</property>
+                    <property name="pixel-size">72</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">No Results Found</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                      <attribute name="scale" value="1.44"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Try a different search</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left-attach">0</property>
+                    <property name="top-attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">empty</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6c4dc5a..05b4227 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -114,6 +114,7 @@ gtk/gtkdragdest.c
 gtk/gtkdragsource.c
 gtk/gtkdrawingarea.c
 gtk/gtkeditable.c
+gtk/gtkemojichooser.c
 gtk/gtkentrybuffer.c
 gtk/gtkentry.c
 gtk/gtkentrycompletion.c
@@ -329,6 +330,7 @@ gtk/ui/gtkassistant.ui
 gtk/ui/gtkcolorchooserdialog.ui
 gtk/ui/gtkcoloreditor.ui
 gtk/ui/gtkdialog.ui
+gtk/ui/gtkemojichooser.ui
 gtk/ui/gtkfilechooserdialog.ui
 gtk/ui/gtkfilechooserwidget.ui
 gtk/ui/gtkfontchooserdialog.ui



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