[epiphany/wip/chergert/completion] wip on new completion bar
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [epiphany/wip/chergert/completion] wip on new completion bar
- Date: Fri, 7 Apr 2017 08:45:31 +0000 (UTC)
commit 5245681c2a1aaf54415207d13f97d73782a036aa
Author: Christian Hergert <chergert redhat com>
Date: Fri Apr 7 01:44:30 2017 -0700
wip on new completion bar
build-aux/Makefile.am.gresources | 62 ++
configure.ac | 3 +-
lib/egg/Makefile.am | 37 +-
lib/egg/egg-animation.c | 1247 +++++++++++++++++++++++++++++++++
lib/egg/egg-animation.h | 81 +++
lib/egg/egg-elastic-bin.c | 278 ++++++++
lib/egg/egg-elastic-bin.h | 44 ++
lib/egg/egg-frame-source.c | 130 ++++
lib/egg/egg-frame-source.h | 32 +
lib/egg/egg-suggestion-entry-buffer.c | 408 +++++++++++
lib/egg/egg-suggestion-entry-buffer.h | 52 ++
lib/egg/egg-suggestion-entry.c | 663 ++++++++++++++++++
lib/egg/egg-suggestion-entry.css | 53 ++
lib/egg/egg-suggestion-entry.h | 64 ++
lib/egg/egg-suggestion-popover.c | 796 +++++++++++++++++++++
lib/egg/egg-suggestion-popover.h | 49 ++
lib/egg/egg-suggestion-popover.ui | 42 ++
lib/egg/egg-suggestion-row.c | 209 ++++++
lib/egg/egg-suggestion-row.h | 49 ++
lib/egg/egg-suggestion-row.ui | 53 ++
lib/egg/egg-suggestion.c | 351 +++++++++
lib/egg/egg-suggestion.h | 66 ++
lib/egg/egg.gresources.xml | 8 +
lib/egg/eggtreemultidnd.c | 2 +
lib/widgets/ephy-location-entry.c | 4 +-
lib/widgets/ephy-location-entry.h | 4 +-
src/Makefile.am | 2 +
src/ephy-location-controller.c | 133 +----
src/ephy-suggestion-model.c | 417 +++++++++++
src/ephy-suggestion-model.h | 46 ++
30 files changed, 5253 insertions(+), 132 deletions(-)
---
diff --git a/build-aux/Makefile.am.gresources b/build-aux/Makefile.am.gresources
new file mode 100644
index 0000000..d1b849b
--- /dev/null
+++ b/build-aux/Makefile.am.gresources
@@ -0,0 +1,62 @@
+# Rules for generating gresources using glib-compile-resources
+#
+# Define:
+# glib_resources_h = header template file
+# glib_resources_c = source template file
+# glib_resources_xml = path to *.gresource.xml
+# glib_resources_namespace = c prefix for resources
+#
+# before including Makefile.am.resources. You will also need to have
+# the following targets already defined:
+#
+# CLEANFILES
+# DISTCLEANFILES
+# BUILT_SOURCES
+# EXTRA_DIST
+#
+# Author: Christian Hergert <christian hergert me>
+
+# Basic sanity checks
+$(if $(GLIB_COMPILE_RESOURCES),,$(error Need to define GLIB_COMPILE_RESOURCES))
+
+$(if $(or $(glib_resources_h), \
+ $(glib_resources_c)),, \
+ $(error Need to define glib_resources_h and glib_resources_c))
+
+$(if $(glib_resources_xml),,$(error Need to define glib_resources_xml))
+$(if $(glib_resources_namespace),,$(error Need to define glib_resources_namespace))
+
+resources_xml=$(addprefix $(srcdir)/,$(glib_resources_xml))
+resources_srcdir=$(dir $(resources_xml))
+
+DISTCLEANFILES += $(glib_resources_h) $(glib_resources_c)
+BUILT_SOURCES += $(glib_resources_h) $(glib_resources_c)
+CLEANFILES += stamp-resources $(glib_resources_c) $(glib_resources_h)
+EXTRA_DIST += \
+ $(glib_resources_xml) \
+ $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(resources_srcdir) --generate-dependencies
$(resources_xml)) \
+ $(NULL)
+
+stamp-resources: $(glib_resources_c) $(resources_xml)
+ $(AM_V_GEN)$(GLIB_COMPILE_RESOURCES) \
+ --target=xgen-gr.h \
+ --sourcedir=$(resources_srcdir) \
+ --generate-header \
+ --c-name $(glib_resources_namespace) \
+ $(resources_xml) \
+ && (cmp -s xgen-gr.h $(glib_resources_h) || cp -f xgen-gr.h $(glib_resources_h)) \
+ && rm -f xgen-gr.h \
+ && echo timestamp > $(@F)
+
+$(glib_resources_h): stamp-resources
+ @true
+
+$(glib_resources_c): $(resources_xml) $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(resources_srcdir)
--generate-dependencies $(resources_xml))
+ $(AM_V_GEN)$(GLIB_COMPILE_RESOURCES) \
+ --target=xgen-gr.c \
+ --sourcedir=$(resources_srcdir) \
+ --generate-source \
+ --c-name $(glib_resources_namespace) \
+ $(resources_xml) \
+ && (cmp -s xgen-gr.c $(glib_resources_c) || cp -f xgen-gr.c $(glib_resources_c)) \
+ && rm -f xgen-gr.c
diff --git a/configure.ac b/configure.ac
index f7976ce..88fe2ff 100644
--- a/configure.ac
+++ b/configure.ac
@@ -50,6 +50,7 @@ AC_SUBST([EPIPHANY_CHANGELOG_START],[epiphany_changelog_start])
AC_PROG_CC
AC_PATH_PROG([GLIB_MKENUMS],[glib-mkenums])
+AC_PATH_PROG([GLIB_COMPILE_RESOURCES], [glib-compile-resources])
dnl Note: I want to enable subdir-objects here, to silence Automake's many
dnl warnings about not using it. But we can't do this until we require Automake
@@ -62,7 +63,7 @@ dnl it is a bug after all, and updating for bugfixes is just how software works.
dnl Note also: the issue is that a directory _literally_ named $(top_srcdir)
dnl will be created under embed/web-extension, and the build will fail due to
dnl misplaced .Plo files (used for dependency tracking).
-AM_INIT_AUTOMAKE([1.11 foreign dist-xz no-dist-gzip tar-ustar])
+AM_INIT_AUTOMAKE([1.11 foreign dist-xz no-dist-gzip tar-ustar subdir-objects])
AM_SILENT_RULES([yes])
AM_MAINTAINER_MODE([enable])
diff --git a/lib/egg/Makefile.am b/lib/egg/Makefile.am
index f56b24e..bf9c143 100644
--- a/lib/egg/Makefile.am
+++ b/lib/egg/Makefile.am
@@ -1,13 +1,42 @@
noinst_LTLIBRARIES = libegg.la
libegg_la_SOURCES = \
- eggtreemultidnd.c \
+ egg-animation.c \
+ egg-animation.h \
+ egg-elastic-bin.c \
+ egg-elastic-bin.h \
+ egg-frame-source.c \
+ egg-frame-source.h \
+ egg-suggestion.c \
+ egg-suggestion-entry-buffer.c \
+ egg-suggestion-entry-buffer.h \
+ egg-suggestion-entry.c \
+ egg-suggestion-entry.h \
+ egg-suggestion.h \
+ egg-suggestion-popover.c \
+ egg-suggestion-popover.h \
+ egg-suggestion-row.c \
+ egg-suggestion-row.h \
+ eggtreemultidnd.c \
eggtreemultidnd.h
-libegg_la_CPPFLAGS = \
- -DG_LOG_DOMAIN=\""Egg"\" \
- $(GTK_CFLAGS)
+nodist_libegg_la_SOURCES = \
+ egg-resources.c \
+ egg-resources.h
+
+libegg_la_CPPFLAGS = $(GTK_CFLAGS)
libegg_la_LIBADD = $(GTK_LIBS)
+BUILT_SOURCES =
+CLEANFILES =
+DISTCLEANFILES =
+EXTRA_DIST =
+
+glib_resources_xml = egg.gresources.xml
+glib_resources_c = egg-resources.c
+glib_resources_h = egg-resources.h
+glib_resources_namespace = egg
+include $(top_srcdir)/build-aux/Makefile.am.gresources
+
-include $(top_srcdir)/git.mk
diff --git a/lib/egg/egg-animation.c b/lib/egg/egg-animation.c
new file mode 100644
index 0000000..ad01b16
--- /dev/null
+++ b/lib/egg/egg-animation.c
@@ -0,0 +1,1247 @@
+/* egg-animation.c
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#include <glib/gi18n.h>
+#include <gobject/gvaluecollector.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "egg-animation.h"
+#include "egg-frame-source.h"
+
+#define FALLBACK_FRAME_RATE 60
+
+typedef gdouble (*AlphaFunc) (gdouble offset);
+typedef void (*TweenFunc) (const GValue *begin,
+ const GValue *end,
+ GValue *value,
+ gdouble offset);
+
+typedef struct
+{
+ gboolean is_child; /* Does GParamSpec belong to parent widget */
+ GParamSpec *pspec; /* GParamSpec of target property */
+ GValue begin; /* Begin value in animation */
+ GValue end; /* End value in animation */
+} Tween;
+
+
+struct _EggAnimation
+{
+ GInitiallyUnowned parent_instance;
+
+ gpointer target; /* Target object to animate */
+ guint64 begin_msec; /* Time in which animation started */
+ guint duration_msec; /* Duration of animation */
+ guint mode; /* Tween mode */
+ gulong tween_handler; /* GSource or signal handler */
+ gulong after_paint_handler; /* signal handler */
+ gdouble last_offset; /* Track our last offset */
+ GArray *tweens; /* Array of tweens to perform */
+ GdkFrameClock *frame_clock; /* An optional frame-clock for sync. */
+ GDestroyNotify notify; /* Notify callback */
+ gpointer notify_data; /* Data for notify */
+};
+
+G_DEFINE_TYPE (EggAnimation, egg_animation, G_TYPE_INITIALLY_UNOWNED)
+
+enum {
+ PROP_0,
+ PROP_DURATION,
+ PROP_FRAME_CLOCK,
+ PROP_MODE,
+ PROP_TARGET,
+ LAST_PROP
+};
+
+
+enum {
+ TICK,
+ LAST_SIGNAL
+};
+
+
+/*
+ * Helper macros.
+ */
+#define LAST_FUNDAMENTAL 64
+#define TWEEN(type) \
+ static void \
+ tween_ ## type (const GValue * begin, \
+ const GValue * end, \
+ GValue * value, \
+ gdouble offset) \
+ { \
+ g ## type x = g_value_get_ ## type (begin); \
+ g ## type y = g_value_get_ ## type (end); \
+ g_value_set_ ## type (value, x + ((y - x) * offset)); \
+ }
+
+
+/*
+ * Globals.
+ */
+static AlphaFunc alpha_funcs[EGG_ANIMATION_LAST];
+static gboolean debug;
+static GParamSpec *properties[LAST_PROP];
+static guint signals[LAST_SIGNAL];
+static TweenFunc tween_funcs[LAST_FUNDAMENTAL];
+static guint slow_down_factor = 1;
+
+
+/*
+ * Tweeners for basic types.
+ */
+TWEEN (int);
+TWEEN (uint);
+TWEEN (long);
+TWEEN (ulong);
+TWEEN (float);
+TWEEN (double);
+
+
+/**
+ * egg_animation_alpha_ease_in_cubic:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_CUBIC means the valu ewill be transformed into
+ * cubic acceleration (x * x * x).
+ */
+static gdouble
+egg_animation_alpha_ease_in_cubic (gdouble offset)
+{
+ return offset * offset * offset;
+}
+
+
+static gdouble
+egg_animation_alpha_ease_out_cubic (gdouble offset)
+{
+ gdouble p = offset - 1.0;
+
+ return p * p * p + 1.0;
+}
+
+static gdouble
+egg_animation_alpha_ease_in_out_cubic (gdouble offset)
+{
+ if (offset < .5)
+ return egg_animation_alpha_ease_in_cubic (offset * 2.0) / 2.0;
+ else
+ return .5 + egg_animation_alpha_ease_out_cubic ((offset - .5) * 2.0) / 2.0;
+}
+
+
+/**
+ * egg_animation_alpha_linear:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_LINEAR means no tranformation will be made.
+ *
+ * Returns: @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_linear (gdouble offset)
+{
+ return offset;
+}
+
+
+/**
+ * egg_animation_alpha_ease_in_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_IN_QUAD means that the value will be transformed
+ * into a quadratic acceleration.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_in_quad (gdouble offset)
+{
+ return offset * offset;
+}
+
+
+/**
+ * egg_animation_alpha_ease_out_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_OUT_QUAD means that the value will be transformed
+ * into a quadratic deceleration.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_out_quad (gdouble offset)
+{
+ return -1.0 * offset * (offset - 2.0);
+}
+
+
+/**
+ * egg_animation_alpha_ease_in_out_quad:
+ * @offset: (in): The position within the animation; 0.0 to 1.0.
+ *
+ * An alpha function to transform the offset within the animation.
+ * @EGG_ANIMATION_EASE_IN_OUT_QUAD means that the value will be transformed
+ * into a quadratic acceleration for the first half, and quadratic
+ * deceleration the second half.
+ *
+ * Returns: A tranformation of @offset.
+ * Side effects: None.
+ */
+static gdouble
+egg_animation_alpha_ease_in_out_quad (gdouble offset)
+{
+ offset *= 2.0;
+ if (offset < 1.0)
+ return 0.5 * offset * offset;
+ offset -= 1.0;
+ return -0.5 * (offset * (offset - 2.0) - 1.0);
+}
+
+
+/**
+ * egg_animation_load_begin_values:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Load the begin values for all the properties we are about to
+ * animate.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_load_begin_values (EggAnimation *animation)
+{
+ GtkContainer *container;
+ Tween *tween;
+ guint i;
+
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+ for (i = 0; i < animation->tweens->len; i++)
+ {
+ tween = &g_array_index (animation->tweens, Tween, i);
+ g_value_reset (&tween->begin);
+ if (tween->is_child)
+ {
+ container = GTK_CONTAINER (gtk_widget_get_parent (animation->target));
+ gtk_container_child_get_property (container,
+ animation->target,
+ tween->pspec->name,
+ &tween->begin);
+ }
+ else
+ {
+ g_object_get_property (animation->target,
+ tween->pspec->name,
+ &tween->begin);
+ }
+ }
+}
+
+
+/**
+ * egg_animation_unload_begin_values:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Unloads the begin values for the animation. This might be particularly
+ * useful once we support pointer types.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_unload_begin_values (EggAnimation *animation)
+{
+ Tween *tween;
+ guint i;
+
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+ for (i = 0; i < animation->tweens->len; i++)
+ {
+ tween = &g_array_index (animation->tweens, Tween, i);
+ g_value_reset (&tween->begin);
+ }
+}
+
+
+/**
+ * egg_animation_get_offset:
+ * @animation: A #EggAnimation.
+ * @frame_time: the time to present the frame, or 0 for current timing.
+ *
+ * Retrieves the position within the animation from 0.0 to 1.0. This
+ * value is calculated using the msec of the beginning of the animation
+ * and the current time.
+ *
+ * Returns: The offset of the animation from 0.0 to 1.0.
+ */
+static gdouble
+egg_animation_get_offset (EggAnimation *animation,
+ gint64 frame_time)
+{
+ gdouble offset;
+ gint64 frame_msec;
+
+ g_return_val_if_fail (EGG_IS_ANIMATION (animation), 0.0);
+
+ if (frame_time == 0)
+ {
+ if (animation->frame_clock != NULL)
+ frame_time = gdk_frame_clock_get_frame_time (animation->frame_clock);
+ else
+ frame_time = g_get_monotonic_time ();
+ }
+
+ frame_msec = frame_time / 1000L;
+
+ offset = (gdouble) (frame_msec - animation->begin_msec) /
+ (gdouble) MAX (animation->duration_msec, 1);
+
+ return CLAMP (offset, 0.0, 1.0);
+}
+
+
+/**
+ * egg_animation_update_property:
+ * @animation: (in): A #EggAnimation.
+ * @target: (in): A #GObject.
+ * @tween: (in): a #Tween containing the property.
+ * @value: (in): The new value for the property.
+ *
+ * Updates the value of a property on an object using @value.
+ *
+ * Side effects: The property of @target is updated.
+ */
+static void
+egg_animation_update_property (EggAnimation *animation,
+ gpointer target,
+ Tween *tween,
+ const GValue *value)
+{
+ g_assert (EGG_IS_ANIMATION (animation));
+ g_assert (G_IS_OBJECT (target));
+ g_assert (tween);
+ g_assert (value);
+
+ g_object_set_property (target, tween->pspec->name, value);
+}
+
+
+/**
+ * egg_animation_update_child_property:
+ * @animation: (in): A #EggAnimation.
+ * @target: (in): A #GObject.
+ * @tween: (in): A #Tween containing the property.
+ * @value: (in): The new value for the property.
+ *
+ * Updates the value of the parent widget of the target to @value.
+ *
+ * Side effects: The property of @target<!-- -->'s parent widget is updated.
+ */
+static void
+egg_animation_update_child_property (EggAnimation *animation,
+ gpointer target,
+ Tween *tween,
+ const GValue *value)
+{
+ GtkWidget *parent;
+
+ g_assert (EGG_IS_ANIMATION (animation));
+ g_assert (G_IS_OBJECT (target));
+ g_assert (tween);
+ g_assert (value);
+
+ parent = gtk_widget_get_parent (GTK_WIDGET (target));
+ gtk_container_child_set_property (GTK_CONTAINER (parent),
+ target,
+ tween->pspec->name,
+ value);
+}
+
+
+/**
+ * egg_animation_get_value_at_offset:
+ * @animation: (in): A #EggAnimation.
+ * @offset: (in): The offset in the animation from 0.0 to 1.0.
+ * @tween: (in): A #Tween containing the property.
+ * @value: (out): A #GValue in which to store the property.
+ *
+ * Retrieves a value for a particular position within the animation.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_get_value_at_offset (EggAnimation *animation,
+ gdouble offset,
+ Tween *tween,
+ GValue *value)
+{
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+ g_return_if_fail (tween != NULL);
+ g_return_if_fail (value != NULL);
+ g_return_if_fail (value->g_type == tween->pspec->value_type);
+
+ if (value->g_type < LAST_FUNDAMENTAL)
+ {
+ /*
+ * If you hit the following assertion, you need to add a function
+ * to create the new value at the given offset.
+ */
+ g_assert (tween_funcs[value->g_type]);
+ tween_funcs[value->g_type](&tween->begin, &tween->end, value, offset);
+ }
+ else
+ {
+ /*
+ * TODO: Support complex transitions.
+ */
+ if (offset >= 1.0)
+ g_value_copy (&tween->end, value);
+ }
+}
+
+static void
+egg_animation_set_frame_clock (EggAnimation *animation,
+ GdkFrameClock *frame_clock)
+{
+ if (animation->frame_clock != frame_clock)
+ {
+ g_clear_object (&animation->frame_clock);
+ animation->frame_clock = frame_clock ? g_object_ref (frame_clock) : NULL;
+ }
+}
+
+static void
+egg_animation_set_target (EggAnimation *animation,
+ gpointer target)
+{
+ g_assert (!animation->target);
+
+ animation->target = g_object_ref (target);
+
+ if (GTK_IS_WIDGET (animation->target))
+ egg_animation_set_frame_clock (animation,
+ gtk_widget_get_frame_clock (animation->target));
+}
+
+
+/**
+ * egg_animation_tick:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Moves the object properties to the next position in the animation.
+ *
+ * Returns: %TRUE if the animation has not completed; otherwise %FALSE.
+ * Side effects: None.
+ */
+static gboolean
+egg_animation_tick (EggAnimation *animation,
+ gdouble offset)
+{
+ gdouble alpha;
+ GValue value = { 0 };
+ Tween *tween;
+ guint i;
+
+ g_return_val_if_fail (EGG_IS_ANIMATION (animation), FALSE);
+
+ if (offset == animation->last_offset)
+ return offset < 1.0;
+
+ alpha = alpha_funcs[animation->mode](offset);
+
+ /*
+ * Update property values.
+ */
+ for (i = 0; i < animation->tweens->len; i++)
+ {
+ tween = &g_array_index (animation->tweens, Tween, i);
+ g_value_init (&value, tween->pspec->value_type);
+ egg_animation_get_value_at_offset (animation, alpha, tween, &value);
+ if (!tween->is_child)
+ {
+ egg_animation_update_property (animation,
+ animation->target,
+ tween,
+ &value);
+ }
+ else
+ {
+ egg_animation_update_child_property (animation,
+ animation->target,
+ tween,
+ &value);
+ }
+ g_value_unset (&value);
+ }
+
+ /*
+ * Notify anyone interested in the tick signal.
+ */
+ g_signal_emit (animation, signals[TICK], 0);
+
+ /*
+ * Flush any outstanding events to the graphics server (in the case of X).
+ */
+#if !GTK_CHECK_VERSION (3, 13, 0)
+ if (GTK_IS_WIDGET (animation->target))
+ {
+ GdkWindow *window;
+
+ if ((window = gtk_widget_get_window (GTK_WIDGET (animation->target))))
+ gdk_window_flush (window);
+ }
+#endif
+
+ animation->last_offset = offset;
+
+ return offset < 1.0;
+}
+
+
+/**
+ * egg_animation_timeout_cb:
+ * @user_data: (in): A #EggAnimation.
+ *
+ * Timeout from the main loop to move to the next step of the animation.
+ *
+ * Returns: %TRUE until the animation has completed; otherwise %FALSE.
+ * Side effects: None.
+ */
+static gboolean
+egg_animation_timeout_cb (gpointer user_data)
+{
+ EggAnimation *animation = user_data;
+ gboolean ret;
+ gdouble offset;
+
+ offset = egg_animation_get_offset (animation, 0);
+
+ if (!(ret = egg_animation_tick (animation, offset)))
+ egg_animation_stop (animation);
+
+ return ret;
+}
+
+
+static gboolean
+egg_animation_widget_tick_cb (GdkFrameClock *frame_clock,
+ EggAnimation *animation)
+{
+ gboolean ret = G_SOURCE_REMOVE;
+
+ g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+ g_assert (EGG_IS_ANIMATION (animation));
+
+ if (animation->tween_handler)
+ {
+ gdouble offset;
+
+ offset = egg_animation_get_offset (animation, 0);
+
+ if (!(ret = egg_animation_tick (animation, offset)))
+ egg_animation_stop (animation);
+ }
+
+ return ret;
+}
+
+
+static void
+egg_animation_widget_after_paint_cb (GdkFrameClock *frame_clock,
+ EggAnimation *animation)
+{
+ gint64 base_time;
+ gint64 interval;
+ gint64 next_frame_time;
+ gdouble offset;
+
+ g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+ g_assert (EGG_IS_ANIMATION (animation));
+
+ base_time = gdk_frame_clock_get_frame_time (frame_clock);
+ gdk_frame_clock_get_refresh_info (frame_clock, base_time, &interval, &next_frame_time);
+
+ offset = egg_animation_get_offset (animation, next_frame_time);
+
+ egg_animation_tick (animation, offset);
+}
+
+
+/**
+ * egg_animation_start:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Start the animation. When the animation stops, the internal reference will
+ * be dropped and the animation may be finalized.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_start (EggAnimation *animation)
+{
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+ g_return_if_fail (!animation->tween_handler);
+
+ g_object_ref_sink (animation);
+ egg_animation_load_begin_values (animation);
+
+ if (animation->frame_clock)
+ {
+ animation->begin_msec = gdk_frame_clock_get_frame_time (animation->frame_clock) / 1000UL;
+ animation->tween_handler =
+ g_signal_connect (animation->frame_clock,
+ "update",
+ G_CALLBACK (egg_animation_widget_tick_cb),
+ animation);
+ animation->after_paint_handler =
+ g_signal_connect (animation->frame_clock,
+ "after-paint",
+ G_CALLBACK (egg_animation_widget_after_paint_cb),
+ animation);
+ gdk_frame_clock_begin_updating (animation->frame_clock);
+ }
+ else
+ {
+ animation->begin_msec = g_get_monotonic_time () / 1000UL;
+ animation->tween_handler = egg_frame_source_add (FALLBACK_FRAME_RATE,
+ egg_animation_timeout_cb,
+ animation);
+ }
+}
+
+
+static void
+egg_animation_notify (EggAnimation *self)
+{
+ g_assert (EGG_IS_ANIMATION (self));
+
+ if (self->notify != NULL)
+ {
+ GDestroyNotify notify = self->notify;
+ gpointer data = self->notify_data;
+
+ self->notify = NULL;
+ self->notify_data = NULL;
+
+ notify (data);
+ }
+}
+
+
+/**
+ * egg_animation_stop:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Stops a running animation. The internal reference to the animation is
+ * dropped and therefore may cause the object to finalize.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_stop (EggAnimation *animation)
+{
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+
+ if (animation->tween_handler)
+ {
+ if (animation->frame_clock)
+ {
+ gdk_frame_clock_end_updating (animation->frame_clock);
+ g_signal_handler_disconnect (animation->frame_clock, animation->tween_handler);
+ g_signal_handler_disconnect (animation->frame_clock, animation->after_paint_handler);
+ animation->tween_handler = 0;
+ }
+ else
+ {
+ g_source_remove (animation->tween_handler);
+ animation->tween_handler = 0;
+ }
+ egg_animation_unload_begin_values (animation);
+ egg_animation_notify (animation);
+ g_object_unref (animation);
+ }
+}
+
+
+/**
+ * egg_animation_add_property:
+ * @animation: (in): A #EggAnimation.
+ * @pspec: (in): A #ParamSpec of @target or a #GtkWidget<!-- -->'s parent.
+ * @value: (in): The new value for the property at the end of the animation.
+ *
+ * Adds a new property to the set of properties to be animated during the
+ * lifetime of the animation.
+ *
+ * Side effects: None.
+ */
+void
+egg_animation_add_property (EggAnimation *animation,
+ GParamSpec *pspec,
+ const GValue *value)
+{
+ Tween tween = { 0 };
+ GType type;
+
+ g_return_if_fail (EGG_IS_ANIMATION (animation));
+ g_return_if_fail (pspec != NULL);
+ g_return_if_fail (value != NULL);
+ g_return_if_fail (value->g_type);
+ g_return_if_fail (animation->target);
+ g_return_if_fail (!animation->tween_handler);
+
+ type = G_TYPE_FROM_INSTANCE (animation->target);
+ tween.is_child = !g_type_is_a (type, pspec->owner_type);
+ if (tween.is_child)
+ {
+ if (!GTK_IS_WIDGET (animation->target))
+ {
+ g_critical (_("Cannot locate property %s in class %s"),
+ pspec->name, g_type_name (type));
+ return;
+ }
+ }
+
+ tween.pspec = g_param_spec_ref (pspec);
+ g_value_init (&tween.begin, pspec->value_type);
+ g_value_init (&tween.end, pspec->value_type);
+ g_value_copy (value, &tween.end);
+ g_array_append_val (animation->tweens, tween);
+}
+
+
+/**
+ * egg_animation_dispose:
+ * @object: (in): A #EggAnimation.
+ *
+ * Releases any object references the animation contains.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_dispose (GObject *object)
+{
+ EggAnimation *self = EGG_ANIMATION (object);
+
+ g_clear_object (&self->target);
+ g_clear_object (&self->frame_clock);
+
+ G_OBJECT_CLASS (egg_animation_parent_class)->dispose (object);
+}
+
+
+/**
+ * egg_animation_finalize:
+ * @object: (in): A #EggAnimation.
+ *
+ * Finalizes the object and releases any resources allocated.
+ *
+ * Side effects: None.
+ */
+static void
+egg_animation_finalize (GObject *object)
+{
+ EggAnimation *self = EGG_ANIMATION (object);
+ Tween *tween;
+ guint i;
+
+ for (i = 0; i < self->tweens->len; i++)
+ {
+ tween = &g_array_index (self->tweens, Tween, i);
+ g_value_unset (&tween->begin);
+ g_value_unset (&tween->end);
+ g_param_spec_unref (tween->pspec);
+ }
+
+ g_array_unref (self->tweens);
+
+ G_OBJECT_CLASS (egg_animation_parent_class)->finalize (object);
+}
+
+
+/**
+ * egg_animation_set_property:
+ * @object: (in): A #GObject.
+ * @prop_id: (in): The property identifier.
+ * @value: (in): The given property.
+ * @pspec: (in): A #ParamSpec.
+ *
+ * Set a given #GObject property.
+ */
+static void
+egg_animation_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EggAnimation *animation = EGG_ANIMATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_DURATION:
+ animation->duration_msec = g_value_get_uint (value) * slow_down_factor;
+ break;
+
+ case PROP_FRAME_CLOCK:
+ egg_animation_set_frame_clock (animation, g_value_get_object (value));
+ break;
+
+ case PROP_MODE:
+ animation->mode = g_value_get_enum (value);
+ break;
+
+ case PROP_TARGET:
+ egg_animation_set_target (animation, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+
+/**
+ * egg_animation_class_init:
+ * @klass: (in): A #EggAnimationClass.
+ *
+ * Initializes the GObjectClass.
+ *
+ * Side effects: Properties, signals, and vtables are initialized.
+ */
+static void
+egg_animation_class_init (EggAnimationClass *klass)
+{
+ GObjectClass *object_class;
+ const gchar *slow_down_factor_env;
+
+ debug = !!g_getenv ("EGG_ANIMATION_DEBUG");
+ slow_down_factor_env = g_getenv ("EGG_ANIMATION_SLOW_DOWN_FACTOR");
+
+ if (slow_down_factor_env)
+ slow_down_factor = MAX (1, atoi (slow_down_factor_env));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = egg_animation_dispose;
+ object_class->finalize = egg_animation_finalize;
+ object_class->set_property = egg_animation_set_property;
+
+ /**
+ * EggAnimation:duration:
+ *
+ * The "duration" property is the total number of milliseconds that the
+ * animation should run before being completed.
+ */
+ properties[PROP_DURATION] =
+ g_param_spec_uint ("duration",
+ "Duration",
+ "The duration of the animation",
+ 0,
+ G_MAXUINT,
+ 250,
+ (G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ properties[PROP_FRAME_CLOCK] =
+ g_param_spec_object ("frame-clock",
+ "Frame Clock",
+ "An optional frame-clock to synchronize with.",
+ GDK_TYPE_FRAME_CLOCK,
+ (G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * EggAnimation:mode:
+ *
+ * The "mode" property is the Alpha function that should be used to
+ * determine the offset within the animation based on the current
+ * offset in the animations duration.
+ */
+ properties[PROP_MODE] =
+ g_param_spec_enum ("mode",
+ "Mode",
+ "The animation mode",
+ EGG_TYPE_ANIMATION_MODE,
+ EGG_ANIMATION_LINEAR,
+ (G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * EggAnimation:target:
+ *
+ * The "target" property is the #GObject that should have its properties
+ * animated.
+ */
+ properties[PROP_TARGET] =
+ g_param_spec_object ("target",
+ "Target",
+ "The target of the animation",
+ G_TYPE_OBJECT,
+ (G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+ /**
+ * EggAnimation::tick:
+ *
+ * The "tick" signal is emitted on each frame in the animation.
+ */
+ signals[TICK] = g_signal_new ("tick",
+ EGG_TYPE_ANIMATION,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+#define SET_ALPHA(_T, _t) \
+ alpha_funcs[EGG_ANIMATION_ ## _T] = egg_animation_alpha_ ## _t
+
+ SET_ALPHA (LINEAR, linear);
+ SET_ALPHA (EASE_IN_QUAD, ease_in_quad);
+ SET_ALPHA (EASE_OUT_QUAD, ease_out_quad);
+ SET_ALPHA (EASE_IN_OUT_QUAD, ease_in_out_quad);
+ SET_ALPHA (EASE_IN_CUBIC, ease_in_cubic);
+ SET_ALPHA (EASE_OUT_CUBIC, ease_out_cubic);
+ SET_ALPHA (EASE_IN_OUT_CUBIC, ease_in_out_cubic);
+
+#define SET_TWEEN(_T, _t) \
+ G_STMT_START { \
+ guint idx = G_TYPE_ ## _T; \
+ tween_funcs[idx] = tween_ ## _t; \
+ } G_STMT_END
+
+ SET_TWEEN (INT, int);
+ SET_TWEEN (UINT, uint);
+ SET_TWEEN (LONG, long);
+ SET_TWEEN (ULONG, ulong);
+ SET_TWEEN (FLOAT, float);
+ SET_TWEEN (DOUBLE, double);
+}
+
+
+/**
+ * egg_animation_init:
+ * @animation: (in): A #EggAnimation.
+ *
+ * Initializes the #EggAnimation instance.
+ *
+ * Side effects: Everything.
+ */
+static void
+egg_animation_init (EggAnimation *animation)
+{
+ animation->duration_msec = 250;
+ animation->mode = EGG_ANIMATION_EASE_IN_OUT_QUAD;
+ animation->tweens = g_array_new (FALSE, FALSE, sizeof (Tween));
+ animation->last_offset = -G_MINDOUBLE;
+}
+
+
+/**
+ * egg_animation_mode_get_type:
+ *
+ * Retrieves the GType for #EggAnimationMode.
+ *
+ * Returns: A GType.
+ * Side effects: GType registered on first call.
+ */
+GType
+egg_animation_mode_get_type (void)
+{
+ static GType type_id = 0;
+ static const GEnumValue values[] = {
+ { EGG_ANIMATION_LINEAR, "EGG_ANIMATION_LINEAR", "linear" },
+ { EGG_ANIMATION_EASE_IN_QUAD, "EGG_ANIMATION_EASE_IN_QUAD", "ease-in-quad" },
+ { EGG_ANIMATION_EASE_IN_OUT_QUAD, "EGG_ANIMATION_EASE_IN_OUT_QUAD", "ease-in-out-quad" },
+ { EGG_ANIMATION_EASE_OUT_QUAD, "EGG_ANIMATION_EASE_OUT_QUAD", "ease-out-quad" },
+ { EGG_ANIMATION_EASE_IN_CUBIC, "EGG_ANIMATION_EASE_IN_CUBIC", "ease-in-cubic" },
+ { EGG_ANIMATION_EASE_OUT_CUBIC, "EGG_ANIMATION_EASE_OUT_CUBIC", "ease-out-cubic" },
+ { EGG_ANIMATION_EASE_IN_OUT_CUBIC, "EGG_ANIMATION_EASE_IN_OUT_CUBIC", "ease-in-out-cubic" },
+ { 0 }
+ };
+
+ if (G_UNLIKELY (!type_id))
+ type_id = g_enum_register_static ("EggAnimationMode", values);
+ return type_id;
+}
+
+/**
+ * egg_object_animatev:
+ * @object: A #GObject.
+ * @mode: The animation mode.
+ * @duration_msec: The duration in milliseconds.
+ * @frame_clock: (nullable): The #GdkFrameClock to synchronize to.
+ * @first_property: The first property to animate.
+ * @args: A variadac list of arguments
+ *
+ * Returns: (transfer none): A #EggAnimation.
+ */
+EggAnimation *
+egg_object_animatev (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ const gchar *first_property,
+ va_list args)
+{
+ EggAnimation *animation;
+ GObjectClass *klass;
+ GObjectClass *pklass;
+ const gchar *name;
+ GParamSpec *pspec;
+ GtkWidget *parent;
+ GValue value = { 0 };
+ gchar *error = NULL;
+ GType type;
+ GType ptype;
+ gboolean enable_animations;
+
+ g_return_val_if_fail (first_property != NULL, NULL);
+ g_return_val_if_fail (mode < EGG_ANIMATION_LAST, NULL);
+
+ if ((frame_clock == NULL) && GTK_IS_WIDGET (object))
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (object));
+
+ /*
+ * If we have a frame clock, then we must be in the gtk thread and we
+ * should check GtkSettings for disabled animations. If we are disabled,
+ * we will just make the timeout immediate.
+ */
+ if (frame_clock != NULL)
+ {
+ g_object_get (gtk_settings_get_default (),
+ "gtk-enable-animations", &enable_animations,
+ NULL);
+
+ if (enable_animations == FALSE)
+ duration_msec = 0;
+ }
+
+ name = first_property;
+ type = G_TYPE_FROM_INSTANCE (object);
+ klass = G_OBJECT_GET_CLASS (object);
+ animation = g_object_new (EGG_TYPE_ANIMATION,
+ "duration", duration_msec,
+ "frame-clock", frame_clock,
+ "mode", mode,
+ "target", object,
+ NULL);
+
+ do
+ {
+ /*
+ * First check for the property on the object. If that does not exist
+ * then check if the object has a parent and look at its child
+ * properties (if it's a GtkWidget).
+ */
+ if (!(pspec = g_object_class_find_property (klass, name)))
+ {
+ if (!g_type_is_a (type, GTK_TYPE_WIDGET))
+ {
+ g_critical (_("Failed to find property %s in %s"),
+ name, g_type_name (type));
+ goto failure;
+ }
+ if (!(parent = gtk_widget_get_parent (object)))
+ {
+ g_critical (_("Failed to find property %s in %s"),
+ name, g_type_name (type));
+ goto failure;
+ }
+ pklass = G_OBJECT_GET_CLASS (parent);
+ ptype = G_TYPE_FROM_INSTANCE (parent);
+ if (!(pspec = gtk_container_class_find_child_property (pklass, name)))
+ {
+ g_critical (_("Failed to find property %s in %s or parent %s"),
+ name, g_type_name (type), g_type_name (ptype));
+ goto failure;
+ }
+ }
+
+ g_value_init (&value, pspec->value_type);
+ G_VALUE_COLLECT (&value, args, 0, &error);
+ if (error != NULL)
+ {
+ g_critical (_("Failed to retrieve va_list value: %s"), error);
+ g_free (error);
+ goto failure;
+ }
+
+ egg_animation_add_property (animation, pspec, &value);
+ g_value_unset (&value);
+ }
+ while ((name = va_arg (args, const gchar *)));
+
+ egg_animation_start (animation);
+
+ return animation;
+
+failure:
+ g_object_ref_sink (animation);
+ g_object_unref (animation);
+ return NULL;
+}
+
+/**
+ * egg_object_animate:
+ * @object: (in): A #GObject.
+ * @mode: (in): The animation mode.
+ * @duration_msec: (in): The duration in milliseconds.
+ * @first_property: (in): The first property to animate.
+ *
+ * Animates the properties of @object. The can be set in a similar manner to g_object_set(). They
+ * will be animated from their current value to the target value over the time period.
+ *
+ * Return value: (transfer none): A #EggAnimation.
+ * Side effects: None.
+ */
+EggAnimation*
+egg_object_animate (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ const gchar *first_property,
+ ...)
+{
+ EggAnimation *animation;
+ va_list args;
+
+ va_start (args, first_property);
+ animation = egg_object_animatev (object,
+ mode,
+ duration_msec,
+ frame_clock,
+ first_property,
+ args);
+ va_end (args);
+
+ return animation;
+}
+
+/**
+ * egg_object_animate_full:
+ *
+ * Return value: (transfer none): A #EggAnimation.
+ */
+EggAnimation*
+egg_object_animate_full (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ GDestroyNotify notify,
+ gpointer notify_data,
+ const gchar *first_property,
+ ...)
+{
+ EggAnimation *animation;
+ va_list args;
+
+ va_start (args, first_property);
+ animation = egg_object_animatev (object,
+ mode,
+ duration_msec,
+ frame_clock,
+ first_property,
+ args);
+ va_end (args);
+
+ animation->notify = notify;
+ animation->notify_data = notify_data;
+
+ return animation;
+}
+
+guint
+egg_animation_calculate_duration (GdkMonitor *monitor,
+ gdouble from_value,
+ gdouble to_value)
+{
+ GdkRectangle geom;
+ gdouble distance_units;
+ gdouble distance_mm;
+ gdouble mm_per_frame;
+ gint height_mm;
+ gint refresh_rate;
+ gint n_frames;
+ guint ret;
+
+#define MM_PER_SECOND (100.0)
+#define MIN_FRAMES_PER_ANIM (5)
+#define MAX_FRAMES_PER_ANIM (500)
+
+ g_assert (GDK_IS_MONITOR (monitor));
+ g_assert (from_value >= 0.0);
+ g_assert (to_value >= 0.0);
+
+ /*
+ * Get various monitor information we'll need to calculate the duration of
+ * the animation. We need the physical space of the monitor, the refresh
+ * rate, and geometry so that we can limit how many device units we will
+ * traverse per-frame of the animation. Failure to deal with the physical
+ * space results in jittery animations to the user.
+ *
+ * It would also be nice to take into account the acceleration curve so that
+ * we know the max amount of jump per frame, but that is getting into
+ * diminishing returns since we can just average it out.
+ */
+ height_mm = gdk_monitor_get_height_mm (monitor);
+ gdk_monitor_get_geometry (monitor, &geom);
+ refresh_rate = gdk_monitor_get_refresh_rate (monitor);
+ if (refresh_rate == 0)
+ refresh_rate = 60000;
+
+ /*
+ * The goal here is to determine the number of millimeters that we need to
+ * animate given a transition of distance_unit pixels. Since we are dealing
+ * with physical units (mm), we don't need to take into account the device
+ * scale underneath the widget. The equation comes out the same.
+ */
+
+ distance_units = ABS (from_value - to_value);
+ distance_mm = distance_units / (gdouble)geom.height * height_mm;
+ mm_per_frame = MM_PER_SECOND / (refresh_rate / 1000.0);
+ n_frames = (distance_mm / mm_per_frame) + 1;
+
+ ret = n_frames * (1000.0 / (refresh_rate / 1000.0));
+ ret = CLAMP (ret,
+ MIN_FRAMES_PER_ANIM * (1000000.0 / refresh_rate),
+ MAX_FRAMES_PER_ANIM * (1000000.0 / refresh_rate));
+
+ return ret;
+
+#undef MM_PER_SECOND
+#undef MIN_FRAMES_PER_ANIM
+#undef MAX_FRAMES_PER_ANIM
+}
diff --git a/lib/egg/egg-animation.h b/lib/egg/egg-animation.h
new file mode 100644
index 0000000..0f52782
--- /dev/null
+++ b/lib/egg/egg-animation.h
@@ -0,0 +1,81 @@
+/* egg-animation.h
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * 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_ANIMATION_H
+#define EGG_ANIMATION_H
+
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_ANIMATION (egg_animation_get_type())
+#define EGG_TYPE_ANIMATION_MODE (egg_animation_mode_get_type())
+
+G_DECLARE_FINAL_TYPE (EggAnimation, egg_animation,
+ EGG, ANIMATION, GInitiallyUnowned)
+
+typedef enum _EggAnimationMode EggAnimationMode;
+
+enum _EggAnimationMode
+{
+ EGG_ANIMATION_LINEAR,
+ EGG_ANIMATION_EASE_IN_QUAD,
+ EGG_ANIMATION_EASE_OUT_QUAD,
+ EGG_ANIMATION_EASE_IN_OUT_QUAD,
+ EGG_ANIMATION_EASE_IN_CUBIC,
+ EGG_ANIMATION_EASE_OUT_CUBIC,
+ EGG_ANIMATION_EASE_IN_OUT_CUBIC,
+
+ EGG_ANIMATION_LAST
+};
+
+GType egg_animation_mode_get_type (void);
+void egg_animation_start (EggAnimation *animation);
+void egg_animation_stop (EggAnimation *animation);
+void egg_animation_add_property (EggAnimation *animation,
+ GParamSpec *pspec,
+ const GValue *value);
+
+EggAnimation *egg_object_animatev (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ const gchar *first_property,
+ va_list args);
+EggAnimation* egg_object_animate (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ const gchar *first_property,
+ ...) G_GNUC_NULL_TERMINATED;
+EggAnimation* egg_object_animate_full (gpointer object,
+ EggAnimationMode mode,
+ guint duration_msec,
+ GdkFrameClock *frame_clock,
+ GDestroyNotify notify,
+ gpointer notify_data,
+ const gchar *first_property,
+ ...) G_GNUC_NULL_TERMINATED;
+
+guint egg_animation_calculate_duration (GdkMonitor *monitor,
+ gdouble from_value,
+ gdouble to_value);
+
+G_END_DECLS
+
+#endif /* EGG_ANIMATION_H */
diff --git a/lib/egg/egg-elastic-bin.c b/lib/egg/egg-elastic-bin.c
new file mode 100644
index 0000000..3e90c36
--- /dev/null
+++ b/lib/egg/egg-elastic-bin.c
@@ -0,0 +1,278 @@
+/* egg-elastic-bin.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-elastic-bin"
+
+#include "egg-animation.h"
+#include "egg-elastic-bin.h"
+
+#define MM_PER_SECOND (100.0)
+#define MIN_FRAMES_PER_ANIM (5)
+#define MAX_FRAMES_PER_ANIM (500)
+
+#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 TRACE_MSG(m,...) _TRACE(" MSG: %s():%u: "m, G_STRFUNC, __LINE__, __VA_ARGS__)
+# define ENTRY _TRACE(" ENTRY: %s(): %u", G_STRFUNC, __LINE__)
+# define EXIT do { _TRACE(" EXIT: %s(): %u", G_STRFUNC, __LINE__); return; } while (0)
+# define RETURN(r) do { _TRACE(" EXIT: %s(): %u", G_STRFUNC, __LINE__); return (r); } while (0)
+#else
+# define TRACE_MSG(m,...) do { } while (0)
+# define ENTRY do { } while (0)
+# define EXIT return
+# define RETURN(r) return (r)
+#endif
+
+typedef struct
+{
+ GtkAdjustment *hadj;
+ EggAnimation *hanim;
+ gint cached_min_height;
+ gint cached_nat_height;
+} EggElasticBinPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggElasticBin, egg_elastic_bin, GTK_TYPE_BIN)
+
+static void
+egg_elastic_bin_cancel_animation (EggElasticBin *self)
+{
+ EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+ ENTRY;
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+
+ if (priv->hanim != NULL)
+ {
+ EggAnimation *anim = priv->hanim;
+
+ g_object_remove_weak_pointer (G_OBJECT (priv->hanim), (gpointer *)&priv->hanim);
+ priv->hanim = NULL;
+
+ egg_animation_stop (anim);
+ }
+
+ EXIT;
+}
+
+static guint
+egg_elastic_bin_calculate_duration (EggElasticBin *self,
+ gdouble from_value,
+ gdouble to_value)
+{
+ GdkDisplay *display;
+ GdkMonitor *monitor;
+ GdkWindow *window;
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+ g_assert (from_value >= 0.0);
+ g_assert (to_value >= 0.0);
+
+ if (NULL != (display = gtk_widget_get_display (GTK_WIDGET (self))) &&
+ NULL != (window = gtk_widget_get_window (GTK_WIDGET (self))) &&
+ NULL != (monitor = gdk_display_get_monitor_at_window (display, window)))
+ return egg_animation_calculate_duration (monitor, from_value, to_value);
+
+ return 0;
+}
+
+static void
+egg_elastic_bin_animate_to (EggElasticBin *self,
+ gdouble value)
+{
+ EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+ guint duration;
+
+ ENTRY;
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+
+ egg_elastic_bin_cancel_animation (self);
+
+ duration = egg_elastic_bin_calculate_duration (self,
+ gtk_adjustment_get_value (priv->hadj),
+ value);
+
+ TRACE_MSG ("Duration is %u milliseconds", duration);
+
+ priv->hanim = egg_object_animate (priv->hadj,
+ EGG_ANIMATION_EASE_OUT_CUBIC,
+ duration,
+ gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+ "value", value,
+ NULL);
+ g_object_add_weak_pointer (G_OBJECT (priv->hanim), (gpointer *)&priv->hanim);
+
+ EXIT;
+}
+
+static void
+egg_elastic_bin_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *min_height,
+ gint *nat_height)
+{
+ EggElasticBin *self = (EggElasticBin *)widget;
+ EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+ ENTRY;
+
+ TRACE_MSG ("width=%d", width);
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+
+ /*
+ * We must always chain up to the parent get_preferred_height_for_width()
+ * so that we can detect changes while we are animating.
+ */
+
+ GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->get_preferred_height_for_width (widget, width,
min_height, nat_height);
+
+ /*
+ * If we are animating the widget, and the size request hasn't changed since
+ * our last animation frame, go ahead and process that now.
+ */
+
+ if (*min_height == priv->cached_min_height &&
+ *nat_height == priv->cached_nat_height &&
+ priv->hanim != NULL)
+ {
+ *min_height = priv->cached_min_height;
+ *nat_height = (gint)gtk_adjustment_get_value (priv->hadj);
+
+ TRACE_MSG ("Fast path min=%d nat=%d", *min_height, *nat_height);
+
+ if (*nat_height == priv->cached_nat_height)
+ egg_elastic_bin_cancel_animation (self);
+
+ EXIT;
+ }
+
+ if (*min_height != priv->cached_min_height || *nat_height != priv->cached_nat_height)
+ {
+ priv->cached_min_height = *min_height;
+ priv->cached_nat_height = *nat_height;
+
+ TRACE_MSG ("New requested height is min=%d nat=%d",
+ *min_height, *nat_height);
+
+ if (*min_height > (gint)gtk_adjustment_get_value (priv->hadj))
+ gtk_adjustment_set_value (priv->hadj, *min_height);
+
+ *min_height = priv->cached_min_height;
+ *nat_height = (gint)gtk_adjustment_get_value (priv->hadj);
+
+ egg_elastic_bin_animate_to (self, priv->cached_nat_height);
+
+ TRACE_MSG ("!!! min=%d nat=%d", *min_height, *nat_height);
+
+ EXIT;
+ }
+
+ TRACE_MSG ("*** min=%d nat=%d", *min_height, *nat_height);
+
+ EXIT;
+}
+
+static void
+egg_elastic_bin_hadj_value_changed (EggElasticBin *self,
+ GtkAdjustment *adj)
+{
+ ENTRY;
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+ g_assert (GTK_IS_ADJUSTMENT (adj));
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ EXIT;
+}
+
+static void
+egg_elastic_bin_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ g_assert (GTK_IS_WIDGET (widget));
+ g_assert (allocation != NULL);
+
+ TRACE_MSG ("Allocating %d,%d %dx%d",
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+
+ GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->size_allocate (widget, allocation);
+}
+
+static GtkSizeRequestMode
+egg_elastic_bin_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+egg_elastic_bin_destroy (GtkWidget *widget)
+{
+ EggElasticBin *self = (EggElasticBin *)widget;
+
+ g_assert (EGG_IS_ELASTIC_BIN (self));
+
+ egg_elastic_bin_cancel_animation (self);
+
+ GTK_WIDGET_CLASS (egg_elastic_bin_parent_class)->destroy (widget);
+}
+
+static void
+egg_elastic_bin_finalize (GObject *object)
+{
+ EggElasticBin *self = (EggElasticBin *)object;
+ EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+ g_clear_object (&priv->hadj);
+
+ G_OBJECT_CLASS (egg_elastic_bin_parent_class)->finalize (object);
+}
+
+static void
+egg_elastic_bin_class_init (EggElasticBinClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = egg_elastic_bin_finalize;
+
+ widget_class->destroy = egg_elastic_bin_destroy;
+ widget_class->get_preferred_height_for_width = egg_elastic_bin_get_preferred_height_for_width;
+ widget_class->size_allocate = egg_elastic_bin_size_allocate;
+ widget_class->get_request_mode = egg_elastic_bin_get_request_mode;
+
+ gtk_widget_class_set_css_name (widget_class, "elastic");
+}
+
+static void
+egg_elastic_bin_init (EggElasticBin *self)
+{
+ EggElasticBinPrivate *priv = egg_elastic_bin_get_instance_private (self);
+
+ priv->hadj = gtk_adjustment_new (0, 0, G_MAXINT, 1, 1, 1);
+
+ g_signal_connect_object (priv->hadj,
+ "value-changed",
+ G_CALLBACK (egg_elastic_bin_hadj_value_changed),
+ self,
+ G_CONNECT_SWAPPED);
+}
diff --git a/lib/egg/egg-elastic-bin.h b/lib/egg/egg-elastic-bin.h
new file mode 100644
index 0000000..dcd0805
--- /dev/null
+++ b/lib/egg/egg-elastic-bin.h
@@ -0,0 +1,44 @@
+/* egg-elastic-bin.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_ELASTIC_BIN_H
+#define EGG_ELASTIC_BIN_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_ELASTIC_BIN (egg_elastic_bin_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggElasticBin, egg_elastic_bin, EGG, ELASTIC_BIN, GtkBin)
+
+struct _EggElasticBinClass
+{
+ GtkBinClass parent_class;
+
+ gpointer _reserved1;
+ gpointer _reserved2;
+ gpointer _reserved3;
+ gpointer _reserved4;
+};
+
+GtkWidget *egg_elastic_bin_new (void);
+
+G_END_DECLS
+
+#endif /* EGG_ELASTIC_BIN_H */
diff --git a/lib/egg/egg-frame-source.c b/lib/egg/egg-frame-source.c
new file mode 100644
index 0000000..64a8593
--- /dev/null
+++ b/lib/egg/egg-frame-source.c
@@ -0,0 +1,130 @@
+/* egg-frame-source.c
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#include "egg-frame-source.h"
+
+typedef struct
+{
+ GSource parent;
+ guint fps;
+ guint frame_count;
+ gint64 start_time;
+} EggFrameSource;
+
+static gboolean
+egg_frame_source_prepare (GSource *source,
+ gint *timeout_)
+{
+ EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
+ gint64 current_time;
+ guint elapsed_time;
+ guint new_frame_num;
+ guint frame_time;
+
+ current_time = g_source_get_time(source) / 1000;
+ elapsed_time = current_time - fsource->start_time;
+ new_frame_num = elapsed_time * fsource->fps / 1000;
+
+ /* If time has gone backwards or the time since the last frame is
+ * greater than the two frames worth then reset the time and do a
+ * frame now */
+ if (new_frame_num < fsource->frame_count ||
+ new_frame_num - fsource->frame_count > 2) {
+ /* Get the frame time rounded up to the nearest ms */
+ frame_time = (1000 + fsource->fps - 1) / fsource->fps;
+
+ /* Reset the start time */
+ fsource->start_time = current_time;
+
+ /* Move the start time as if one whole frame has elapsed */
+ fsource->start_time -= frame_time;
+ fsource->frame_count = 0;
+ *timeout_ = 0;
+ return TRUE;
+ } else if (new_frame_num > fsource->frame_count) {
+ *timeout_ = 0;
+ return TRUE;
+ } else {
+ *timeout_ = (fsource->frame_count + 1) * 1000 / fsource->fps - elapsed_time;
+ return FALSE;
+ }
+}
+
+static gboolean
+egg_frame_source_check (GSource *source)
+{
+ gint timeout_;
+ return egg_frame_source_prepare(source, &timeout_);
+}
+
+static gboolean
+egg_frame_source_dispatch (GSource *source,
+ GSourceFunc source_func,
+ gpointer user_data)
+{
+ EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
+ gboolean ret;
+
+ if ((ret = source_func(user_data)))
+ fsource->frame_count++;
+ return ret;
+}
+
+static GSourceFuncs source_funcs = {
+ egg_frame_source_prepare,
+ egg_frame_source_check,
+ egg_frame_source_dispatch,
+};
+
+/**
+ * egg_frame_source_add:
+ * @frames_per_sec: (in): Target frames per second.
+ * @callback: (in) (scope notified): A #GSourceFunc to execute.
+ * @user_data: (in): User data for @callback.
+ *
+ * Creates a new frame source that will execute when the timeout interval
+ * for the source has elapsed. The timing will try to synchronize based
+ * on the end time of the animation.
+ *
+ * Returns: A source id that can be removed with g_source_remove().
+ */
+guint
+egg_frame_source_add (guint frames_per_sec,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ EggFrameSource *fsource;
+ GSource *source;
+ guint ret;
+
+ g_return_val_if_fail (frames_per_sec > 0, 0);
+ g_return_val_if_fail (frames_per_sec <= 120, 0);
+
+ source = g_source_new(&source_funcs, sizeof(EggFrameSource));
+ fsource = (EggFrameSource *)(gpointer)source;
+ fsource->fps = frames_per_sec;
+ fsource->frame_count = 0;
+ fsource->start_time = g_get_monotonic_time() / 1000;
+ g_source_set_callback(source, callback, user_data, NULL);
+ g_source_set_name(source, "EggFrameSource");
+
+ ret = g_source_attach(source, NULL);
+ g_source_unref(source);
+
+ return ret;
+}
diff --git a/lib/egg/egg-frame-source.h b/lib/egg/egg-frame-source.h
new file mode 100644
index 0000000..3ba234f
--- /dev/null
+++ b/lib/egg/egg-frame-source.h
@@ -0,0 +1,32 @@
+/* egg-frame-source.h
+ *
+ * Copyright (C) 2010-2016 Christian Hergert <christian hergert me>
+ *
+ * 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_FRAME_SOURCE_H
+#define EGG_FRAME_SOURCE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+guint egg_frame_source_add (guint frames_per_sec,
+ GSourceFunc callback,
+ gpointer user_data);
+
+G_END_DECLS
+
+#endif /* EGG_FRAME_SOURCE_H */
diff --git a/lib/egg/egg-suggestion-entry-buffer.c b/lib/egg/egg-suggestion-entry-buffer.c
new file mode 100644
index 0000000..68d3a6e
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-entry-buffer.h b/lib/egg/egg-suggestion-entry-buffer.h
new file mode 100644
index 0000000..e8078b7
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-entry.c b/lib/egg/egg-suggestion-entry.c
new file mode 100644
index 0000000..4bd8c26
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-entry.css b/lib/egg/egg-suggestion-entry.css
new file mode 100644
index 0000000..fe837db
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-entry.h b/lib/egg/egg-suggestion-entry.h
new file mode 100644
index 0000000..4c0f07b
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-popover.c b/lib/egg/egg-suggestion-popover.c
new file mode 100644
index 0000000..17fdbbe
--- /dev/null
+++ b/lib/egg/egg-suggestion-popover.c
@@ -0,0 +1,796 @@
+/* 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_revealer_set_transition_duration (self->revealer, duration);
+ gtk_widget_show (GTK_WIDGET (self));
+ gtk_revealer_set_reveal_child (self->revealer, TRUE);
+}
+
+void
+egg_suggestion_popover_popdown (EggSuggestionPopover *self)
+{
+ g_assert (EGG_IS_SUGGESTION_POPOVER (self));
+
+ gtk_revealer_set_transition_duration (self->revealer, 100);
+ 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);
+}
+
+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/lib/egg/egg-suggestion-popover.h b/lib/egg/egg-suggestion-popover.h
new file mode 100644
index 0000000..bdfba5f
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-popover.ui b/lib/egg/egg-suggestion-popover.ui
new file mode 100644
index 0000000..265e326
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-row.c b/lib/egg/egg-suggestion-row.c
new file mode 100644
index 0000000..604678a
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-row.h b/lib/egg/egg-suggestion-row.h
new file mode 100644
index 0000000..87a6925
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion-row.ui b/lib/egg/egg-suggestion-row.ui
new file mode 100644
index 0000000..bf79bc4
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion.c b/lib/egg/egg-suggestion.c
new file mode 100644
index 0000000..d28d059
--- /dev/null
+++ b/lib/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/lib/egg/egg-suggestion.h b/lib/egg/egg-suggestion.h
new file mode 100644
index 0000000..05a70ce
--- /dev/null
+++ b/lib/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 *replace_typed_text);
+
+G_END_DECLS
+
+#endif /* EGG_SUGGESTION_H */
diff --git a/lib/egg/egg.gresources.xml b/lib/egg/egg.gresources.xml
new file mode 100644
index 0000000..a4030bd
--- /dev/null
+++ b/lib/egg/egg.gresources.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/libegg-private">
+ <file>egg-suggestion-entry.css</file>
+ <file>egg-suggestion-popover.ui</file>
+ <file>egg-suggestion-row.ui</file>
+ </gresource>
+</gresources>
diff --git a/lib/egg/eggtreemultidnd.c b/lib/egg/eggtreemultidnd.c
index a238abd..03e3dee 100644
--- a/lib/egg/eggtreemultidnd.c
+++ b/lib/egg/eggtreemultidnd.c
@@ -16,6 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#define G_LOG_DOMAIN "Egg"
+
#include <config.h>
#include <string.h>
diff --git a/lib/widgets/ephy-location-entry.c b/lib/widgets/ephy-location-entry.c
index 72b7e12..467dd54 100644
--- a/lib/widgets/ephy-location-entry.c
+++ b/lib/widgets/ephy-location-entry.c
@@ -54,7 +54,7 @@
*/
struct _EphyLocationEntry {
- GtkEntry parent_instance;
+ EggSuggestionEntry parent_instance;
GtkTreeModel *model;
@@ -115,7 +115,7 @@ static gint signals[LAST_SIGNAL] = { 0 };
static void ephy_location_entry_title_widget_interface_init (EphyTitleWidgetInterface *iface);
-G_DEFINE_TYPE_WITH_CODE (EphyLocationEntry, ephy_location_entry, GTK_TYPE_ENTRY,
+G_DEFINE_TYPE_WITH_CODE (EphyLocationEntry, ephy_location_entry, EGG_TYPE_SUGGESTION_ENTRY,
G_IMPLEMENT_INTERFACE (EPHY_TYPE_TITLE_WIDGET,
ephy_location_entry_title_widget_interface_init))
diff --git a/lib/widgets/ephy-location-entry.h b/lib/widgets/ephy-location-entry.h
index 490524a..97e4e47 100644
--- a/lib/widgets/ephy-location-entry.h
+++ b/lib/widgets/ephy-location-entry.h
@@ -25,13 +25,15 @@
#include <gtk/gtk.h>
+#include "egg/egg-suggestion-entry.h"
+
#include "ephy-security-levels.h"
G_BEGIN_DECLS
#define EPHY_TYPE_LOCATION_ENTRY (ephy_location_entry_get_type())
-G_DECLARE_FINAL_TYPE (EphyLocationEntry, ephy_location_entry, EPHY, LOCATION_ENTRY, GtkEntry)
+G_DECLARE_FINAL_TYPE (EphyLocationEntry, ephy_location_entry, EPHY, LOCATION_ENTRY, EggSuggestionEntry)
typedef enum {
EPHY_LOCATION_ENTRY_BOOKMARK_ICON_HIDDEN,
diff --git a/src/Makefile.am b/src/Makefile.am
index aab514a..a2d8aec 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -62,6 +62,8 @@ libephymain_la_SOURCES = \
ephy-session.h \
ephy-shell.c \
ephy-shell.h \
+ ephy-suggestion-model.c \
+ ephy-suggestion-model.h \
ephy-window.c \
ephy-window.h \
languages.h \
diff --git a/src/ephy-location-controller.c b/src/ephy-location-controller.c
index 37fd6e7..89ad807 100644
--- a/src/ephy-location-controller.c
+++ b/src/ephy-location-controller.c
@@ -23,7 +23,6 @@
#include "ephy-location-controller.h"
-#include "ephy-completion-model.h"
#include "ephy-debug.h"
#include "ephy-dnd.h"
#include "ephy-embed-container.h"
@@ -31,6 +30,7 @@
#include "ephy-link.h"
#include "ephy-location-entry.h"
#include "ephy-shell.h"
+#include "ephy-suggestion-model.h"
#include "ephy-title-widget.h"
#include "ephy-widgets-type-builtins.h"
@@ -75,18 +75,7 @@ enum {
static GParamSpec *obj_properties[LAST_PROP];
G_DEFINE_TYPE_WITH_CODE (EphyLocationController, ephy_location_controller, G_TYPE_OBJECT,
- G_IMPLEMENT_INTERFACE (EPHY_TYPE_LINK,
- NULL))
-
-static gboolean
-match_func (GtkEntryCompletion *completion,
- const char *key,
- GtkTreeIter *iter,
- gpointer data)
-{
- /* We want every row in the model to show up. */
- return TRUE;
-}
+ G_IMPLEMENT_INTERFACE (EPHY_TYPE_LINK, NULL))
static void
entry_drag_data_received_cb (GtkWidget *widget,
@@ -181,33 +170,18 @@ entry_activate_cb (GtkEntry *entry,
}
static void
-update_done_cb (EphyHistoryService *service,
- gboolean success,
- gpointer result_data,
- gpointer user_data)
-{
- /* FIXME: this hack is needed for the completion entry popup
- * to resize smoothly. See:
- * https://bugzilla.gnome.org/show_bug.cgi?id=671074 */
- gtk_entry_completion_complete (GTK_ENTRY_COMPLETION (user_data));
-}
-
-static void
user_changed_cb (GtkWidget *widget, EphyLocationController *controller)
{
const char *address;
- GtkTreeModel *model;
- GtkEntryCompletion *completion;
+ GListModel *model;
address = ephy_title_widget_get_address (EPHY_TITLE_WIDGET (widget));
LOG ("user_changed_cb, address %s", address);
- completion = gtk_entry_get_completion (GTK_ENTRY (widget));
- model = gtk_entry_completion_get_model (completion);
+ model = egg_suggestion_entry_get_model (EGG_SUGGESTION_ENTRY (widget));
- ephy_completion_model_update_for_string (EPHY_COMPLETION_MODEL (model), address,
- update_done_cb, completion);
+ ephy_suggestion_model_query_async (EPHY_SUGGESTION_MODEL (model), address, NULL, NULL, NULL);
}
static void
@@ -290,84 +264,12 @@ switch_page_cb (GtkNotebook *notebook,
}
static void
-action_activated_cb (GtkEntryCompletion *completion,
- int index,
- EphyLocationController *controller)
-{
- GtkWidget *entry;
- char *content;
- char *url;
- char **engine_names;
-
- entry = gtk_entry_completion_get_entry (completion);
- content = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
- if (content == NULL)
- return;
-
- engine_names = ephy_search_engine_manager_get_names (controller->search_engine_manager);
- url = ephy_search_engine_manager_build_search_address (controller->search_engine_manager,
- engine_names[index],
- content);
- g_strfreev (engine_names);
-
- ephy_link_open (EPHY_LINK (controller), url, NULL,
- ephy_link_flags_from_current_event ());
- g_free (content);
- g_free (url);
-}
-
-static void
-fill_entry_completion_with_actions (GtkEntryCompletion *completion,
- EphyLocationController *controller)
-{
- char **engine_names;
-
- engine_names = ephy_search_engine_manager_get_names (controller->search_engine_manager);
-
- controller->num_search_engines_actions = 0;
-
- for (guint i = 0; engine_names[i] != NULL; i++) {
- gtk_entry_completion_insert_action_text (completion, i, engine_names[i]);
- controller->num_search_engines_actions++;
- }
-
- g_strfreev (engine_names);
-}
-
-static void
-add_completion_actions (EphyLocationController *controller,
- EphyLocationEntry *lentry)
-{
- GtkEntryCompletion *completion = gtk_entry_get_completion (GTK_ENTRY (lentry));
-
- fill_entry_completion_with_actions (completion, controller);
- g_signal_connect (completion, "action_activated",
- G_CALLBACK (action_activated_cb), controller);
-}
-
-static void
-search_engines_changed_cb (EphySearchEngineManager *manager,
- gpointer data)
-{
- EphyLocationController *controller;
- GtkEntryCompletion *completion;
-
- controller = EPHY_LOCATION_CONTROLLER (data);
- completion = gtk_entry_get_completion (GTK_ENTRY (controller->title_widget));
-
- for (guint i = 0; i < controller->num_search_engines_actions; i++)
- gtk_entry_completion_delete_action (completion, 0);
-
- fill_entry_completion_with_actions (completion, controller);
-}
-
-static void
ephy_location_controller_constructed (GObject *object)
{
EphyLocationController *controller = EPHY_LOCATION_CONTROLLER (object);
EphyHistoryService *history_service;
EphyBookmarksManager *bookmarks_manager;
- EphyCompletionModel *model;
+ EphySuggestionModel *model;
GtkWidget *notebook, *widget;
G_OBJECT_CLASS (ephy_location_controller_parent_class)->constructed (object);
@@ -387,28 +289,11 @@ ephy_location_controller_constructed (GObject *object)
history_service = ephy_embed_shell_get_global_history_service (ephy_embed_shell_get_default ());
bookmarks_manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
- model = ephy_completion_model_new (history_service, bookmarks_manager);
- ephy_location_entry_set_completion (EPHY_LOCATION_ENTRY (controller->title_widget),
- GTK_TREE_MODEL (model),
- EPHY_COMPLETION_TEXT_COL,
- EPHY_COMPLETION_ACTION_COL,
- EPHY_COMPLETION_KEYWORDS_COL,
- EPHY_COMPLETION_RELEVANCE_COL,
- EPHY_COMPLETION_URL_COL,
- EPHY_COMPLETION_EXTRA_COL,
- EPHY_COMPLETION_FAVICON_COL);
+ model = ephy_suggestion_model_new (history_service, bookmarks_manager);
+ egg_suggestion_entry_set_model (EGG_SUGGESTION_ENTRY (controller->title_widget),
+ G_LIST_MODEL (model));
g_object_unref (model);
- ephy_location_entry_set_match_func (EPHY_LOCATION_ENTRY (controller->title_widget),
- match_func,
- controller->title_widget,
- NULL);
-
- add_completion_actions (controller, EPHY_LOCATION_ENTRY (controller->title_widget));
-
- g_signal_connect (controller->search_engine_manager, "changed",
- G_CALLBACK (search_engines_changed_cb), controller);
-
g_object_bind_property (controller, "editable",
controller->title_widget, "editable",
G_BINDING_SYNC_CREATE);
diff --git a/src/ephy-suggestion-model.c b/src/ephy-suggestion-model.c
new file mode 100644
index 0000000..63cfd06
--- /dev/null
+++ b/src/ephy-suggestion-model.c
@@ -0,0 +1,417 @@
+/* ephy-suggestion-model.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 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/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "egg-suggestion.h"
+#include "ephy-suggestion-model.h"
+
+#define MAX_COMPLETION_HISTORY_URLS 8
+
+struct _EphySuggestionModel
+{
+ GObject parent;
+ EphyHistoryService *history_service;
+ EphyBookmarksManager *bookmarks_manager;
+ GSequence *items;
+ GSList *search_terms;
+};
+
+enum {
+ PROP_0,
+ PROP_BOOKMARKS_MANAGER,
+ PROP_HISTORY_SERVICE,
+ N_PROPS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphySuggestionModel, ephy_suggestion_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ephy_suggestion_model_finalize (GObject *object)
+{
+ EphySuggestionModel *self = (EphySuggestionModel *)object;
+
+ g_clear_object (&self->bookmarks_manager);
+ g_clear_object (&self->history_service);
+ g_clear_pointer (&self->items, g_sequence_free);
+
+ g_slist_free_full (self->search_terms, (GDestroyNotify)g_regex_unref);
+
+ G_OBJECT_CLASS (ephy_suggestion_model_parent_class)->finalize (object);
+}
+
+static void
+ephy_suggestion_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_HISTORY_SERVICE:
+ g_value_set_object (value, self->history_service);
+ break;
+
+ case PROP_BOOKMARKS_MANAGER:
+ g_value_set_object (value, self->bookmarks_manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_suggestion_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_HISTORY_SERVICE:
+ self->history_service = g_value_dup_object (value);
+ break;
+
+ case PROP_BOOKMARKS_MANAGER:
+ self->bookmarks_manager = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_suggestion_model_class_init (EphySuggestionModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ephy_suggestion_model_finalize;
+ object_class->get_property = ephy_suggestion_model_get_property;
+ object_class->set_property = ephy_suggestion_model_set_property;
+
+ properties [PROP_BOOKMARKS_MANAGER] =
+ g_param_spec_object ("bookmarks-manager",
+ "Bookmarks Manager",
+ "The bookmarks manager for suggestions",
+ EPHY_TYPE_BOOKMARKS_MANAGER,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_HISTORY_SERVICE] =
+ g_param_spec_object ("history-service",
+ "History Service",
+ "The history service for suggestions",
+ EPHY_TYPE_HISTORY_SERVICE,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ephy_suggestion_model_init (EphySuggestionModel *self)
+{
+ self->items = g_sequence_new (g_object_unref);
+}
+
+static GType
+ephy_suggestion_model_get_item_type (GListModel *model)
+{
+ return EGG_TYPE_SUGGESTION;
+}
+
+static guint
+ephy_suggestion_model_get_n_items (GListModel *model)
+{
+ EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (model);
+
+ return g_sequence_get_length (self->items);
+}
+
+static gpointer
+ephy_suggestion_model_get_item (GListModel *model,
+ guint position)
+{
+ EphySuggestionModel *self = EPHY_SUGGESTION_MODEL (model);
+ GSequenceIter *iter;
+ EggSuggestion *suggestion;
+
+ iter = g_sequence_get_iter_at_pos (self->items, position);
+ suggestion = g_sequence_get (iter);
+
+ return g_object_ref (suggestion);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = ephy_suggestion_model_get_item_type;
+ iface->get_item = ephy_suggestion_model_get_item;
+ iface->get_n_items = ephy_suggestion_model_get_n_items;
+}
+
+EphySuggestionModel *
+ephy_suggestion_model_new (EphyHistoryService *history_service,
+ EphyBookmarksManager *bookmarks_manager)
+{
+ g_return_val_if_fail (EPHY_IS_HISTORY_SERVICE (history_service), NULL);
+ g_return_val_if_fail (EPHY_IS_BOOKMARKS_MANAGER (bookmarks_manager), NULL);
+
+ return g_object_new (EPHY_TYPE_SUGGESTION_MODEL,
+ "history-service", history_service,
+ "bookmarks-manager", bookmarks_manager,
+ NULL);
+}
+
+static void
+update_search_terms (EphySuggestionModel *self,
+ const char *text)
+{
+ const char *current;
+ const char *ptr;
+ char *tmp;
+ char *term;
+ GRegex *term_regex;
+ GRegex *quote_regex;
+ gint count;
+ gboolean inside_quotes = FALSE;
+
+ g_assert (EPHY_IS_SUGGESTION_MODEL (self));
+
+ if (self->search_terms) {
+ g_slist_free_full (self->search_terms, (GDestroyNotify)g_regex_unref);
+ self->search_terms = NULL;
+ }
+
+ quote_regex = g_regex_new ("\"", G_REGEX_OPTIMIZE,
+ G_REGEX_MATCH_NOTEMPTY, NULL);
+
+ /*
+ * This code loops through the string using pointer arythmetics.
+ * Although the string we are handling may contain UTF-8 chars
+ * this works because only ASCII chars affect what is actually
+ * copied from the string as a search term.
+ */
+ for (count = 0, current = ptr = text; ptr[0] != '\0'; ptr++, count++) {
+ /*
+ * If we found a double quote character; we will
+ * consume bytes up until the next quote, or
+ * end of line;
+ */
+ if (ptr[0] == '"')
+ inside_quotes = !inside_quotes;
+
+ /*
+ * If we found a space, and we are not looking for a
+ * closing double quote, or if the next char is the
+ * end of the string, append what we have already as
+ * a search term.
+ */
+ if (((ptr[0] == ' ') && (!inside_quotes)) || ptr[1] == '\0') {
+ /*
+ * We special-case the end of the line because
+ * we would otherwise not copy the last character
+ * of the search string, since the for loop will
+ * stop before that.
+ */
+ if (ptr[1] == '\0')
+ count++;
+
+ /*
+ * remove quotes, and quote any regex-sensitive
+ * characters
+ */
+ tmp = g_regex_escape_string (current, count);
+ term = g_regex_replace (quote_regex, tmp, -1, 0,
+ "", G_REGEX_MATCH_NOTEMPTY, NULL);
+ g_strstrip (term);
+ g_free (tmp);
+
+ /* we don't want empty search terms */
+ if (term[0] != '\0') {
+ term_regex = g_regex_new (term,
+ G_REGEX_CASELESS | G_REGEX_OPTIMIZE,
+ G_REGEX_MATCH_NOTEMPTY, NULL);
+ self->search_terms = g_slist_append (self->search_terms, term_regex);
+ }
+ g_free (term);
+
+ /* count will be incremented by the for loop */
+ count = -1;
+ current = ptr + 1;
+ }
+ }
+
+ g_regex_unref (quote_regex);
+}
+
+static gboolean
+should_add_bookmark_to_model (EphySuggestionModel *self,
+ const char *search_string,
+ const char *title,
+ const char *location)
+{
+ gboolean ret = TRUE;
+
+ if (self->search_terms) {
+ GSList *iter;
+ GRegex *current = NULL;
+
+ for (iter = self->search_terms; iter != NULL; iter = iter->next) {
+ current = (GRegex *)iter->data;
+ if ((!g_regex_match (current, title ? title : "", G_REGEX_MATCH_NOTEMPTY, NULL)) &&
+ (!g_regex_match (current, location ? location : "", G_REGEX_MATCH_NOTEMPTY, NULL))) {
+ ret = FALSE;
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+static void
+query_completed_cb (EphyHistoryService *service,
+ gboolean success,
+ gpointer result_data,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = user_data;
+ EphySuggestionModel *self;
+ const gchar *query;
+ GSequence *bookmarks;
+ GList *urls = NULL;
+ guint removed;
+ guint added = 0;
+
+ self = g_task_get_source_object (task);
+ query = g_task_get_task_data (task);
+
+ removed = g_sequence_get_length (self->items);
+
+ g_clear_pointer (&self->items, g_sequence_free);
+ self->items = g_sequence_new (g_object_unref);
+
+ /* Add bookmarks */
+ bookmarks = ephy_bookmarks_manager_get_bookmarks (self->bookmarks_manager);
+ for (GSequenceIter *iter = g_sequence_get_begin_iter (bookmarks);
+ !g_sequence_iter_is_end (iter);
+ iter = g_sequence_iter_next (iter))
+ {
+ EphyBookmark *bookmark;
+ const char *url, *title;
+
+ bookmark = g_sequence_get (iter);
+
+ url = ephy_bookmark_get_url (bookmark);
+ title = ephy_bookmark_get_title (bookmark);
+
+ if (should_add_bookmark_to_model (self, query, title, url))
+ {
+ g_autoptr(EggSuggestion) suggestion = NULL;
+
+ suggestion = g_object_new (EGG_TYPE_SUGGESTION,
+ "title", url,
+ "subtitle", title,
+ "id", url,
+ NULL);
+ g_sequence_append (self->items, g_steal_pointer (&suggestion));
+ added++;
+ }
+ }
+
+ /* History */
+ urls = (GList *)result_data;
+
+ for (const GList *p = g_list_last (urls); p != NULL; p = p->prev)
+ {
+ EphyHistoryURL *url = (EphyHistoryURL *)p->data;
+ g_autoptr(EggSuggestion) suggestion = NULL;
+ g_autofree gchar *title = g_markup_escape_text (url->url, -1);
+ g_autofree gchar *subtitle = g_markup_escape_text (url->title, -1);
+
+ suggestion = g_object_new (EGG_TYPE_SUGGESTION,
+ "icon-name", "web-browser-symbolic",
+ "id", url->url,
+ "title", title,
+ "subtitle", subtitle,
+ NULL);
+ g_sequence_prepend (self->items, g_steal_pointer (&suggestion));
+ added++;
+ }
+
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added);
+
+ g_task_return_boolean (task, TRUE);
+}
+
+void
+ephy_suggestion_model_query_async (EphySuggestionModel *self,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autofree gchar **strings = NULL;
+ GList *qlist = NULL;
+
+ g_return_if_fail (EPHY_IS_SUGGESTION_MODEL (self));
+ g_return_if_fail (query != NULL);
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, ephy_suggestion_model_query_async);
+ g_task_set_task_data (task, g_strdup (query), g_free);
+
+ /* Split the search string. */
+ strings = g_strsplit (query, " ", -1);
+ for (guint i = 0; strings[i]; i++)
+ qlist = g_list_append (qlist, g_steal_pointer (&strings[i]));
+
+ update_search_terms (self, query);
+
+ ephy_history_service_find_urls (self->history_service,
+ 0, 0,
+ MAX_COMPLETION_HISTORY_URLS, 0,
+ qlist,
+ EPHY_HISTORY_SORT_MOST_VISITED,
+ cancellable,
+ (EphyHistoryJobCallback)query_completed_cb,
+ g_steal_pointer (&task));
+}
+
+gboolean
+ephy_suggestion_model_query_finish (EphySuggestionModel *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (EPHY_IS_SUGGESTION_MODEL (self), FALSE);
+ g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/src/ephy-suggestion-model.h b/src/ephy-suggestion-model.h
new file mode 100644
index 0000000..614c90a
--- /dev/null
+++ b/src/ephy-suggestion-model.h
@@ -0,0 +1,46 @@
+/* ephy-suggestion-model.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 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 EPHY_SUGGESTION_MODEL_H
+#define EPHY_SUGGESTION_MODEL_H
+
+#include <gio/gio.h>
+
+#include "ephy-history-service.h"
+#include "ephy-bookmarks-manager.h"
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_SUGGESTION_MODEL (ephy_suggestion_model_get_type())
+
+G_DECLARE_FINAL_TYPE (EphySuggestionModel, ephy_suggestion_model, EPHY, SUGGESTION_MODEL, GObject)
+
+EphySuggestionModel *ephy_suggestion_model_new (EphyHistoryService *history_service,
+ EphyBookmarksManager *bookmarks_manager);
+void ephy_suggestion_model_query_async (EphySuggestionModel *self,
+ const gchar *query,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean ephy_suggestion_model_query_finish (EphySuggestionModel *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* EPHY_SUGGESTION_MODEL_H */
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]