[gnome-builder/document-manager] documents: start importing document abstraction



commit 934a4ac70588ae807321d4023afa4a9c628f14eb
Author: Christian Hergert <christian hergert me>
Date:   Sun Dec 7 02:39:24 2014 -0800

    documents: start importing document abstraction

 src/documents/gb-document-grid.c        |  614 ++++++++++++++++++++++++++++
 src/documents/gb-document-grid.h        |   73 ++++
 src/documents/gb-document-manager.c     |  306 ++++++++-------
 src/documents/gb-document-manager.h     |   33 +-
 src/documents/gb-document-menu-button.c |  650 ++++++++++++++++++++++++++++++
 src/documents/gb-document-menu-button.h |   67 +++
 src/documents/gb-document-split.c       |   37 ++
 src/documents/gb-document-split.h       |   38 ++
 src/documents/gb-document-stack.c       |  667 +++++++++++++++++++++++++++++++
 src/documents/gb-document-stack.h       |   77 ++++
 src/documents/gb-document-view.c        |  202 ++++++++++
 src/documents/gb-document-view.h        |   64 +++
 src/documents/gb-document.h             |   12 +-
 13 files changed, 2674 insertions(+), 166 deletions(-)
---
diff --git a/src/documents/gb-document-grid.c b/src/documents/gb-document-grid.c
new file mode 100644
index 0000000..c2c5bb2
--- /dev/null
+++ b/src/documents/gb-document-grid.c
@@ -0,0 +1,614 @@
+/* gb-document-grid.c
+ *
+ * Copyright (C) 2014 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 "document-manager"
+
+#include <glib/gi18n.h>
+
+#include "gb-document-grid.h"
+
+struct _GbDocumentGridPrivate
+{
+  GbDocumentManager *document_manager;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbDocumentGrid, gb_document_grid, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_DOCUMENT_MANAGER,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void gb_document_grid_reposition (GbDocumentGrid *grid);
+
+GtkWidget *
+gb_document_grid_new (void)
+{
+  return g_object_new (GB_TYPE_DOCUMENT_GRID, NULL);
+}
+
+static void
+gb_document_grid_create_view (GbDocumentGrid  *grid,
+                              GbDocument      *document,
+                              GbDocumentSplit  split,
+                              GbDocumentStack *stack)
+{
+  GtkWidget *target_stack = NULL;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  switch (split)
+    {
+    case GB_DOCUMENT_SPLIT_LEFT:
+      target_stack = gb_document_grid_get_stack_before (grid, stack);
+      if (!target_stack)
+        target_stack = gb_document_grid_add_stack_before (grid, stack);
+      break;
+
+    case GB_DOCUMENT_SPLIT_RIGHT:
+      target_stack = gb_document_grid_get_stack_after (grid, stack);
+      if (!target_stack)
+        target_stack = gb_document_grid_add_stack_after (grid, stack);
+      break;
+
+    case GB_DOCUMENT_SPLIT_NONE:
+      break;
+
+    default:
+      g_return_if_reached ();
+    }
+
+  gb_document_stack_focus_document (GB_DOCUMENT_STACK (target_stack), document);
+}
+
+static void
+gb_document_grid_remove_stack (GbDocumentGrid  *grid,
+                               GbDocumentStack *stack)
+{
+  GList *stacks;
+  GList *iter;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  stacks = gb_document_grid_get_stacks (grid);
+
+  /* refuse to remove the stack if there is only one */
+  if (g_list_length (stacks) == 1)
+    return;
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      GbDocumentStack *item = GB_DOCUMENT_STACK (iter->data);
+
+      if (item == stack)
+        {
+          if (!iter->prev)
+            {
+              GtkWidget *paned;
+              GtkWidget *child2;
+
+              /*
+               * This is the first stack in the grid. All we need to do to get
+               * to a consistent state is to take the child2 paned and replace
+               * our toplevel paned with it.
+               */
+              paned = gtk_bin_get_child (GTK_BIN (grid));
+              child2 = gtk_paned_get_child2 (GTK_PANED (paned));
+              g_object_ref (child2);
+              gtk_container_remove (GTK_CONTAINER (paned), child2);
+              gtk_container_remove (GTK_CONTAINER (grid), paned);
+              gtk_container_add (GTK_CONTAINER (grid), child2);
+              g_object_unref (child2);
+            }
+          else if (!iter->next)
+            {
+              GtkWidget *paned;
+              GtkWidget *grandparent;
+
+              /*
+               * This is the last stack in the grid. All we need to do to get
+               * to a consistent state is remove our parent paned from the
+               * grandparent.
+               */
+              paned = gtk_widget_get_parent (GTK_WIDGET (stack));
+              grandparent = gtk_widget_get_parent (paned);
+              gtk_container_remove (GTK_CONTAINER (grandparent), paned);
+            }
+          else if (iter->next && iter->prev)
+            {
+              GtkWidget *grandparent;
+              GtkWidget *paned;
+              GtkWidget *child2;
+
+              /*
+               * This stack is somewhere in the middle. All we need to do to
+               * get into a consistent state is take our parent paneds child2
+               * and put it in our parent's location.
+               */
+              paned = gtk_widget_get_parent (GTK_WIDGET (stack));
+              grandparent = gtk_widget_get_parent (paned);
+              child2 = gtk_paned_get_child2 (GTK_PANED (paned));
+              g_object_ref (child2);
+              gtk_container_remove (GTK_CONTAINER (paned), child2);
+              gtk_container_remove (GTK_CONTAINER (grandparent), paned);
+              gtk_container_add (GTK_CONTAINER (grandparent), child2);
+              g_object_unref (child2);
+            }
+          else
+            g_assert_not_reached ();
+
+          gb_document_grid_reposition (grid);
+
+          break;
+        }
+    }
+
+  g_list_free (stacks);
+}
+
+static void
+gb_document_grid_stack_empty (GbDocumentGrid  *grid,
+                              GbDocumentStack *stack)
+{
+  GList *stacks;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  stacks = gb_document_grid_get_stacks (grid);
+
+  g_assert (stacks != NULL);
+
+  if (g_list_length (stacks) == 1)
+    goto cleanup;
+
+  gb_document_grid_remove_stack (grid, stack);
+
+cleanup:
+  g_list_free (stacks);
+}
+
+static GtkPaned *
+gb_document_grid_create_paned (GbDocumentGrid *grid)
+{
+  return g_object_new (GTK_TYPE_PANED,
+                       "orientation", GTK_ORIENTATION_HORIZONTAL,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static GbDocumentStack *
+gb_document_grid_create_stack (GbDocumentGrid *grid)
+{
+  GbDocumentStack *stack;
+
+  stack = g_object_new (GB_TYPE_DOCUMENT_STACK,
+                        "document-manager", grid->priv->document_manager,
+                        "visible", TRUE,
+                        NULL);
+
+  g_signal_connect_object (stack,
+                           "create-view",
+                           G_CALLBACK (gb_document_grid_create_view),
+                           grid,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (stack,
+                           "empty",
+                           G_CALLBACK (gb_document_grid_stack_empty),
+                           grid,
+                           G_CONNECT_SWAPPED);
+
+  return stack;
+}
+
+GbDocumentManager *
+gb_document_grid_get_document_manager (GbDocumentGrid *grid)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+
+  return grid->priv->document_manager;
+}
+
+void
+gb_document_grid_set_document_manager (GbDocumentGrid    *grid,
+                                       GbDocumentManager *document_manager)
+{
+  GbDocumentGridPrivate *priv;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+  g_return_if_fail (!document_manager ||
+                    GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  priv = grid->priv;
+
+  if (priv->document_manager != document_manager)
+    {
+      g_clear_object (&priv->document_manager);
+
+      if (document_manager)
+        {
+          GList *list;
+          GList *iter;
+
+          priv->document_manager = g_object_ref (document_manager);
+
+          list = gb_document_grid_get_stacks (grid);
+
+          for (iter = list; iter; iter = iter->next)
+            gb_document_stack_set_document_manager (iter->data,
+                                                    document_manager);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (grid),
+                                gParamSpecs [PROP_DOCUMENT_MANAGER]);
+    }
+}
+
+static void
+gb_document_grid_reposition (GbDocumentGrid *grid)
+{
+  GtkAllocation alloc;
+  GtkWidget *paned;
+  GtkWidget *stack;
+  guint count = 0;
+  guint position;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+
+  gtk_widget_get_allocation (GTK_WIDGET (grid), &alloc);
+
+  paned = gtk_bin_get_child (GTK_BIN (grid));
+
+  if (!GTK_IS_PANED (paned))
+    return;
+
+  stack = gtk_paned_get_child1 (GTK_PANED (paned));
+  do
+    {
+      count++;
+      stack = gb_document_grid_get_stack_after (grid,
+                                                GB_DOCUMENT_STACK (stack));
+    }
+  while (stack);
+
+  position = alloc.width / count;
+
+  stack = gtk_paned_get_child1 (GTK_PANED (paned));
+  do
+    {
+      paned = gtk_widget_get_parent (stack);
+      gtk_paned_set_position (GTK_PANED (paned), position);
+      stack = gb_document_grid_get_stack_after (grid,
+                                                GB_DOCUMENT_STACK (stack));
+    }
+  while (stack);
+}
+
+/**
+ * gb_document_grid_get_stacks:
+ *
+ * Fetches all of the stacks in the grid. The resulting #GList should be
+ * freed with g_list_free().
+ *
+ * Returns: (transfer container) (element-type GbDocumentStack*): A #GList.
+ */
+GList *
+gb_document_grid_get_stacks (GbDocumentGrid *grid)
+{
+  GtkWidget *paned;
+  GList *list = NULL;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+
+  paned = gtk_bin_get_child (GTK_BIN (grid));
+
+  while (paned)
+    {
+      GtkWidget *stack;
+
+      stack = gtk_paned_get_child1 (GTK_PANED (paned));
+
+      if (GB_IS_DOCUMENT_STACK (stack))
+        list = g_list_append (list, stack);
+
+      paned = gtk_paned_get_child2 (GTK_PANED (paned));
+    }
+
+  return list;
+}
+
+GtkWidget *
+gb_document_grid_add_stack_before (GbDocumentGrid  *grid,
+                                   GbDocumentStack *stack)
+{
+  GbDocumentStack *new_stack;
+  GtkWidget *parent;
+  GtkWidget *grandparent;
+  GtkPaned *new_paned;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+
+  new_paned = gb_document_grid_create_paned (grid);
+  new_stack = gb_document_grid_create_stack (grid);
+  gtk_container_add (GTK_CONTAINER (new_paned), GTK_WIDGET (new_stack));
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (stack));
+  grandparent = gtk_widget_get_parent (GTK_WIDGET (parent));
+
+  if (GTK_IS_PANED (grandparent))
+    {
+      g_object_ref (parent);
+      gtk_container_remove (GTK_CONTAINER (grandparent), GTK_WIDGET (parent));
+      gtk_container_add_with_properties (GTK_CONTAINER (grandparent),
+                                         GTK_WIDGET (new_paned),
+                                         "shrink", FALSE,
+                                         "resize", TRUE,
+                                         NULL);
+      gtk_container_add_with_properties (GTK_CONTAINER (new_paned),
+                                         GTK_WIDGET (parent),
+                                         "shrink", FALSE,
+                                         "resize", TRUE,
+                                         NULL);
+      g_object_unref (parent);
+    }
+  else if (GB_IS_DOCUMENT_GRID (grandparent))
+    {
+      g_object_ref (parent);
+      gtk_container_remove (GTK_CONTAINER (grandparent), GTK_WIDGET (parent));
+      gtk_container_add (GTK_CONTAINER (grandparent), GTK_WIDGET (new_paned));
+      gtk_container_add_with_properties (GTK_CONTAINER (new_paned), parent,
+                                         "shrink", FALSE,
+                                         "resize", TRUE,
+                                         NULL);
+      g_object_unref (parent);
+    }
+  else
+    g_assert_not_reached ();
+
+  gb_document_grid_reposition (grid);
+
+  return GTK_WIDGET (new_stack);
+}
+
+GtkWidget *
+gb_document_grid_add_stack_after  (GbDocumentGrid  *grid,
+                                   GbDocumentStack *stack)
+{
+  GbDocumentStack *new_stack;
+  GtkWidget *parent;
+  GtkPaned *new_paned;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+
+  new_paned = gb_document_grid_create_paned (grid);
+  new_stack = gb_document_grid_create_stack (grid);
+  gtk_container_add (GTK_CONTAINER (new_paned), GTK_WIDGET (new_stack));
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (stack));
+
+  if (GTK_IS_PANED (parent))
+    {
+      GtkWidget *child2;
+
+      child2 = gtk_paned_get_child2 (GTK_PANED (parent));
+
+      if (child2)
+        {
+          g_object_ref (child2);
+          gtk_container_remove (GTK_CONTAINER (parent), child2);
+        }
+
+      gtk_container_add_with_properties (GTK_CONTAINER (parent),
+                                         GTK_WIDGET (new_paned),
+                                         "shrink", FALSE,
+                                         "resize", TRUE,
+                                         NULL);
+
+      if (child2)
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (new_paned), child2,
+                                             "shrink", FALSE,
+                                             "resize", TRUE,
+                                             NULL);
+          g_object_unref (child2);
+        }
+    }
+  else
+    g_assert_not_reached ();
+
+  gb_document_grid_reposition (grid);
+
+  return GTK_WIDGET (new_stack);
+}
+
+GtkWidget *
+gb_document_grid_get_stack_before (GbDocumentGrid  *grid,
+                                   GbDocumentStack *stack)
+{
+  GtkWidget *parent;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (stack));
+
+  if (GTK_IS_PANED (parent))
+    {
+      parent = gtk_widget_get_parent (parent);
+      if (GTK_IS_PANED (parent))
+        return gtk_paned_get_child1 (GTK_PANED (parent));
+    }
+
+  return NULL;
+}
+
+GtkWidget *
+gb_document_grid_get_stack_after (GbDocumentGrid  *grid,
+                                  GbDocumentStack *stack)
+{
+  GtkWidget *parent;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_GRID (grid), NULL);
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (stack));
+
+  if (GTK_IS_PANED (parent))
+    {
+      GtkWidget *child2;
+
+      child2 = gtk_paned_get_child2 (GTK_PANED (parent));
+      if (GTK_IS_PANED (child2))
+        return gtk_paned_get_child1 (GTK_PANED (child2));
+    }
+
+  return NULL;
+}
+
+void
+gb_document_grid_focus_document (GbDocumentGrid *grid,
+                                 GbDocument     *document)
+{
+  GList *stacks;
+  GList *iter;
+
+  g_return_if_fail (GB_IS_DOCUMENT_GRID (grid));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+
+  stacks = gb_document_grid_get_stacks (grid);
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      GbDocumentStack *stack = iter->data;
+      GtkWidget *view;
+
+      view = gb_document_stack_find_with_document (stack, document);
+
+      if (view)
+        {
+          gb_document_stack_focus_document (stack, document);
+          goto cleanup;
+        }
+    }
+
+  g_assert (stacks);
+
+  gb_document_stack_focus_document (stacks->data, document);
+
+cleanup:
+  g_list_free (stacks);
+}
+
+static void
+gb_document_grid_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GbDocumentGrid *self = GB_DOCUMENT_GRID(object);
+
+  switch (prop_id)
+    {
+    case PROP_DOCUMENT_MANAGER:
+      g_value_set_object (value, gb_document_grid_get_document_manager (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_grid_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GbDocumentGrid *self = GB_DOCUMENT_GRID(object);
+
+  switch (prop_id)
+    {
+    case PROP_DOCUMENT_MANAGER:
+      gb_document_grid_set_document_manager (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_grid_finalize (GObject *object)
+{
+  GbDocumentGridPrivate *priv = GB_DOCUMENT_GRID (object)->priv;
+
+  g_clear_object (&priv->document_manager);
+
+  G_OBJECT_CLASS (gb_document_grid_parent_class)->finalize (object);
+}
+
+static void
+gb_document_grid_class_init (GbDocumentGridClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gb_document_grid_finalize;
+  object_class->get_property = gb_document_grid_get_property;
+  object_class->set_property = gb_document_grid_set_property;
+
+  /**
+   * GbDocumentGrid:document-manager:
+   *
+   * The "document-manager" property contains the manager for all open
+   * "buffers" (known as #GbDocument within Builder).
+   */
+  gParamSpecs [PROP_DOCUMENT_MANAGER] =
+    g_param_spec_object ("document-manager",
+                         _("Document Manager"),
+                         _("The document manager for the document grid."),
+                         GB_TYPE_DOCUMENT_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_DOCUMENT_MANAGER,
+                                   gParamSpecs [PROP_DOCUMENT_MANAGER]);
+}
+
+static void
+gb_document_grid_init (GbDocumentGrid *self)
+{
+  GbDocumentStack *stack;
+  GtkPaned *paned;
+
+  self->priv = gb_document_grid_get_instance_private (self);
+
+  paned = gb_document_grid_create_paned (self);
+  stack = gb_document_grid_create_stack (self);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (paned), GTK_WIDGET (stack),
+                                     "shrink", FALSE,
+                                     "resize", TRUE,
+                                     NULL);
+
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (paned));
+}
diff --git a/src/documents/gb-document-grid.h b/src/documents/gb-document-grid.h
new file mode 100644
index 0000000..fd0f268
--- /dev/null
+++ b/src/documents/gb-document-grid.h
@@ -0,0 +1,73 @@
+/* gb-document-grid.h
+ *
+ * Copyright (C) 2014 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 GB_DOCUMENT_GRID_H
+#define GB_DOCUMENT_GRID_H
+
+#include <gtk/gtk.h>
+
+#include "gb-document-manager.h"
+#include "gb-document-stack.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_DOCUMENT_GRID            (gb_document_grid_get_type())
+#define GB_DOCUMENT_GRID(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_GRID, 
GbDocumentGrid))
+#define GB_DOCUMENT_GRID_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_GRID, 
GbDocumentGrid const))
+#define GB_DOCUMENT_GRID_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_DOCUMENT_GRID, 
GbDocumentGridClass))
+#define GB_IS_DOCUMENT_GRID(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GB_TYPE_DOCUMENT_GRID))
+#define GB_IS_DOCUMENT_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_DOCUMENT_GRID))
+#define GB_DOCUMENT_GRID_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_DOCUMENT_GRID, 
GbDocumentGridClass))
+
+typedef struct _GbDocumentGrid        GbDocumentGrid;
+typedef struct _GbDocumentGridClass   GbDocumentGridClass;
+typedef struct _GbDocumentGridPrivate GbDocumentGridPrivate;
+
+struct _GbDocumentGrid
+{
+  GtkBin parent;
+
+  /*< private >*/
+  GbDocumentGridPrivate *priv;
+};
+
+struct _GbDocumentGridClass
+{
+  GtkBinClass parent;
+};
+
+GType              gb_document_grid_get_type             (void);
+GtkWidget         *gb_document_grid_new                  (void);
+void               gb_document_grid_set_document_manager (GbDocumentGrid    *grid,
+                                                          GbDocumentManager *manager);
+GbDocumentManager *gb_document_grid_get_document_manager (GbDocumentGrid    *grid);
+GtkWidget         *gb_document_grid_add_stack_after      (GbDocumentGrid    *grid,
+                                                          GbDocumentStack   *stack);
+GtkWidget         *gb_document_grid_add_stack_before     (GbDocumentGrid    *grid,
+                                                          GbDocumentStack   *stack);
+GtkWidget         *gb_document_grid_get_stack_after      (GbDocumentGrid    *grid,
+                                                          GbDocumentStack   *stack);
+GtkWidget         *gb_document_grid_get_stack_before     (GbDocumentGrid    *grid,
+                                                          GbDocumentStack   *stack);
+GList             *gb_document_grid_get_stacks           (GbDocumentGrid    *grid);
+void               gb_document_grid_focus_document       (GbDocumentGrid    *grid,
+                                                          GbDocument        *document);
+
+G_END_DECLS
+
+#endif /* GB_DOCUMENT_GRID_H */
diff --git a/src/documents/gb-document-manager.c b/src/documents/gb-document-manager.c
index 704fc99..4d7554e 100644
--- a/src/documents/gb-document-manager.c
+++ b/src/documents/gb-document-manager.c
@@ -16,31 +16,36 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#define G_LOG_DOMAIN "document-manager"
+
 #include <gtksourceview/gtksource.h>
 
 #include "gb-document-manager.h"
 #include "gb-editor-document.h"
 
-G_DEFINE_TYPE (GbDocumentManager, gb_document_manager, GTK_TYPE_LIST_STORE)
+struct _GbDocumentManagerPrivate
+{
+  GPtrArray *documents;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbDocumentManager, gb_document_manager,
+                            G_TYPE_OBJECT)
 
 enum {
-  COLUMN_DOCUMENT,
-  LAST_COLUMN
+  DOCUMENT_ADDED,
+  DOCUMENT_REMOVED,
+  DOCUMENT_MODIFIED_CHANGED,
+  LAST_SIGNAL
 };
 
+static guint gSignals [LAST_SIGNAL];
+
 GbDocumentManager *
 gb_document_manager_new (void)
 {
   return g_object_new (GB_TYPE_DOCUMENT_MANAGER, NULL);
 }
 
-/**
- * gb_document_manager_get_default:
- *
- * Retrieves the singleton instance of #GbDocumentManager.
- *
- * Returns: (transfer none): A #GbDocumentManager.
- */
 GbDocumentManager *
 gb_document_manager_get_default (void)
 {
@@ -52,206 +57,211 @@ gb_document_manager_get_default (void)
   return instance;
 }
 
-/**
- * gb_document_manager_find_by_file:
- * @manager: A #GbDocumentManager.
- * @file: A #GFile.
- *
- * This function will attempt to locate a previously stored buffer that matches
- * the requsted file.
- *
- * If located, that buffer will be returned. Otherwise, %NULL is returned.
- *
- * Returns: (transfer none): A #GbDocument or %NULL.
- */
+guint
+gb_document_manager_get_count (GbDocumentManager *manager)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_MANAGER (manager), 0);
+
+  return manager->priv->documents->len;
+}
+
 GbDocument *
