Add an emoji completion popup
authorMatthias Clasen <mclasen@redhat.com>
Sat, 19 Aug 2017 18:06:47 +0000 (14:06 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Mon, 23 Apr 2018 22:54:43 +0000 (18:54 -0400)
This widget provides entry completion-like functionality
for Emoji codes like :grin: or :kiss:.

gtk/gtkemojicompletion.c [new file with mode: 0644]
gtk/gtkemojicompletion.h [new file with mode: 0644]
gtk/meson.build
gtk/theme/Adwaita/_common.scss
gtk/ui/gtkemojicompletion.ui [new file with mode: 0644]
po/POTFILES.in

diff --git a/gtk/gtkemojicompletion.c b/gtk/gtkemojicompletion.c
new file mode 100644 (file)
index 0000000..f1868c7
--- /dev/null
@@ -0,0 +1,665 @@
+/* gtkemojicompletion.c: An Emoji picker 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 "gtkemojicompletion.h"
+
+#include "gtkentryprivate.h"
+#include "gtkbox.h"
+#include "gtkcssprovider.h"
+#include "gtklistbox.h"
+#include "gtklabel.h"
+#include "gtkpopover.h"
+#include "gtkintl.h"
+#include "gtkprivate.h"
+#include "gtkgesturelongpress.h"
+#include "gtkflowbox.h"
+#include "gtkstack.h"
+
+struct _GtkEmojiCompletion
+{
+  GtkPopover parent_instance;
+
+  GtkEntry *entry;
+  char *text;
+  guint length;
+  guint offset;
+  gulong changed_id;
+  guint n_matches;
+
+  GtkWidget *list;
+  GtkWidget *active;
+  GtkWidget *active_variation;
+
+  GVariant *data;
+
+  GtkGesture *long_press;
+};
+
+struct _GtkEmojiCompletionClass {
+  GtkPopoverClass parent_class;
+};
+
+static void connect_signals    (GtkEmojiCompletion *completion,
+                                GtkEntry           *entry);
+static void disconnect_signals (GtkEmojiCompletion *completion);
+static int populate_completion (GtkEmojiCompletion *completion,
+                                const char          *text,
+                                guint                offset);
+
+#define MAX_ROWS 5
+
+G_DEFINE_TYPE (GtkEmojiCompletion, gtk_emoji_completion, GTK_TYPE_POPOVER)
+
+static void
+gtk_emoji_completion_finalize (GObject *object)
+{
+  GtkEmojiCompletion *completion = GTK_EMOJI_COMPLETION (object);
+
+  disconnect_signals (completion);
+
+  g_free (completion->text);
+  g_variant_unref (completion->data);
+
+  g_clear_object (&completion->long_press);
+
+  G_OBJECT_CLASS (gtk_emoji_completion_parent_class)->finalize (object);
+}
+
+static void
+update_completion (GtkEmojiCompletion *completion)
+{
+  const char *text;
+  guint length;
+  guint n_matches;
+
+  n_matches = 0;
+
+  text = gtk_entry_get_text (GTK_ENTRY (completion->entry));
+  length = strlen (text);
+
+  if (length > 0)
+    {
+      gboolean found_candidate = FALSE;
+      const char *p;
+
+      p = text + length;
+      do
+        {
+next:
+          p = g_utf8_prev_char (p);
+          if (*p == ':')
+            {
+              if (p + 1 == text + length)
+                goto next;
+
+              if (p == text || !g_unichar_isalnum (g_utf8_get_char (p - 1)))
+                {
+                  found_candidate = TRUE;
+                }
+
+              break;
+            }
+        }
+      while (g_unichar_isalnum (g_utf8_get_char (p)) || *p == '_');
+
+      if (found_candidate)
+        n_matches = populate_completion (completion, p, 0);
+    }
+
+  if (n_matches > 0)
+    gtk_popover_popup (GTK_POPOVER (completion));
+  else
+    gtk_popover_popdown (GTK_POPOVER (completion));
+}
+
+static void
+entry_changed (GtkEntry *entry, GtkEmojiCompletion *completion)
+{
+  update_completion (completion);
+}
+
+static void
+emoji_activated (GtkWidget          *row,
+                 GtkEmojiCompletion *completion)
+{
+  const char *emoji;
+  guint length;
+
+  gtk_popover_popdown (GTK_POPOVER (completion));
+
+  emoji = (const char *)g_object_get_data (G_OBJECT (row), "text");
+
+  g_signal_handler_block (completion->entry, completion->changed_id);
+
+  length = g_utf8_strlen (gtk_entry_get_text (completion->entry), -1);
+  gtk_entry_set_positions (completion->entry, length - completion->length, length);
+  gtk_entry_enter_text (completion->entry, emoji);
+
+  g_signal_handler_unblock (completion->entry, completion->changed_id);
+}
+
+static void
+row_activated (GtkListBox    *list,
+               GtkListBoxRow *row,
+               gpointer       data)
+{
+  GtkEmojiCompletion *completion = data;
+
+  emoji_activated (GTK_WIDGET (row), completion);
+}
+
+static void
+child_activated (GtkFlowBox      *box,
+                 GtkFlowBoxChild *child,
+                 gpointer         data)
+{
+  GtkEmojiCompletion *completion = data;
+
+  g_print ("child activated\n");
+  emoji_activated (GTK_WIDGET (child), completion);
+}
+
+static void
+move_active_row (GtkEmojiCompletion *completion,
+                 int                 direction)
+{
+  GtkWidget *child;
+  GtkWidget *base;
+
+  for (child = gtk_widget_get_first_child (completion->list);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      gtk_widget_unset_state_flags (child, GTK_STATE_FLAG_PRELIGHT);
+      base = GTK_WIDGET (g_object_get_data (G_OBJECT (child), "base"));
+      gtk_widget_unset_state_flags (base, GTK_STATE_FLAG_PRELIGHT);
+    }
+
+  if (completion->active != NULL)
+    {
+      if (direction == 1)
+        completion->active = gtk_widget_get_next_sibling (completion->active);
+      else
+        completion->active = gtk_widget_get_prev_sibling (completion->active);
+    }
+
+  if (completion->active == NULL)
+    {
+      if (direction == 1)
+        completion->active = gtk_widget_get_first_child (completion->list);
+      else
+        completion->active = gtk_widget_get_last_child (completion->list);
+    }
+
+  if (completion->active != NULL)
+    gtk_widget_set_state_flags (completion->active, GTK_STATE_FLAG_PRELIGHT, FALSE);
+
+  if (completion->active_variation)
+    {
+      gtk_widget_unset_state_flags (completion->active_variation, GTK_STATE_FLAG_PRELIGHT);
+      completion->active_variation = NULL;
+    }
+}
+
+static void
+activate_active_row (GtkEmojiCompletion *completion)
+{
+  if (GTK_IS_FLOW_BOX_CHILD (completion->active_variation))
+    emoji_activated (completion->active_variation, completion);
+  else if (completion->active != NULL)
+    emoji_activated (completion->active, completion);
+}
+
+static void
+show_variations (GtkEmojiCompletion *completion,
+                 GtkWidget          *row,
+                 gboolean            visible)
+{
+  GtkWidget *stack;
+  GtkWidget *box;
+  gboolean is_visible;
+
+  if (!row)
+    return;
+
+  stack = GTK_WIDGET (g_object_get_data (G_OBJECT (row), "stack"));
+  box = gtk_stack_get_child_by_name (GTK_STACK (stack), "variations");
+  if (!box)
+    return;
+
+  is_visible = gtk_stack_get_visible_child (GTK_STACK (stack)) == box;
+  if (is_visible == visible)
+    return;
+
+  if (visible)
+    gtk_widget_unset_state_flags (row, GTK_STATE_FLAG_PRELIGHT);
+  else
+    gtk_widget_set_state_flags (row, GTK_STATE_FLAG_PRELIGHT, FALSE);
+
+  gtk_stack_set_visible_child_name (GTK_STACK (stack), visible ? "variations" : "text");
+  if (completion->active_variation)
+    {
+      gtk_widget_unset_state_flags (completion->active_variation, GTK_STATE_FLAG_PRELIGHT);
+      completion->active_variation = NULL;
+    }
+}
+
+static gboolean
+move_active_variation (GtkEmojiCompletion *completion,
+                       int                 direction)
+{
+  GtkWidget *base;
+  GtkWidget *stack;
+  GtkWidget *box;
+  GtkWidget *next;
+
+  if (!completion->active)
+    return FALSE;
+
+  base = GTK_WIDGET (g_object_get_data (G_OBJECT (completion->active), "base"));
+  stack = GTK_WIDGET (g_object_get_data (G_OBJECT (completion->active), "stack"));
+  box = gtk_stack_get_child_by_name (GTK_STACK (stack), "variations");
+
+  if (gtk_stack_get_visible_child (GTK_STACK (stack)) != box)
+    return FALSE;
+
+  next = NULL;
+
+  if (!completion->active_variation)
+    next = base;
+  else if (completion->active_variation == base && direction == 1)
+    next = gtk_widget_get_first_child (box);
+  else if (completion->active_variation == gtk_widget_get_first_child (box) && direction == -1)
+    next = base;
+  else if (direction == 1)
+    next = gtk_widget_get_next_sibling (completion->active_variation);
+  else if (direction == -1)
+    next = gtk_widget_get_prev_sibling (completion->active_variation);
+
+  if (next)
+    {
+      if (completion->active_variation)
+        gtk_widget_unset_state_flags (completion->active_variation, GTK_STATE_FLAG_PRELIGHT);
+      completion->active_variation = next;
+      gtk_widget_set_state_flags (completion->active_variation, GTK_STATE_FLAG_PRELIGHT, FALSE);
+    }
+
+  return next != NULL;
+}
+
+static gboolean
+entry_key_press (GtkEntry           *entry,
+                 GdkEventKey        *event,
+                 GtkEmojiCompletion *completion)
+{
+  guint keyval;
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (completion)))
+    return FALSE;
+
+  gdk_event_get_keyval ((GdkEvent*)event, &keyval);
+
+  if (keyval == GDK_KEY_Escape)
+    {
+      gtk_popover_popdown (GTK_POPOVER (completion));
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Tab)
+    {
+      show_variations (completion, completion->active, FALSE);
+
+      guint offset = completion->offset + MAX_ROWS;
+      if (offset >= completion->n_matches)
+        offset = 0;
+      populate_completion (completion, completion->text, offset);
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Up)
+    {
+      show_variations (completion, completion->active, FALSE);
+
+      move_active_row (completion, -1);
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Down)
+    {
+      show_variations (completion, completion->active, FALSE);
+
+      move_active_row (completion, 1);
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Return ||
+      keyval == GDK_KEY_KP_Enter ||
+      keyval == GDK_KEY_ISO_Enter)
+    {
+      activate_active_row (completion);
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Right)
+    {
+      show_variations (completion, completion->active, TRUE);
+      move_active_variation (completion, 1);
+      return TRUE;
+    }
+
+  if (keyval == GDK_KEY_Left)
+    {
+      if (!move_active_variation (completion, -1))
+        show_variations (completion, completion->active, FALSE);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+entry_focus_out (GtkWidget *entry,
+                 GParamSpec *pspec,
+                 GtkEmojiCompletion *completion)
+{
+  if (!gtk_widget_has_focus (entry))
+    gtk_popover_popdown (GTK_POPOVER (completion));
+  return FALSE;
+}
+
+static void
+connect_signals (GtkEmojiCompletion *completion,
+                 GtkEntry           *entry)
+{
+  completion->entry = entry;
+
+  completion->changed_id = g_signal_connect (entry, "changed", G_CALLBACK (entry_changed), completion);
+  g_signal_connect (entry, "key-press-event", G_CALLBACK (entry_key_press), completion);
+  g_signal_connect (entry, "notify::has-focus", G_CALLBACK (entry_focus_out), completion);
+}
+
+static void
+disconnect_signals (GtkEmojiCompletion *completion)
+{
+  g_signal_handlers_disconnect_by_func (completion->entry, entry_changed, completion);
+  g_signal_handlers_disconnect_by_func (completion->entry, entry_key_press, completion);
+  g_signal_handlers_disconnect_by_func (completion->entry, entry_focus_out, completion);
+
+  completion->entry = NULL;
+}
+
+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
+get_text (GVariant *emoji_data,
+          gunichar  modifier,
+          char     *text,
+          gsize     length)
+{
+  GVariant *codes;
+  int i;
+  char *p;
+
+  p = text;
+  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)
+        code = modifier;
+      if (code != 0)
+        p += g_unichar_to_utf8 (code, p);
+    }
+  g_variant_unref (codes);
+  p[0] = 0;
+}
+
+static void
+add_emoji_variation (GtkWidget *box,
+                     GVariant  *emoji_data,
+                     gunichar   modifier)
+{
+  GtkWidget *child;
+  GtkWidget *label;
+  PangoAttrList *attrs;
+  char text[64];
+
+  get_text (emoji_data, modifier, text, 64);
+
+  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), "text", g_strdup (text), g_free);
+  g_object_set_data_full (G_OBJECT (child), "emoji-data",
+                          g_variant_ref (emoji_data),
+                          (GDestroyNotify)g_variant_unref);
+  if (modifier != 0)
+    g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier));
+
+  gtk_container_add (GTK_CONTAINER (child), label);
+  gtk_flow_box_insert (GTK_FLOW_BOX (box), child, -1);
+}
+
+static void
+add_emoji (GtkWidget          *list,
+           GVariant           *emoji_data,
+           GtkEmojiCompletion *completion)
+{
+  GtkWidget *child;
+  GtkWidget *label;
+  GtkWidget *box;
+  PangoAttrList *attrs;
+  char text[64];
+  const char *shortname;
+  GtkWidget *stack;
+  gunichar modifier;
+
+  get_text (emoji_data, 0, text, 64);
+
+  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);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), "emoji");
+
+  child = gtk_list_box_row_new ();
+  gtk_widget_set_focus_on_click (child, FALSE);
+  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+  gtk_container_add (GTK_CONTAINER (child), box);
+  gtk_box_pack_start (GTK_BOX (box), label);
+  g_object_set_data (G_OBJECT (child), "base", label);
+
+  stack = gtk_stack_new ();
+  gtk_stack_set_homogeneous (GTK_STACK (stack), TRUE);
+  gtk_stack_set_transition_type (GTK_STACK (stack), GTK_STACK_TRANSITION_TYPE_OVER_RIGHT_LEFT);
+  gtk_box_pack_start (GTK_BOX (box), stack);
+  g_object_set_data (G_OBJECT (child), "stack", stack);
+
+  g_variant_get_child (emoji_data, 2, "&s", &shortname);
+  label = gtk_label_new (shortname);
+  gtk_label_set_xalign (GTK_LABEL (label), 0);
+
+  gtk_stack_add_named (GTK_STACK (stack), label, "text");
+
+  if (has_variations (emoji_data))
+    {
+      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), 5);
+      gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (box), 5);
+      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);
+      g_signal_connect (box, "child-activated", G_CALLBACK (child_activated), completion);
+      for (modifier = 0x1f3fb; modifier <= 0x1f3ff; modifier++)
+        add_emoji_variation (box, emoji_data, modifier);
+
+      gtk_stack_add_named (GTK_STACK (stack), box, "variations");
+    }
+
+  g_object_set_data_full (G_OBJECT (child), "text", g_strdup (text), g_free);
+  g_object_set_data_full (G_OBJECT (child), "emoji-data",
+                          g_variant_ref (emoji_data), (GDestroyNotify)g_variant_unref);
+  gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji-completion-row");
+
+  gtk_list_box_insert (GTK_LIST_BOX (list), child, -1);
+}
+
+static int
+populate_completion (GtkEmojiCompletion *completion,
+                     const char          *text,
+                     guint                offset)
+{
+  GList *children, *l;
+  int n_matches;
+  int n_added;
+  GVariantIter iter;
+  GVariant *item;
+
+  text = g_strdup (text);
+  g_free (completion->text);
+  completion->text = g_strdup (text);
+  completion->length = g_utf8_strlen (text, -1);
+  completion->offset = offset;
+
+  children = gtk_container_get_children (GTK_CONTAINER (completion->list));
+  for (l = children; l; l = l->next)
+    gtk_widget_destroy (GTK_WIDGET (l->data));
+  g_list_free (children);
+
+  completion->active = NULL;
+
+  n_matches = 0;
+  n_added = 0;
+  g_variant_iter_init (&iter, completion->data);
+  while ((item = g_variant_iter_next_value (&iter)))
+    {
+      const char *shortname;
+
+      g_variant_get_child (item, 2, "&s", &shortname);
+      if (g_str_has_prefix (shortname, text))
+        {
+          n_matches++;
+
+          if (n_matches > offset && n_added < MAX_ROWS)
+            {
+              add_emoji (completion->list, item, completion);
+              n_added++;
+            }
+        }
+    }
+
+  completion->n_matches = n_matches;
+
+  if (n_added > 0)
+    {
+      completion->active = gtk_widget_get_first_child (completion->list);
+      gtk_widget_set_state_flags (completion->active, GTK_STATE_FLAG_PRELIGHT, FALSE);
+    }
+
+  return n_added;
+}
+
+static void
+long_pressed_cb (GtkGesture *gesture,
+                 double      x,
+                 double      y,
+                 gpointer    data)
+{
+  GtkEmojiCompletion *completion = data;
+  GtkWidget *row;
+
+  row = GTK_WIDGET (gtk_list_box_get_row_at_y (GTK_LIST_BOX (completion->list), y));
+  if (!row)
+    return;
+
+  show_variations (completion, row, TRUE);
+}
+
+static void
+gtk_emoji_completion_init (GtkEmojiCompletion *completion)
+{
+  g_autoptr(GBytes) bytes = NULL;
+
+  gtk_widget_init_template (GTK_WIDGET (completion));
+
+  bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/emoji.data", 0, NULL);
+  completion->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, TRUE));
+
+  completion->long_press = gtk_gesture_long_press_new (completion->list);
+  g_signal_connect (completion->long_press, "pressed", G_CALLBACK (long_pressed_cb), completion);
+}
+
+static void
+gtk_emoji_completion_class_init (GtkEmojiCompletionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gtk_emoji_completion_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkemojicompletion.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, GtkEmojiCompletion, list);
+
+  gtk_widget_class_bind_template_callback (widget_class, row_activated);
+}
+
+GtkWidget *
+gtk_emoji_completion_new (GtkEntry *entry)
+{
+  GtkEmojiCompletion *completion;
+
+  completion = GTK_EMOJI_COMPLETION (g_object_new (GTK_TYPE_EMOJI_COMPLETION,
+                                                   "relative-to", entry,
+                                                   NULL));
+
+  connect_signals (completion, entry);
+
+  return GTK_WIDGET (completion);
+}
diff --git a/gtk/gtkemojicompletion.h b/gtk/gtkemojicompletion.h
new file mode 100644 (file)
index 0000000..ff7cb1f
--- /dev/null
@@ -0,0 +1,41 @@
+/* gtkemojicompletion.h: An Emoji picker 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/gtkentry.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_EMOJI_COMPLETION                 (gtk_emoji_completion_get_type ())
+#define GTK_EMOJI_COMPLETION(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_EMOJI_COMPLETION, GtkEmojiCompletion))
+#define GTK_EMOJI_COMPLETION_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_EMOJI_COMPLETION, GtkEmojiCompletionClass))
+#define GTK_IS_EMOJI_COMPLETION(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_EMOJI_COMPLETION))
+#define GTK_IS_EMOJI_COMPLETION_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_EMOJI_COMPLETION))
+#define GTK_EMOJI_COMPLETION_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_EMOJI_COMPLETION, GtkEmojiCompletionClass))
+
+typedef struct _GtkEmojiCompletion      GtkEmojiCompletion;
+typedef struct _GtkEmojiCompletionClass GtkEmojiCompletionClass;
+
+GType      gtk_emoji_completion_get_type (void) G_GNUC_CONST;
+GtkWidget *gtk_emoji_completion_new      (GtkEntry *entry);
+
+G_END_DECLS
index 075cc9069fdeb3d57817a39b2773955df1f16852..db30e4c1eceffcd7fb21e38c440e6668c8c9b5b8 100644 (file)
@@ -197,6 +197,7 @@ gtk_public_sources = files([
   'gtkdrawingarea.c',
   'gtkeditable.c',
   'gtkemojichooser.c',
+  'gtkemojicompletion.c',
   'gtkentry.c',
   'gtkentrybuffer.c',
   'gtkentrycompletion.c',
index d65f368860645f863981048c72d46ed9c519df30..6317fdb9306b36f3619943d76fc7a5bfffb5dd66 100644 (file)
@@ -4556,7 +4556,7 @@ button.emoji-section {
   &:checked label { opacity: 1; }
 }
 
-.emoji {
+popover.emoji-picker .emoji {
   font-size: x-large;
   padding: 6px;
   border-radius: 6px;
@@ -4565,3 +4565,17 @@ button.emoji-section {
     background: $selected_bg_color;
   }
 }
+
+popover.emoji-completion arrow {
+  border: none;
+  background: none;
+}
+
+popover.emoji-completion contents row box {
+  border-spacing: 10px;
+  padding: 2px 10px;
+}
+
+popover.emoji-completion .emoji:hover {
+  background-color: $popover_hover_color;
+}
diff --git a/gtk/ui/gtkemojicompletion.ui b/gtk/ui/gtkemojicompletion.ui
new file mode 100644 (file)
index 0000000..f7a5e1b
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk40">
+  <template class="GtkEmojiCompletion" parent="GtkPopover">
+    <property name="modal">0</property>
+    <style>
+      <class name="emoji-completion"/>
+    </style>
+    <child>
+      <object class="GtkListBox" id="list">
+        <property name="selection-mode">none</property>
+        <property name="activate-on-single-click">1</property>
+        <signal name="row-activated" handler="row_activated"/>
+      </object>
+    </child>
+  </template>
+</interface>
index 627262bf471bd718e71414afb91ff83a968713c2..6ebeaaac51438d85752cec299346a2f44a3ffdeb 100644 (file)
@@ -126,6 +126,7 @@ gtk/gtkdragsource.c
 gtk/gtkdrawingarea.c
 gtk/gtkeditable.c
 gtk/gtkemojichooser.c
+gtk/gtkemojicompletion.c
 gtk/gtkentrybuffer.c
 gtk/gtkentry.c
 gtk/gtkentrycompletion.c
@@ -350,6 +351,7 @@ gtk/ui/gtkcolorchooserdialog.ui
 gtk/ui/gtkcoloreditor.ui
 gtk/ui/gtkdialog.ui
 gtk/ui/gtkemojichooser.ui
+gtk/ui/gtkemojicompletion.ui
 gtk/ui/gtkfilechooserdialog.ui
 gtk/ui/gtkfilechooserwidget.ui
 gtk/ui/gtkfontchooserdialog.ui