[gtk+/wip/matthiasc/emoji-picker: 9/13] Add an emoji completion popup



commit ac468cd06bd8e8d7efe60a8e4cfb98bbd790fbb9
Author: Matthias Clasen <mclasen redhat com>
Date:   Sat Aug 19 14:06:47 2017 -0400

    Add an emoji completion popup
    
    This widget provides entry completion-like functionality
    for Emoji codes like :grin: or :kiss:.

 gtk/gtkemojicompletion.c       |  409 ++++++++++++++++++++++++++++++++++++++++
 gtk/gtkemojicompletion.h       |   41 ++++
 gtk/meson.build                |    1 +
 gtk/theme/Adwaita/_common.scss |   10 +
 gtk/ui/gtkemojicompletion.ui   |   16 ++
 po/POTFILES.in                 |    2 +
 6 files changed, 479 insertions(+), 0 deletions(-)
---
diff --git a/gtk/gtkemojicompletion.c b/gtk/gtkemojicompletion.c
new file mode 100644
index 0000000..1b803d2
--- /dev/null
+++ b/gtk/gtkemojicompletion.c
@@ -0,0 +1,409 @@
+/* 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"
+
+struct _GtkEmojiCompletion
+{
+  GtkPopover parent_instance;
+
+  GtkEntry *entry;
+  char *text;
+  guint length;
+  guint offset;
+  gulong changed_id;
+  guint n_matches;
+
+  GtkWidget *list;
+  GtkWidget *active;
+
+  GVariant *data;
+};
+
+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_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 (GtkListBox    *list,
+                 GtkListBoxRow *row,
+                 gpointer       data)
+{
+  GtkEmojiCompletion *completion = data;
+  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
+move_active_row (GtkEmojiCompletion *completion,
+                 int                 direction)
+{
+  GtkWidget *child;
+
+  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);
+    }
+
+  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);
+}
+
+static void
+activate_active_row (GtkEmojiCompletion *completion)
+{
+  if (completion->active != NULL)
+    emoji_activated (GTK_LIST_BOX (completion->list),
+                     GTK_LIST_BOX_ROW (completion->active),
+                     completion);
+}
+
+static gboolean
+entry_key_press (GtkEntry           *entry,
+                 GdkEventKey        *event,
+                 GtkEmojiCompletion *completion)
+{
+  if (!gtk_widget_get_visible (GTK_WIDGET (completion)))
+    return FALSE;
+
+  if (event->keyval == GDK_KEY_Escape)
+    {
+      gtk_popover_popdown (GTK_POPOVER (completion));
+      return TRUE;
+    }
+
+  if (event->keyval == GDK_KEY_Tab)
+    {
+      guint offset = completion->offset + MAX_ROWS;
+      if (offset >= completion->n_matches)
+        offset = 0;
+      populate_completion (completion, completion->text, offset);
+      return TRUE;
+    }
+
+  if (event->keyval == GDK_KEY_Up)
+    {
+      move_active_row (completion, -1);
+      return TRUE;
+    }
+
+  if (event->keyval == GDK_KEY_Down)
+    {
+      move_active_row (completion, 1);
+      return TRUE;
+    }
+
+  if (event->keyval == GDK_KEY_Return ||
+      event->keyval == GDK_KEY_KP_Enter ||
+      event->keyval == GDK_KEY_ISO_Enter)
+    {
+      activate_active_row (completion);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+entry_focus_out (GtkWidget *entry,
+                 GdkEventFocus *event,
+                 GtkEmojiCompletion *completion)
+{
+  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, "focus-out-event", 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 void
+add_emoji (GtkWidget *list,
+           GVariant  *item)
+{
+  GtkWidget *child;
+  GtkWidget *label;
+  GtkWidget *box;
+  PangoAttrList *attrs;
+  GVariant *codes;
+  char text[64];
+  char *p = text;
+  int i;
+  const char *shortname;
+
+  codes = g_variant_get_child_value (item, 0);
+  for (i = 0; i < g_variant_n_children (codes); i++)
+    {
+      gunichar code;
+
+      g_variant_get_child (codes, i, "u", &code);
+      if (code != 0)
+        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_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_variant_get_child (item, 2, "&s", &shortname);
+  label = gtk_label_new (shortname);
+  gtk_box_pack_start (GTK_BOX (box), label);
+
+  g_object_set_data_full (G_OBJECT (child), "text", g_strdup (text), g_free);
+  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 = 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);
+              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
+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));
+}
+
+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, emoji_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 0000000..ff7cb1f
--- /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/meson.build b/gtk/meson.build
index c961f29..559f4d3 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -142,6 +142,7 @@ gtk_public_sources = files([
   'gtkdrawingarea.c',
   'gtkeditable.c',
   'gtkemojichooser.c',
+  'gtkemojicompletion.c',
   'gtkentry.c',
   'gtkentrybuffer.c',
   'gtkentrycompletion.c',
diff --git a/gtk/theme/Adwaita/_common.scss b/gtk/theme/Adwaita/_common.scss
index 5bad9d7..da9772b 100644
--- a/gtk/theme/Adwaita/_common.scss
+++ b/gtk/theme/Adwaita/_common.scss
@@ -4494,3 +4494,13 @@ 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;
+}
diff --git a/gtk/ui/gtkemojicompletion.ui b/gtk/ui/gtkemojicompletion.ui
new file mode 100644
index 0000000..8398edc
--- /dev/null
+++ b/gtk/ui/gtkemojicompletion.ui
@@ -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="emoji_activated"/>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6ff3206..ee14480 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -114,6 +114,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
@@ -331,6 +332,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


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