-gb_document_manager_find_by_file (GbDocumentManager *manager,
-                                  GFile             *file)
+gb_document_manager_find_with_file (GbDocumentManager *manager,
+                                    GFile             *file)
 {
-  GtkTreeIter iter;
+  guint i;
 
   g_return_val_if_fail (GB_IS_DOCUMENT_MANAGER (manager), NULL);
-  g_return_val_if_fail (G_IS_FILE (file), NULL);
 
-  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (manager), &iter))
+  for (i = 0; i < manager->priv->documents->len; i++)
     {
-      do
-        {
-          GbEditorDocument *document;
-          GtkSourceFile *source_file;
-          GFile *location;
-          GValue value = { 0 };
+      GbDocument *document;
 
-          gtk_tree_model_get_value (GTK_TREE_MODEL (manager), &iter,
-                                    COLUMN_DOCUMENT, &value);
+      document = g_ptr_array_index (manager->priv->documents, i);
 
-          if (G_VALUE_HOLDS (&value, GB_TYPE_EDITOR_DOCUMENT))
-            {
-              document = g_value_get_object (&value);
-
-              source_file = gb_editor_document_get_file (document);
-              location = gtk_source_file_get_location (source_file);
+      if (GB_IS_EDITOR_DOCUMENT (document))
+        {
+          GtkSourceFile *sfile;
+          GFile *location;
 
-              if (g_file_equal (location, file))
-                {
-                  g_value_unset (&value);
-                  return GB_DOCUMENT (document);
-                }
-            }
+          sfile = gb_editor_document_get_file (GB_EDITOR_DOCUMENT (document));
+          location = gtk_source_file_get_location (sfile);
 
-          g_value_unset (&value);
+          if (g_file_equal (location, file))
+            return document;
         }
-      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (manager), &iter));
     }
 
   return NULL;
 }
 
