[gnome-builder/wip/path-bar: 8/11] egg: start on hierarchical listbox widget



commit 12e2c5fe254859a062347f9043300adb7e2d70df
Author: Christian Hergert <christian hergert me>
Date:   Fri Aug 14 14:34:59 2015 -0600

    egg: start on hierarchical listbox widget
    
    The test code for this is just that. Test code. It leaks, it's crappy, but
    it does enough for me to figure how I'd like animations and other stuff
    to work.

 contrib/egg/Makefile.am           |    6 +
 contrib/egg/egg-directory-model.c |  475 +++++++++++++++++++++++++++++++
 contrib/egg/egg-directory-model.h |   46 +++
 contrib/egg/egg-rect.c            |  171 ++++++++++++
 contrib/egg/egg-rect.h            |   41 +++
 contrib/egg/egg-stack-list.c      |  553 +++++++++++++++++++++++++++++++++++++
 contrib/egg/egg-stack-list.h      |   56 ++++
 tests/Makefile.am                 |    6 +
 tests/test-stack-list.c           |  259 +++++++++++++++++
 9 files changed, 1613 insertions(+), 0 deletions(-)
---
diff --git a/contrib/egg/Makefile.am b/contrib/egg/Makefile.am
index 2370e96..d4da3c8 100644
--- a/contrib/egg/Makefile.am
+++ b/contrib/egg/Makefile.am
@@ -7,16 +7,22 @@ libegg_la_SOURCES = \
        egg-binding-group.h \
        egg-counter.c \
        egg-counter.h \
+       egg-directory-model.c \
+       egg-directory-model.h \
        egg-frame-source.c \
        egg-frame-source.h \
        egg-heap.c \
        egg-heap.h \
+       egg-rect.c \
+       egg-rect.h \
        egg-search-bar.c \
        egg-search-bar.h \
        egg-settings-sandwich.c \
        egg-settings-sandwich.h \
        egg-signal-group.c \
        egg-signal-group.h \
+       egg-stack-list.c \
+       egg-stack-list.h \
        egg-state-machine.c \
        egg-state-machine.h \
        egg-state-machine-action.c \
