[gnome-builder] egg: add EggSuggestionEntry



commit 340bf11cd7f6ca5c48b53b5f2e21bd820e2e7979
Author: Christian Hergert <chergert redhat com>
Date:   Fri Apr 7 14:31:12 2017 -0700

    egg: add EggSuggestionEntry
    
    This is a new completion replacement for GtkEntry that gives us more
    control over how things are rendered and positioning and animations etc

 contrib/egg/Makefile.am                   |   21 +
 contrib/egg/egg-suggestion-entry-buffer.c |  408 ++++++++++++++
 contrib/egg/egg-suggestion-entry-buffer.h |   52 ++
 contrib/egg/egg-suggestion-entry.c        |  663 +++++++++++++++++++++++
 contrib/egg/egg-suggestion-entry.css      |   53 ++
 contrib/egg/egg-suggestion-entry.h        |   64 +++
 contrib/egg/egg-suggestion-popover.c      |  831 +++++++++++++++++++++++++++++
 contrib/egg/egg-suggestion-popover.h      |   49 ++
 contrib/egg/egg-suggestion-popover.ui     |   42 ++
 contrib/egg/egg-suggestion-row.c          |  209 ++++++++
 contrib/egg/egg-suggestion-row.h          |   49 ++
 contrib/egg/egg-suggestion-row.ui         |   53 ++
 contrib/egg/egg-suggestion.c              |  351 ++++++++++++
 contrib/egg/egg-suggestion.h              |   66 +++
 contrib/egg/egg.gresource.xml             |    9 +-
 contrib/egg/test-suggestion-buffer.c      |  102 ++++
 16 files changed, 3019 insertions(+), 3 deletions(-)
---
diff --git a/contrib/egg/Makefile.am b/contrib/egg/Makefile.am
index 6a97101..4f6d592 100644
--- a/contrib/egg/Makefile.am
+++ b/contrib/egg/Makefile.am
@@ -38,6 +38,11 @@ headers_DATA =                        \
        egg-slider.h                  \
        egg-state-machine-buildable.h \
        egg-state-machine.h           \
+       egg-suggestion.h              \
+       egg-suggestion-entry.h        \
+       egg-suggestion-entry-buffer.h \
+       egg-suggestion-popover.h      \
+       egg-suggestion-row.h          \
        egg-task-cache.h              \
        egg-three-grid.h              \
        egg-widget-action-group.h     \
@@ -74,6 +79,11 @@ libegg_private_la_SOURCES =           \
        egg-slider.c                  \
        egg-state-machine-buildable.c \
        egg-state-machine.c           \
+       egg-suggestion.c              \
+       egg-suggestion-entry.c        \
+       egg-suggestion-entry-buffer.c \
+       egg-suggestion-popover.c      \
+       egg-suggestion-row.c          \
        egg-task-cache.c              \
        egg-three-grid.c              \
        egg-widget-action-group.c     \
@@ -101,6 +111,17 @@ libegg_private_la_LIBADD =            \
        $(NULL)
 
 
+TESTS =
+noinst_PROGRAMS =
+
+
+TESTS += test-suggestion-buffer
+noinst_PROGRAMS += test-suggestion-buffer
+test_suggestion_buffer_SOURCES = test-suggestion-buffer.c
+test_suggestion_buffer_CFLAGS = $(EGG_CFLAGS)
+test_suggestion_buffer_LDADD = $(EGG_LIBS) libegg-private.la
+
+
 if HAVE_INTROSPECTION
 -include $(INTROSPECTION_MAKEFILE)
 