-static gboolean
-gb_document_manager_find_document (GbDocumentManager *manager,
-                                   GbDocument        *document,
-                                   GtkTreeIter       *iter)
+/**
+ * gb_document_manager_get_documents:
+ *
+ * Fetches a #GList of all the documents loaded by #GbDocumentManager.
+ *
+ * Returns: (transfer container) (element-type GbDocument*): #GList of
+ *   #GbDocument. Free list with g_list_free().
+ */
+GList *
+gb_document_manager_get_documents (GbDocumentManager *manager)
 {
-  g_return_val_if_fail (GB_IS_DOCUMENT_MANAGER (manager), FALSE);
-  g_return_val_if_fail (GB_IS_DOCUMENT (document), FALSE);
-
-  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (manager), iter))
-    {
-      do
-        {
-          GValue value = { 0 };
+  GList *list = NULL;
+  guint i;
 
-          gtk_tree_model_get_value (GTK_TREE_MODEL (manager), iter,
-                                    COLUMN_DOCUMENT, &value);
+  g_return_val_if_fail (GB_IS_DOCUMENT_MANAGER (manager), NULL);
 
-          if (G_VALUE_HOLDS_OBJECT (&value) &&
-              (g_value_get_object (&value) == (void *)document))
-            {
-              g_value_unset (&value);
-              return TRUE;
-            }
+  for (i = 0; i < manager->priv->documents->len; i++)
+    {
+      GbDocument *document;
 
-          g_value_unset (&value);
-        }
-      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (manager), iter));
+      document = g_ptr_array_index (manager->priv->documents, i);
+      list = g_list_prepend (list, document);
     }
 
-  return FALSE;
+  return list;
 }
 
 static void
-gb_document_manager_document_changed (GbDocumentManager *manager,
-                                      GbDocument        *document)
+gb_document_manager_document_modified (GbDocumentManager *manager,
+                                       GParamSpec        *pspec,
+                                       GbDocument        *document)
 {
-  GtkTreeIter iter;
-
   g_return_if_fail (GB_IS_DOCUMENT_MANAGER (manager));
   g_return_if_fail (GB_IS_DOCUMENT (document));
 
-  if (gb_document_manager_find_document (manager, document, &iter))
-    {
-      GtkTreePath *tree_path;
-
-      tree_path = gtk_tree_model_get_path (GTK_TREE_MODEL (manager), &iter);
-      gtk_tree_model_row_changed (GTK_TREE_MODEL (manager), tree_path, &iter);
-      gtk_tree_path_free (tree_path);
-    }
+  g_signal_emit (manager, gSignals [DOCUMENT_MODIFIED_CHANGED], 0, document);
 }
 
-static void
-gb_document_manager_on_notify_can_save (GbDocumentManager *manager,
-                                        GParamSpec        *pspec,
-                                        GbDocument        *document)
+void
+gb_document_manager_add (GbDocumentManager *manager,
+                         GbDocument        *document)
 {
+  guint i;
+
   g_return_if_fail (GB_IS_DOCUMENT_MANAGER (manager));
   g_return_if_fail (GB_IS_DOCUMENT (document));
 
-  gb_document_manager_document_changed (manager, document);
-}
+  for (i = 0; i < manager->priv->documents->len; i++)
+    {
+      GbDocument *item;
 
-static void
-gb_document_manager_on_notify_title (GbDocumentManager *manager,
-                                     GParamSpec        *pspec,
-                                     GbDocument        *document)
-{
-  g_return_if_fail (GB_IS_DOCUMENT_MANAGER (manager));
-  g_return_if_fail (GB_IS_DOCUMENT (document));
+      item = g_ptr_array_index (manager->priv->documents, i);
 
-  gb_document_manager_document_changed (manager, document);
+      if (item == document)
+        {
+          g_warning ("GbDocumentManager already contains document \"%s\"",
+                     gb_document_get_title (document));
+          return;
+        }
+    }
+
+  g_signal_connect_object (document,
+                           "notify::modified",
+                           G_CALLBACK (gb_document_manager_document_modified),
+                           manager,
+                           G_CONNECT_SWAPPED);
+
+  g_ptr_array_add (manager->priv->documents, g_object_ref (document));
+
+  g_signal_emit (manager, gSignals [DOCUMENT_ADDED], 0, document);
 }
 
-/**
- * gb_document_manager_add_document:
- * @manager: A #GbDocumentManager.
- * @document: A #GbDocument.
- *
- * Adds A #GbDocument to the collection of buffers managed by the
- * #GbDocumentManager instance.
- */
 void
-gb_document_manager_add_document (GbDocumentManager *manager,
-                                  GbDocument        *document)
+gb_document_manager_remove (GbDocumentManager *manager,
+                            GbDocument        *document)
 {
-  GtkTreeIter iter;
+  guint i;
 
   g_return_if_fail (GB_IS_DOCUMENT_MANAGER (manager));
   g_return_if_fail (GB_IS_DOCUMENT (document));
 
-  g_signal_connect_object (document,
-                           "notify::title",
-                           G_CALLBACK (gb_document_manager_on_notify_title),
-                           manager,
-                           G_CONNECT_SWAPPED);
+  for (i = 0; i < manager->priv->documents->len; i++)
+    {
+      GbDocument *item;
 
-  g_signal_connect_object (document,
-                           "notify::can-save",
-                           G_CALLBACK (gb_document_manager_on_notify_can_save),
-                           manager,
-                           G_CONNECT_SWAPPED);
+      item = g_ptr_array_index (manager->priv->documents, i);
 
-  gtk_list_store_append (GTK_LIST_STORE (manager), &iter);
-  gtk_list_store_set (GTK_LIST_STORE (manager), &iter,
-                      COLUMN_DOCUMENT, document,
-                      -1);
+      if (item == document)
+        {
+          g_signal_handlers_disconnect_by_func (item,
+                                                gb_document_manager_document_modified,
+                                                manager);
+          g_ptr_array_remove_index_fast (manager->priv->documents, i);
+          g_signal_emit (manager, gSignals [DOCUMENT_REMOVED], 0, item);
+          g_object_unref (item);
+          break;
+        }
+    }
 }
 
-/**
- * gb_document_manager_remove_document:
- *
- * Removes the #GbDocument instance @document from being managed by
- * the #GbDocumentManager instance.
- *
- * Returns: %TRUE if the document was removed.
- */
-gboolean
-gb_document_manager_remove_document (GbDocumentManager *manager,
-                                     GbDocument        *document)
+static void
+gb_document_manager_finalize (GObject *object)
 {
-  GtkTreeIter iter;
+  GbDocumentManagerPrivate *priv = GB_DOCUMENT_MANAGER (object)->priv;
+  GbDocumentManager *manager = (GbDocumentManager *)object;
 
-  g_return_val_if_fail (GB_IS_DOCUMENT_MANAGER (manager), FALSE);
-  g_return_val_if_fail (GB_IS_DOCUMENT (document), FALSE);
-
-  if (gb_document_manager_find_document (manager, document, &iter))
+  while (priv->documents->len)
     {
-      gtk_list_store_remove (GTK_LIST_STORE (manager), &iter);
-      return TRUE;
+      GbDocument *document;
+
+      document = GB_DOCUMENT (g_ptr_array_index (priv->documents, 0));
+      gb_document_manager_remove (manager, document);
     }
 
-  return FALSE;
+  g_clear_pointer (&priv->documents, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (gb_document_manager_parent_class)->finalize (object);
 }
 
 static void
 gb_document_manager_class_init (GbDocumentManagerClass *klass)
 {
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gb_document_manager_finalize;
+
+  gSignals [DOCUMENT_ADDED] =
+    g_signal_new ("document-added",
+                  GB_TYPE_DOCUMENT_MANAGER,
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GbDocumentManagerClass, document_added),
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_DOCUMENT);
+
+  gSignals [DOCUMENT_MODIFIED_CHANGED] =
+    g_signal_new ("document-modified-changed",
+                  GB_TYPE_DOCUMENT_MANAGER,
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GbDocumentManagerClass,
+                                   document_modified_changed),
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_DOCUMENT);
+
+  gSignals [DOCUMENT_REMOVED] =
+    g_signal_new ("document-removed",
+                  GB_TYPE_DOCUMENT_MANAGER,
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GbDocumentManagerClass, document_removed),
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_DOCUMENT);
 }
 
 static void
 gb_document_manager_init (GbDocumentManager *self)
 {
-  GType column_types[] = { GB_TYPE_DOCUMENT };
-
-  gtk_list_store_set_column_types (GTK_LIST_STORE (self),
-                                   G_N_ELEMENTS (column_types),
-                                   column_types);
+  self->priv = gb_document_manager_get_instance_private (self);
+  self->priv->documents = g_ptr_array_new ();
 }