diff --git a/contrib/egg/egg-directory-model.c b/contrib/egg/egg-directory-model.c
new file mode 100644
index 0000000..5f6486c
--- /dev/null
+++ b/contrib/egg/egg-directory-model.c
@@ -0,0 +1,475 @@
+/* egg-directory-model.c
+ *
+ * Copyright (C) 2015 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 3 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-directory-model"
+
+#include <glib/gi18n.h>
+
+#include "egg-directory-model.h"
+
+#define NEXT_FILES_CHUNK_SIZE 25
+
+struct _EggDirectoryModel
+{
+  GObject                       parent_instance;
+
+  GCancellable                 *cancellable;
+  GFile                        *directory;
+  GSequence                    *items;
+  GFileMonitor                 *monitor;
+
+  EggDirectoryModelVisibleFunc  visible_func;
+  gpointer                      visible_func_data;
+  GDestroyNotify                visible_func_destroy;
+};
+
+static void list_model_iface_init      (GListModelInterface *iface);
+static void egg_directory_model_reload (EggDirectoryModel   *self);
+
+G_DEFINE_TYPE_EXTENDED (EggDirectoryModel, egg_directory_model, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_DIRECTORY,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static gint
+compare_display_name (gconstpointer a,
+                      gconstpointer b,
+                      gpointer      data)
+{
+  GFileInfo *file_info_a = (GFileInfo *)a;
+  GFileInfo *file_info_b = (GFileInfo *)b;
+  const gchar *display_name_a = g_file_info_get_display_name (file_info_a);
+  const gchar *display_name_b = g_file_info_get_display_name (file_info_b);
+  g_autofree gchar *name_a = g_utf8_collate_key_for_filename (display_name_a, -1);
+  g_autofree gchar *name_b = g_utf8_collate_key_for_filename (display_name_b, -1);
+
+  return g_utf8_collate (name_a, name_b);
+}
+
+static gint
+compare_directories_first (gconstpointer a,
+                           gconstpointer b,
+                           gpointer      data)
+{
+  GFileInfo *file_info_a = (GFileInfo *)a;
+  GFileInfo *file_info_b = (GFileInfo *)b;
+  GFileType file_type_a = g_file_info_get_file_type (file_info_a);
+  GFileType file_type_b = g_file_info_get_file_type (file_info_b);
+
+  if (file_type_a == file_type_b)
+    return compare_display_name (a, b, data);
+
+  return (file_type_a == G_FILE_TYPE_DIRECTORY) ? -1 : 1;
+}
+
+static void
+egg_directory_model_remove_all (EggDirectoryModel *self)
+{
+  GSequence *seq;
+  guint length;
+
+  g_assert (EGG_IS_DIRECTORY_MODEL (self));
+
+  length = g_sequence_get_length (self->items);
+
+  if (length > 0)
+    {
+      seq = self->items;
+      self->items = g_sequence_new (g_object_unref);
+      g_list_model_items_changed (G_LIST_MODEL (self), 0, length, 0);
+      g_sequence_free (seq);
+    }
+}
+
+static void
+egg_directory_model_take_item (EggDirectoryModel *self,
+                               GFileInfo         *file_info)
+{
+  GSequenceIter *iter;
+  guint position;
+
+  g_assert (EGG_IS_DIRECTORY_MODEL (self));
+  g_assert (G_IS_FILE_INFO (file_info));
+
+  if ((self->visible_func != NULL) &&
+      !self->visible_func (self, self->directory, file_info, self->visible_func_data))
+    {
+      g_object_unref (file_info);
+      return;
+    }
+
+  iter = g_sequence_insert_sorted (self->items,
+                                   file_info,
+                                   compare_directories_first,
+                                   NULL);
+  position = g_sequence_iter_get_position (iter);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+}
+
+static void
+egg_directory_model_next_files_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GFileEnumerator *enumerator = (GFileEnumerator *)object;
+  g_autoptr(GTask) task = user_data;
+  EggDirectoryModel *self;
+  GList *files;
+  GList *iter;
+
+  g_assert (G_IS_FILE_ENUMERATOR (enumerator));
+  g_assert (G_IS_TASK (task));
+
+  if (!(files = g_file_enumerator_next_files_finish (enumerator, result, NULL)))
+    return;
+
+  self = g_task_get_source_object (task);
+
+  g_assert (EGG_IS_DIRECTORY_MODEL (self));
+
+  for (iter = files; iter; iter = iter->next)
+    {
+      GFileInfo *file_info = iter->data;
+
+      egg_directory_model_take_item (self, file_info);
+    }
+
+  g_list_free (files);
+
+  g_file_enumerator_next_files_async (enumerator,
+                                      NEXT_FILES_CHUNK_SIZE,
+                                      G_PRIORITY_DEFAULT,
+                                      g_task_get_cancellable (task),
+                                      egg_directory_model_next_files_cb,
+                                      g_object_ref (task));
+}
+
+static void
+egg_directory_model_enumerate_children_cb (GObject      *object,
+                                           GAsyncResult *result,
+                                           gpointer      user_data)
+{
+  GFile *directory = (GFile *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+
+  g_assert (G_IS_FILE (directory));
+  g_assert (G_IS_TASK (task));
+
+  if (!(enumerator = g_file_enumerate_children_finish (directory, result, NULL)))
+    return;
+
+  g_file_enumerator_next_files_async (enumerator,
+                                      NEXT_FILES_CHUNK_SIZE,
+                                      G_PRIORITY_DEFAULT,
+                                      g_task_get_cancellable (task),
+                                      egg_directory_model_next_files_cb,
+                                      g_object_ref (task));
+}
+
+static void
+egg_directory_model_remove_file (EggDirectoryModel *self,
+                                 GFile             *file)
+{
+  g_autofree gchar *name = NULL;
+  GSequenceIter *iter;
+
+  g_assert (G_IS_FILE (file));
+
+  name = g_file_get_basename (file);
+
+  /*
+   * We have to lookup linearly since the items will likely be
+   * sorted by name, directory, file-system ordering, or some
+   * combination thereof.
+   */
+
+  for (iter = g_sequence_get_begin_iter (self->items);
+       !g_sequence_iter_is_end (iter);
+       iter = g_sequence_iter_next (iter))
+    {
+      GFileInfo *file_info = g_sequence_get (iter);
+      const gchar *file_info_name = g_file_info_get_name (file_info);
+
+      if (0 == g_strcmp0 (file_info_name, name))
+        {
+          guint position;
+
+          position = g_sequence_iter_get_position (iter);
+          g_sequence_remove (iter);
+          g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+          break;
+        }
+    }
+}
+
+static void
+egg_directory_model_directory_changed (EggDirectoryModel *self,
+                                       GFile             *file,
+                                       GFile             *other_file,
+                                       GFileMonitorEvent  event_type,
+                                       GFileMonitor      *monitor)
+{
+  g_assert (EGG_IS_DIRECTORY_MODEL (self));
+
+  switch ((int)event_type)
+    {
+    case G_FILE_MONITOR_EVENT_CREATED:
+      /*
+       * TODO: incremental changes
+       *
+       * When adding, we need to first add the GFileInfo for the file with all
+       * of the attributes we load in the primary case.
+       */
+      egg_directory_model_reload (self);
+      break;
+
+    case G_FILE_MONITOR_EVENT_DELETED:
+      egg_directory_model_remove_file (self, file);
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+egg_directory_model_reload (EggDirectoryModel *self)
+{
+  g_assert (EGG_IS_DIRECTORY_MODEL (self));
+
+  if (self->monitor != NULL)
+    {
+      g_file_monitor_cancel (self->monitor);
+      g_signal_handlers_disconnect_by_func (self->monitor,
+                                            G_CALLBACK (egg_directory_model_directory_changed),
+                                            self);
+      g_clear_object (&self->monitor);
+    }
+
+  if (self->cancellable != NULL)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  egg_directory_model_remove_all (self);
+
+  if (self->directory != NULL)
+    {
+      g_autoptr(GTask) task = NULL;
+
+      self->cancellable = g_cancellable_new ();
+      task = g_task_new (self, self->cancellable, NULL, NULL);
+
+      g_file_enumerate_children_async (self->directory,
+                                       G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                                       G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                       G_FILE_ATTRIBUTE_STANDARD_TYPE","
+                                       G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
+                                       G_FILE_QUERY_INFO_NONE,
+                                       G_PRIORITY_DEFAULT,
+                                       self->cancellable,
+                                       egg_directory_model_enumerate_children_cb,
+                                       g_object_ref (task));
+
+      self->monitor = g_file_monitor_directory (self->directory,
+                                                G_FILE_MONITOR_NONE,
+                                                self->cancellable,
+                                                NULL);
+
+      g_signal_connect_object (self->monitor,
+                               "changed",
+                               G_CALLBACK (egg_directory_model_directory_changed),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
+}
+
+static void
+egg_directory_model_finalize (GObject *object)
+{
+  EggDirectoryModel *self = (EggDirectoryModel *)object;
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->directory);
+  g_clear_pointer (&self->items, g_sequence_free);
+
+  if (self->visible_func_destroy)
+    self->visible_func_destroy (self->visible_func_data);
+
+  G_OBJECT_CLASS (egg_directory_model_parent_class)->finalize (object);
+}
+
+static void
+egg_directory_model_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  EggDirectoryModel *self = EGG_DIRECTORY_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, egg_directory_model_get_directory (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_directory_model_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  EggDirectoryModel *self = EGG_DIRECTORY_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      egg_directory_model_set_directory (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_directory_model_class_init (EggDirectoryModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = egg_directory_model_finalize;
+  object_class->get_property = egg_directory_model_get_property;
+  object_class->set_property = egg_directory_model_set_property;
+
+  gParamSpecs [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         _("Directory"),
+                         _("The directory to list files from."),
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+egg_directory_model_init (EggDirectoryModel *self)
+{
+  self->items = g_sequence_new (g_object_unref);
+}
+
+GListModel *
+egg_directory_model_new (GFile *directory)
+{
+  return g_object_new (EGG_TYPE_DIRECTORY_MODEL,
+                       "directory", directory,
+                       NULL);
+}
+
+GFile *
+egg_directory_model_get_directory (EggDirectoryModel *self)
+{
+  g_return_val_if_fail (EGG_IS_DIRECTORY_MODEL (self), NULL);
+
+  return self->directory;
+}
+
+void
+egg_directory_model_set_directory (EggDirectoryModel *self,
+                                   GFile             *directory)
+{
+  g_return_if_fail (EGG_IS_DIRECTORY_MODEL (self));
+  g_return_if_fail (!directory || G_IS_FILE (directory));
+
+  if (g_set_object (&self->directory, directory))
+    {
+      egg_directory_model_reload (self);
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_DIRECTORY]);
+    }
+}
+
+static guint
+egg_directory_model_get_n_items (GListModel *model)
+{
+  EggDirectoryModel *self = (EggDirectoryModel *)model;
+
+  g_return_val_if_fail (EGG_IS_DIRECTORY_MODEL (self), 0);
+
+  return g_sequence_get_length (self->items);
+}
+
+static GType
+egg_directory_model_get_item_type (GListModel *model)
+{
+  return G_TYPE_FILE_INFO;
+}
+
+static gpointer
+egg_directory_model_get_item (GListModel *model,
+                              guint       position)
+{
+  EggDirectoryModel *self = (EggDirectoryModel *)model;
+  GSequenceIter *iter;
+  gpointer ret;
+
+  g_return_val_if_fail (EGG_IS_DIRECTORY_MODEL (self), NULL);
+
+  if ((iter = g_sequence_get_iter_at_pos (self->items, position)) &&
+      (ret = g_sequence_get (iter)))
+    return g_object_ref (ret);
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = egg_directory_model_get_n_items;
+  iface->get_item = egg_directory_model_get_item;
+  iface->get_item_type = egg_directory_model_get_item_type;
+}
+
+void
+egg_directory_model_set_visible_func (EggDirectoryModel            *self,
+                                      EggDirectoryModelVisibleFunc  visible_func,
+                                      gpointer                      user_data,
+                                      GDestroyNotify                user_data_free_func)
+{
+  g_return_if_fail (EGG_IS_DIRECTORY_MODEL (self));
+
+  if (self->visible_func_destroy != NULL)
+    self->visible_func_destroy (self->visible_func_data);
+
+  self->visible_func = visible_func;
+  self->visible_func_data = user_data;
+  self->visible_func_destroy = user_data_free_func;
+
+  egg_directory_model_reload (self);
+}
diff --git a/contrib/egg/egg-directory-model.h b/contrib/egg/egg-directory-model.h
new file mode 100644
index 0000000..1377491
--- /dev/null
+++ b/contrib/egg/egg-directory-model.h
@@ -0,0 +1,46 @@
+/* egg-directory-model.h
+ *
+ * Copyright (C) 2015 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 3 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_DIRECTORY_MODEL_H
+#define EGG_DIRECTORY_MODEL_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_DIRECTORY_MODEL (egg_directory_model_get_type())
+
+G_DECLARE_FINAL_TYPE (EggDirectoryModel, egg_directory_model, EGG, DIRECTORY_MODEL, GObject)
+
+typedef gboolean (*EggDirectoryModelVisibleFunc) (EggDirectoryModel *self,
+                                                  GFile             *directory,
+                                                  GFileInfo         *file_info,
+                                                  gpointer           user_data);
+
+GListModel *egg_directory_model_new              (GFile                        *directory);
+GFile      *egg_directory_model_get_directory    (EggDirectoryModel            *self);
+void        egg_directory_model_set_directory    (EggDirectoryModel            *self,
+                                                  GFile                        *directory);
+void        egg_directory_model_set_visible_func (EggDirectoryModel            *self,
+                                                  EggDirectoryModelVisibleFunc  visible_func,
+                                                  gpointer                      user_data,
+                                                  GDestroyNotify                user_data_free_func);
+
+G_END_DECLS
+
+#endif /* EGG_DIRECTORY_MODEL_H */
diff --git a/contrib/egg/egg-rect.c b/contrib/egg/egg-rect.c
new file mode 100644
index 0000000..aea1d85
--- /dev/null
+++ b/contrib/egg/egg-rect.c
@@ -0,0 +1,171 @@
+/* egg-rect.c
+ *
+ * Copyright (C) 2015 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 "egg-rect.h"
+
+struct _EggRect
+{
+  GObject parent_instance;
+
+  gint x;
+  gint y;
+  gint width;
+  gint height;
+};
+
+enum {
+  PROP_0,
+  PROP_X,
+  PROP_Y,
+  PROP_WIDTH,
+  PROP_HEIGHT,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (EggRect, egg_rect, G_TYPE_OBJECT)
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void
+egg_rect_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  EggRect *self = EGG_RECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_X:
+      g_value_set_int (value, self->x);
+      break;
+
+    case PROP_Y:
+      g_value_set_int (value, self->y);
+      break;
+
+    case PROP_WIDTH:
+      g_value_set_int (value, self->width);
+      break;
+
+    case PROP_HEIGHT:
+      g_value_set_int (value, self->height);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_rect_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  EggRect *self = EGG_RECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_X:
+      self->x = g_value_get_int (value);
+      break;
+
+    case PROP_Y:
+      self->y = g_value_get_int (value);
+      break;
+
+    case PROP_WIDTH:
+      self->width = g_value_get_int (value);
+      break;
+
+    case PROP_HEIGHT:
+      self->height = g_value_get_int (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_rect_class_init (EggRectClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = egg_rect_get_property;
+  object_class->set_property = egg_rect_set_property;
+
+  gParamSpecs [PROP_X] =
+    g_param_spec_int ("x",
+                      "X",
+                      "X",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_Y] =
+    g_param_spec_int ("y",
+                      "Y",
+                      "Y",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_WIDTH] =
+    g_param_spec_int ("width",
+                      "Width",
+                      "Width",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_HEIGHT] =
+    g_param_spec_int ("height",
+                      "Height",
+                      "Height",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+egg_rect_init (EggRect *rect)
+{
+}
+
+void
+egg_rect_get_rect (EggRect      *self,
+                   GdkRectangle *rect)
+{
+  g_return_if_fail (EGG_IS_RECT (self));
+  g_return_if_fail (rect != NULL);
+
+  rect->x = self->x;
+  rect->y = self->y;
+  rect->width = self->width;
+  rect->height = self->height;
+}
diff --git a/contrib/egg/egg-rect.h b/contrib/egg/egg-rect.h
new file mode 100644
index 0000000..61c052f
--- /dev/null
+++ b/contrib/egg/egg-rect.h
@@ -0,0 +1,41 @@
+/* egg-rect.h
+ *
+ * Copyright (C) 2015 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_RECT_H
+#define EGG_RECT_H
+
+#include <gdk/gdk.h>
+
+/*
+ * This is just a helper object for animating rectangles.
+ * It allows us to use egg_object_animate() to animate
+ * coordinates.
+ */
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_RECT (egg_rect_get_type())
+
+G_DECLARE_FINAL_TYPE (EggRect, egg_rect, EGG, RECT, GObject)
+
+void egg_rect_get_rect (EggRect      *self,
+                        GdkRectangle *rect);
+
+G_END_DECLS
+
+#endif /* EGG_RECT_H */
diff --git a/contrib/egg/egg-stack-list.c b/contrib/egg/egg-stack-list.c
new file mode 100644
index 0000000..661efc9
--- /dev/null
+++ b/contrib/egg/egg-stack-list.c
@@ -0,0 +1,553 @@
+/* egg-stack-list.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "egg-stack-list"
+
+#include <glib/gi18n.h>
+
+#include "egg-animation.h"
+#include "egg-rect.h"
+#include "egg-stack-list.h"
+
+#define FADE_DURATION  250
+#define SLIDE_DURATION 350
+
+typedef struct
+{
+  GtkOverlay        *overlay;
+  GtkScrolledWindow *scroller;
+  GtkBox            *box;
+  GtkListBox        *headers;
+  GtkListBox        *content;
+  GtkListBox        *fake_list;
+  GtkStack          *flip_stack;
+
+  GPtrArray         *models;
+
+  GtkListBoxRow     *activated;
+
+  GtkListBoxRow     *animating;
+  EggAnimation      *animation;
+  EggRect           *animating_rect;
+} EggStackListPrivate;
+
+typedef struct
+{
+  GListModel                   *model;
+  GtkWidget                    *header;
+  EggStackListCreateWidgetFunc  create_widget_func;
+  gpointer                      user_data;
+  GDestroyNotify                user_data_free_func;
+} ModelInfo;
+
+G_DEFINE_TYPE_WITH_PRIVATE (EggStackList, egg_stack_list, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_MODEL,
+  LAST_PROP
+};
+
+enum {
+  HEADER_ACTIVATED,
+  ROW_ACTIVATED,
+  LAST_SIGNAL
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint gSignals [LAST_SIGNAL];
+
+static void
+model_info_free (gpointer data)
+{
+  ModelInfo *info = data;
+
+  g_object_unref (info->model);
+  if (info->user_data_free_func)
+    info->user_data_free_func (info->user_data);
+  g_slice_free (ModelInfo, info);
+}
+
+static GtkWidget *
+egg_stack_list_create_widget_func (gpointer item,
+                                   gpointer user_data)
+{
+  ModelInfo *info = user_data;
+
+  return info->create_widget_func (item, info->user_data);
+}
+
+static void
+egg_stack_list_content_row_activated (EggStackList  *self,
+                                      GtkListBoxRow *row,
+                                      GtkListBox    *box)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_STACK_LIST (self));
+  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
+  g_return_if_fail (GTK_IS_LIST_BOX (box));
+
+  priv->activated = row;
+
+  g_signal_emit (self, gSignals [ROW_ACTIVATED], 0, row);
+
+  priv->activated = NULL;
+}
+
+static void
+egg_stack_list_header_row_activated (EggStackList  *self,
+                                     GtkListBoxRow *row,
+                                     GtkListBox    *box)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  g_return_if_fail (EGG_IS_STACK_LIST (self));
+  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
+  g_return_if_fail (GTK_IS_LIST_BOX (box));
+
+  priv->activated = row;
+
+  g_signal_emit (self, gSignals [HEADER_ACTIVATED], 0, row);
+
+  priv->activated = NULL;
+}
+
+static gboolean
+egg_stack_list__overlay__get_child_position (EggStackList *self,
+                                             GtkWidget    *widget,
+                                             GdkRectangle *rect,
+                                             GtkOverlay   *overlay)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  g_assert (EGG_IS_STACK_LIST (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (rect != NULL);
+  g_assert (GTK_IS_OVERLAY (overlay));
+
+  egg_rect_get_rect (priv->animating_rect, rect);
+
+  return TRUE;
+}
+
+static void
+egg_stack_list_end_anim (EggStackList *self)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+  GtkListBoxRow *header;
+  ModelInfo *info;
+
+  g_assert (EGG_IS_STACK_LIST (self));
+  g_assert (priv->animating != NULL);
+  g_assert (priv->models->len > 0);
+
+  info = g_ptr_array_index (priv->models, priv->models->len - 1);
+  header = g_object_ref (priv->animating);
+
+  priv->animating = NULL;
+
+  if (priv->animation != NULL)
+    {
+      egg_animation_stop (priv->animation);
+      g_clear_object (&priv->animation);
+    }
+
+  g_assert (header != NULL);
+  g_assert (GTK_IS_LIST_BOX_ROW (header));
+  g_assert (gtk_widget_get_parent (GTK_WIDGET (header)) == GTK_WIDGET (priv->overlay));
+
+  gtk_container_remove (GTK_CONTAINER (priv->overlay),
+                        GTK_WIDGET (header));
+
+  gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header));
+
+  gtk_list_box_bind_model (priv->content,
+                           info->model,
+                           egg_stack_list_create_widget_func,
+                           info,
+                           NULL);
+
+  gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack), GTK_WIDGET (priv->scroller));
+
+  g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_MODEL]);
+
+  g_object_unref (header);
+}
+
+static void
+animation_finished (gpointer data)
+{
+  EggStackListPrivate *priv;
+  EggStackList *self;
+  GtkListBoxRow *row;
+  gpointer *closure = data;
+
+  g_assert (closure != NULL);
+  g_assert (EGG_IS_STACK_LIST (closure [0]));
+  g_assert (GTK_IS_LIST_BOX_ROW (closure [1]));
+
+  self = closure [0];
+  row = closure [1];
+
+  priv = egg_stack_list_get_instance_private (self);
+
+  if (row == priv->animating)
+    egg_stack_list_end_anim (self);
+
+  g_object_unref (closure[0]);
+  g_object_unref (closure[1]);
+  g_free (closure);
+}
+
+static void
+egg_stack_list_begin_anim (EggStackList       *self,
+                           GtkListBoxRow      *row,
+                           const GdkRectangle *begin_area,
+                           const GdkRectangle *end_area)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+  GdkFrameClock *frame_clock;
+  gpointer *closure;
+
+  g_assert (EGG_IS_STACK_LIST (self));
+  g_assert (row != NULL);
+  g_assert (begin_area != NULL);
+  g_assert (end_area != NULL);
+
+  priv->animating = row;
+
+  g_object_set (priv->animating_rect,
+                "x", begin_area->x,
+                "y", begin_area->y,
+                "width", begin_area->width,
+                "height", begin_area->height,
+                NULL);
+
+  frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+
+  closure = g_new0 (gpointer, 2);
+  closure [0] = g_object_ref (self);
+  closure [1] = g_object_ref_sink (row);
+
+  gtk_overlay_add_overlay (GTK_OVERLAY (priv->overlay), GTK_WIDGET (row));
+
+  priv->animation = egg_object_animate_full (priv->animating_rect,
+                                             EGG_ANIMATION_EASE_OUT_CUBIC,
+                                             SLIDE_DURATION,
+                                             frame_clock,
+                                             animation_finished,
+                                             closure,
+                                             "x", end_area->x,
+                                             "y", end_area->y,
+                                             "width", end_area->width,
+                                             "height", end_area->height,
+                                             NULL);
+
+  g_object_ref (priv->animation);
+
+  g_signal_connect_object (priv->animating_rect,
+                           "notify",
+                           G_CALLBACK (gtk_widget_queue_resize),
+                           priv->animating,
+                           G_CONNECT_SWAPPED);
+
+  gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack),
+                               GTK_WIDGET (priv->fake_list));
+}
+
+static void
+egg_stack_list_finalize (GObject *object)
+{
+  EggStackList *self = (EggStackList *)object;
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  g_clear_pointer (&priv->models, g_ptr_array_unref);
+  g_clear_object (&priv->animating_rect);
+  g_clear_object (&priv->animation);
+
+  G_OBJECT_CLASS (egg_stack_list_parent_class)->finalize (object);
+}
+
+static void
+egg_stack_list_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  EggStackList *self = EGG_STACK_LIST (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODEL:
+      g_value_set_object (value, egg_stack_list_get_model (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+egg_stack_list_class_init (EggStackListClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = egg_stack_list_finalize;
+  object_class->get_property = egg_stack_list_get_property;
+
+  gParamSpecs [PROP_MODEL] =
+    g_param_spec_object ("model",
+                         _("Model"),
+                         _("Model"),
+                         G_TYPE_LIST_MODEL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+
+  gSignals [HEADER_ACTIVATED] =
+    g_signal_new ("header-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggStackListClass, header_activated),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  GTK_TYPE_LIST_BOX_ROW);
+
+  gSignals [ROW_ACTIVATED] =
+    g_signal_new ("row-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (EggStackListClass, row_activated),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  GTK_TYPE_LIST_BOX_ROW);
+}
+
+static void
+egg_stack_list_init (EggStackList *self)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  priv->animating_rect = g_object_new (EGG_TYPE_RECT, NULL);
+
+  priv->models = g_ptr_array_new_with_free_func (model_info_free);
+
+  priv->overlay = g_object_new (GTK_TYPE_OVERLAY,
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect_object (priv->overlay,
+                           "get-child-position",
+                           G_CALLBACK (egg_stack_list__overlay__get_child_position),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->overlay));
+
+  priv->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "vexpand", TRUE,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (priv->box));
+
+  priv->headers = g_object_new (GTK_TYPE_LIST_BOX,
+                                "selection-mode", GTK_SELECTION_NONE,
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect_object (priv->headers,
+                           "row-activated",
+                           G_CALLBACK (egg_stack_list_header_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->headers)),
+                               "stack-header");
+  gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->headers));
+
+  priv->flip_stack = g_object_new (GTK_TYPE_STACK,
+                                   "transition-duration", FADE_DURATION,
+                                   "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE,
+                                   "visible", TRUE,
+                                   "vexpand", TRUE,
+                                   NULL);
+  gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->flip_stack));
+
+  priv->scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                                 "shadow-type", GTK_SHADOW_NONE,
+                                 "vexpand", TRUE,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->scroller));
+
+  priv->content = g_object_new (GTK_TYPE_LIST_BOX,
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect_object (priv->content,
+                           "row-activated",
+                           G_CALLBACK (egg_stack_list_content_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (priv->scroller), GTK_WIDGET (priv->content));
+
+  priv->fake_list = g_object_new (GTK_TYPE_LIST_BOX,
+                                  "visible", TRUE,
+                                  NULL);
+  gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->fake_list));
+}
+
+GtkWidget *
+egg_stack_list_new (void)
+{
+  return g_object_new (EGG_TYPE_STACK_LIST, NULL);
+}
+
+void
+egg_stack_list_push (EggStackList                 *self,
+                     GtkWidget                    *header,
+                     GListModel                   *model,
+                     EggStackListCreateWidgetFunc  create_widget_func,
+                     gpointer                      user_data,
+                     GDestroyNotify                user_data_free_func)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+  ModelInfo *info;
+  GdkRectangle current_area;
+  GdkRectangle target_area;
+  gint nat_height;
+
+  g_return_if_fail (EGG_IS_STACK_LIST (self));
+  g_return_if_fail (GTK_IS_WIDGET (header));
+  g_return_if_fail (G_IS_LIST_MODEL (model));
+  g_return_if_fail (create_widget_func != NULL);
+
+  if (priv->animating != NULL)
+    egg_stack_list_end_anim (self);
+
+  if (!GTK_IS_LIST_BOX_ROW (header))
+    header = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                           "child", header,
+                           "visible", TRUE,
+                           NULL);
+
+  info = g_slice_new0 (ModelInfo);
+  info->header = header;
+  info->model = g_object_ref (model);
+  info->create_widget_func = create_widget_func;
+  info->user_data = user_data;
+  info->user_data_free_func = user_data_free_func;
+
+  g_ptr_array_add (priv->models, info);
+
+  /*
+   * Nothing to animate, make everything happen immediately.
+   */
+  if (priv->activated == NULL)
+    {
+      gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header));
+      gtk_list_box_bind_model (priv->content,
+                               model,
+                               egg_stack_list_create_widget_func,
+                               info,
+                               NULL);
+      g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_MODEL]);
+      return;
+    }
+
+  /*
+   * Get the location to begin the animation.
+   */
+  gtk_widget_get_allocation (GTK_WIDGET (priv->activated), &current_area);
+  gtk_widget_translate_coordinates (GTK_WIDGET (priv->activated),
+                                    GTK_WIDGET (priv->overlay),
+                                    current_area.x, current_area.y,
+                                    &current_area.x, &current_area.y);
+
+  /*
+   * Get the location to end the animation.
+   */
+  gtk_widget_get_allocation (GTK_WIDGET (priv->headers), &target_area);
+  gtk_widget_get_preferred_height (GTK_WIDGET (header), NULL, &nat_height);
+  target_area.y += target_area.height;
+  target_area.height = nat_height;
+  gtk_widget_translate_coordinates (GTK_WIDGET (header),
+                                    GTK_WIDGET (priv->overlay),
+                                    target_area.x, target_area.y,
+                                    &target_area.x, &target_area.y);
+
+  egg_stack_list_begin_anim (self, GTK_LIST_BOX_ROW (header), &current_area, &target_area);
+}
+
+void
+egg_stack_list_pop (EggStackList *self)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+  ModelInfo *info;
+
+  g_return_if_fail (EGG_IS_STACK_LIST (self));
+
+  if (priv->models->len == 0)
+    return;
+
+  if (priv->animating != NULL)
+    egg_stack_list_end_anim (self);
+
+  info = g_ptr_array_index (priv->models, priv->models->len - 1);
+
+  gtk_container_remove (GTK_CONTAINER (priv->headers), GTK_WIDGET (info->header));
+  gtk_list_box_bind_model (priv->content, NULL, NULL, NULL, NULL);
+  g_ptr_array_remove_index (priv->models, priv->models->len - 1);
+
+  if (priv->models->len > 0)
+    {
+      info = g_ptr_array_index (priv->models, priv->models->len - 1);
+      gtk_list_box_bind_model (priv->content,
+                               info->model,
+                               egg_stack_list_create_widget_func,
+                               info,
+                               NULL);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_MODEL]);
+}
+
+GListModel *
+egg_stack_list_get_model (EggStackList *self)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+  ModelInfo *info;
+
+  g_return_val_if_fail (EGG_IS_STACK_LIST (self), NULL);
+
+  if (priv->models->len == 0)
+    return NULL;
+
+  info = g_ptr_array_index (priv->models, priv->models->len - 1);
+
+  return info->model;
+}
+
+guint
+egg_stack_list_get_depth (EggStackList *self)
+{
+  EggStackListPrivate *priv = egg_stack_list_get_instance_private (self);
+
+  g_return_val_if_fail (EGG_IS_STACK_LIST (self), 0);
+
+  return priv->models->len;
+}
diff --git a/contrib/egg/egg-stack-list.h b/contrib/egg/egg-stack-list.h
new file mode 100644
index 0000000..da08362
--- /dev/null
+++ b/contrib/egg/egg-stack-list.h
@@ -0,0 +1,56 @@
+/* egg-stack-list.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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 EGG_STACK_LIST_H
+#define EGG_STACK_LIST_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define EGG_TYPE_STACK_LIST (egg_stack_list_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (EggStackList, egg_stack_list, EGG, STACK_LIST, GtkBin)
+
+struct _EggStackListClass
+{
+  GtkBinClass parent_instance;
+
+  void (*row_activated)    (EggStackList  *self,
+                            GtkListBoxRow *row);
+  void (*header_activated) (EggStackList  *self,
+                            GtkListBoxRow *row);
+};
+
+typedef GtkWidget *(*EggStackListCreateWidgetFunc) (gpointer item,
+                                                    gpointer user_data);
+
+GtkWidget *egg_stack_list_new        (void);
+void       egg_stack_list_push       (EggStackList                 *self,
+                                      GtkWidget                    *header,
+                                      GListModel                   *model,
+                                      EggStackListCreateWidgetFunc  create_widget_func,
+                                      gpointer                      user_data,
+                                      GDestroyNotify                user_data_free_func);
+void        egg_stack_list_pop       (EggStackList                 *self);
+GListModel *egg_stack_list_get_model (EggStackList                 *self);
+guint       egg_stack_list_get_depth (EggStackList                 *self);
+
+G_END_DECLS
+
+#endif /* EGG_STACK_LIST_H */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index bb008f1..cdf6a48 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -133,6 +133,12 @@ test_vim_LDADD = \
        $(NULL)
 
 
