[gnome-builder] egg: add EggSuggestionEntry
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder] egg: add EggSuggestionEntry
- Date: Fri, 7 Apr 2017 21:56:18 +0000 (UTC)
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]