diff --git a/src/documents/gb-document-manager.h b/src/documents/gb-document-manager.h
index 4ca5962..6c01fde 100644
--- a/src/documents/gb-document-manager.h
+++ b/src/documents/gb-document-manager.h
@@ -19,7 +19,7 @@
 #ifndef GB_DOCUMENT_MANAGER_H
 #define GB_DOCUMENT_MANAGER_H
 
-#include <gtk/gtk.h>
+#include <gio/gio.h>
 
 #include "gb-document.h"
 
@@ -39,7 +39,7 @@ typedef struct _GbDocumentManagerPrivate GbDocumentManagerPrivate;
 
 struct _GbDocumentManager
 {
-  GtkListStore parent;
+  GObject parent;
 
   /*< private >*/
   GbDocumentManagerPrivate *priv;
@@ -47,18 +47,27 @@ struct _GbDocumentManager
 
 struct _GbDocumentManagerClass
 {
-  GtkListStoreClass parent;
+  GObjectClass parent;
+
+  void (*document_added)           (GbDocumentManager *manager,
+                                     GbDocument        *document);
+  void (*document_removed)          (GbDocumentManager *manager,
+                                     GbDocument        *document);
+  void (*document_modified_changed) (GbDocumentManager *manager,
+                                     GbDocument        *document);
 };
 
-GType              gb_document_manager_get_type        (void);
-GbDocumentManager *gb_document_manager_new             (void);
-GbDocumentManager *gb_document_manager_get_default     (void);
-GbDocument        *gb_document_manager_find_by_file    (GbDocumentManager *manager,
-                                                        GFile             *file);
-void               gb_document_manager_add_document    (GbDocumentManager *manager,
-                                                        GbDocument        *document);
-gboolean           gb_document_manager_remove_document (GbDocumentManager *manager,
-                                                        GbDocument        *document);
+GType              gb_document_manager_get_type       (void);
+GbDocumentManager *gb_document_manager_new            (void);
+GbDocumentManager *gb_document_manager_get_default    (void);
+void               gb_document_manager_add            (GbDocumentManager *manager,
+                                                       GbDocument        *document);
+void               gb_document_manager_remove         (GbDocumentManager *manager,
+                                                       GbDocument        *document);
+GList             *gb_document_manager_get_documents  (GbDocumentManager *manager);
+guint              gb_document_manager_get_count      (GbDocumentManager *manager);
+GbDocument        *gb_document_manager_find_with_file (GbDocumentManager *manager,
+                                                       GFile             *file);
 
 G_END_DECLS
 
diff --git a/src/documents/gb-document-menu-button.c b/src/documents/gb-document-menu-button.c
new file mode 100644
index 0000000..24f6fc2
--- /dev/null
+++ b/src/documents/gb-document-menu-button.c
@@ -0,0 +1,650 @@
+/* gb-document-menu-button.c
+ *
+ * Copyright (C) 2014 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 "document-menu-button"
+
+#include <glib/gi18n.h>
+
+#include "gb-document-menu-button.h"
+#include "gb-glib.h"
+
+struct _GbDocumentMenuButtonPrivate
+{
+  /* Objects owned by menu button */
+  GbDocumentManager *document_manager;
+  GHashTable        *focus_time;
+
+  /* Weak references */
+  GbDocument        *selected_document;
+
+  /* Unowned references */
+  GBinding          *title_binding;
+  GBinding          *modified_binding;
+
+  /* Widgets owned by Template */
+  GtkLabel          *label;
+  GtkLabel          *modified_label;
+  GtkListBox        *list_box;
+  GtkPopover        *popover;
+  GtkScrolledWindow *scrolled_window;
+  GtkSearchEntry    *search_entry;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbDocumentMenuButton,
+                            gb_document_menu_button,
+                            GTK_TYPE_MENU_BUTTON)
+
+enum {
+  PROP_0,
+  PROP_DOCUMENT_MANAGER,
+  LAST_PROP
+};
+
+enum {
+  DOCUMENT_SELECTED,
+  LAST_SIGNAL
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint       gSignals [LAST_SIGNAL];
+
+GtkWidget *
+gb_document_menu_button_new (void)
+{
+  return g_object_new (GB_TYPE_DOCUMENT_MENU_BUTTON, NULL);
+}
+
+GbDocumentManager *
+gb_document_menu_button_get_document_manager (GbDocumentMenuButton *button)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button), NULL);
+
+  return button->priv->document_manager;
+}
+
+static GtkListBoxRow *
+gb_document_menu_button_create_row (GbDocumentMenuButton *button,
+                                    GbDocument           *document)
+{
+  GtkListBoxRow *row;
+  GtkLabel *label;
+  GtkLabel *modified;
+  GtkBox *box;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button), NULL);
+  g_return_val_if_fail (GB_IS_DOCUMENT (document), NULL);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data (G_OBJECT (row), "GB_DOCUMENT", document);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "margin-top", 3,
+                        "margin-bottom", 3,
+                        "margin-start", 6,
+                        "margin-end", 3,
+                        "hexpand", FALSE,
+                        "visible", TRUE,
+                        "valign", GTK_ALIGN_BASELINE,
+                        "xalign", 0.0f,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+
+  modified = g_object_new (GTK_TYPE_LABEL,
+                           "label", "•",
+                           "visible", FALSE,
+                           "valign", GTK_ALIGN_BASELINE,
+                           "xalign", 0.0f,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (modified));
+
+  g_object_bind_property (document, "title", label, "label",
+                          G_BINDING_SYNC_CREATE);
+
+  g_object_bind_property (document, "modified", modified, "visible",
+                          G_BINDING_SYNC_CREATE);
+
+  return row;
+}
+
+static void
+gb_document_menu_button_update_sensitive (GbDocumentMenuButton *button)
+{
+  gboolean sensitive = FALSE;
+  guint count;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  if (button->priv->document_manager)
+    {
+      count = gb_document_manager_get_count (button->priv->document_manager);
+      sensitive = (count > 0);
+    }
+
+  gtk_widget_set_sensitive (GTK_WIDGET (button), sensitive);
+}
+
+static void
+gb_document_menu_button_add_document (GbDocumentMenuButton *button,
+                                      GbDocument           *document,
+                                      GbDocumentManager    *document_manager)
+{
+  GtkListBoxRow *row;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+  g_return_if_fail (GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  row = gb_document_menu_button_create_row (button, document);
+
+  gtk_list_box_insert (button->priv->list_box, GTK_WIDGET (row), -1);
+
+  gb_document_menu_button_update_sensitive (button);
+}
+
+static void
+gb_document_menu_button_remove_document (GbDocumentMenuButton *button,
+                                         GbDocument           *document,
+                                         GbDocumentManager    *document_manager)
+{
+  GQuark qname;
+  GList *list;
+  GList *iter;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+  g_return_if_fail (GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  g_hash_table_remove (button->priv->focus_time, document);
+
+  list = gtk_container_get_children (GTK_CONTAINER (button->priv->list_box));
+  qname = g_quark_from_static_string ("GB_DOCUMENT");
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      GtkListBoxRow *row = iter->data;
+      GbDocument *item;
+
+      if (!GTK_IS_LIST_BOX_ROW (iter->data))
+        continue;
+
+      item = g_object_get_qdata (G_OBJECT (row), qname);
+
+      if (item == document)
+        {
+          gtk_container_remove (GTK_CONTAINER (button->priv->list_box),
+                                GTK_WIDGET (row));
+          break;
+        }
+    }
+
+  g_list_free (list);
+
+  gb_document_menu_button_update_sensitive (button);
+}
+
+static void
+gb_document_menu_button_connect (GbDocumentMenuButton *button,
+                                 GbDocumentManager    *document_manager)
+{
+  GList *documents;
+  GList *iter;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  g_signal_connect_object (document_manager,
+                           "document-added",
+                           G_CALLBACK (gb_document_menu_button_add_document),
+                           button,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (document_manager,
+                           "document-removed",
+                           G_CALLBACK (gb_document_menu_button_remove_document),
+                           button,
+                           G_CONNECT_SWAPPED);
+
+  documents = gb_document_manager_get_documents (document_manager);
+
+  for (iter = documents; iter; iter = iter->next)
+    {
+      GbDocument *document;
+
+      document = GB_DOCUMENT (iter->data);
+      gb_document_menu_button_add_document (button, document, document_manager);
+    }
+
+  g_list_free (documents);
+}
+
+static void
+gb_document_menu_button_disconnect (GbDocumentMenuButton *button,
+                                    GbDocumentManager    *document_manager)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  g_signal_handlers_disconnect_by_func (document_manager,
+                                        gb_document_menu_button_add_document,
+                                        button);
+
+  g_signal_handlers_disconnect_by_func (document_manager,
+                                        gb_document_menu_button_remove_document,
+                                        button);
+}
+
+void
+gb_document_menu_button_set_document_manager (GbDocumentMenuButton *button,
+                                              GbDocumentManager    *document_manager)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (!document_manager || GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  if (document_manager != button->priv->document_manager)
+    {
+      if (button->priv->document_manager)
+        {
+          gb_document_menu_button_disconnect (button, document_manager);
+          g_clear_object (&button->priv->document_manager);
+        }
+
+      if (document_manager)
+        {
+          button->priv->document_manager = g_object_ref (document_manager);
+          gb_document_menu_button_connect (button, document_manager);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (button),
+                                gParamSpecs [PROP_DOCUMENT_MANAGER]);
+    }
+}
+
+void
+gb_document_menu_button_select_document (GbDocumentMenuButton *button,
+                                         GbDocument           *document)
+{
+  GbDocumentMenuButtonPrivate *priv;
+  gsize value;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (!document || GB_IS_DOCUMENT (document));
+
+  priv = button->priv;
+
+  if (priv->title_binding)
+    {
+      g_binding_unbind (priv->title_binding);
+      priv->title_binding = NULL;
+    }
+
+  if (priv->modified_binding)
+    {
+      g_binding_unbind (priv->modified_binding);
+      priv->modified_binding = NULL;
+    }
+
+  gb_clear_weak_pointer (&priv->selected_document);
+  gb_set_weak_pointer (document, &priv->selected_document);
+
+  if (document)
+    {
+      priv->title_binding =
+        g_object_bind_property (document, "title", priv->label, "label",
+                                G_BINDING_SYNC_CREATE);
+
+      priv->modified_binding =
+        g_object_bind_property (document, "modified",
+                                priv->modified_label, "visible",
+                                G_BINDING_SYNC_CREATE);
+
+      value = g_get_monotonic_time () / (G_USEC_PER_SEC / 10);
+      g_hash_table_replace (priv->focus_time, document,
+                            GSIZE_TO_POINTER (value));
+
+      g_signal_emit (button, gSignals [DOCUMENT_SELECTED], 0, document);
+    }
+  else
+    {
+      gtk_label_set_label (priv->label, NULL);
+      gtk_widget_set_visible (GTK_WIDGET (priv->modified_label), FALSE);
+    }
+
+  gb_document_menu_button_update_sensitive (button);
+
+  gtk_list_box_invalidate_sort (priv->list_box);
+}
+
+static gboolean
+gb_document_menu_button_do_hide (gpointer data)
+{
+  GbDocumentMenuButton *button = data;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button), FALSE);
+
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), FALSE);
+  g_object_unref (button);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gb_document_menu_button_delayed_hide (GbDocumentMenuButton *button)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  g_timeout_add (200,
+                 gb_document_menu_button_do_hide,
+                 g_object_ref (button));
+}
+
+static void
+gb_document_menu_button_row_activated (GbDocumentMenuButton *button,
+                                       GtkListBoxRow        *row,
+                                       GtkListBox           *list_box)
+{
+  GbDocument *document;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
+  g_return_if_fail (GTK_IS_LIST_BOX (list_box));
+
+  document = g_object_get_data (G_OBJECT (row), "GB_DOCUMENT");
+
+  if (GB_IS_DOCUMENT (document))
+    gb_document_menu_button_select_document (button, document);
+
+  gb_document_menu_button_delayed_hide (button);
+}
+
+static void
+gb_document_menu_button_header_func (GtkListBoxRow *row,
+                                     GtkListBoxRow *before,
+                                     gpointer       user_data)
+{
+  g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
+
+  if (before)
+    {
+      GtkWidget *widget;
+
+      widget = g_object_new (GTK_TYPE_SEPARATOR,
+                             "orientation", GTK_ORIENTATION_HORIZONTAL,
+                             "visible", TRUE,
+                             NULL);
+      gtk_list_box_row_set_header (row, widget);
+    }
+}
+
+static gint
+gb_document_menu_button_sort_func (GtkListBoxRow *row1,
+                                   GtkListBoxRow *row2,
+                                   gpointer       user_data)
+{
+
+  GbDocumentMenuButton *button = user_data;
+  GbDocument *doc1 = g_object_get_data (G_OBJECT (row1), "GB_DOCUMENT");
+  GbDocument *doc2 = g_object_get_data (G_OBJECT (row2), "GB_DOCUMENT");
+  gpointer value1 = g_hash_table_lookup (button->priv->focus_time, doc1);
+  gpointer value2 = g_hash_table_lookup (button->priv->focus_time, doc2);
+
+  if (!value1 && !value2)
+    return g_strcmp0 (gb_document_get_title (doc1),
+                      gb_document_get_title (doc2));
+
+  if (!value1)
+    return 1;
+
+  if (!value2)
+    return -1;
+
+  return GPOINTER_TO_INT (value2 - value1);
+}
+
+static gboolean
+gb_document_menu_button_filter_func (GtkListBoxRow *row,
+                                     gpointer       user_data)
+{
+  GbDocumentMenuButton *button = user_data;
+  GbDocument *document;
+  const gchar *title;
+  const gchar *str;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button), FALSE);
+
+  document = g_object_get_data (G_OBJECT (row), "GB_DOCUMENT");
+  title = gb_document_get_title (document);
+  str = gtk_entry_get_text (GTK_ENTRY (button->priv->search_entry));
+
+  /*
+   * TODO: Replace this with a proper fuzzy search with scoring and
+   *       highlighting. Score should include the distance between
+   *       matched characters.
+   */
+  for (; title && *str; str = g_utf8_next_char (str))
+    {
+      gunichar c = g_utf8_get_char (str);
+
+      if (g_unichar_isspace (c))
+        continue;
+
+      title = g_utf8_strchr (title, -1, c);
+
+      if (title)
+        title = g_utf8_next_char (title);
+    }
+
+  return title && ((str == NULL) || (*str == '\0'));
+}
+
+static void
+gb_document_menu_button_search_changed (GbDocumentMenuButton *button,
+                                        GtkEditable          *editable)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  gtk_list_box_invalidate_filter (button->priv->list_box);
+}
+
+static void
+gb_document_menu_button_search_activate (GbDocumentMenuButton *button,
+                                         GtkEditable          *editable)
+{
+  GtkListBoxRow *row;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  row = gtk_list_box_get_row_at_y (button->priv->list_box, 1);
+
+  if (row)
+    {
+      GbDocument *document;
+
+      document = g_object_get_data (G_OBJECT (row), "GB_DOCUMENT");
+      gb_document_menu_button_select_document (button, document);
+    }
+}
+
+static void
+gb_document_menu_button_clicked (GtkButton *button)
+{
+  GbDocumentMenuButton *self = (GbDocumentMenuButton *)button;
+
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  gtk_entry_set_text (GTK_ENTRY (self->priv->search_entry), "");
+
+  GTK_BUTTON_CLASS (gb_document_menu_button_parent_class)->clicked (button);
+}
+
+static void
+gb_document_menu_button_constructed (GObject *object)
+{
+  GbDocumentMenuButton *button = (GbDocumentMenuButton *)object;
+
+  G_OBJECT_CLASS (gb_document_menu_button_parent_class)->constructed (object);
+
+  g_signal_connect_object (button->priv->list_box,
+                           "row-activated",
+                           G_CALLBACK (gb_document_menu_button_row_activated),
+                           button,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (button->priv->search_entry,
+                           "activate",
+                           G_CALLBACK (gb_document_menu_button_search_activate),
+                           button,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (button->priv->search_entry,
+                           "changed",
+                           G_CALLBACK (gb_document_menu_button_search_changed),
+                           button,
+                           G_CONNECT_SWAPPED);
+
+  gtk_list_box_set_header_func (button->priv->list_box,
+                                gb_document_menu_button_header_func,
+                                button,
+                                NULL);
+
+  gtk_list_box_set_sort_func (button->priv->list_box,
+                              gb_document_menu_button_sort_func,
+                              button,
+                              NULL);
+
+  gtk_list_box_set_filter_func (button->priv->list_box,
+                                gb_document_menu_button_filter_func,
+                                button,
+                                NULL);
+}
+
+static void
+gb_document_menu_button_finalize (GObject *object)
+{
+  GbDocumentMenuButtonPrivate *priv = GB_DOCUMENT_MENU_BUTTON (object)->priv;
+
+  g_clear_object (&priv->document_manager);
+  g_clear_pointer (&priv->focus_time, g_hash_table_unref);
+
+  G_OBJECT_CLASS (gb_document_menu_button_parent_class)->finalize (object);
+}
+
+static void
+gb_document_menu_button_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  GbDocumentMenuButton *self = GB_DOCUMENT_MENU_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_DOCUMENT_MANAGER:
+      g_value_set_object (value,
+                          gb_document_menu_button_get_document_manager (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_menu_button_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  GbDocumentMenuButton *self = GB_DOCUMENT_MENU_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_DOCUMENT_MANAGER:
+      gb_document_menu_button_set_document_manager (self,
+                                                    g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_menu_button_class_init (GbDocumentMenuButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
+
+  object_class->constructed = gb_document_menu_button_constructed;
+  object_class->finalize = gb_document_menu_button_finalize;
+  object_class->get_property = gb_document_menu_button_get_property;
+  object_class->set_property = gb_document_menu_button_set_property;
+
+  button_class->clicked = gb_document_menu_button_clicked;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/builder/ui/gb-document-menu-button.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, label);
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, list_box);
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, modified_label);
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, popover);
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, scrolled_window);
+  gtk_widget_class_bind_template_child_private (widget_class, GbDocumentMenuButton, search_entry);
+
+  gParamSpecs [PROP_DOCUMENT_MANAGER] =
+    g_param_spec_object ("document-manager",
+                         _("Document Manager"),
+                         _("The document manager for the button."),
+                         GB_TYPE_DOCUMENT_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_DOCUMENT_MANAGER,
+                                   gParamSpecs [PROP_DOCUMENT_MANAGER]);
+
+  gSignals [DOCUMENT_SELECTED] =
+    g_signal_new ("document-selected",
+                  GB_TYPE_DOCUMENT_MENU_BUTTON,
+                  G_SIGNAL_RUN_FIRST,
+                  G_STRUCT_OFFSET (GbDocumentMenuButtonClass,
+                                   document_selected),
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  GB_TYPE_DOCUMENT);
+}
+
+static void
+gb_document_menu_button_init (GbDocumentMenuButton *self)
+{
+  self->priv = gb_document_menu_button_get_instance_private (self);
+
+  self->priv->focus_time = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gb_document_menu_button_update_sensitive (self);
+}
diff --git a/src/documents/gb-document-menu-button.h b/src/documents/gb-document-menu-button.h
new file mode 100644
index 0000000..116c346
--- /dev/null
+++ b/src/documents/gb-document-menu-button.h
@@ -0,0 +1,67 @@
+/* gb-document-menu-button.h
+ *
+ * Copyright (C) 2014 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 GB_DOCUMENT_MENU_BUTTON_H
+#define GB_DOCUMENT_MENU_BUTTON_H
+
+#include <gtk/gtk.h>
+
+#include "gb-document.h"
+#include "gb-document-manager.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_DOCUMENT_MENU_BUTTON            (gb_document_menu_button_get_type())
+#define GB_DOCUMENT_MENU_BUTTON(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GB_TYPE_DOCUMENT_MENU_BUTTON, GbDocumentMenuButton))
+#define GB_DOCUMENT_MENU_BUTTON_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
GB_TYPE_DOCUMENT_MENU_BUTTON, GbDocumentMenuButton const))
+#define GB_DOCUMENT_MENU_BUTTON_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  
GB_TYPE_DOCUMENT_MENU_BUTTON, GbDocumentMenuButtonClass))
+#define GB_IS_DOCUMENT_MENU_BUTTON(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
GB_TYPE_DOCUMENT_MENU_BUTTON))
+#define GB_IS_DOCUMENT_MENU_BUTTON_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  
GB_TYPE_DOCUMENT_MENU_BUTTON))
+#define GB_DOCUMENT_MENU_BUTTON_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  
GB_TYPE_DOCUMENT_MENU_BUTTON, GbDocumentMenuButtonClass))
+
+typedef struct _GbDocumentMenuButton        GbDocumentMenuButton;
+typedef struct _GbDocumentMenuButtonClass   GbDocumentMenuButtonClass;
+typedef struct _GbDocumentMenuButtonPrivate GbDocumentMenuButtonPrivate;
+
+struct _GbDocumentMenuButton
+{
+  GtkMenuButton parent;
+
+  /*< private >*/
+  GbDocumentMenuButtonPrivate *priv;
+};
+
+struct _GbDocumentMenuButtonClass
+{
+  GtkMenuButtonClass parent;
+
+  void (*document_selected) (GbDocumentMenuButton *button,
+                             GbDocument           *document);
+};
+
+GType              gb_document_menu_button_get_type             (void);
+GtkWidget         *gb_document_menu_button_new                  (void);
+GbDocumentManager *gb_document_menu_button_get_document_manager (GbDocumentMenuButton *button);
+void               gb_document_menu_button_set_document_manager (GbDocumentMenuButton *button,
+                                                                 GbDocumentManager    *document_manager);
+void               gb_document_menu_button_select_document      (GbDocumentMenuButton *button,
+                                                                 GbDocument           *document);
+
+G_END_DECLS
+
+#endif /* GB_DOCUMENT_MENU_BUTTON_H */
diff --git a/src/documents/gb-document-split.c b/src/documents/gb-document-split.c
new file mode 100644
index 0000000..64281d5
--- /dev/null
+++ b/src/documents/gb-document-split.c
@@ -0,0 +1,37 @@
+/* gb-document-split.c
+ *
+ * Copyright (C) 2014 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/>.
+ */
+
+#include "gb-document-split.h"
+
+GType
+gb_document_split_get_type (void)
+{
+  static GType type_id;
+
+  const GEnumValue values[] = {
+    { GB_DOCUMENT_SPLIT_NONE, "GB_DOCUMENT_SPLIT_NONE", "NONE" },
+    { GB_DOCUMENT_SPLIT_LEFT, "GB_DOCUMENT_SPLIT_LEFT", "LEFT" },
+    { GB_DOCUMENT_SPLIT_LEFT, "GB_DOCUMENT_SPLIT_RIGHT", "RIGHT" },
+    { 0 }
+  };
+
+  if (!type_id)
+    type_id = g_enum_register_static ("GbDocumentSplit", values);
+
+  return type_id;
+}
diff --git a/src/documents/gb-document-split.h b/src/documents/gb-document-split.h
new file mode 100644
index 0000000..7ee3f68
--- /dev/null
+++ b/src/documents/gb-document-split.h
@@ -0,0 +1,38 @@
+/* gb-document-split.h
+ *
+ * Copyright (C) 2014 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 GB_DOCUMENT_SPLIT_H
+#define GB_DOCUMENT_SPLIT_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_DOCUMENT_SPLIT (gb_document_split_get_type())
+
+typedef enum {
+  GB_DOCUMENT_SPLIT_NONE  = 0,
+  GB_DOCUMENT_SPLIT_RIGHT = 1,
+  GB_DOCUMENT_SPLIT_LEFT  = 2,
+} GbDocumentSplit;
+
+GType gb_document_split_get_type (void);
+
+G_END_DECLS
+
+#endif /* GB_DOCUMENT_SPLIT_H */
diff --git a/src/documents/gb-document-stack.c b/src/documents/gb-document-stack.c
new file mode 100644
index 0000000..8223ea1
--- /dev/null
+++ b/src/documents/gb-document-stack.c
@@ -0,0 +1,667 @@
+/* gb-document-stack.c
+ *
+ * Copyright (C) 2014 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 "document-stack"
+
+#include <glib/gi18n.h>
+
+#include "gb-document-menu-button.h"
+#include "gb-document-stack.h"
+#include "gb-glib.h"
+
+struct _GbDocumentStackPrivate
+{
+  /* Objects ownen by GbDocumentStack */
+  GbDocumentManager    *document_manager;
+
+  /* Weak references */
+  GbDocumentView       *active_view;
+
+  /* GtkWidgets owned by GtkWidgetClass template */
+  GbDocumentMenuButton *document_button;
+  GtkStack             *controls;
+  GtkButton            *close;
+  GtkStack             *stack;
+  GtkMenuButton        *stack_menu;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbDocumentStack, gb_document_stack, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_ACTIVE_VIEW,
+  PROP_DOCUMENT_MANAGER,
+  LAST_PROP
+};
+
+enum {
+  CREATE_VIEW,
+  EMPTY,
+  LAST_SIGNAL
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint       gSignals [LAST_SIGNAL];
+
+GtkWidget *
+gb_document_stack_new (void)
+{
+  return g_object_new (GB_TYPE_DOCUMENT_STACK, NULL);
+}
+
+static void
+gb_document_stack_remove_view (GbDocumentStack *stack,
+                               GbDocumentView  *view)
+{
+  GbDocument *document = NULL;
+  GtkWidget *visible_child;
+  GtkWidget *controls;
+  gboolean visible;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (GB_IS_DOCUMENT_VIEW (view));
+
+  /* Release our weak pointer */
+  if (view == stack->priv->active_view)
+    gb_clear_weak_pointer (&stack->priv->active_view);
+
+  /* Notify the document view it is being closed */
+  gb_document_view_close (view);
+
+  /* Remove the document view and its controls */
+  controls = gb_document_view_get_controls (view);
+  if (controls)
+    gtk_container_remove (GTK_CONTAINER (stack->priv->controls), controls);
+  gtk_container_remove (GTK_CONTAINER (stack->priv->stack), GTK_WIDGET (view));
+
+  /*
+   * Set the visible child to the first document view. We probably want to
+   * be more intelligent about this in the future. visible_child may be
+   * NULL, which will clear the title string in the menu button.
+   */
+  visible_child = gtk_stack_get_visible_child (stack->priv->stack);
+  if (visible_child)
+    document = gb_document_view_get_document (GB_DOCUMENT_VIEW (visible_child));
+  gb_document_menu_button_select_document (stack->priv->document_button,
+                                           document);
+
+  /* Only show close and stack menu if we have children */
+  visible = (visible_child != NULL);
+  gtk_widget_set_visible (GTK_WIDGET (stack->priv->close), visible);
+  gtk_widget_set_visible (GTK_WIDGET (stack->priv->stack_menu), visible);
+
+  if (!visible_child)
+    g_signal_emit (stack, gSignals [EMPTY], 0);
+}
+
+/**
+ * gb_document_stack_get_document_manager:
+ *
+ * Fetches the document manager for the stack.
+ *
+ * Returns: (transfer none): A #GbDocumentManager
+ */
+GbDocumentManager *
+gb_document_stack_get_document_manager (GbDocumentStack *stack)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+
+  return stack->priv->document_manager;
+}
+
+/**
+ * gb_document_stack_set_document_manager:
+ * @document_manager: A #GbDocumentManager.
+ *
+ * Sets the document manager for the stack. All existing views will be removed
+ * if the #GbDocumentManager is different than the current document manager.
+ */
+void
+gb_document_stack_set_document_manager (GbDocumentStack   *stack,
+                                        GbDocumentManager *document_manager)
+{
+  GbDocumentStackPrivate *priv;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (!document_manager ||
+                    GB_IS_DOCUMENT_MANAGER (document_manager));
+
+  priv = stack->priv;
+
+  if (document_manager != priv->document_manager)
+    {
+      if (priv->document_manager)
+        {
+          GList *list;
+          GList *iter;
+
+          /* Release our document manager */
+          g_clear_object (&priv->document_manager);
+          gb_document_menu_button_set_document_manager (priv->document_button,
+                                                        NULL);
+
+          /* Remove any views lingering in here */
+          list = gtk_container_get_children (GTK_CONTAINER (priv->stack));
+          for (iter = list; iter; iter = iter->next)
+            gb_document_stack_remove_view (stack, iter->data);
+          g_list_free (list);
+        }
+
+      if (document_manager)
+        {
+          priv->document_manager = g_object_ref (document_manager);
+          gb_document_menu_button_set_document_manager (priv->document_button,
+                                                        document_manager);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (stack),
+                                gParamSpecs [PROP_DOCUMENT_MANAGER]);
+    }
+}
+
+/**
+ * gb_document_stack_get_active_view:
+ *
+ * Fetches the active view for the document stack.
+ *
+ * Returns: (transfer none): A #GbDocumentView or %NULL.
+ */
+GbDocumentView *
+gb_document_stack_get_active_view (GbDocumentStack *stack)
+{
+  GtkWidget *child;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+
+  child = gtk_stack_get_visible_child (stack->priv->stack);
+  if (GB_IS_DOCUMENT_VIEW (child))
+    return GB_DOCUMENT_VIEW (child);
+
+  return NULL;
+}
+
+void
+gb_document_stack_set_active_view (GbDocumentStack *stack,
+                                   GbDocumentView  *active_view)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (!active_view || GB_IS_DOCUMENT_VIEW (active_view));
+
+  if (active_view != stack->priv->active_view)
+    {
+      gb_clear_weak_pointer (&stack->priv->active_view);
+      gb_set_weak_pointer (active_view, &stack->priv->active_view);
+
+      if (active_view)
+        {
+          GtkWidget *controls;
+
+          gtk_stack_set_visible_child (stack->priv->stack,
+                                       GTK_WIDGET (active_view));
+
+          controls = gb_document_view_get_controls (active_view);
+
+          if (controls)
+            gtk_stack_set_visible_child (stack->priv->controls, controls);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (stack),
+                                gParamSpecs [PROP_ACTIVE_VIEW]);
+    }
+}
+
+/**
+ * gb_document_stack_find_with_document:
+ *
+ * Finds the first #GbDocumentView containing @document.
+ *
+ * Returns: (transfer none): A #GbDocumentView or %NULL.
+ */
+GtkWidget *
+gb_document_stack_find_with_document (GbDocumentStack *stack,
+                                      GbDocument      *document)
+{
+  GbDocumentView *ret = NULL;
+  GList *list;
+  GList *iter;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+  g_return_val_if_fail (GB_IS_DOCUMENT (document), NULL);
+
+  list = gtk_container_get_children (GTK_CONTAINER (stack->priv->stack));
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      GbDocument *value;
+
+      value = gb_document_view_get_document (GB_DOCUMENT_VIEW (iter->data));
+      if (!value)
+        continue;
+
+      if (document == value)
+        {
+          ret = GB_DOCUMENT_VIEW (iter->data);
+          break;
+        }
+    }
+
+  g_list_free (list);
+
+  return GTK_WIDGET (ret);
+}
+
+/**
+ * gb_document_stack_find_with_type:
+ *
+ * Finds the first #GbDocumentView of type @type_id. The view may be a subclass
+ * of type #GType provided.
+ *
+ * Returns: (transfer none): A #GbDocumentView or %NULL.
+ */
+GtkWidget *
+gb_document_stack_find_with_type (GbDocumentStack *stack,
+                                  GType            type_id)
+{
+  GbDocumentView *ret = NULL;
+  GList *list;
+  GList *iter;
+
+  g_return_val_if_fail (GB_IS_DOCUMENT_STACK (stack), NULL);
+  g_return_val_if_fail (g_type_is_a (type_id, GB_TYPE_DOCUMENT_VIEW), NULL);
+
+  list = gtk_container_get_children (GTK_CONTAINER (stack->priv->stack));
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      GbDocument *value;
+
+      value = gb_document_view_get_document (GB_DOCUMENT_VIEW (iter->data));
+      if (!value)
+        continue;
+
+      if (g_type_is_a (G_TYPE_FROM_INSTANCE (value), type_id))
+        {
+          ret = GB_DOCUMENT_VIEW (iter->data);
+          break;
+        }
+    }
+
+  g_list_free (list);
+
+  return GTK_WIDGET (ret);
+}
+
+void
+gb_document_stack_focus_document (GbDocumentStack *stack,
+                                  GbDocument      *document)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+
+  gb_document_menu_button_select_document (stack->priv->document_button,
+                                           document);
+}
+
+static void
+gb_document_stack_document_selected (GbDocumentStack      *stack,
+                                     GbDocument           *document,
+                                     GbDocumentMenuButton *button)
+{
+  GtkWidget *view;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (GB_IS_DOCUMENT (document));
+  g_return_if_fail (GB_IS_DOCUMENT_MENU_BUTTON (button));
+
+  view = gb_document_stack_find_with_document (stack, document);
+
+  if (!view)
+    {
+      GtkWidget *controls;
+
+      view = gb_document_create_view (document);
+
+      if (!view)
+        {
+          g_warning ("Failed to create view");
+          return;
+        }
+
+      gtk_container_add (GTK_CONTAINER (stack->priv->stack), view);
+
+      controls = gb_document_view_get_controls (GB_DOCUMENT_VIEW (view));
+
+      if (controls)
+        gtk_container_add (GTK_CONTAINER (stack->priv->controls), controls);
+    }
+
+  gtk_widget_set_visible (GTK_WIDGET (stack->priv->close), TRUE);
+  gtk_widget_set_visible (GTK_WIDGET (stack->priv->stack_menu), TRUE);
+  gb_document_stack_set_active_view (stack, GB_DOCUMENT_VIEW (view));
+  gtk_widget_grab_focus (view);
+
+  return;
+}
+
+static void
+gb_document_stack_close_clicked (GbDocumentStack *stack,
+                                 GtkButton       *button)
+{
+  GbDocumentStackPrivate *priv;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+  g_return_if_fail (GTK_IS_BUTTON (button));
+
+  priv = stack->priv;
+
+  if (priv->active_view)
+    gb_document_stack_remove_view (stack, priv->active_view);
+}
+
+static void
+gb_document_stack_grab_focus (GtkWidget *widget)
+{
+  GbDocumentStack *stack = (GbDocumentStack *)widget;
+  GbDocumentView *active_view;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  active_view = gb_document_stack_get_active_view (stack);
+  if (active_view)
+    gtk_widget_grab_focus (GTK_WIDGET (active_view));
+}
+
+static void
+gb_document_stack_constructed (GObject *object)
+{
+  GbDocumentStack *stack = (GbDocumentStack *)object;
+  GApplication *app;
+
+  G_OBJECT_CLASS (gb_document_stack_parent_class)->constructed (object);
+
+  app = g_application_get_default ();
+
+  if (GTK_IS_APPLICATION (app))
+    {
+      GMenu *menu;
+
+      menu = gtk_application_get_menu_by_id (GTK_APPLICATION (app),
+                                             "gb-document-stack-menu");
+
+      if (menu)
+        gtk_menu_button_set_menu_model (stack->priv->stack_menu,
+                                        G_MENU_MODEL (menu));
+    }
+
+  g_signal_connect_object (stack->priv->document_button,
+                           "document-selected",
+                           G_CALLBACK (gb_document_stack_document_selected),
+                           stack,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (stack->priv->close,
+                           "clicked",
+                           G_CALLBACK (gb_document_stack_close_clicked),
+                           stack,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gb_document_stack_move_document_left (GSimpleAction *action,
+                                      GVariant      *parameter,
+                                      gpointer       user_data)
+{
+  GbDocumentStack *stack = user_data;
+  GbDocumentView *view;
+  GbDocument *document;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  view = gb_document_stack_get_active_view (stack);
+  if (!view)
+    return;
+
+  document = gb_document_view_get_document (view);
+  if (!document)
+    return;
+
+  g_signal_emit (stack, gSignals [CREATE_VIEW], 0, document,
+                 GB_DOCUMENT_SPLIT_LEFT);
+
+  gb_document_stack_remove_view (stack, view);
+}
+
+static void
+gb_document_stack_move_document_right (GSimpleAction *action,
+                                       GVariant      *parameter,
+                                       gpointer       user_data)
+{
+  GbDocumentStack *stack = user_data;
+  GbDocumentView *view;
+  GbDocument *document;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  view = gb_document_stack_get_active_view (stack);
+  if (!view)
+    return;
+
+  document = gb_document_view_get_document (view);
+  if (!document)
+    return;
+
+  g_signal_emit (stack, gSignals [CREATE_VIEW], 0, document,
+                 GB_DOCUMENT_SPLIT_RIGHT);
+
+  gb_document_stack_remove_view (stack, view);
+}
+
+static void
+gb_document_stack_split_document_left (GSimpleAction *action,
+                                       GVariant      *parameter,
+                                       gpointer       user_data)
+{
+  GbDocumentStack *stack = user_data;
+  GbDocumentView *view;
+  GbDocument *document;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  view = gb_document_stack_get_active_view (stack);
+  if (!view)
+    return;
+
+  document = gb_document_view_get_document (view);
+  if (!document)
+    return;
+
+  g_signal_emit (stack, gSignals [CREATE_VIEW], 0, document,
+                 GB_DOCUMENT_SPLIT_LEFT);
+}
+
+static void
+gb_document_stack_split_document_right (GSimpleAction *action,
+                                        GVariant      *parameter,
+                                        gpointer       user_data)
+{
+  GbDocumentStack *stack = user_data;
+  GbDocumentView *view;
+  GbDocument *document;
+
+  g_return_if_fail (GB_IS_DOCUMENT_STACK (stack));
+
+  view = gb_document_stack_get_active_view (stack);
+  if (!view)
+    return;
+
+  document = gb_document_view_get_document (view);
+  if (!document)
+    return;
+
+  g_signal_emit (stack, gSignals [CREATE_VIEW], 0, document,
+                 GB_DOCUMENT_SPLIT_RIGHT);
+}
+
+static void
+gb_document_stack_finalize (GObject *object)
+{
+  GbDocumentStackPrivate *priv = GB_DOCUMENT_STACK (object)->priv;
+
+  gb_clear_weak_pointer (&priv->active_view);
+  g_clear_object (&priv->document_manager);
+
+  G_OBJECT_CLASS (gb_document_stack_parent_class)->finalize (object);
+}
+
+static void
+gb_document_stack_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  GbDocumentStack *self = GB_DOCUMENT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_VIEW:
+      g_value_set_object (value, gb_document_stack_get_active_view (self));
+      break;
+
+    case PROP_DOCUMENT_MANAGER:
+      g_value_set_object (value, gb_document_stack_get_document_manager (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_stack_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  GbDocumentStack *self = GB_DOCUMENT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_VIEW:
+      gb_document_stack_set_active_view (self, g_value_get_object (value));
+      break;
+
+    case PROP_DOCUMENT_MANAGER:
+      gb_document_stack_set_document_manager (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_stack_class_init (GbDocumentStackClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = gb_document_stack_constructed;
+  object_class->finalize = gb_document_stack_finalize;
+  object_class->get_property = gb_document_stack_get_property;
+  object_class->set_property = gb_document_stack_set_property;
+
+  widget_class->grab_focus = gb_document_stack_grab_focus;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/gb-document-stack.ui");
+  gtk_widget_class_bind_template_child_internal_private (widget_class, GbDocumentStack, close);
+  gtk_widget_class_bind_template_child_internal_private (widget_class, GbDocumentStack, stack);
+  gtk_widget_class_bind_template_child_internal_private (widget_class, GbDocumentStack, stack_menu);
+  gtk_widget_class_bind_template_child_internal_private (widget_class, GbDocumentStack, controls);
+  gtk_widget_class_bind_template_child_internal_private (widget_class, GbDocumentStack, document_button);
+
+  gParamSpecs [PROP_ACTIVE_VIEW] =
+    g_param_spec_object ("active-view",
+                         _("Active View"),
+                         _("The active view within the stack."),
+                         GB_TYPE_DOCUMENT_VIEW,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_ACTIVE_VIEW,
+                                   gParamSpecs [PROP_ACTIVE_VIEW]);
+
+  gParamSpecs [PROP_DOCUMENT_MANAGER] =
+    g_param_spec_object ("document-manager",
+                         _("Document Manager"),
+                         _("The document manager for the stack."),
+                         GB_TYPE_DOCUMENT_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_DOCUMENT_MANAGER,
+                                   gParamSpecs [PROP_DOCUMENT_MANAGER]);
+
+  gSignals [CREATE_VIEW] =
+    g_signal_new ("create-view",
+                  GB_TYPE_DOCUMENT_STACK,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GbDocumentStackClass, create_view),
+                  NULL, NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  2,
+                  GB_TYPE_DOCUMENT,
+                  GB_TYPE_DOCUMENT_SPLIT);
+
+  /**
+   * GbDocumentStack::empty:
+   *
+   * This signal is emitted when the last document view has been closed.
+   * The parent grid may want to destroy the grid when this occurs.
+   */
+  gSignals [EMPTY] =
+    g_signal_new ("empty",
+                  GB_TYPE_DOCUMENT_STACK,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GbDocumentStackClass, empty),
+                  NULL, NULL,
+                  g_cclosure_marshal_generic,
+                  G_TYPE_NONE,
+                  0);
+
+  g_type_ensure (GB_TYPE_DOCUMENT_MENU_BUTTON);
+}
+
+static void
+gb_document_stack_init (GbDocumentStack *self)
+{
+  const GActionEntry entries[] = {
+    { "move-document-left", gb_document_stack_move_document_left },
+    { "move-document-right", gb_document_stack_move_document_right },
+    { "split-document-left", gb_document_stack_split_document_left },
+    { "split-document-right", gb_document_stack_split_document_right },
+  };
+  GSimpleActionGroup *actions;
+
+  self->priv = gb_document_stack_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions), entries,
+                                   G_N_ELEMENTS (entries), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "stack",
+                                  G_ACTION_GROUP (actions));
+  g_clear_object (&actions);
+}
diff --git a/src/documents/gb-document-stack.h b/src/documents/gb-document-stack.h
new file mode 100644
index 0000000..420557f
--- /dev/null
+++ b/src/documents/gb-document-stack.h
@@ -0,0 +1,77 @@
+/* gb-document-stack.h
+ *
+ * Copyright (C) 2014 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 GB_DOCUMENT_STACK_H
+#define GB_DOCUMENT_STACK_H
+
+#include <gtk/gtk.h>
+
+#include "gb-document-manager.h"
+#include "gb-document-split.h"
+#include "gb-document-view.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_DOCUMENT_STACK            (gb_document_stack_get_type())
+#define GB_DOCUMENT_STACK(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_STACK, 
GbDocumentStack))
+#define GB_DOCUMENT_STACK_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_STACK, 
GbDocumentStack const))
+#define GB_DOCUMENT_STACK_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_DOCUMENT_STACK, 
GbDocumentStackClass))
+#define GB_IS_DOCUMENT_STACK(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GB_TYPE_DOCUMENT_STACK))
+#define GB_IS_DOCUMENT_STACK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_DOCUMENT_STACK))
+#define GB_DOCUMENT_STACK_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_DOCUMENT_STACK, 
GbDocumentStackClass))
+
+typedef struct _GbDocumentStack        GbDocumentStack;
+typedef struct _GbDocumentStackClass   GbDocumentStackClass;
+typedef struct _GbDocumentStackPrivate GbDocumentStackPrivate;
+
+struct _GbDocumentStack
+{
+  GtkBox parent;
+
+  /*< private >*/
+  GbDocumentStackPrivate *priv;
+};
+
+struct _GbDocumentStackClass
+{
+  GtkBoxClass parent;
+
+  void (*create_view) (GbDocumentStack *stack,
+                       GbDocument      *document,
+                       GbDocumentSplit  split);
+  void (*empty)       (GbDocumentStack *stack);
+};
+
+GType              gb_document_stack_get_type             (void);
+GtkWidget         *gb_document_stack_new                  (void);
+void               gb_document_stack_focus_document       (GbDocumentStack   *stack,
+                                                           GbDocument        *document);
+GbDocumentManager *gb_document_stack_get_document_manager (GbDocumentStack   *stack);
+void               gb_document_stack_set_document_manager (GbDocumentStack   *stack,
+                                                           GbDocumentManager *manager);
+GbDocumentView    *gb_document_stack_get_active_view      (GbDocumentStack   *stack);
+void               gb_document_stack_set_active_view      (GbDocumentStack   *stack,
+                                                           GbDocumentView    *view);
+GtkWidget         *gb_document_stack_find_with_document   (GbDocumentStack   *stack,
+                                                           GbDocument        *document);
+GtkWidget         *gb_document_stack_find_with_type       (GbDocumentStack   *stack,
+                                                           GType              type_id);
+
+G_END_DECLS
+
+#endif /* GB_DOCUMENT_STACK_H */
diff --git a/src/documents/gb-document-view.c b/src/documents/gb-document-view.c
new file mode 100644
index 0000000..2c6f92b
--- /dev/null
+++ b/src/documents/gb-document-view.c
@@ -0,0 +1,202 @@
+/* gb-document-view.c
+ *
+ * Copyright (C) 2014 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 "document-view"
+
+#include <glib/gi18n.h>
+
+#include "gb-document-view.h"
+
+struct _GbDocumentViewPrivate
+{
+  GtkBox *controls;
+};
+
+static void buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GbDocumentView,
+                                  gb_document_view,
+                                  GTK_TYPE_BOX,
+                                  G_ADD_PRIVATE (GbDocumentView)
+                                  G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+                                                         buildable_init))
+
+enum {
+  PROP_0,
+  PROP_CONTROLS,
+  PROP_DOCUMENT,
+  LAST_PROP
+};
+
+enum {
+  CLOSE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+static guint       gSignals [LAST_SIGNAL];
+
+/**
+ * gb_document_view_get_controls:
+ *
+ * This returns a #GtkBox that can be used to place controls that should be
+ * displayed at the top of the document stack. It is available in the Gtk
+ * template as an internal child named "controls".
+ *
+ * Returns: (transfer none): A #GtkBox
+ */
+GtkWidget *
+gb_document_view_get_controls (GbDocumentView *view)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_VIEW (view), NULL);
+
+  return GTK_WIDGET (view->priv->controls);
+}
+
+/**
+ * gb_document_view_get_document:
+ *
+ * Retrieves the #GbDocument being viewed.
+ *
+ * Returns: (transfer none): A #GbDocument.
+ */
+GbDocument *
+gb_document_view_get_document (GbDocumentView *view)
+{
+  g_return_val_if_fail (GB_IS_DOCUMENT_VIEW (view), NULL);
+
+  if (GB_DOCUMENT_VIEW_GET_CLASS (view)->get_document)
+    return GB_DOCUMENT_VIEW_GET_CLASS (view)->get_document (view);
+
+  g_warning ("%s() does not implement get_document() vfunc.",
+             g_type_name (G_TYPE_FROM_INSTANCE (view)));
+
+  return NULL;
+}
+
+void
+gb_document_view_close (GbDocumentView *view)
+{
+  g_return_if_fail (GB_IS_DOCUMENT_VIEW (view));
+
+  g_signal_emit (view, gSignals [CLOSE], 0);
+}
+
+static GObject *
+gb_document_view_get_internal_child (GtkBuildable *buildable,
+                                     GtkBuilder   *builder,
+                                     const gchar  *childname)
+{
+  GbDocumentView *view = (GbDocumentView *)buildable;
+
+  if (g_strcmp0 (childname, "controls") == 0)
+    return G_OBJECT (view->priv->controls);
+
+  return NULL;
+}
+
+static void
+gb_document_view_destroy (GtkWidget *widget)
+{
+  GbDocumentViewPrivate *priv = GB_DOCUMENT_VIEW (widget)->priv;
+
+  g_clear_object (&priv->controls);
+
+  GTK_WIDGET_CLASS (gb_document_view_parent_class)->destroy (widget);
+}
+
+static void
+gb_document_view_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GbDocumentView *self = GB_DOCUMENT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTROLS:
+      g_value_set_object (value, gb_document_view_get_controls (self));
+      break;
+
+    case PROP_DOCUMENT:
+      g_value_set_object (value, gb_document_view_get_document (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_document_view_class_init (GbDocumentViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = gb_document_view_get_property;
+
+  widget_class->destroy = gb_document_view_destroy;
+
+  gParamSpecs [PROP_CONTROLS] =
+    g_param_spec_object ("controls",
+                         _("Controls"),
+                         _("The widget containing the view controls."),
+                         GTK_TYPE_BOX,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_CONTROLS,
+                                   gParamSpecs [PROP_CONTROLS]);
+
+  gParamSpecs [PROP_DOCUMENT] =
+    g_param_spec_object ("document",
+                         _("Document"),
+                         _("The document being viewed."),
+                         GB_TYPE_DOCUMENT,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+  g_object_class_install_property (object_class, PROP_DOCUMENT,
+                                   gParamSpecs [PROP_DOCUMENT]);
+
+  gSignals [CLOSE] =
+    g_signal_new ("close",
+                  GB_TYPE_DOCUMENT_VIEW,
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (GbDocumentViewClass, close),
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  0);
+}
+
+static void
+gb_document_view_init (GbDocumentView *self)
+{
+  self->priv = gb_document_view_get_instance_private (self);
+
+  self->priv->controls = g_object_new (GTK_TYPE_BOX,
+                                       "orientation", GTK_ORIENTATION_HORIZONTAL,
+                                       "visible", TRUE,
+                                       NULL);
+  g_object_ref_sink (self->priv->controls);
+}
+
+static void
+buildable_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = gb_document_view_get_internal_child;
+}
diff --git a/src/documents/gb-document-view.h b/src/documents/gb-document-view.h
new file mode 100644
index 0000000..b50be8c
--- /dev/null
+++ b/src/documents/gb-document-view.h
@@ -0,0 +1,64 @@
+/* gb-document-view.h
+ *
+ * Copyright (C) 2014 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 GB_DOCUMENT_VIEW_H
+#define GB_DOCUMENT_VIEW_H
+
+#include <gtk/gtk.h>
+
+#include "gb-document.h"
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_DOCUMENT_VIEW            (gb_document_view_get_type())
+#define GB_DOCUMENT_VIEW(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_VIEW, 
GbDocumentView))
+#define GB_DOCUMENT_VIEW_CONST(obj)      (G_TYPE_CHECK_INSTANCE_CAST ((obj), GB_TYPE_DOCUMENT_VIEW, 
GbDocumentView const))
+#define GB_DOCUMENT_VIEW_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass),  GB_TYPE_DOCUMENT_VIEW, 
GbDocumentViewClass))
+#define GB_IS_DOCUMENT_VIEW(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GB_TYPE_DOCUMENT_VIEW))
+#define GB_IS_DOCUMENT_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),  GB_TYPE_DOCUMENT_VIEW))
+#define GB_DOCUMENT_VIEW_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj),  GB_TYPE_DOCUMENT_VIEW, 
GbDocumentViewClass))
+
+typedef struct _GbDocumentView        GbDocumentView;
+typedef struct _GbDocumentViewClass   GbDocumentViewClass;
+typedef struct _GbDocumentViewPrivate GbDocumentViewPrivate;
+
+struct _GbDocumentView
+{
+  GtkBox parent;
+
+  /*< private >*/
+  GbDocumentViewPrivate *priv;
+};
+
+struct _GbDocumentViewClass
+{
+  GtkBoxClass parent;
+
+  GbDocument *(*get_document) (GbDocumentView *view);
+  gboolean    (*close)        (GbDocumentView *view);
+};
+
+GType               gb_document_view_get_type     (void);
+GbDocumentView     *gb_document_view_new          (void);
+void                gb_document_view_close        (GbDocumentView *view);
+GbDocument         *gb_document_view_get_document (GbDocumentView *view);
+GtkWidget          *gb_document_view_get_controls (GbDocumentView *view);
+
+G_END_DECLS
+
+#endif /* GB_DOCUMENT_VIEW_H */
diff --git a/src/documents/gb-document.h b/src/documents/gb-document.h
index f572dac..bce458f 100644
--- a/src/documents/gb-document.h
+++ b/src/documents/gb-document.h
@@ -19,7 +19,7 @@
 #ifndef GB_DOCUMENT_H
 #define GB_DOCUMENT_H
 
-#include "gb-tab.h"
+#include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
@@ -28,22 +28,22 @@ G_BEGIN_DECLS
 #define GB_IS_DOCUMENT(obj)            (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GB_TYPE_DOCUMENT))
 #define GB_DOCUMENT_GET_INTERFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), GB_TYPE_DOCUMENT, 
GbDocumentInterface))
 
-typedef struct _GbDocument      GbDocument;
+typedef struct _GbDocument          GbDocument;
 typedef struct _GbDocumentInterface GbDocumentInterface;
 
 struct _GbDocumentInterface
 {
   GTypeInterface parent;
 
-  gboolean     (*get_can_save) (GbDocument *document);
+  gboolean     (*get_modified) (GbDocument *document);
   const gchar *(*get_title)    (GbDocument *document);
-  GbTab       *(*create_tab)   (GbDocument *document);
+  GtkWidget   *(*create_view)  (GbDocument *document);
 };
 
 GType        gb_document_get_type     (void) G_GNUC_CONST;
-gboolean     gb_document_get_can_save (GbDocument *document);
+gboolean     gb_document_get_modified (GbDocument *document);
 const gchar *gb_document_get_title    (GbDocument *document);
-GbTab       *gb_document_create_tab   (GbDocument *document);
+GtkWidget   *gb_document_create_view  (GbDocument *document);
 
 G_END_DECLS
 


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