+misc_programs += test-stack-list
+test_stack_list_SOURCES = test-stack-list.c
+test_stack_list_CFLAGS = $(tests_cflags)
+test_stack_list_LDADD = $(tests_libs)
+
+
 misc_programs += test-ide-source-view
 test_ide_source_view_SOURCES = test-ide-source-view.c
 test_ide_source_view_CFLAGS = $(tests_cflags)
diff --git a/tests/test-stack-list.c b/tests/test-stack-list.c
new file mode 100644
index 0000000..822a89b
--- /dev/null
+++ b/tests/test-stack-list.c
@@ -0,0 +1,259 @@
+/* test-stack-list.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+/* this code leaks, it's just a quick prototype */
+
+#include <gtk/gtk.h>
+
+#include "egg-directory-model.h"
+#include "egg-stack-list.h"
+
+#define TEST_CSS \
+  "EggStackList GtkListBox.view {" \
+  " background-color: #fafafa;" \
+  "}\n" \
+  "EggStackList GtkListBox GtkListBoxRow {" \
+  " background-color: #f2f2f2;" \
+  " color: #2e3436;" \
+  " padding-bottom: 3px;" \
+  "}\n" \
+  "EggStackList GtkListBox.stack-header GtkListBoxRow {" \
+  " background-color: #fafafa;" \
+  " color: #919191;" \
+  "}\n" \
+  "EggStackList GtkListBox.stack-header GtkListBoxRow:last-child {" \
+  " border-bottom: 1px solid #dbdbdb;" \
+  " color: #000000;" \
+  "}\n"
+
+static GtkWidget *
+create_row_func (gpointer item,
+                 gboolean is_header,
+                 gpointer user_data)
+{
+  GFileInfo *file_info = item;
+  GFile *parent = user_data;
+  GtkWidget *row;
+  GtkWidget *box;
+  GtkWidget *image;
+  GtkWidget *label;
+  g_autoptr(GIcon) local_gicon = NULL;
+  GObject *gicon;
+  const gchar *display_name;
+  g_autoptr(GFile) copy = NULL;
+
+  g_assert (!file_info || G_IS_FILE_INFO (file_info));
+  g_assert (G_IS_FILE (parent));
+
+  if (parent == NULL)
+    parent = copy = g_file_new_for_path (".");
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+
+  g_object_set_data_full (G_OBJECT (row),
+                          "G_FILE_INFO",
+                          file_info ? g_object_ref (file_info) : NULL,
+                          g_object_unref);
+
+  g_object_set_data_full (G_OBJECT (row),
+                          "G_FILE",
+                          g_file_get_child (parent, file_info ? g_file_info_get_name (file_info) : "."),
+                          g_object_unref);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "border-width", 3,
+                      "spacing", 6,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
+
+  if (is_header)
+    {
+      local_gicon = g_themed_icon_new ("folder-open-symbolic");
+      gicon = G_OBJECT (local_gicon);
+    }
+  else
+    gicon = g_file_info_get_attribute_object (file_info, G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON);
+
+  display_name = g_file_info_get_display_name (file_info);
+
+  image = g_object_new (GTK_TYPE_IMAGE,
+                        "gicon", gicon,
+                        "visible", TRUE,
+                        "margin-start", 6,
+                        "margin-end", 3,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", display_name,
+                        "hexpand", TRUE,
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+
+  return row;
+}
+
+static GtkWidget *
+create_regular_func (gpointer item,
+                     gpointer user_data)
+{
+  return create_row_func (item, FALSE, user_data);
+}
+
+static GtkWidget *
+create_header_func (gpointer item,
+                     gpointer user_data)
+{
+  return create_row_func (item, TRUE, user_data);
+}
+
+static gboolean
+hide_dot_files (EggDirectoryModel *model,
+                GFile             *directory,
+                GFileInfo         *file_info,
+                gpointer           user_data)
+{
+  const gchar *name = g_file_info_get_display_name (file_info);
+  return (name && *name != '.');
+}
+
+static void
+header_activated (EggStackList  *stack_list,
+                  GtkListBoxRow *row,
+                  gpointer       user_data)
+{
+  GListModel *model;
+  GFile *directory;
+
+  directory = g_object_get_data (G_OBJECT (row), "G_FILE");
+  model = egg_stack_list_get_model (stack_list);
+
+  while (model != NULL)
+    {
+      GFile *current = egg_directory_model_get_directory (EGG_DIRECTORY_MODEL (model));
+
+      if (g_file_equal (current, directory))
+        break;
+
+      if (egg_stack_list_get_depth (stack_list) == 1)
+        break;
+
+      egg_stack_list_pop (stack_list);
+
+      model = egg_stack_list_get_model (stack_list);
+    }
+}
+
+static void
+row_activated (EggStackList  *stack_list,
+               GtkListBoxRow *row,
+               gpointer       user_data)
+{
+  GFileInfo *file_info;
+  EggDirectoryModel *model;
+  g_autoptr(GFile) child = NULL;
+  GFile *directory;
+
+  model = EGG_DIRECTORY_MODEL (egg_stack_list_get_model (stack_list));
+  directory = egg_directory_model_get_directory (model);
+  file_info = g_object_get_data (G_OBJECT (row), "G_FILE_INFO");
+
+  if ((file_info == NULL) || (G_FILE_TYPE_DIRECTORY != g_file_info_get_file_type (file_info)))
+    return;
+
+  child = g_file_get_child (directory, g_file_info_get_name (file_info));
+  model = EGG_DIRECTORY_MODEL (egg_directory_model_new (child));
+
+  egg_stack_list_push (stack_list,
+                       create_header_func (file_info, directory),
+                       G_LIST_MODEL (model),
+                       create_regular_func,
+                       directory, NULL);
+
+  g_object_unref (model);
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  GtkWindow *window;
+  EggStackList *stack_list;
+  GListModel *model;
+  GFile *root;
+  GFile *directory;
+  GFileInfo *info;
+  GtkCssProvider *provider;
+
+  gtk_init (&argc, &argv);
+
+  provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_data (provider, TEST_CSS, -1, NULL);
+  gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                             GTK_STYLE_PROVIDER (provider),
+                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+  window = g_object_new (GTK_TYPE_WINDOW,
+                         "default-width", 300,
+                         "default-height", 700,
+                         NULL);
+
+  root = g_file_new_for_path (g_get_current_dir ());
+  directory = g_file_get_parent (root);
+
+  info = g_file_info_new ();
+  g_file_info_set_name (info, g_file_get_basename (directory));
+  g_file_info_set_display_name (info, g_file_get_basename (directory));
+  g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
+
+  model = egg_directory_model_new (directory);
+  egg_directory_model_set_visible_func (EGG_DIRECTORY_MODEL (model), hide_dot_files, NULL, NULL);
+
+  stack_list = g_object_new (EGG_TYPE_STACK_LIST,
+                             "visible", TRUE,
+                             NULL);
+  g_signal_connect (stack_list,
+                    "header-activated",
+                    G_CALLBACK (header_activated),
+                    NULL);
+  g_signal_connect (stack_list,
+                    "row-activated",
+                    G_CALLBACK (row_activated),
+                    NULL);
+  gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (stack_list));
+
+  egg_stack_list_push (stack_list,
+                       create_header_func (info, directory),
+                       model,
+                       create_regular_func,
+                       directory,
+                       NULL);
+
+  g_signal_connect (window, "delete-event", gtk_main_quit, NULL);
+  gtk_window_present (window);
+
+  gtk_main ();
+
+  return 0;
+}


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