diff --git a/contrib/egg/egg-suggestion-entry-buffer.c b/contrib/egg/egg-suggestion-entry-buffer.c
new file mode 100644
index 0000000..68d3a6e
--- /dev/null
+++ b/contrib/egg/egg-suggestion-entry-buffer.c
@@ -0,0 +1,408 @@
+/* egg-suggestion-entry-buffer.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-entry-buffer"
+
+#include <string.h>
+
+#include "egg-suggestion-entry-buffer.h"
+
+typedef struct
+{
+  EggSuggestion *suggestion;
+  gchar         *text;
+  gchar         *suffix;
+  guint          in_insert : 1;
+  guint          in_delete : 1;
+} EggSuggestionEntryBufferPrivate;
+
+enum {
+  PROP_0,
+  PROP_SUGGESTION,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestionEntryBuffer, egg_suggestion_entry_buffer, GTK_TYPE_ENTRY_BUFFER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+egg_suggestion_entry_buffer_drop_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suffix != NULL)
+    {
+      guint length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      guint suffix_len = strlen (priv->suffix);
+
+      g_clear_pointer (&priv->suffix, g_free);
+      gtk_entry_buffer_emit_deleted_text (GTK_ENTRY_BUFFER (self), length, suffix_len);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_insert_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suggestion != NULL)
+    {
+      g_autofree gchar *suffix = NULL;
+      const gchar *text;
+      guint length;
+
+      length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      text = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER 
(self), NULL);
+      suffix = egg_suggestion_suggest_suffix (priv->suggestion, text);
+
+      if (suffix != NULL)
+        {
+          priv->suffix = g_steal_pointer (&suffix);
+          gtk_entry_buffer_emit_inserted_text (GTK_ENTRY_BUFFER (self),
+                                               length,
+                                               priv->suffix,
+                                               g_utf8_strlen (priv->suffix, -1));
+        }
+    }
+}
+
+const gchar *
+egg_suggestion_entry_buffer_get_typed_text (EggSuggestionEntryBuffer *self)
+{
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), NULL);
+
+  return GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (GTK_ENTRY_BUFFER 
(self), NULL);
+}
+
+static const gchar *
+egg_suggestion_entry_buffer_get_text (GtkEntryBuffer *buffer,
+                                      gsize          *n_bytes)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->text == NULL)
+    {
+      const gchar *text;
+      GString *str = NULL;
+
+      text = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_text (buffer, n_bytes);
+
+      str = g_string_new (text);
+      if (priv->suffix != NULL)
+        g_string_append (str, priv->suffix);
+      priv->text = g_string_free (str, FALSE);
+    }
+
+  return priv->text;
+}
+
+static guint
+egg_suggestion_entry_buffer_get_length (GtkEntryBuffer *buffer)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint ret;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length (buffer);
+
+  if (priv->suffix != NULL)
+    ret += strlen (priv->suffix);
+
+  return ret;
+}
+
+static void
+egg_suggestion_entry_buffer_inserted_text (GtkEntryBuffer *buffer,
+                                           guint           position,
+                                           const gchar    *chars,
+                                           guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  g_clear_pointer (&priv->text, g_free);
+
+  GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->inserted_text (buffer, position, chars, 
n_chars);
+}
+
+static void
+egg_suggestion_entry_buffer_deleted_text (GtkEntryBuffer *buffer,
+                                          guint           position,
+                                          guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  g_clear_pointer (&priv->text, g_free);
+
+  GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->deleted_text (buffer, position, 
n_chars);
+}
+
+static guint
+egg_suggestion_entry_buffer_insert_text (GtkEntryBuffer *buffer,
+                                         guint           position,
+                                         const gchar    *chars,
+                                         guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint ret = 0;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+  g_assert (chars != NULL || n_chars == 0);
+  g_assert (priv->in_insert == FALSE);
+
+  priv->in_insert = TRUE;
+
+  if (n_chars == 0)
+    goto failure;
+
+  egg_suggestion_entry_buffer_drop_suggestion (self);
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->insert_text (buffer, position, 
chars, n_chars);
+  if (ret < n_chars)
+    goto failure;
+
+  egg_suggestion_entry_buffer_insert_suggestion (self);
+
+failure:
+  priv->in_insert = FALSE;
+
+  return ret;
+}
+
+static guint
+egg_suggestion_entry_buffer_delete_text (GtkEntryBuffer *buffer,
+                                         guint           position,
+                                         guint           n_chars)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)buffer;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+  guint length;
+  guint ret = 0;
+
+  g_assert (GTK_IS_ENTRY_BUFFER (buffer));
+
+  priv->in_delete = TRUE;
+
+  length = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length (buffer);
+
+  if (position >= length)
+    goto failure;
+
+  if (position + n_chars > length)
+    n_chars = length - position;
+
+  egg_suggestion_entry_buffer_drop_suggestion (self);
+
+  ret = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->delete_text (buffer, position, 
n_chars);
+
+  if (ret != 0 && priv->suggestion != NULL)
+    egg_suggestion_entry_buffer_insert_suggestion (self);
+
+failure:
+  priv->in_delete = FALSE;
+
+  return ret;
+}
+
+static void
+egg_suggestion_entry_buffer_finalize (GObject *object)
+{
+  EggSuggestionEntryBuffer *self = (EggSuggestionEntryBuffer *)object;
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_clear_object (&priv->suggestion);
+  g_clear_pointer (&priv->text, g_free);
+  g_clear_pointer (&priv->suffix, g_free);
+
+  G_OBJECT_CLASS (egg_suggestion_entry_buffer_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_entry_buffer_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  EggSuggestionEntryBuffer *self = EGG_SUGGESTION_ENTRY_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      g_value_set_object (value, egg_suggestion_entry_buffer_get_suggestion (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  EggSuggestionEntryBuffer *self = EGG_SUGGESTION_ENTRY_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      egg_suggestion_entry_buffer_set_suggestion (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_buffer_class_init (EggSuggestionEntryBufferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkEntryBufferClass *entry_buffer_class = GTK_ENTRY_BUFFER_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_entry_buffer_finalize;
+  object_class->get_property = egg_suggestion_entry_buffer_get_property;
+  object_class->set_property = egg_suggestion_entry_buffer_set_property;
+
+  entry_buffer_class->inserted_text = egg_suggestion_entry_buffer_inserted_text;
+  entry_buffer_class->deleted_text = egg_suggestion_entry_buffer_deleted_text;
+  entry_buffer_class->get_text = egg_suggestion_entry_buffer_get_text;
+  entry_buffer_class->get_length = egg_suggestion_entry_buffer_get_length;
+  entry_buffer_class->insert_text = egg_suggestion_entry_buffer_insert_text;
+  entry_buffer_class->delete_text = egg_suggestion_entry_buffer_delete_text;
+
+  properties [PROP_SUGGESTION] =
+    g_param_spec_object ("suggestion",
+                         "Suggestion",
+                         "The suggestion currently selected",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+egg_suggestion_entry_buffer_init (EggSuggestionEntryBuffer *self)
+{
+}
+
+EggSuggestionEntryBuffer *
+egg_suggestion_entry_buffer_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_ENTRY_BUFFER, NULL);
+}
+
+/**
+ * egg_suggestion_entry_buffer_get_suggestion:
+ * @self: a #EggSuggestionEntryBuffer
+ *
+ * Gets the #EggSuggestion that is the current "preview suffix" of the
+ * text in the entry.
+ *
+ * Returns: (transfer none) (nullable): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_entry_buffer_get_suggestion (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), NULL);
+
+  return priv->suggestion;
+}
+
+/**
+ * egg_suggestion_entry_buffer_set_suggestion:
+ * @self: a #EggSuggestionEntryBuffer
+ * @suggestion: (nullable): An #EggSuggestion or %NULL
+ *
+ * Sets the current suggestion for the entry buffer.
+ *
+ * The suggestion is used to get a potential suffix for the current entry
+ * text. This allows the entry to show "preview text" after the entered
+ * text for what might be inserted should they activate the current item.
+ */
+void
+egg_suggestion_entry_buffer_set_suggestion (EggSuggestionEntryBuffer *self,
+                                            EggSuggestion            *suggestion)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (priv->suggestion != suggestion)
+    {
+      egg_suggestion_entry_buffer_drop_suggestion (self);
+      g_set_object (&priv->suggestion, suggestion);
+      if (!priv->in_delete && !priv->in_insert)
+        egg_suggestion_entry_buffer_insert_suggestion (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]);
+    }
+}
+
+guint
+egg_suggestion_entry_buffer_get_typed_length (EggSuggestionEntryBuffer *self)
+{
+  const gchar *text;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self), 0);
+
+  text = egg_suggestion_entry_buffer_get_typed_text (self);
+
+  return text ? g_utf8_strlen (text, -1) : 0;
+}
+
+void
+egg_suggestion_entry_buffer_commit (EggSuggestionEntryBuffer *self)
+{
+  EggSuggestionEntryBufferPrivate *priv = egg_suggestion_entry_buffer_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY_BUFFER (self));
+
+  if (priv->suffix != NULL)
+    {
+      g_autofree gchar *suffix = g_steal_pointer (&priv->suffix);
+      guint position;
+
+      g_clear_object (&priv->suggestion);
+      position = GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->get_length 
(GTK_ENTRY_BUFFER (self));
+      GTK_ENTRY_BUFFER_CLASS (egg_suggestion_entry_buffer_parent_class)->insert_text (GTK_ENTRY_BUFFER 
(self),
+                                                                                      position,
+                                                                                      suffix,
+                                                                                      g_utf8_strlen (suffix, 
-1));
+    }
+}
diff --git a/contrib/egg/egg-suggestion-entry-buffer.h b/contrib/egg/egg-suggestion-entry-buffer.h
new file mode 100644
index 0000000..e8078b7
--- /dev/null
+++ b/contrib/egg/egg-suggestion-entry-buffer.h
@@ -0,0 +1,52 @@
+/* egg-suggestion-entry-buffer.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef EGG_SUGGESTION_ENTRY_BUFFER_H
+#define EGG_SUGGESTION_ENTRY_BUFFER_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ENTRY_BUFFER (egg_suggestion_entry_buffer_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionEntryBuffer, egg_suggestion_entry_buffer, EGG, 
SUGGESTION_ENTRY_BUFFER, GtkEntryBuffer)
+
+struct _EggSuggestionEntryBufferClass
+{
+  GtkEntryBufferClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+EggSuggestionEntryBuffer *egg_suggestion_entry_buffer_new              (void);
+EggSuggestion            *egg_suggestion_entry_buffer_get_suggestion   (EggSuggestionEntryBuffer *self);
+void                      egg_suggestion_entry_buffer_set_suggestion   (EggSuggestionEntryBuffer *self,
+                                                                        EggSuggestion            
*suggestion);
+const gchar              *egg_suggestion_entry_buffer_get_typed_text   (EggSuggestionEntryBuffer *self);
+guint                     egg_suggestion_entry_buffer_get_typed_length (EggSuggestionEntryBuffer *self);
+void                      egg_suggestion_entry_buffer_commit           (EggSuggestionEntryBuffer *self);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ENTRY_BUFFER_H */
diff --git a/contrib/egg/egg-suggestion-entry.c b/contrib/egg/egg-suggestion-entry.c
new file mode 100644
index 0000000..4bd8c26
--- /dev/null
+++ b/contrib/egg/egg-suggestion-entry.c
@@ -0,0 +1,663 @@
+/* egg-suggestion-entry.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-entry"
+
+#include <glib/gi18n.h>
+
+#include "egg-suggestion.h"
+#include "egg-suggestion-entry.h"
+#include "egg-suggestion-entry-buffer.h"
+#include "egg-suggestion-popover.h"
+
+#if 0
+# define _TRACE_LEVEL (1<<G_LOG_LEVEL_USER_SHIFT)
+# define _TRACE(...) do { g_log(G_LOG_DOMAIN, _TRACE_LEVEL, __VA_ARGS__); } while (0)
+# define EGG_TRACE_MSG(m,...) _TRACE("   MSG: %s():%u: "m, G_STRFUNC, __LINE__, __VA_ARGS__)
+# define EGG_ENTRY _TRACE(" ENTRY: %s(): %u", G_STRFUNC, __LINE__)
+# define EGG_EXIT do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return; } while (0)
+# define EGG_RETURN(r) do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return (r); } while (0)
+# define EGG_GOTO(_l) do { _TRACE("  GOTO: %s(): %u: %s", G_STRFUNC, __LINE__, #_l); goto _l; } while (0)
+#else
+# define EGG_TRACE_MSG(m,...) do { } while (0)
+# define EGG_ENTRY            do { } while (0)
+# define EGG_EXIT             return
+# define EGG_RETURN(r)        return (r)
+# define EGG_GOTO(_l)         goto _l
+#endif
+
+typedef struct
+{
+  EggSuggestionPopover     *popover;
+  EggSuggestionEntryBuffer *buffer;
+  GListModel               *model;
+  gulong                    changed_handler;
+} EggSuggestionEntryPrivate;
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  PROP_TYPED_TEXT,
+  N_PROPS
+};
+
+enum {
+  ACTIVATE_SUGGESTION,
+  HIDE_SUGGESTIONS,
+  MOVE_SUGGESTION,
+  SHOW_SUGGESTIONS,
+  SUGGESTION_ACTIVATED,
+  N_SIGNALS
+};
+
+static void buildable_iface_init (GtkBuildableIface    *iface);
+static void editable_iface_init  (GtkEditableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EggSuggestionEntry, egg_suggestion_entry, GTK_TYPE_ENTRY,
+                         G_ADD_PRIVATE (EggSuggestionEntry)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+static guint changed_signal_id;
+static GtkEditableInterface *editable_parent_iface;
+
+static void
+egg_suggestion_entry_show_suggestions (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_popup (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_hide_suggestions (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_popdown (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_init_default_css (void)
+{
+  g_autoptr(GtkCssProvider) css_provider = NULL;
+  static gsize initialized;
+
+  if (g_once_init_enter (&initialized))
+    {
+      css_provider = gtk_css_provider_new ();
+      gtk_css_provider_load_from_resource (css_provider,
+                                           "/org/gnome/libegg-private/egg-suggestion-entry.css");
+      gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                                 GTK_STYLE_PROVIDER (css_provider),
+                                                 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - 1);
+      g_once_init_leave (&initialized, TRUE);
+    }
+}
+
+static gboolean
+egg_suggestion_entry_focus_out_event (GtkWidget     *widget,
+                                      GdkEventFocus *event)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (event != NULL);
+
+  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+
+  return GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->focus_out_event (widget, event);
+}
+
+static void
+egg_suggestion_entry_hierarchy_changed (GtkWidget *widget,
+                                        GtkWidget *old_toplevel)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  if (priv->popover != NULL)
+    {
+      GtkWidget *toplevel = gtk_widget_get_ancestor (widget, GTK_TYPE_WINDOW);
+
+      gtk_window_set_transient_for (GTK_WINDOW (priv->popover), GTK_WINDOW (toplevel));
+    }
+}
+
+static gboolean
+egg_suggestion_entry_key_press_event (GtkWidget   *widget,
+                                      GdkEventKey *key)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  /*
+   * If Tab was pressed, and there is uncommitted suggested text,
+   * commit it and stop propagation of the key press.
+   */
+  if (key->keyval == GDK_KEY_Tab && (key->state & GDK_MODIFIER_MASK) == 0)
+    {
+      const gchar *typed_text;
+      EggSuggestion *suggestion;
+
+      typed_text = egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+      suggestion = egg_suggestion_popover_get_selected (priv->popover);
+
+      if (typed_text != NULL && suggestion != NULL)
+        {
+          g_autofree gchar *replace = egg_suggestion_replace_typed_text (suggestion, typed_text);
+
+          g_signal_handler_block (self, priv->changed_handler);
+
+          if (replace != NULL)
+            gtk_entry_set_text (GTK_ENTRY (self), replace);
+          else
+            egg_suggestion_entry_buffer_commit (priv->buffer);
+          gtk_editable_set_position (GTK_EDITABLE (self), -1);
+
+          g_signal_handler_unblock (self, priv->changed_handler);
+
+          return GDK_EVENT_STOP;
+        }
+    }
+
+  return GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->key_press_event (widget, key);
+}
+
+static void
+egg_suggestion_entry_update_attrs (EggSuggestionEntry *self)
+{
+  PangoAttribute *attr;
+  PangoAttrList *list;
+  const gchar *typed_text;
+  const gchar *text;
+  GdkRGBA rgba;
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  gdk_rgba_parse (&rgba, "#666666");
+
+  text = gtk_entry_get_text (GTK_ENTRY (self));
+  typed_text = egg_suggestion_entry_get_typed_text (self);
+
+  list = pango_attr_list_new ();
+  attr = pango_attr_foreground_new (rgba.red * 0xFFFF, rgba.green * 0xFFFF, rgba.blue * 0xFFFF);
+  attr->start_index = strlen (typed_text);
+  attr->end_index = strlen (text);
+  pango_attr_list_insert (list, attr);
+  gtk_entry_set_attributes (GTK_ENTRY (self), list);
+  pango_attr_list_unref (list);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_changed (GtkEditable *editable)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)editable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+  EggSuggestion *suggestion;
+  const gchar *text;
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  /*
+   * If we aren't focused, just ignore everything. One such example might be
+   * updating an URI in a webbrowser.
+   */
+  if (!gtk_widget_has_focus (GTK_WIDGET (editable)))
+    return;
+
+  g_signal_handler_block (self, priv->changed_handler);
+
+  text = egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+
+  if (text == NULL || *text == '\0')
+    {
+      g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+      EGG_GOTO (finish);
+    }
+
+  g_signal_emit (self, signals [SHOW_SUGGESTIONS], 0);
+
+  suggestion = egg_suggestion_popover_get_selected (priv->popover);
+
+  if (suggestion != NULL)
+    {
+      g_object_ref (suggestion);
+      egg_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);
+      g_object_unref (suggestion);
+    }
+
+finish:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TYPED_TEXT]);
+
+  g_signal_handler_unblock (self, priv->changed_handler);
+
+  egg_suggestion_entry_update_attrs (self);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_suggestion_activated (EggSuggestionEntry   *self,
+                                           EggSuggestion        *suggestion,
+                                           EggSuggestionPopover *popover)
+{
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+  g_assert (EGG_IS_SUGGESTION (suggestion));
+  g_assert (EGG_IS_SUGGESTION_POPOVER (popover));
+
+  g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
+  gtk_entry_set_text (GTK_ENTRY (self), "");
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_move_suggestion (EggSuggestionEntry *self,
+                                      gint                amount)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_move_by (priv->popover, amount);
+}
+
+static void
+egg_suggestion_entry_activate_suggestion (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  egg_suggestion_popover_activate_selected (priv->popover);
+
+  EGG_EXIT;
+}
+
+static void
+egg_suggestion_entry_constructed (GObject *object)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)object;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  G_OBJECT_CLASS (egg_suggestion_entry_parent_class)->constructed (object);
+
+  gtk_entry_set_buffer (GTK_ENTRY (self), GTK_ENTRY_BUFFER (priv->buffer));
+}
+
+static void
+egg_suggestion_entry_destroy (GtkWidget *widget)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)widget;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  if (priv->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (priv->popover));
+
+  g_clear_object (&priv->model);
+
+  GTK_WIDGET_CLASS (egg_suggestion_entry_parent_class)->destroy (widget);
+
+  g_assert (priv->popover == NULL);
+}
+
+static void
+egg_suggestion_entry_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  EggSuggestionEntry *self = EGG_SUGGESTION_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, egg_suggestion_entry_get_model (self));
+      break;
+
+    case PROP_TYPED_TEXT:
+      g_value_set_string (value, egg_suggestion_entry_get_typed_text (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  EggSuggestionEntry *self = EGG_SUGGESTION_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      egg_suggestion_entry_set_model (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_entry_class_init (EggSuggestionEntryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkBindingSet *bindings;
+
+  object_class->constructed = egg_suggestion_entry_constructed;
+  object_class->get_property = egg_suggestion_entry_get_property;
+  object_class->set_property = egg_suggestion_entry_set_property;
+
+  widget_class->destroy = egg_suggestion_entry_destroy;
+  widget_class->focus_out_event = egg_suggestion_entry_focus_out_event;
+  widget_class->hierarchy_changed = egg_suggestion_entry_hierarchy_changed;
+  widget_class->key_press_event = egg_suggestion_entry_key_press_event;
+
+  klass->hide_suggestions = egg_suggestion_entry_hide_suggestions;
+  klass->show_suggestions = egg_suggestion_entry_show_suggestions;
+  klass->move_suggestion = egg_suggestion_entry_move_suggestion;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The model to be visualized",
+                         G_TYPE_LIST_MODEL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TYPED_TEXT] =
+    g_param_spec_string ("typed-text",
+                         "Typed Text",
+                         "Typed text into the entry, does not include suggested text",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [HIDE_SUGGESTIONS] =
+    g_signal_new ("hide-suggestions",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, hide_suggestions),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * EggSuggestionEntry::move-suggestion:
+   * @self: A #EggSuggestionEntry
+   * @amount: The number of items to move
+   *
+   * This moves the selected suggestion in the popover by the value
+   * provided. -1 moves up one row, 1, moves down a row.
+   */
+  signals [MOVE_SUGGESTION] =
+    g_signal_new ("move-suggestion",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, move_suggestion),
+                  NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT);
+
+  signals [SHOW_SUGGESTIONS] =
+    g_signal_new ("show-suggestions",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, show_suggestions),
+                  NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  signals [SUGGESTION_ACTIVATED] =
+    g_signal_new ("suggestion-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionEntryClass, suggestion_activated),
+                  NULL, NULL, NULL, G_TYPE_NONE, 1, EGG_TYPE_SUGGESTION);
+
+  widget_class->activate_signal = signals [ACTIVATE_SUGGESTION] =
+    g_signal_new_class_handler ("activate-suggestion",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (egg_suggestion_entry_activate_suggestion),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  bindings = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "hide-suggestions", 0);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_space, GDK_CONTROL_MASK, "show-suggestions", 0);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Up, 0, "move-suggestion", 1, G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Down, 0, "move-suggestion", 1, G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Prior, 0, "move-suggestion", 1, G_TYPE_INT, -10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Next, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Return, 0, "activate-suggestion", 0);
+
+  changed_signal_id = g_signal_lookup ("changed", GTK_TYPE_ENTRY);
+}
+
+static void
+egg_suggestion_entry_init (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  egg_suggestion_entry_init_default_css ();
+
+  priv->changed_handler =
+    g_signal_connect_after (self,
+                            "changed",
+                            G_CALLBACK (egg_suggestion_entry_changed),
+                            NULL);
+
+  priv->popover = g_object_new (EGG_TYPE_SUGGESTION_POPOVER,
+                                "destroy-with-parent", TRUE,
+                                "modal", FALSE,
+                                "relative-to", self,
+                                "type", GTK_WINDOW_POPUP,
+                                NULL);
+  g_signal_connect_object (priv->popover,
+                           "suggestion-activated",
+                           G_CALLBACK (egg_suggestion_entry_suggestion_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect (priv->popover,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &priv->popover);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
+                               "suggestion");
+
+  priv->buffer = egg_suggestion_entry_buffer_new ();
+}
+
+GtkWidget *
+egg_suggestion_entry_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_ENTRY, NULL);
+}
+
+static GObject *
+egg_suggestion_entry_get_internal_child (GtkBuildable *buildable,
+                                         GtkBuilder   *builder,
+                                         const gchar  *childname)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)buildable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  if (g_strcmp0 (childname, "popover") == 0)
+    return G_OBJECT (priv->popover);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = egg_suggestion_entry_get_internal_child;
+}
+
+static void
+egg_suggestion_entry_set_selection_bounds (GtkEditable *editable,
+                                           gint         start_pos,
+                                           gint         end_pos)
+{
+  EggSuggestionEntry *self = (EggSuggestionEntry *)editable;
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_assert (EGG_IS_SUGGESTION_ENTRY (self));
+
+  g_signal_handler_block (self, priv->changed_handler);
+
+  if (end_pos < 0)
+    end_pos = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (priv->buffer));
+
+  if (end_pos > (gint)egg_suggestion_entry_buffer_get_typed_length (priv->buffer))
+    egg_suggestion_entry_buffer_commit (priv->buffer);
+
+  editable_parent_iface->set_selection_bounds (editable, start_pos, end_pos);
+
+  g_signal_handler_unblock (self, priv->changed_handler);
+}
+
+static void
+editable_iface_init (GtkEditableInterface *iface)
+{
+  editable_parent_iface = g_type_interface_peek_parent (iface);
+
+  iface->set_selection_bounds = egg_suggestion_entry_set_selection_bounds;
+}
+
+
+/**
+ * egg_suggestion_entry_get_model:
+ * @self: a #EggSuggestionEntry
+ *
+ * Gets the model being visualized.
+ *
+ * Returns: (nullable) (transfer none): A #GListModel or %NULL.
+ */
+GListModel *
+egg_suggestion_entry_get_model (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return priv->model;
+}
+
+void
+egg_suggestion_entry_set_model (EggSuggestionEntry *self,
+                                GListModel         *model)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY (self));
+  g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), EGG_TYPE_SUGGESTION));
+
+  if (g_set_object (&priv->model, model))
+    {
+      EGG_TRACE_MSG ("Model has %u items",
+                     model ? g_list_model_get_n_items (model) : 0);
+      egg_suggestion_popover_set_model (priv->popover, model);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+      egg_suggestion_entry_update_attrs (self);
+    }
+
+  EGG_EXIT;
+}
+
+/**
+ * egg_suggestion_entry_get_suggestion:
+ * @self: a #EggSuggestionEntry
+ *
+ * Gets the currently selected suggestion.
+ *
+ * Returns: (nullable) (transfer none): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_entry_get_suggestion (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return egg_suggestion_popover_get_selected (priv->popover);
+}
+
+void
+egg_suggestion_entry_set_suggestion (EggSuggestionEntry *self,
+                                     EggSuggestion      *suggestion)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  EGG_ENTRY;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ENTRY (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION_ENTRY (suggestion));
+
+  egg_suggestion_popover_set_selected (priv->popover, suggestion);
+  egg_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);
+
+  EGG_EXIT;
+}
+
+const gchar *
+egg_suggestion_entry_get_typed_text (EggSuggestionEntry *self)
+{
+  EggSuggestionEntryPrivate *priv = egg_suggestion_entry_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ENTRY (self), NULL);
+
+  return egg_suggestion_entry_buffer_get_typed_text (priv->buffer);
+}
diff --git a/contrib/egg/egg-suggestion-entry.css b/contrib/egg/egg-suggestion-entry.css
new file mode 100644
index 0000000..fe837db
--- /dev/null
+++ b/contrib/egg/egg-suggestion-entry.css
@@ -0,0 +1,53 @@
+suggestionpopover {
+  background: transparent;
+}
+
+suggestionpopover > revealer > box {
+  margin: 0px 0px 60px 0px;
+  border-radius: 0 0 7px 7px;
+  border: 1px solid @borders;
+  border-top: none;
+  box-shadow: 0px -10px 30px #000;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow {
+  background: rgba(255,255,255,.95);
+  border-radius: 0 0 7px 7px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list {
+  background: transparent;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:first-child {
+  transition: none;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row {
+  transition: none;
+  border-bottom: 1px solid alpha(@borders, 0.2);
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:selected .title {
+  color: #fff;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row .title {
+  color: #4a86cf;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:first-child:selected {
+  border-radius: 0;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > 
row:last-child:selected:not(:first-child) {
+  border-radius: 0 0 7px 7px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row > box {
+  margin: 4px 8px 4px 8px;
+}
+
+suggestionpopover > revealer > box > elastic > scrolledwindow > viewport > list > row:last-child {
+  border-bottom: none;
+}
diff --git a/contrib/egg/egg-suggestion-entry.h b/contrib/egg/egg-suggestion-entry.h
new file mode 100644
index 0000000..4c0f07b
--- /dev/null
+++ b/contrib/egg/egg-suggestion-entry.h
@@ -0,0 +1,64 @@
+/* egg-suggestion-entry.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef EGG_SUGGESTION_ENTRY_H
+#define EGG_SUGGESTION_ENTRY_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ENTRY (egg_suggestion_entry_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionEntry, egg_suggestion_entry, EGG, SUGGESTION_ENTRY, GtkEntry)
+
+struct _EggSuggestionEntryClass
+{
+  GtkEntryClass parent_class;
+
+  void (*hide_suggestions)     (EggSuggestionEntry *self);
+  void (*show_suggestions)     (EggSuggestionEntry *self);
+  void (*move_suggestion )     (EggSuggestionEntry *self,
+                                gint                amount);
+  void (*suggestion_activated) (EggSuggestionEntry *self,
+                                EggSuggestion      *suggestion);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+GtkWidget     *egg_suggestion_entry_new            (void);
+void           egg_suggestion_entry_set_model      (EggSuggestionEntry *self,
+                                                    GListModel         *model);
+GListModel    *egg_suggestion_entry_get_model      (EggSuggestionEntry *self);
+const gchar   *egg_suggestion_entry_get_typed_text (EggSuggestionEntry *self);
+EggSuggestion *egg_suggestion_entry_get_suggestion (EggSuggestionEntry *self);
+void           egg_suggestion_entry_set_suggestion (EggSuggestionEntry *self,
+                                                    EggSuggestion      *suggestion);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ENTRY_H */
diff --git a/contrib/egg/egg-suggestion-popover.c b/contrib/egg/egg-suggestion-popover.c
new file mode 100644
index 0000000..f8b445b
--- /dev/null
+++ b/contrib/egg/egg-suggestion-popover.c
@@ -0,0 +1,831 @@
+/* egg-suggestion-popover.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-popover"
+
+#include <glib/gi18n.h>
+
+#include "egg-animation.h"
+#include "egg-elastic-bin.h"
+#include "egg-suggestion.h"
+#include "egg-suggestion-popover.h"
+#include "egg-suggestion-row.h"
+
+struct _EggSuggestionPopover
+{
+  GtkWindow           parent_instance;
+
+  GtkWidget          *relative_to;
+  GtkWindow          *transient_for;
+  GtkRevealer        *revealer;
+  GtkScrolledWindow  *scrolled_window;
+  GtkListBox         *list_box;
+
+  GListModel         *model;
+
+  GType               row_type;
+
+  gulong              delete_event_handler;
+  gulong              configure_event_handler;
+  gulong              size_allocate_handler;
+  gulong              items_changed_handler;
+};
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  PROP_RELATIVE_TO,
+  PROP_SELECTED,
+  N_PROPS
+};
+
+enum {
+  SUGGESTION_ACTIVATED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (EggSuggestionPopover, egg_suggestion_popover, GTK_TYPE_WINDOW)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+egg_suggestion_popover_reposition (EggSuggestionPopover *self)
+{
+  gint width;
+  gint x;
+  gint y;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->relative_to == NULL ||
+      self->transient_for == NULL ||
+      !gtk_widget_get_mapped (self->relative_to) ||
+      !gtk_widget_get_mapped (GTK_WIDGET (self->transient_for)))
+    return;
+
+  gtk_window_get_size (self->transient_for, &width, NULL);
+  gtk_widget_set_size_request (GTK_WIDGET (self), width, -1);
+  gtk_window_get_position (self->transient_for, &x, &y);
+
+  /*
+   * XXX: This is just a hack for testing so we get the placement right.
+   *
+   *      What we should really do is allow hte EggSuggestionEntry to set our
+   *      relative-to property by wrapping it. That would all the caller to
+   *      place the popover relative to the main content area of the window
+   *      as might be desired for a URL entry or global application search.
+   */
+
+  gtk_window_move (GTK_WINDOW (self), x, y + 47);
+}
+
+/**
+ * egg_suggestion_popover_get_relative_to:
+ * @self: a #EggSuggestionPopover
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
+ */
+GtkWidget *
+egg_suggestion_popover_get_relative_to (EggSuggestionPopover *self)
+{
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  return self->relative_to;
+}
+
+void
+egg_suggestion_popover_set_relative_to (EggSuggestionPopover *self,
+                                        GtkWidget            *relative_to)
+{
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!relative_to || GTK_IS_WIDGET (relative_to));
+
+  if (self->relative_to != relative_to)
+    {
+      if (self->relative_to != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (self->relative_to,
+                                                G_CALLBACK (gtk_widget_destroyed),
+                                                &self->relative_to);
+          self->relative_to = NULL;
+        }
+
+      if (relative_to != NULL)
+        {
+          self->relative_to = relative_to;
+          g_signal_connect (self->relative_to,
+                            "destroy",
+                            G_CALLBACK (gtk_widget_destroyed),
+                            &self->relative_to);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_TO]);
+    }
+}
+
+static void
+egg_suggestion_popover_hide (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->transient_for != NULL)
+    gtk_window_group_remove_window (gtk_window_get_group (self->transient_for),
+                                    GTK_WINDOW (self));
+
+  g_signal_handler_disconnect (self->transient_for, self->delete_event_handler);
+  g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler);
+  g_signal_handler_disconnect (self->transient_for, self->configure_event_handler);
+
+  self->delete_event_handler = 0;
+  self->size_allocate_handler = 0;
+  self->configure_event_handler = 0;
+
+  self->transient_for = NULL;
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->hide (widget);
+}
+
+static void
+egg_suggestion_popover_transient_for_size_allocate (EggSuggestionPopover *self,
+                                                    GtkAllocation        *allocation,
+                                                    GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (allocation != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  egg_suggestion_popover_reposition (self);
+}
+
+static gboolean
+egg_suggestion_popover_transient_for_delete_event (EggSuggestionPopover *self,
+                                                   GdkEvent             *event,
+                                                   GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (event != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  gtk_widget_hide (GTK_WIDGET (self));
+
+  return FALSE;
+}
+
+static gboolean
+egg_suggestion_popover_transient_for_configure_event (EggSuggestionPopover *self,
+                                                      GdkEvent             *event,
+                                                      GtkWindow            *toplevel)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (event != NULL);
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  gtk_widget_hide (GTK_WIDGET (self));
+
+  return FALSE;
+}
+
+static void
+egg_suggestion_popover_show (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->relative_to != NULL)
+    {
+      GtkWidget *toplevel;
+
+      toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self->relative_to), GTK_TYPE_WINDOW);
+
+      if (GTK_IS_WINDOW (toplevel))
+        {
+          self->transient_for = GTK_WINDOW (toplevel);
+          gtk_window_group_add_window (gtk_window_get_group (self->transient_for),
+                                       GTK_WINDOW (self));
+          self->delete_event_handler =
+            g_signal_connect_object (toplevel,
+                                     "delete-event",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_delete_event),
+                                     self,
+                                     G_CONNECT_SWAPPED);
+          self->size_allocate_handler =
+            g_signal_connect_object (toplevel,
+                                     "size-allocate",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_size_allocate),
+                                     self,
+                                     G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+          self->configure_event_handler =
+            g_signal_connect_object (toplevel,
+                                     "configure-event",
+                                     G_CALLBACK (egg_suggestion_popover_transient_for_configure_event),
+                                     self,
+                                     G_CONNECT_SWAPPED);
+          egg_suggestion_popover_reposition (self);
+        }
+    }
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->show (widget);
+}
+
+static void
+egg_suggestion_popover_screen_changed (GtkWidget *widget,
+                                       GdkScreen *previous_screen)
+{
+  GdkScreen *screen;
+  GdkVisual *visual;
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->screen_changed (widget, previous_screen);
+
+  screen = gtk_widget_get_screen (widget);
+  visual = gdk_screen_get_rgba_visual (screen);
+
+  if (visual != NULL)
+    gtk_widget_set_visual (widget, visual);
+}
+
+static void
+egg_suggestion_popover_realize (GtkWidget *widget)
+{
+  GdkScreen *screen;
+  GdkVisual *visual;
+
+  screen = gtk_widget_get_screen (widget);
+  visual = gdk_screen_get_rgba_visual (screen);
+
+  if (visual != NULL)
+    gtk_widget_set_visual (widget, visual);
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->realize (widget);
+}
+
+static void
+egg_suggestion_popover_notify_child_revealed (EggSuggestionPopover *self,
+                                              GParamSpec           *pspec,
+                                              GtkRevealer          *revealer)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (GTK_IS_REVEALER (revealer));
+
+  if (!gtk_revealer_get_reveal_child (self->revealer))
+    gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static void
+egg_suggestion_popover_list_box_row_activated (EggSuggestionPopover *self,
+                                               EggSuggestionRow     *row,
+                                               GtkListBox           *list_box)
+{
+  EggSuggestion *suggestion;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (EGG_IS_SUGGESTION_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  suggestion = egg_suggestion_row_get_suggestion (row);
+  g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+}
+
+static void
+egg_suggestion_popover_list_box_row_selected (EggSuggestionPopover *self,
+                                              EggSuggestionRow     *row,
+                                              GtkListBox           *list_box)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (!row || EGG_IS_SUGGESTION_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTED]);
+}
+
+static void
+egg_suggestion_popover_destroy (GtkWidget *widget)
+{
+  EggSuggestionPopover *self = (EggSuggestionPopover *)widget;
+
+  if (self->transient_for != NULL)
+    {
+      g_signal_handler_disconnect (self->transient_for, self->size_allocate_handler);
+      g_signal_handler_disconnect (self->transient_for, self->configure_event_handler);
+      g_signal_handler_disconnect (self->transient_for, self->delete_event_handler);
+
+      self->size_allocate_handler = 0;
+      self->configure_event_handler = 0;
+      self->delete_event_handler = 0;
+
+      self->transient_for = NULL;
+    }
+
+  g_clear_object (&self->model);
+
+  egg_suggestion_popover_set_relative_to (self, NULL);
+
+  GTK_WIDGET_CLASS (egg_suggestion_popover_parent_class)->destroy (widget);
+}
+
+static void
+egg_suggestion_popover_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  EggSuggestionPopover *self = EGG_SUGGESTION_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, egg_suggestion_popover_get_model (self));
+      break;
+
+    case PROP_RELATIVE_TO:
+      g_value_set_object (value, egg_suggestion_popover_get_relative_to (self));
+      break;
+
+    case PROP_SELECTED:
+      g_value_set_object (value, egg_suggestion_popover_get_selected (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_popover_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  EggSuggestionPopover *self = EGG_SUGGESTION_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      egg_suggestion_popover_set_model (self, g_value_get_object (value));
+      break;
+
+    case PROP_RELATIVE_TO:
+      egg_suggestion_popover_set_relative_to (self, g_value_get_object (value));
+      break;
+
+    case PROP_SELECTED:
+      egg_suggestion_popover_set_selected (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_popover_class_init (EggSuggestionPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = egg_suggestion_popover_get_property;
+  object_class->set_property = egg_suggestion_popover_set_property;
+
+  widget_class->destroy = egg_suggestion_popover_destroy;
+  widget_class->hide = egg_suggestion_popover_hide;
+  widget_class->screen_changed = egg_suggestion_popover_screen_changed;
+  widget_class->realize = egg_suggestion_popover_realize;
+  widget_class->show = egg_suggestion_popover_show;
+
+  properties [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         "Model",
+                         "The model to be visualized",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RELATIVE_TO] =
+    g_param_spec_object ("relative-to",
+                         "Relative To",
+                         "The widget to be relative to",
+                         GTK_TYPE_WIDGET,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTED] =
+    g_param_spec_object ("selected",
+                         "Selected",
+                         "The selected suggestion",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [SUGGESTION_ACTIVATED] =
+    g_signal_new ("suggestion-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, EGG_TYPE_SUGGESTION);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libegg-private/egg-suggestion-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, revealer);
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, list_box);
+  gtk_widget_class_bind_template_child (widget_class, EggSuggestionPopover, scrolled_window);
+
+  gtk_widget_class_set_css_name (widget_class, "suggestionpopover");
+
+  g_type_ensure (EGG_TYPE_ELASTIC_BIN);
+}
+
+static void
+egg_suggestion_popover_init (EggSuggestionPopover *self)
+{
+  self->row_type = EGG_TYPE_SUGGESTION_ROW;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_COMBO);
+  gtk_window_set_skip_pager_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_skip_taskbar_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_decorated (GTK_WINDOW (self), FALSE);
+  gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+
+  g_signal_connect_object (self->revealer,
+                           "notify::child-revealed",
+                           G_CALLBACK (egg_suggestion_popover_notify_child_revealed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->list_box,
+                           "row-activated",
+                           G_CALLBACK (egg_suggestion_popover_list_box_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->list_box,
+                           "row-selected",
+                           G_CALLBACK (egg_suggestion_popover_list_box_row_selected),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+GtkWidget *
+egg_suggestion_popover_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION_POPOVER, NULL);
+}
+
+void
+egg_suggestion_popover_popup (EggSuggestionPopover *self)
+{
+  guint duration = 250;
+  guint n_items;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL || 0 == (n_items = g_list_model_get_n_items (self->model)))
+    return;
+
+  if (self->relative_to != NULL)
+    {
+      GdkDisplay *display;
+      GdkMonitor *monitor;
+      GdkWindow *window;
+      GtkAllocation alloc;
+      gint min_height;
+      gint nat_height;
+
+      display = gtk_widget_get_display (GTK_WIDGET (self->relative_to));
+      window = gtk_widget_get_window (GTK_WIDGET (self->relative_to));
+      monitor = gdk_display_get_monitor_at_window (display, window);
+
+      gtk_widget_get_preferred_height (GTK_WIDGET (self), &min_height, &nat_height);
+      gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+      duration = egg_animation_calculate_duration (monitor, alloc.height, nat_height);
+    }
+
+  gtk_widget_show (GTK_WIDGET (self));
+
+  gtk_revealer_set_transition_duration (self->revealer, duration);
+  gtk_revealer_set_reveal_child (self->revealer, TRUE);
+}
+
+void
+egg_suggestion_popover_popdown (EggSuggestionPopover *self)
+{
+  GtkAllocation alloc;
+  GdkDisplay *display;
+  GdkMonitor *monitor;
+  GdkWindow *window;
+  guint duration;
+
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+    return;
+
+  display = gtk_widget_get_display (GTK_WIDGET (self->relative_to));
+  window = gtk_widget_get_window (GTK_WIDGET (self->relative_to));
+  monitor = gdk_display_get_monitor_at_window (display, window);
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  duration = egg_animation_calculate_duration (monitor, alloc.height, 0);
+
+  gtk_revealer_set_transition_duration (self->revealer, duration);
+  gtk_revealer_set_reveal_child (self->revealer, FALSE);
+}
+
+static GtkWidget *
+egg_suggestion_popover_create_row (gpointer item,
+                                   gpointer user_data)
+{
+  EggSuggestionPopover *self = user_data;
+  EggSuggestionRow *row;
+  EggSuggestion *suggestion = item;
+
+  g_assert (EGG_IS_SUGGESTION (suggestion));
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  row = g_object_new (self->row_type,
+                      "suggestion", suggestion,
+                      "visible", TRUE,
+                      NULL);
+
+  return GTK_WIDGET (row);
+}
+
+static void
+egg_suggestion_popover_items_changed (EggSuggestionPopover *self,
+                                      guint                 position,
+                                      guint                 removed,
+                                      guint                 added,
+                                      GListModel           *model)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  if (g_list_model_get_n_items (model) == 0)
+    {
+      egg_suggestion_popover_popdown (self);
+      return;
+    }
+
+  /*
+   * If we are currently animating in the initial view of the popover,
+   * then we might need to cancel that animation and rely on the elastic
+   * bin for smooth resizing.
+   */
+  if (gtk_revealer_get_reveal_child (self->revealer) &&
+      !gtk_revealer_get_child_revealed (self->revealer) &&
+      (removed || added))
+    {
+      gtk_revealer_set_transition_duration (self->revealer, 0);
+      gtk_revealer_set_reveal_child (self->revealer, FALSE);
+      gtk_revealer_set_reveal_child (self->revealer, TRUE);
+    }
+}
+
+static void
+egg_suggestion_popover_connect (EggSuggestionPopover *self)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL)
+    return;
+
+  gtk_list_box_bind_model (self->list_box,
+                           self->model,
+                           egg_suggestion_popover_create_row,
+                           self,
+                           NULL);
+
+  self->items_changed_handler =
+    g_signal_connect_object (self->model,
+                             "items-changed",
+                             G_CALLBACK (egg_suggestion_popover_items_changed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  if (g_list_model_get_n_items (self->model) == 0)
+    {
+      egg_suggestion_popover_popdown (self);
+      return;
+    }
+
+  /* select the first row */
+  egg_suggestion_popover_move_by (self, 1);
+}
+
+static void
+egg_suggestion_popover_disconnect (EggSuggestionPopover *self)
+{
+  g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (self->model == NULL)
+    return;
+
+  g_signal_handler_disconnect (self->model, self->items_changed_handler);
+  self->items_changed_handler = 0;
+
+  gtk_list_box_bind_model (self->list_box, NULL, NULL, NULL, NULL);
+}
+
+void
+egg_suggestion_popover_set_model (EggSuggestionPopover *self,
+                                  GListModel           *model)
+{
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!model || G_IS_LIST_MODEL (model));
+  g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), EGG_TYPE_SUGGESTION));
+
+  if (self->model != model)
+    {
+      if (self->model != NULL)
+        {
+          egg_suggestion_popover_disconnect (self);
+          g_clear_object (&self->model);
+        }
+
+      if (model != NULL)
+        {
+          self->model = g_object_ref (model);
+          egg_suggestion_popover_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
+    }
+}
+
+/**
+ * egg_suggestion_popover_get_model:
+ * @self: a #EggSuggestionPopover
+ *
+ * Gets the model being visualized.
+ *
+ * Returns: (nullable) (transfer none): A #GListModel or %NULL.
+ */
+GListModel *
+egg_suggestion_popover_get_model (EggSuggestionPopover *self)
+{
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  return self->model;
+}
+
+static void
+find_index_of_row (GtkWidget *widget,
+                   gpointer   user_data)
+{
+  struct {
+    GtkWidget *row;
+    gint       index;
+    gint       counter;
+  } *row_lookup = user_data;
+
+  if (widget == row_lookup->row)
+    row_lookup->index = row_lookup->counter;
+
+  row_lookup->counter++;
+}
+
+void
+egg_suggestion_popover_move_by (EggSuggestionPopover *self,
+                                gint                  amount)
+{
+  GtkListBoxRow *row;
+  struct {
+    GtkWidget *row;
+    gint       index;
+    gint       counter;
+  } row_lookup;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (NULL == (row = gtk_list_box_get_row_at_index (self->list_box, 0)))
+    return;
+
+  if (NULL == gtk_list_box_get_selected_row (self->list_box))
+    {
+      gtk_list_box_select_row (self->list_box, row);
+      return;
+    }
+
+  /*
+   * It would be nice if we have a bit better API to have control over
+   * this from GtkListBox. move-cursor isn't really sufficient for our
+   * control over position without updating the focus.
+   *
+   * We could look at doing focus redirection to the popover first,
+   * but that isn't exactly a clean solution either and suggests that
+   * we subclass the listbox too.
+   *
+   * This is really inefficient, but in general we won't have that
+   * many results, becuase showin the user a ton of results is not
+   * exactly useful.
+   *
+   * If we do decide to reuse this class in something autocompletion
+   * in a text editor, we'll want to restrategize (including avoiding
+   * the creation of unnecessary rows and row reuse).
+   */
+  row = gtk_list_box_get_selected_row (self->list_box);
+
+  row_lookup.row = GTK_WIDGET (row);
+  row_lookup.counter = 0;
+  row_lookup.index = -1;
+
+  gtk_container_foreach (GTK_CONTAINER (self->list_box),
+                         find_index_of_row,
+                         &row_lookup);
+
+  row_lookup.index += amount;
+  row_lookup.index = CLAMP (row_lookup.index, -0, (gint)g_list_model_get_n_items (self->model) - 1);
+
+  row = gtk_list_box_get_row_at_index (self->list_box, row_lookup.index);
+  gtk_list_box_select_row (self->list_box, row);
+}
+
+static void
+find_suggestion_row (GtkWidget *widget,
+                     gpointer   user_data)
+{
+  EggSuggestionRow *row = EGG_SUGGESTION_ROW (widget);
+  EggSuggestion *suggestion = egg_suggestion_row_get_suggestion (row);
+  struct {
+    EggSuggestion  *suggestion;
+    GtkListBoxRow **row;
+  } *lookup = user_data;
+
+  if (suggestion == lookup->suggestion)
+    *lookup->row = GTK_LIST_BOX_ROW (row);
+}
+
+void
+egg_suggestion_popover_set_selected (EggSuggestionPopover *self,
+                                     EggSuggestion        *suggestion)
+{
+  GtkListBoxRow *row = NULL;
+  struct {
+    EggSuggestion  *suggestion;
+    GtkListBoxRow **row;
+  } lookup = { suggestion, &row };
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (suggestion == NULL)
+    row = gtk_list_box_get_row_at_index (self->list_box, 0);
+  else
+    gtk_container_foreach (GTK_CONTAINER (self->list_box), find_suggestion_row, &lookup);
+
+  if (row != NULL)
+    gtk_list_box_select_row (self->list_box, row);
+}
+
+/**
+ * egg_suggestion_popover_get_selected:
+ * @self: a #EggSuggestionPopover
+ *
+ * Gets the currently selected suggestion.
+ *
+ * Returns: (transfer none) (nullable): An #EggSuggestion or %NULL.
+ */
+EggSuggestion *
+egg_suggestion_popover_get_selected (EggSuggestionPopover *self)
+{
+  EggSuggestionRow *row;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_POPOVER (self), NULL);
+
+  row = EGG_SUGGESTION_ROW (gtk_list_box_get_selected_row (self->list_box));
+  if (row != NULL)
+    return egg_suggestion_row_get_suggestion (row);
+
+  return NULL;
+}
+
+void
+egg_suggestion_popover_activate_selected (EggSuggestionPopover *self)
+{
+  EggSuggestion *suggestion;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_POPOVER (self));
+
+  if (NULL != (suggestion = egg_suggestion_popover_get_selected (self)))
+    g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
+}
diff --git a/contrib/egg/egg-suggestion-popover.h b/contrib/egg/egg-suggestion-popover.h
new file mode 100644
index 0000000..bdfba5f
--- /dev/null
+++ b/contrib/egg/egg-suggestion-popover.h
@@ -0,0 +1,49 @@
+/* egg-suggestion-popover.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#ifndef EGG_SUGGESTION_POPOVER_H
+#define EGG_SUGGESTION_POPOVER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_POPOVER (egg_suggestion_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (EggSuggestionPopover, egg_suggestion_popover, EGG, SUGGESTION_POPOVER, GtkWindow)
+
+GtkWidget     *egg_suggestion_popover_new               (void);
+GtkWidget     *egg_suggestion_popover_get_relative_to   (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_relative_to   (EggSuggestionPopover *self,
+                                                         GtkWidget            *widget);
+void           egg_suggestion_popover_popup             (EggSuggestionPopover *self);
+void           egg_suggestion_popover_popdown           (EggSuggestionPopover *self);
+GListModel    *egg_suggestion_popover_get_model         (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_model         (EggSuggestionPopover *self,
+                                                         GListModel           *model);
+void           egg_suggestion_popover_move_by           (EggSuggestionPopover *self,
+                                                         gint                  amount);
+EggSuggestion *egg_suggestion_popover_get_selected      (EggSuggestionPopover *self);
+void           egg_suggestion_popover_set_selected      (EggSuggestionPopover *self,
+                                                         EggSuggestion        *suggestion);
+void           egg_suggestion_popover_activate_selected (EggSuggestionPopover *self);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_POPOVER_H */
diff --git a/contrib/egg/egg-suggestion-popover.ui b/contrib/egg/egg-suggestion-popover.ui
new file mode 100644
index 0000000..265e326
--- /dev/null
+++ b/contrib/egg/egg-suggestion-popover.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="EggSuggestionPopover" parent="GtkWindow">
+    <child>
+      <object class="GtkRevealer" id="revealer">
+        <property name="reveal-child">false</property>
+        <property name="transition-duration">200</property>
+        <property name="transition-type">slide-down</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="EggElasticBin" id="elastic">
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkScrolledWindow" id="scrolled_window">
+                    <!-- XXX
+                         We should generate max-content-height based on window position
+                         or some other hueristic. (transient-for maybe?)
+                    -->
+                    <property name="max-content-height">500</property>
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="propagate-natural-height">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkListBox" id="list_box">
+                        <property name="selection-mode">browse</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/contrib/egg/egg-suggestion-row.c b/contrib/egg/egg-suggestion-row.c
new file mode 100644
index 0000000..604678a
--- /dev/null
+++ b/contrib/egg/egg-suggestion-row.c
@@ -0,0 +1,209 @@
+/* egg-suggestion-row.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion-row"
+
+#include "egg-suggestion-row.h"
+
+typedef struct
+{
+  EggSuggestion *suggestion;
+
+  GtkImage      *image;
+  GtkLabel      *title;
+  GtkLabel      *separator;
+  GtkLabel      *subtitle;
+} EggSuggestionRowPrivate;
+
+enum {
+  PROP_0,
+  PROP_SUGGESTION,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestionRow, egg_suggestion_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+egg_suggestion_row_disconnect (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+
+  if (priv->suggestion == NULL)
+    return;
+
+  g_object_set (priv->image, "icon-name", NULL, NULL);
+  gtk_label_set_label (priv->title, NULL);
+  gtk_label_set_label (priv->subtitle, NULL);
+}
+
+static void
+egg_suggestion_row_connect (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+  const gchar *icon_name;
+  const gchar *subtitle;
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+  g_return_if_fail (priv->suggestion != NULL);
+
+  icon_name = egg_suggestion_get_icon_name (priv->suggestion);
+  if (icon_name == NULL)
+    icon_name = "web-browser-symbolic";
+
+  g_object_set (priv->image, "icon-name", icon_name, NULL);
+  gtk_label_set_label (priv->title, egg_suggestion_get_title (priv->suggestion));
+
+  subtitle = egg_suggestion_get_subtitle (priv->suggestion);
+  gtk_label_set_label (priv->subtitle, subtitle);
+  gtk_widget_set_visible (GTK_WIDGET (priv->separator), !!subtitle);
+}
+
+static void
+egg_suggestion_row_finalize (GObject *object)
+{
+  EggSuggestionRow *self = (EggSuggestionRow *)object;
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_clear_object (&priv->suggestion);
+
+  G_OBJECT_CLASS (egg_suggestion_row_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_row_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  EggSuggestionRow *self = EGG_SUGGESTION_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      g_value_set_object (value, egg_suggestion_row_get_suggestion (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_row_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  EggSuggestionRow *self = EGG_SUGGESTION_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SUGGESTION:
+      egg_suggestion_row_set_suggestion (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_row_class_init (EggSuggestionRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_row_finalize;
+  object_class->get_property = egg_suggestion_row_get_property;
+  object_class->set_property = egg_suggestion_row_set_property;
+
+  properties [PROP_SUGGESTION] =
+    g_param_spec_object ("suggestion",
+                         "Suggestion",
+                         "The suggestion to display",
+                         EGG_TYPE_SUGGESTION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libegg-private/egg-suggestion-row.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, image);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, title);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, subtitle);
+  gtk_widget_class_bind_template_child_private (widget_class, EggSuggestionRow, separator);
+}
+
+static void
+egg_suggestion_row_init (EggSuggestionRow *self)
+{
+  GtkStyleContext *context;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_add_class (context, "suggestion");
+}
+
+/**
+ * egg_suggestion_row_get_suggestion:
+ * @self: a #EggSuggestionRow
+ *
+ * Gets the suggestion to be displayed.
+ *
+ * Returns: (transfer none): An #EggSuggestion
+ */
+EggSuggestion *
+egg_suggestion_row_get_suggestion (EggSuggestionRow *self)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION_ROW (self), NULL);
+
+  return priv->suggestion;
+}
+
+void
+egg_suggestion_row_set_suggestion (EggSuggestionRow *self,
+                                   EggSuggestion    *suggestion)
+{
+  EggSuggestionRowPrivate *priv = egg_suggestion_row_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION_ROW (self));
+  g_return_if_fail (!suggestion || EGG_IS_SUGGESTION (suggestion));
+
+  if (priv->suggestion != suggestion)
+    {
+      if (priv->suggestion != NULL)
+        {
+          egg_suggestion_row_disconnect (self);
+          g_clear_object (&priv->suggestion);
+        }
+
+      if (suggestion != NULL)
+        {
+          priv->suggestion = g_object_ref (suggestion);
+          egg_suggestion_row_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUGGESTION]);
+    }
+}
diff --git a/contrib/egg/egg-suggestion-row.h b/contrib/egg/egg-suggestion-row.h
new file mode 100644
index 0000000..87a6925
--- /dev/null
+++ b/contrib/egg/egg-suggestion-row.h
@@ -0,0 +1,49 @@
+/* egg-suggestion-row.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef EGG_SUGGESTION_ROW_H
+#define EGG_SUGGESTION_ROW_H
+
+#include <gtk/gtk.h>
+
+#include "egg-suggestion.h"
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION_ROW (egg_suggestion_row_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestionRow, egg_suggestion_row, EGG, SUGGESTION_ROW, GtkListBoxRow)
+
+struct _EggSuggestionRowClass
+{
+  GtkListBoxRowClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+GtkWidget     *egg_suggestion_row_new            (void);
+EggSuggestion *egg_suggestion_row_get_suggestion (EggSuggestionRow *self);
+void           egg_suggestion_row_set_suggestion (EggSuggestionRow *self,
+                                                  EggSuggestion    *suggestion);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_ROW_H */
diff --git a/contrib/egg/egg-suggestion-row.ui b/contrib/egg/egg-suggestion-row.ui
new file mode 100644
index 0000000..bf79bc4
--- /dev/null
+++ b/contrib/egg/egg-suggestion-row.ui
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="EggSuggestionRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="hexpand">false</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="hexpand">false</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <property name="use-markup">true</property>
+            <property name="ellipsize">end</property>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="separator">
+            <property name="hexpand">false</property>
+            <property name="label">—</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="subtitle">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+            <property name="use-markup">true</property>
+            <property name="ellipsize">end</property>
+            <style>
+              <class name="dim-label"/>
+              <class name="subtitle"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/contrib/egg/egg-suggestion.c b/contrib/egg/egg-suggestion.c
new file mode 100644
index 0000000..d28d059
--- /dev/null
+++ b/contrib/egg/egg-suggestion.c
@@ -0,0 +1,351 @@
+/* egg-suggestion.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "egg-suggestion"
+
+#include "egg-suggestion.h"
+
+typedef struct
+{
+  gchar *title;
+  gchar *subtitle;
+  gchar *icon_name;
+  gchar *id;
+} EggSuggestionPrivate;
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_ID,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  REPLACE_TYPED_TEXT,
+  SUGGEST_SUFFIX,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggSuggestion, egg_suggestion, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+egg_suggestion_finalize (GObject *object)
+{
+  EggSuggestion *self = (EggSuggestion *)object;
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->subtitle, g_free);
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->id, g_free);
+
+  G_OBJECT_CLASS (egg_suggestion_parent_class)->finalize (object);
+}
+
+static void
+egg_suggestion_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  EggSuggestion *self = EGG_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, egg_suggestion_get_id (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, egg_suggestion_get_icon_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, egg_suggestion_get_title (self));
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, egg_suggestion_get_subtitle (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  EggSuggestion *self = EGG_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      egg_suggestion_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      egg_suggestion_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      egg_suggestion_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE:
+      egg_suggestion_set_subtitle (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_suggestion_class_init (EggSuggestionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = egg_suggestion_finalize;
+  object_class->get_property = egg_suggestion_get_property;
+  object_class->set_property = egg_suggestion_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The suggestion identifier",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The name of the icon to display",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the suggestion",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "The subtitle of the suggestion",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [REPLACE_TYPED_TEXT] =
+    g_signal_new ("replace-typed-text",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionClass, replace_typed_text),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  signals [SUGGEST_SUFFIX] =
+    g_signal_new ("suggest-suffix",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggSuggestionClass, suggest_suffix),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_STRING, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+egg_suggestion_init (EggSuggestion *self)
+{
+}
+
+const gchar *
+egg_suggestion_get_id (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->id;
+}
+
+const gchar *
+egg_suggestion_get_icon_name (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->icon_name;
+}
+
+const gchar *
+egg_suggestion_get_title (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->title;
+}
+
+const gchar *
+egg_suggestion_get_subtitle (EggSuggestion *self)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  return priv->subtitle;
+}
+
+void
+egg_suggestion_set_icon_name (EggSuggestion *self,
+                              const gchar   *icon_name)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->icon_name, icon_name) != 0)
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+void
+egg_suggestion_set_id (EggSuggestion *self,
+                       const gchar   *id)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->id, id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+void
+egg_suggestion_set_title (EggSuggestion *self,
+                          const gchar   *title)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+void
+egg_suggestion_set_subtitle (EggSuggestion *self,
+                             const gchar   *subtitle)
+{
+  EggSuggestionPrivate *priv = egg_suggestion_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_SUGGESTION (self));
+
+  if (g_strcmp0 (priv->subtitle, subtitle) != 0)
+    {
+      g_free (priv->subtitle);
+      priv->subtitle = g_strdup (subtitle);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUBTITLE]);
+    }
+}
+
+/**
+ * egg_suggestion_suggest_suffix:
+ * @self: a #EggSuggestion
+ * @typed_text: The user entered text
+ *
+ * This function requests potential text to append to @typed_text to make it
+ * more clear to the user what they will be activating by selecting this
+ * suggestion. For example, if they start typing "gno", a potential suggested
+ * suffix might be "me.org" to create "gnome.org".
+ *
+ * Returns: (transfer full) (nullable): Suffix to append to @typed_text
+ *   or %NULL to leave it unchanged.
+ */
+gchar *
+egg_suggestion_suggest_suffix (EggSuggestion *self,
+                               const gchar   *typed_text)
+{
+  gchar *ret = NULL;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+  g_return_val_if_fail (typed_text != NULL, NULL);
+
+  g_signal_emit (self, signals [SUGGEST_SUFFIX], 0, typed_text, &ret);
+
+  return ret;
+}
+
+EggSuggestion *
+egg_suggestion_new (void)
+{
+  return g_object_new (EGG_TYPE_SUGGESTION, NULL);
+}
+
+/**
+ * egg_suggestion_replace_typed_text:
+ * @self: An #EggSuggestion
+ * @typed_text: the text that was typed into the entry
+ *
+ * This function is meant to be used to replace the text in the entry with text
+ * that represents the suggestion most accurately. This happens when the user
+ * presses tab while typing a suggestion. For example, if typing "gno" in the
+ * entry, you might have a suggest_suffix of "me.org" so that the user sees
+ * "gnome.org". But the replace_typed_text might include more data such as
+ * "https://gnome.org"; as it more closely represents the suggestion.
+ *
+ * Returns: (transfer full) (nullable): The replacement text to insert into
+ *   the entry when "tab" is pressed to complete the insertion.
+ */
+gchar *
+egg_suggestion_replace_typed_text (EggSuggestion *self,
+                                   const gchar   *typed_text)
+{
+  gchar *ret = NULL;
+
+  g_return_val_if_fail (EGG_IS_SUGGESTION (self), NULL);
+
+  g_signal_emit (self, signals [REPLACE_TYPED_TEXT], 0, typed_text, &ret);
+
+  return ret;
+}
diff --git a/contrib/egg/egg-suggestion.h b/contrib/egg/egg-suggestion.h
new file mode 100644
index 0000000..a124971
--- /dev/null
+++ b/contrib/egg/egg-suggestion.h
@@ -0,0 +1,66 @@
+/* egg-suggestion.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This file 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.1 of the License, or (at your option)
+ * any later version.
+ *
+ * This file 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 General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#ifndef EGG_SUGGESTION_H
+#define EGG_SUGGESTION_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_SUGGESTION (egg_suggestion_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggSuggestion, egg_suggestion, EGG, SUGGESTION, GObject)
+
+struct _EggSuggestionClass
+{
+  GObjectClass parent_class;
+
+  gchar *(*suggest_suffix)     (EggSuggestion *self,
+                                const gchar   *typed_text);
+  gchar *(*replace_typed_text) (EggSuggestion *self,
+                                const gchar   *typed_text);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+};
+
+EggSuggestion *egg_suggestion_new                (void);
+const gchar   *egg_suggestion_get_id             (EggSuggestion *self);
+void           egg_suggestion_set_id             (EggSuggestion *self,
+                                                  const gchar   *id);
+const gchar   *egg_suggestion_get_icon_name      (EggSuggestion *self);
+void           egg_suggestion_set_icon_name      (EggSuggestion *self,
+                                                  const gchar   *icon_name);
+const gchar   *egg_suggestion_get_title          (EggSuggestion *self);
+void           egg_suggestion_set_title          (EggSuggestion *self,
+                                                  const gchar   *title);
+const gchar   *egg_suggestion_get_subtitle       (EggSuggestion *self);
+void           egg_suggestion_set_subtitle       (EggSuggestion *self,
+                                                  const gchar   *subtitle);
+gchar         *egg_suggestion_suggest_suffix     (EggSuggestion *self,
+                                                  const gchar   *typed_text);
+gchar         *egg_suggestion_replace_typed_text (EggSuggestion *self,
+                                                  const gchar   *typed_text);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_H */
diff --git a/contrib/egg/egg.gresource.xml b/contrib/egg/egg.gresource.xml
index 702e887..7c79795 100644
--- a/contrib/egg/egg.gresource.xml
+++ b/contrib/egg/egg.gresource.xml
@@ -1,8 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/libegg-private">
-    <file>egg-empty-state.ui</file>
-    <file>egg-pill-box.ui</file>
-    <file>egg-simple-popover.ui</file>
+    <file compressed="true">egg-empty-state.ui</file>
+    <file compressed="true">egg-pill-box.ui</file>
+    <file compressed="true">egg-simple-popover.ui</file>
+    <file compressed="true">egg-suggestion-entry.css</file>
+    <file compressed="true">egg-suggestion-popover.ui</file>
+    <file compressed="true">egg-suggestion-row.ui</file>
   </gresource>
 </gresources>
