summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Clasen <mclasen@redhat.com>2017-08-19 14:06:47 -0400
committerMatthias Clasen <mclasen@redhat.com>2018-06-03 09:43:28 -0400
commit65bb238a3f0075d88f6dd82e76de1e86946db001 (patch)
treeef58e768708f80a7457f29652e1b2cbe22969054
parent7e9ae85dd4f2ce9b3402b6aac60e424c70194dbc (diff)
downloadgtk+-65bb238a3f0075d88f6dd82e76de1e86946db001.tar.gz
Add an emoji completion popup
This widget provides entry completion-like functionality for Emoji codes like :grin: or :kiss:.
-rw-r--r--gtk/Makefile.am2
-rw-r--r--gtk/gtkemojicompletion.c699
-rw-r--r--gtk/gtkemojicompletion.h41
-rw-r--r--gtk/theme/Adwaita/_common.scss16
-rw-r--r--gtk/ui/gtkemojicompletion.ui17
-rw-r--r--po/POTFILES.in2
6 files changed, 776 insertions, 1 deletions
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index a903078efa..54a2ca4908 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -469,6 +469,7 @@ gtk_private_h_sources = \
gtkdialogprivate.h \
gtkdndprivate.h \
gtkemojichooser.h \
+ gtkemojicompletion.h \
gtkentryprivate.h \
gtkeventcontrollerprivate.h \
gtkfilechooserembed.h \
@@ -747,6 +748,7 @@ gtk_base_c_sources = \
gtkdrawingarea.c \
gtkeditable.c \
gtkemojichooser.c \
+ gtkemojicompletion.c \
gtkentry.c \
gtkentrybuffer.c \
gtkentrycompletion.c \
diff --git a/gtk/gtkemojicompletion.c b/gtk/gtkemojicompletion.c
new file mode 100644
index 0000000000..664629be49
--- /dev/null
+++ b/gtk/gtkemojicompletion.c
@@ -0,0 +1,699 @@
+/* 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;
+
+ emoji_activated (GTK_WIDGET (child), completion);
+}
+
+static void
+move_active_row (GtkEmojiCompletion *completion,
+ int direction)
+{
+ GtkWidget *child;
+ GtkWidget *base;
+ GList *children, *l, *active, *last;
+
+ active = NULL;
+ last = NULL;
+ children = gtk_container_get_children (GTK_CONTAINER (completion->list));
+ for (l = children; l; l = l->next)
+ {
+ child = l->data;
+
+ if (completion->active == child)
+ active = l;
+
+ if (l->next == NULL)
+ last = l;
+
+ 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 = (active && active->next) ? active->next->data : NULL;
+ else
+ completion->active = (active && active->prev) ? active->prev->data : NULL;
+ }
+
+ if (completion->active == NULL)
+ {
+ if (direction == 1)
+ completion->active = children->data;
+ else
+ completion->active = last->data;
+ }
+
+ 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;
+ }
+
+ g_list_free (children);
+}
+
+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;
+ GList *children, *l, *active;
+
+ 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;
+
+ active = NULL;
+ children = gtk_container_get_children (GTK_CONTAINER (box));
+ for (l = children; l; l = l->next)
+ {
+ if (l->data == completion->active_variation)
+ active = l;
+ }
+
+ if (!completion->active_variation)
+ next = base;
+ else if (completion->active_variation == base && direction == 1)
+ next = children->data;
+ else if (completion->active_variation == children->data && direction == -1)
+ next = base;
+ else if (direction == 1)
+ next = (active && active->next) ? active->next->data : NULL;
+ else if (direction == -1)
+ next = (active && active->prev) ? active->prev->data : NULL;
+
+ 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);
+ }
+
+ g_list_free (children);
+
+ 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);
+ gtk_widget_show (label);
+ 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_widget_show (child);
+ 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);
+ gtk_widget_show (label);
+ 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_show (child);
+ gtk_widget_set_focus_on_click (child, FALSE);
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show (box);
+ gtk_container_add (GTK_CONTAINER (child), box);
+ gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
+ g_object_set_data (G_OBJECT (child), "base", label);
+
+ stack = gtk_stack_new ();
+ gtk_widget_show (stack);
+ 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, FALSE, FALSE, 0);
+ g_object_set_data (G_OBJECT (child), "stack", stack);
+
+ g_variant_get_child (emoji_data, 2, "&s", &shortname);
+ label = gtk_label_new (shortname);
+ gtk_widget_show (label);
+ 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_widget_show (box);
+ 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)
+ {
+ GList *children;
+
+ children = gtk_container_get_children (GTK_CONTAINER (completion->list));
+ completion->active = children->data;
+ g_list_free (children);
+ 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
index 0000000000..ff7cb1fa63
--- /dev/null
+++ b/gtk/gtkemojicompletion.h
@@ -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
diff --git a/gtk/theme/Adwaita/_common.scss b/gtk/theme/Adwaita/_common.scss
index 85c60c41de..f095083b8f 100644
--- a/gtk/theme/Adwaita/_common.scss
+++ b/gtk/theme/Adwaita/_common.scss
@@ -4557,7 +4557,7 @@ button.emoji-section {
&:checked label { opacity: 1; }
}
-.emoji {
+popover.emoji-picker .emoji {
font-size: x-large;
padding: 6px;
border-radius: 6px;
@@ -4566,3 +4566,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: $popover_hover_color;
+}
diff --git a/gtk/ui/gtkemojicompletion.ui b/gtk/ui/gtkemojicompletion.ui
new file mode 100644
index 0000000000..7f5f3ef49a
--- /dev/null
+++ b/gtk/ui/gtkemojicompletion.ui
@@ -0,0 +1,17 @@
+<?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="visible">1</property>
+ <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>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b72b837f29..a2cd6f5c86 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -136,6 +136,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
@@ -357,6 +358,7 @@ gtk/ui/gtkcoloreditor.ui
gtk/ui/gtkdialog.ui
gtk/ui/gtkemojichooser.ui
gtk/ui/gtkfilechooserbutton.ui
+gtk/ui/gtkemojicompletion.ui
gtk/ui/gtkfilechooserdialog.ui
gtk/ui/gtkfilechooserwidget.ui
gtk/ui/gtkfontbutton.ui