diff --git a/contrib/egg/test-suggestion-buffer.c b/contrib/egg/test-suggestion-buffer.c
new file mode 100644
index 0000000..f28684e
--- /dev/null
+++ b/contrib/egg/test-suggestion-buffer.c
@@ -0,0 +1,102 @@
+#include "egg-suggestion-entry-buffer.h"
+
+static gchar *
+suggest_suffix (EggSuggestion *suggestion,
+                const gchar   *query,
+                const gchar   *suffix)
+{
+  return g_strdup (suffix);
+}
+
+static void
+test_basic (void)
+{
+  g_autoptr(EggSuggestionEntryBuffer) buffer = NULL;
+  g_autoptr(EggSuggestion) suggestion = NULL;
+  g_autoptr(EggSuggestion) suggestion2 = NULL;
+  const gchar *text;
+  guint len;
+  guint n_chars;
+
+  buffer = egg_suggestion_entry_buffer_new ();
+
+  suggestion = egg_suggestion_new ();
+  egg_suggestion_set_id (suggestion, "some-id");
+  egg_suggestion_set_title (suggestion, "this is the title");
+  egg_suggestion_set_subtitle (suggestion, "this is the subtitle");
+  egg_suggestion_set_icon_name (suggestion, "gtk-missing-symbolic");
+  g_signal_connect (suggestion, "suggest-suffix", G_CALLBACK (suggest_suffix), "abcd");
+
+  suggestion2 = egg_suggestion_new ();
+  g_signal_connect (suggestion2, "suggest-suffix", G_CALLBACK (suggest_suffix), "99999");
+
+  egg_suggestion_entry_buffer_set_suggestion (buffer, suggestion);
+  g_assert (suggestion == egg_suggestion_entry_buffer_get_suggestion (buffer));
+
+  gtk_entry_buffer_insert_text (GTK_ENTRY_BUFFER (buffer), 0, "1234", 4);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 8);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "1234abcd");
+
+  n_chars = gtk_entry_buffer_insert_text (GTK_ENTRY_BUFFER (buffer), 4, "z", 1);
+  g_assert_cmpint (n_chars, ==, 1);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 9);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "1234zabcd");
+
+  n_chars = gtk_entry_buffer_delete_text (GTK_ENTRY_BUFFER (buffer), 1, 1);
+  g_assert_cmpint (n_chars, ==, 1);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 8);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "134zabcd");
+
+  egg_suggestion_entry_buffer_set_suggestion (buffer, NULL);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 4);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "134z");
+
+  egg_suggestion_entry_buffer_set_suggestion (buffer, suggestion2);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 9);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "134z99999");
+
+  egg_suggestion_entry_buffer_set_suggestion (buffer, suggestion);
+
+  len = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpint (len, ==, 8);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "134zabcd");
+
+  /* Fail by trying to delete the extended text */
+  len = gtk_entry_buffer_delete_text (GTK_ENTRY_BUFFER (buffer), 4, 4);
+  g_assert_cmpint (len, ==, 0);
+
+  text = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (buffer));
+  g_assert_cmpstr (text, ==, "134zabcd");
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  gtk_init (&argc, &argv);
+  g_test_add_func ("/Egg/SuggestionEntryBuffer/basic", test_basic);
+  return g_test_run ();
+}


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