[gnome-builder/wip/chergert/perspective] libide: start on IdeLayout abstraction for libide



commit dac1b995c4b731dc3149716c9cc63ff2cdb8eb37
Author: Christian Hergert <chergert redhat com>
Date:   Fri Nov 13 01:35:18 2015 -0800

    libide: start on IdeLayout abstraction for libide
    
    This is mostly a cleanup/simplification/port of GbView, GbViewStack,
    and GbViewGrid.
    
    We are going to drop the GbDocument/GbView split that was prevelant
    previously because it caused much more pain than it was worth.

 data/ui/ide-layout-stack.ui           |  262 ++++++++++
 data/ui/ide-layout-view.ui            |   71 +++
 libide/Makefile.am                    |   12 +
 libide/ide-enums.c.in                 |    1 +
 libide/ide-layout-grid.c              |  898 +++++++++++++++++++++++++++++++++
 libide/ide-layout-grid.h              |   50 ++
 libide/ide-layout-stack-actions.c     |  304 +++++++++++
 libide/ide-layout-stack-actions.h     |   30 ++
 libide/ide-layout-stack-private.h     |   66 +++
 libide/ide-layout-stack-split.h       |   44 ++
 libide/ide-layout-stack.c             |  755 +++++++++++++++++++++++++++
 libide/ide-layout-stack.h             |   42 ++
 libide/ide-layout-view.c              |  365 +++++++++++++
 libide/ide-layout-view.h              |   68 +++
 libide/resources/libide.gresource.xml |    2 +
 15 files changed, 2970 insertions(+), 0 deletions(-)
---
diff --git a/data/ui/ide-layout-stack.ui b/data/ui/ide-layout-stack.ui
new file mode 100644
index 0000000..63561b5
--- /dev/null
+++ b/data/ui/ide-layout-stack.ui
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeLayoutStack" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="notebook"/>
+              <class name="header"/>
+            </style>
+            <child>
+              <object class="GtkEventBox" id="header_event_box">
+                <property name="above-child">false</property>
+                <property name="visible-window">false</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">horizontal</property>
+                    <property name="hexpand">true</property>
+                    <property name="visible">true</property>
+                    <property name="margin-bottom">3</property>
+                    <property name="margin-end">7</property>
+                    <property name="margin-start">6</property>
+                    <property name="margin-top">3</property>
+                    <child>
+                      <object class="GtkMenuButton" id="views_button">
+                        <property name="visible">true</property>
+                        <property name="focus-on-click">false</property>
+                        <property name="popover">views_popover</property>
+                        <property name="sensitive">false</property>
+                        <style>
+                          <class name="image-button"/>
+                          <class name="flat"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">true</property>
+                            <property name="icon-name">view-list-symbolic</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="pack-type">start</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="margin-start">3</property>
+                        <property name="margin-end">3</property>
+                        <property name="margin-top">4</property>
+                        <property name="margin-bottom">4</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="orientation">horizontal</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="navigation"/>
+                        </style>
+                        <child>
+                          <object class="GtkButton" id="go_backward">
+                            <property name="visible">true</property>
+                            <property name="action-name">view-stack.go-backward</property>
+                            <property name="focus-on-click">false</property>
+                            <style>
+                              <class name="flat"/>
+                              <class name="image-button"/>
+                            </style>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">go-previous-symbolic</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton" id="go_forward">
+                            <property name="visible">true</property>
+                            <property name="action-name">view-stack.go-forward</property>
+                            <property name="focus-on-click">false</property>
+                            <style>
+                              <class name="flat"/>
+                              <class name="image-button"/>
+                            </style>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">go-next-symbolic</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="margin-start">3</property>
+                        <property name="margin-end">3</property>
+                        <property name="margin-top">4</property>
+                        <property name="margin-bottom">4</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkMenuButton" id="document_button">
+                        <property name="focus-on-click">false</property>
+                        <property name="hexpand">false</property>
+                        <property name="popover">popover</property>
+                        <!-- Sensitive is not being respected,
+                             likely due to popover being set. -->
+                        <property name="sensitive">false</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="flat"/>
+                          <class name="text-button"/>
+                          <class name="document-button"/>
+                        </style>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="spacing">6</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkLabel" id="title_label">
+                                <property name="hexpand">false</property>
+                                <property name="visible">true</property>
+                                <property name="ellipsize">start</property>
+                                <property name="valign">baseline</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="modified_label">
+                                <property name="halign">fill</property>
+                                <property name="hexpand">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="label">•</property>
+                                <property name="valign">baseline</property>
+                                <property name="visible">false</property>
+                              </object>
+                              <packing>
+                                <property name="pack-type">end</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkArrow">
+                                <property name="arrow-type">down</property>
+                                <property name="margin-top">2</property>
+                                <property name="valign">baseline</property>
+                                <property name="visible">true</property>
+                              </object>
+                              <packing>
+                                <property name="pack-type">end</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="padding">6</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="close_button">
+                        <property name="action-name">view-stack.close</property>
+                        <property name="visible">true</property>
+                        <property name="focus-on-click">false</property>
+                        <style>
+                          <class name="image-button"/>
+                          <class name="flat"/>
+                          <class name="small-button"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">true</property>
+                            <property name="icon-name">window-close-symbolic</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSeparator">
+                        <property name="margin-start">3</property>
+                        <property name="margin-end">3</property>
+                        <property name="margin-top">4</property>
+                        <property name="margin-bottom">4</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                      </object>
+                      <packing>
+                        <!--
+                            this padding is to make things line up with header bar.
+                            unfortunately, this was annoying to get right with css.
+                            feel free to come fix it.
+                        -->
+                        <property name="padding">1</property>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="controls">
+                        <property name="visible">true</property>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="homogeneous">false</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkPopover" id="popover">
+  </object>
+  <object class="GtkPopover" id="views_popover">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GbScrolledWindow">
+            <property name="max-content-height">400</property>
+            <property name="min-content-height">30</property>
+            <property name="min-content-width">100</property>
+            <property name="max-content-width">600</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkListBox" id="views_listbox">
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/ide-layout-view.ui b/data/ui/ide-layout-view.ui
new file mode 100644
index 0000000..56a904e
--- /dev/null
+++ b/data/ui/ide-layout-view.ui
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="IdeLayoutView" parent="GtkBox">
+  </template>
+  <menu id="menu">
+    <section>
+      <attribute name="id">splits-section</attribute>
+      <attribute name="display-hint">horizontal-buttons</attribute>
+      <attribute name="label" translatable="yes">Split</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Split Left</attribute>
+        <attribute name="action">view-stack.split-left</attribute>
+        <attribute name="verb-icon">builder-split-tab-left-symbolic</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Split Right</attribute>
+        <attribute name="action">view-stack.split-right</attribute>
+        <attribute name="verb-icon">builder-split-tab-right-symbolic</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Split Down</attribute>
+        <attribute name="action">view-stack.split-down</attribute>
+        <attribute name="verb-icon">builder-split-tab-symbolic</attribute>
+      </item>
+    </section>
+    <section>
+      <attribute name="id">move-section</attribute>
+      <attribute name="display-hint">horizontal-buttons</attribute>
+      <attribute name="label" translatable="yes">Move</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Move Left</attribute>
+        <attribute name="action">view-stack.move-left</attribute>
+        <attribute name="verb-icon">builder-move-left-symbolic</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Move Right</attribute>
+        <attribute name="action">view-stack.move-right</attribute>
+        <attribute name="verb-icon">builder-move-right-symbolic</attribute>
+      </item>
+    </section>
+    <section>
+      <attribute name="id">preview-section</attribute>
+    </section>
+    <section>
+      <attribute name="id">save-section</attribute>
+      <item>
+        <attribute name="label" translatable="yes">_Save</attribute>
+        <attribute name="action">view.save</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Save As</attribute>
+        <attribute name="action">view.save-as</attribute>
+      </item>
+    </section>
+    <section>
+      <attribute name="id">print-section</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Print</attribute>
+        <attribute name="action">view.print</attribute>
+      </item>
+    </section>
+    <section>
+      <attribute name="id">close-section</attribute>
+      <item>
+        <attribute name="label" translatable="yes">_Close</attribute>
+        <attribute name="action">view-stack.close</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/libide/Makefile.am b/libide/Makefile.am
index bfff518..020bbbd 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -94,8 +94,19 @@ libide_1_0_la_public_sources = \
        ide-indenter.h \
        ide-layout.c \
        ide-layout.h \
+       ide-layout-grid.c \
+       ide-layout-grid.h \
        ide-layout-pane.c \
        ide-layout-pane.h \
+       ide-layout-stack.c \
+       ide-layout-stack.h \
+       ide-layout-stack-actions.c \
+       ide-layout-stack-actions.h \
+       ide-layout-stack-private.h \
+       ide-layout-view.c \
+       ide-layout-view.h \
+       ide-loader.c \
+       ide-loader.h \
        ide-log.c \
        ide-log.h \
        ide-macros.h \
@@ -420,6 +431,7 @@ glib_enum_headers =  \
        ide-diagnostic.h \
        ide-highlighter.h \
        ide-indent-style.h \
+       ide-layout-stack-split.h \
        ide-source-view.h \
        ide-symbol.h \
        ide-thread-pool.h \
diff --git a/libide/ide-enums.c.in b/libide/ide-enums.c.in
index dec08b3..ccdac74 100644
--- a/libide/ide-enums.c.in
+++ b/libide/ide-enums.c.in
@@ -10,6 +10,7 @@
 #include "ide-doap.h"
 #include "ide-highlighter.h"
 #include "ide-indent-style.h"
+#include "ide-layout-stack-split.h"
 #include "ide-source-view.h"
 #include "ide-symbol.h"
 #include "ide-thread-pool.h"
diff --git a/libide/ide-layout-grid.c b/libide/ide-layout-grid.c
new file mode 100644
index 0000000..102a42d
--- /dev/null
+++ b/libide/ide-layout-grid.c
@@ -0,0 +1,898 @@
+/* ide-layout-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 "ide-layout-grid"
+
+#include <glib/gi18n.h>
+
+#include "ide-layout-grid.h"
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-private.h"
+#include "ide-layout-view.h"
+
+struct _IdeLayoutGrid
+{
+  GtkBin          parent_instance;
+
+  IdeLayoutStack *last_focus;
+};
+
+G_DEFINE_TYPE (IdeLayoutGrid, ide_layout_grid, GTK_TYPE_BIN)
+
+static void ide_layout_grid_make_homogeneous (IdeLayoutGrid *self);
+
+GtkWidget *
+ide_layout_grid_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_GRID, NULL);
+}
+
+static void
+ide_layout_grid_remove_stack (IdeLayoutGrid  *self,
+                              IdeLayoutStack *stack)
+{
+  GtkWidget *new_focus;
+  GList *stacks;
+  GList *iter;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (stack));
+
+  stacks = ide_layout_grid_get_stacks (self);
+
+  /* refuse to remove the stack if there is only one */
+  if (g_list_length (stacks) == 1)
+    return;
+
+  new_focus = ide_layout_grid_get_stack_before (self, stack);
+  if (!new_focus)
+    new_focus = ide_layout_grid_get_stack_after (self, stack);
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      IdeLayoutStack *item = IDE_LAYOUT_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 (self));
+              child2 = gtk_paned_get_child2 (GTK_PANED (paned));
+              g_object_ref (child2);
+              gtk_container_remove (GTK_CONTAINER (paned), child2);
+              gtk_container_remove (GTK_CONTAINER (self), paned);
+              gtk_container_add (GTK_CONTAINER (self), 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 ();
+
+          ide_layout_grid_make_homogeneous (self);
+
+          break;
+        }
+    }
+
+  if (new_focus)
+    gtk_widget_grab_focus (new_focus);
+
+  g_list_free (stacks);
+}
+
+static GtkWidget *
+ide_layout_grid_get_first_stack (IdeLayoutGrid *self)
+{
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  child = gtk_bin_get_child (GTK_BIN (self));
+
+  if (GTK_IS_PANED (child))
+    {
+      child = gtk_paned_get_child1 (GTK_PANED (child));
+      if (IDE_IS_LAYOUT_STACK (child))
+        return child;
+    }
+
+  return NULL;
+}
+
+static GtkWidget *
+ide_layout_grid_get_last_stack (IdeLayoutGrid *self)
+{
+  GtkWidget *child;
+  GtkWidget *child2;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  child = gtk_bin_get_child (GTK_BIN (self));
+
+  while (GTK_IS_PANED (child) &&
+         (child2 = gtk_paned_get_child2 (GTK_PANED (child))))
+    child = child2;
+
+  child = gtk_paned_get_child1 (GTK_PANED (child));
+
+  if (IDE_IS_LAYOUT_STACK (child))
+    return child;
+
+  return NULL;
+}
+
+static void
+ide_layout_grid_focus_neighbor (IdeLayoutGrid    *self,
+                                GtkDirectionType  dir,
+                                IdeLayoutStack   *stack)
+{
+  GtkWidget *active_view;
+  GtkWidget *neighbor = NULL;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (stack));
+
+  switch ((int)dir)
+    {
+    case GTK_DIR_UP:
+    case GTK_DIR_TAB_BACKWARD:
+      active_view = ide_layout_stack_get_active_view (stack);
+      if (active_view && gtk_widget_child_focus (active_view, dir))
+        break;
+      /* fallthrough */
+    case GTK_DIR_LEFT:
+      neighbor = ide_layout_grid_get_stack_before (self, stack);
+      if (!neighbor)
+        neighbor = ide_layout_grid_get_last_stack (self);
+      break;
+
+    case GTK_DIR_DOWN:
+    case GTK_DIR_TAB_FORWARD:
+      active_view = ide_layout_stack_get_active_view (stack);
+      if (active_view && gtk_widget_child_focus (active_view, dir))
+        break;
+      /* fallthrough */
+    case GTK_DIR_RIGHT:
+      neighbor = ide_layout_grid_get_stack_after (self, stack);
+      if (!neighbor)
+        neighbor = ide_layout_grid_get_first_stack (self);
+      break;
+
+    default:
+      break;
+    }
+
+  if (neighbor != NULL)
+    gtk_widget_grab_focus (neighbor);
+}
+
+static void
+ide_layout_grid_focus_neighbor_action (GSimpleAction *action,
+                                       GVariant      *param,
+                                       gpointer       user_data)
+{
+  IdeLayoutGrid *self = user_data;
+  GtkDirectionType dir;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  dir = g_variant_get_int32 (param);
+
+  if (self->last_focus)
+    ide_layout_grid_focus_neighbor (self, dir, self->last_focus);
+}
+
+static void
+ide_layout_grid_stack_empty (IdeLayoutGrid  *self,
+                          IdeLayoutStack *stack)
+{
+  GList *stacks;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (stack));
+
+  stacks = ide_layout_grid_get_stacks (self);
+
+  g_assert (stacks != NULL);
+
+  if (g_list_length (stacks) == 1)
+    goto cleanup;
+
+  ide_layout_grid_focus_neighbor (self, GTK_DIR_LEFT, stack);
+  ide_layout_grid_remove_stack (self, stack);
+
+cleanup:
+  g_list_free (stacks);
+}
+
+static void
+ide_layout_grid_stack_split (IdeLayoutGrid      *self,
+                             IdeLayoutView      *view,
+                             IdeLayoutGridSplit  split,
+                             IdeLayoutStack     *stack)
+{
+  GtkWidget *target_stack = NULL;
+  IdeLayoutView *target_view = NULL;
+
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_LAYOUT_STACK (stack));
+
+  switch (split)
+    {
+    case IDE_LAYOUT_GRID_SPLIT_LEFT:
+      target_view = ide_layout_view_create_split (view);
+      if (target_view == NULL)
+        return;
+
+      target_stack = ide_layout_grid_get_stack_before (self, stack);
+      if (target_stack == NULL)
+        target_stack = ide_layout_grid_add_stack_before (self, stack);
+
+      ide_layout_stack_add (GTK_CONTAINER (target_stack), GTK_WIDGET (target_view));
+      ide_layout_stack_set_active_view (IDE_LAYOUT_STACK (target_stack), GTK_WIDGET (target_view));
+
+      break;
+
+    case IDE_LAYOUT_GRID_SPLIT_MOVE_LEFT:
+      target_stack = ide_layout_grid_get_stack_before (self, stack);
+      if (target_stack == NULL)
+        target_stack = ide_layout_grid_add_stack_before (self, stack);
+
+      g_object_ref (view);
+      ide_layout_stack_remove (stack, GTK_WIDGET (view));
+      ide_layout_stack_add (GTK_CONTAINER (target_stack), GTK_WIDGET (view));
+      ide_layout_stack_set_active_view (IDE_LAYOUT_STACK (target_stack), GTK_WIDGET (view));
+      g_object_unref (view);
+
+      break;
+
+    case IDE_LAYOUT_GRID_SPLIT_RIGHT:
+      target_view = ide_layout_view_create_split (view);
+      if (target_view == NULL)
+        return;
+
+      target_stack = ide_layout_grid_get_stack_after (self, stack);
+      if (target_stack == NULL)
+        target_stack = ide_layout_grid_add_stack_after (self, stack);
+
+      ide_layout_stack_add (GTK_CONTAINER (target_stack), GTK_WIDGET (target_view));
+      ide_layout_stack_set_active_view (IDE_LAYOUT_STACK (target_stack), GTK_WIDGET (target_view));
+
+      break;
+
+    case IDE_LAYOUT_GRID_SPLIT_MOVE_RIGHT:
+      target_stack = ide_layout_grid_get_stack_after (self, stack);
+      if (target_stack == NULL)
+        target_stack = ide_layout_grid_add_stack_after (self, stack);
+
+      g_object_ref (view);
+      ide_layout_stack_remove (stack, GTK_WIDGET (view));
+      ide_layout_stack_add (GTK_CONTAINER (target_stack), GTK_WIDGET (view));
+      ide_layout_stack_set_active_view (IDE_LAYOUT_STACK (target_stack), GTK_WIDGET (view));
+      g_object_unref (view);
+
+      break;
+
+    default:
+      g_assert_not_reached ();
+      break;
+    }
+}
+
+static GtkPaned *
+ide_layout_grid_create_paned (IdeLayoutGrid *self)
+{
+  return g_object_new (GTK_TYPE_PANED,
+                       "orientation", GTK_ORIENTATION_HORIZONTAL,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static IdeLayoutStack *
+ide_layout_grid_create_stack (IdeLayoutGrid *self)
+{
+  IdeLayoutStack *stack;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  stack = g_object_new (IDE_TYPE_LAYOUT_STACK,
+                        "visible", TRUE,
+                        NULL);
+
+  g_signal_connect_object (stack,
+                           "empty",
+                           G_CALLBACK (ide_layout_grid_stack_empty),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (stack,
+                           "split",
+                           G_CALLBACK (ide_layout_grid_stack_split),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  return stack;
+}
+
+static void
+ide_layout_grid_make_homogeneous (IdeLayoutGrid *self)
+{
+  GtkWidget *child;
+  GList *stacks;
+  GList *iter;
+  GtkAllocation alloc;
+  guint count;
+  guint position;
+  gint handle_size = 1;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  child = gtk_bin_get_child (GTK_BIN (self));
+  gtk_widget_style_get (child, "handle-size", &handle_size, NULL);
+
+  stacks = ide_layout_grid_get_stacks (self);
+  count = MAX (1, g_list_length (stacks));
+  position = (alloc.width - (handle_size * (count - 1))) / count;
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      GtkWidget *parent;
+
+      parent = gtk_widget_get_parent (iter->data);
+      g_assert (GTK_IS_PANED (parent));
+
+      gtk_paned_set_position (GTK_PANED (parent), position);
+    }
+
+  g_list_free (stacks);
+}
+
+/**
+ * ide_layout_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 Ide.LayoutStack): A #GList.
+ */
+GList *
+ide_layout_grid_get_stacks (IdeLayoutGrid *self)
+{
+  GtkWidget *paned;
+  GList *list = NULL;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  paned = gtk_bin_get_child (GTK_BIN (self));
+
+  while (paned)
+    {
+      GtkWidget *stack;
+
+      stack = gtk_paned_get_child1 (GTK_PANED (paned));
+
+      if (IDE_IS_LAYOUT_STACK (stack))
+        list = g_list_append (list, stack);
+
+      paned = gtk_paned_get_child2 (GTK_PANED (paned));
+    }
+
+#ifndef IDE_DISABLE_TRACE
+  {
+    GList *iter;
+
+    for (iter = list; iter; iter = iter->next)
+      g_assert (IDE_IS_LAYOUT_STACK (iter->data));
+  }
+#endif
+
+  return list;
+}
+
+/**
+ * ide_layout_grid_add_stack_before:
+ *
+ * Returns: (transfer none) (type Ide.LayoutStack): The new view stack.
+ */
+GtkWidget *
+ide_layout_grid_add_stack_before (IdeLayoutGrid  *self,
+                                  IdeLayoutStack *stack)
+{
+  IdeLayoutStack *new_stack;
+  GtkWidget *parent;
+  GtkWidget *grandparent;
+  GtkPaned *new_paned;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  new_paned = ide_layout_grid_create_paned (self);
+  new_stack = ide_layout_grid_create_stack (self);
+  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 (IDE_IS_LAYOUT_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 ();
+
+  ide_layout_grid_make_homogeneous (self);
+
+  return GTK_WIDGET (new_stack);
+}
+
+/**
+ * ide_layout_grid_add_stack_after:
+ *
+ * Returns: (transfer none) (type Ide.LayoutStack): The new view stack.
+ */
+GtkWidget *
+ide_layout_grid_add_stack_after (IdeLayoutGrid  *self,
+                                 IdeLayoutStack *stack)
+{
+  IdeLayoutStack *new_stack;
+  GtkWidget *parent;
+  GtkPaned *new_paned;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  new_paned = ide_layout_grid_create_paned (self);
+  new_stack = ide_layout_grid_create_stack (self);
+  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 ();
+
+  ide_layout_grid_make_homogeneous (self);
+
+  return GTK_WIDGET (new_stack);
+}
+
+/**
+ * ide_layout_grid_get_stack_before:
+ *
+ * Returns: (nullable) (transfer none) (type Ide.LayoutStack): The view stack.
+ */
+GtkWidget *
+ide_layout_grid_get_stack_before (IdeLayoutGrid  *self,
+                                  IdeLayoutStack *stack)
+{
+  GtkWidget *parent;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_LAYOUT_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;
+}
+
+/**
+ * ide_layout_grid_get_stack_after:
+ *
+ * Returns: (nullable) (transfer none) (type Ide.LayoutStack): The view stack.
+ */
+GtkWidget *
+ide_layout_grid_get_stack_after (IdeLayoutGrid  *self,
+                              IdeLayoutStack *stack)
+{
+  GtkWidget *parent;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_LAYOUT_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;
+}
+
+static void
+ide_layout_grid_grab_focus (GtkWidget *widget)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
+  GList *stacks;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+
+  if (self->last_focus)
+    {
+      gtk_widget_grab_focus (GTK_WIDGET (self->last_focus));
+      return;
+    }
+
+  stacks = ide_layout_grid_get_stacks (self);
+  if (stacks)
+    gtk_widget_grab_focus (stacks->data);
+  g_list_free (stacks);
+}
+
+static void
+ide_layout_grid_set_focus (IdeLayoutGrid  *self,
+                           IdeLayoutStack *stack)
+{
+  if (self->last_focus)
+    {
+      GtkStyleContext *style_context;
+
+      style_context = gtk_widget_get_style_context (GTK_WIDGET (self->last_focus));
+      gtk_style_context_remove_class (style_context, "focused");
+      ide_clear_weak_pointer (&self->last_focus);
+    }
+
+  if (stack != NULL)
+    {
+      GtkStyleContext *style_context;
+
+      style_context = gtk_widget_get_style_context (GTK_WIDGET (stack));
+      gtk_style_context_add_class (style_context, "focused");
+      ide_set_weak_pointer (&self->last_focus, stack);
+    }
+}
+
+static void
+ide_layout_grid_toplevel_set_focus (GtkWidget     *toplevel,
+                                    GtkWidget     *focus,
+                                    IdeLayoutGrid *self)
+{
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (!focus || GTK_IS_WIDGET (focus));
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  /*
+   * Always remove focus style, but don't necessarily drop our last_focus
+   * pointer, since we'll need that to restore things. Style will be
+   * reapplied if we found a focus widget.
+   */
+  if (self->last_focus)
+    {
+      GtkStyleContext *style_context;
+
+      style_context = gtk_widget_get_style_context (GTK_WIDGET (self->last_focus));
+      gtk_style_context_remove_class (style_context, "focused");
+    }
+
+  if (focus != NULL)
+    {
+      GtkWidget *parent = focus;
+
+      while (parent && !IDE_IS_LAYOUT_STACK (parent))
+        {
+          if (GTK_IS_POPOVER (parent))
+            parent = gtk_popover_get_relative_to (GTK_POPOVER (parent));
+          else
+            parent = gtk_widget_get_parent (parent);
+        }
+
+      if (IDE_IS_LAYOUT_STACK (parent))
+        ide_layout_grid_set_focus (self, IDE_LAYOUT_STACK (parent));
+    }
+}
+
+static void
+ide_layout_grid_toplevel_is_maximized (GtkWidget     *toplevel,
+                                       GParamSpec    *pspec,
+                                       IdeLayoutGrid *self)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+
+  ide_layout_grid_make_homogeneous (self);
+}
+
+static void
+ide_layout_grid_hierarchy_changed (GtkWidget *widget,
+                                   GtkWidget *previous_toplevel)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
+  GtkWidget *toplevel;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+
+  if (GTK_IS_WINDOW (previous_toplevel))
+    {
+      g_signal_handlers_disconnect_by_func (previous_toplevel,
+                                            G_CALLBACK (ide_layout_grid_toplevel_set_focus),
+                                            self);
+      g_signal_handlers_disconnect_by_func (previous_toplevel,
+                                            G_CALLBACK (ide_layout_grid_toplevel_is_maximized),
+                                            self);
+    }
+
+  toplevel = gtk_widget_get_toplevel (widget);
+  if (GTK_IS_WINDOW (toplevel))
+    {
+      g_signal_connect (toplevel,
+                        "set-focus",
+                        G_CALLBACK (ide_layout_grid_toplevel_set_focus),
+                        self);
+      g_signal_connect (toplevel,
+                        "notify::is-maximized",
+                        G_CALLBACK (ide_layout_grid_toplevel_is_maximized),
+                        self);
+    }
+}
+
+static void
+ide_layout_grid_size_allocate (GtkWidget     *widget,
+                               GtkAllocation *alloc)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
+  GArray *values;
+  GtkAllocation prev_alloc;
+  GList *stacks;
+  GList *iter;
+  gsize i;
+
+  g_assert (GTK_IS_WIDGET (widget));
+
+  /*
+   * The following code tries to get the width of each stack as a percentage of the
+   * view grids width. After size allocate, we attempt to place the position values
+   * at the matching percentage. This is needed since we have "recursive panes".
+   * A multi-pane would probably make this unnecessary.
+   */
+  gtk_widget_get_allocation (GTK_WIDGET (self), &prev_alloc);
+  values = g_array_new (FALSE, FALSE, sizeof (gdouble));
+  stacks = ide_layout_grid_get_stacks (self);
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      GtkWidget *parent;
+      guint position;
+      gdouble value;
+
+      parent = gtk_widget_get_parent (iter->data);
+      position = gtk_paned_get_position (GTK_PANED (parent));
+      value = position / (gdouble)prev_alloc.width;
+      g_array_append_val (values, value);
+    }
+
+  GTK_WIDGET_CLASS (ide_layout_grid_parent_class)->size_allocate (widget, alloc);
+
+  for (iter = stacks, i = 0; iter; iter = iter->next, i++)
+    {
+      GtkWidget *parent;
+      gdouble value;
+
+      parent = gtk_widget_get_parent (iter->data);
+      value = g_array_index (values, gdouble, i);
+      gtk_paned_set_position (GTK_PANED (parent), value * alloc->width);
+    }
+
+  g_array_free (values, TRUE);
+  g_list_free (stacks);
+}
+
+static void
+ide_layout_grid_finalize (GObject *object)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)object;
+
+  ide_clear_weak_pointer (&self->last_focus);
+
+  G_OBJECT_CLASS (ide_layout_grid_parent_class)->finalize (object);
+}
+
+static void
+ide_layout_grid_class_init (IdeLayoutGridClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_layout_grid_finalize;
+
+  widget_class->grab_focus = ide_layout_grid_grab_focus;
+  widget_class->hierarchy_changed = ide_layout_grid_hierarchy_changed;
+  widget_class->size_allocate = ide_layout_grid_size_allocate;
+}
+
+static void
+ide_layout_grid_init (IdeLayoutGrid *self)
+{
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  static const GActionEntry entries[] = {
+    { "focus-neighbor", ide_layout_grid_focus_neighbor_action, "i" },
+  };
+  IdeLayoutStack *stack;
+  GtkPaned *paned;
+
+  paned = ide_layout_grid_create_paned (self);
+  stack = ide_layout_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));
+
+  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), "view-grid", G_ACTION_GROUP (actions));
+}
+
+/**
+ * ide_layout_grid_get_last_focus:
+ * @self: A #IdeLayoutGrid.
+ *
+ * Gets the last focused #IdeLayoutStack.
+ *
+ * Returns: (transfer none) (nullable): A #IdeLayoutStack or %NULL.
+ */
+GtkWidget *
+ide_layout_grid_get_last_focus (IdeLayoutGrid *self)
+{
+  GtkWidget *ret = NULL;
+  GList *list;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  if (self->last_focus != NULL)
+    return GTK_WIDGET (self->last_focus);
+
+  list = ide_layout_grid_get_stacks (self);
+  ret = list ? list->data : NULL;
+  g_list_free (list);
+
+  return ret;
+}
+
+/**
+ * ide_layout_grid_foreach_view:
+ * @self: A #IdeLayoutGrid.
+ * @callback: (scope call): A #GtkCallback
+ * @user_data: user data for @callback.
+ *
+ * Calls @callback for every view found in the #IdeLayoutGrid.
+ */
+void
+ide_layout_grid_foreach_view (IdeLayoutGrid *self,
+                              GtkCallback    callback,
+                              gpointer       user_data)
+{
+  GList *stacks;
+  GList *iter;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (callback != NULL);
+
+  stacks = ide_layout_grid_get_stacks (self);
+
+  for (iter = stacks; iter; iter = iter->next)
+    {
+      IdeLayoutStack *stack = iter->data;
+
+      ide_layout_stack_foreach_view (stack, callback, user_data);
+    }
+
+  g_list_free (stacks);
+}
diff --git a/libide/ide-layout-grid.h b/libide/ide-layout-grid.h
new file mode 100644
index 0000000..662c7d7
--- /dev/null
+++ b/libide/ide-layout-grid.h
@@ -0,0 +1,50 @@
+/* ide-layout-grid.h
+ *
+ * Copyright (C) 2014-2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_GRID_H
+#define IDE_LAYOUT_GRID_H
+
+#include <gtk/gtk.h>
+
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-split.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_GRID (ide_layout_grid_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeLayoutGrid, ide_layout_grid, IDE, LAYOUT_GRID, GtkBin)
+
+GtkWidget  *ide_layout_grid_new              (void);
+GtkWidget  *ide_layout_grid_add_stack_after  (IdeLayoutGrid  *grid,
+                                              IdeLayoutStack *stack);
+GtkWidget  *ide_layout_grid_add_stack_before (IdeLayoutGrid  *grid,
+                                              IdeLayoutStack *stack);
+GtkWidget  *ide_layout_grid_get_stack_after  (IdeLayoutGrid  *grid,
+                                              IdeLayoutStack *stack);
+GtkWidget  *ide_layout_grid_get_stack_before (IdeLayoutGrid  *grid,
+                                              IdeLayoutStack *stack);
+GList      *ide_layout_grid_get_stacks       (IdeLayoutGrid  *grid);
+GtkWidget  *ide_layout_grid_get_last_focus   (IdeLayoutGrid  *self);
+void        ide_layout_grid_foreach_view     (IdeLayoutGrid  *self,
+                                              GtkCallback     callback,
+                                              gpointer        user_data);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_GRID_H */
diff --git a/libide/ide-layout-stack-actions.c b/libide/ide-layout-stack-actions.c
new file mode 100644
index 0000000..1c91110
--- /dev/null
+++ b/libide/ide-layout-stack-actions.c
@@ -0,0 +1,304 @@
+/* ide-layout-stack-actions.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "gb-view-stack"
+
+#include "ide-debug.h"
+#include "ide-layout.h"
+#include "ide-layout-grid.h"
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-actions.h"
+#include "ide-layout-stack-private.h"
+#include "ide-layout-view.h"
+
+static void
+ide_layout_stack_actions_close_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)object;
+  g_autoptr(IdeLayout) view = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+
+  ide_layout_stack_remove (self, GTK_WIDGET (view));
+
+  /*
+   * Force the view to destroy. This helps situation where plugins are holding
+   * onto a reference that can't easily be broken automatically.
+   */
+  gtk_widget_destroy (GTK_WIDGET (view));
+}
+
+
+static void
+ide_layout_stack_actions_close (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+
+  /*
+   * Queue until we are out of this potential signalaction. (Which mucks things
+   * up since it expects the be able to continue working with the widget).
+   */
+  task = g_task_new (self, NULL, ide_layout_stack_actions_close_cb, g_object_ref (active_view));
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_layout_stack_actions_move_left (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  g_signal_emit_by_name (self, "split", active_view, IDE_LAYOUT_GRID_SPLIT_MOVE_LEFT);
+}
+
+static void
+ide_layout_stack_actions_move_right (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  g_signal_emit_by_name (self, "split", active_view, IDE_LAYOUT_GRID_SPLIT_MOVE_RIGHT);
+}
+
+static void
+do_split_down_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  g_autoptr(GSimpleAction) action = user_data;
+  GTask *task = (GTask *)result;
+  IdeLayoutView *view = (IdeLayoutView *)object;
+  GVariant *param = g_task_get_task_data (task);
+  gboolean split_view = g_variant_get_boolean (param);
+
+  ide_layout_view_set_split_view (view, split_view);
+  g_simple_action_set_state (action, param);
+}
+
+static void
+ide_layout_stack_actions_split_down (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (!IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  task = g_task_new (active_view, NULL, do_split_down_cb, g_object_ref (action));
+  g_task_set_task_data (task, g_variant_ref (param), (GDestroyNotify)g_variant_unref);
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_layout_stack_actions_split_left (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  g_signal_emit_by_name (self, "split", active_view, IDE_LAYOUT_GRID_SPLIT_LEFT);
+}
+
+static void
+ide_layout_stack_actions_split_right (GSimpleAction *action,
+                                      GVariant      *param,
+                                      gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  g_signal_emit_by_name (self, "split", active_view, IDE_LAYOUT_GRID_SPLIT_RIGHT);
+}
+
+static void
+ide_layout_stack_actions_next_view (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+  GtkWidget *new_view;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  if (g_list_length (self->focus_history) <= 1)
+    return;
+
+  new_view = g_list_last (self->focus_history)->data;
+  g_assert (IDE_IS_LAYOUT_VIEW (new_view));
+
+  ide_layout_stack_set_active_view (self, new_view);
+
+  IDE_EXIT;
+}
+
+static void
+ide_layout_stack_actions_previous_view (GSimpleAction *action,
+                                        GVariant      *param,
+                                        gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  GtkWidget *active_view;
+  GtkWidget *new_view;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  active_view = ide_layout_stack_get_active_view (self);
+  if (active_view == NULL || !IDE_IS_LAYOUT_VIEW (active_view))
+    return;
+
+  if (g_list_length (self->focus_history) <= 1)
+    return;
+
+  g_assert (active_view);
+  g_assert (self->focus_history);
+  g_assert (self->focus_history->next);
+  g_assert (active_view == self->focus_history->data);
+
+  new_view = self->focus_history->next->data;
+  g_assert (IDE_IS_LAYOUT_VIEW (new_view));
+
+  self->focus_history = g_list_remove_link (self->focus_history, self->focus_history);
+  self->focus_history = g_list_append (self->focus_history, active_view);
+
+  ide_layout_stack_set_active_view (self, new_view);
+
+  IDE_EXIT;
+}
+
+static void
+ide_layout_stack_actions_go_forward (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (ide_back_forward_list_get_can_go_forward (self->back_forward_list))
+    ide_back_forward_list_go_forward (self->back_forward_list);
+}
+
+static void
+ide_layout_stack_actions_go_backward (GSimpleAction *action,
+                                      GVariant      *param,
+                                      gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (ide_back_forward_list_get_can_go_backward (self->back_forward_list))
+    ide_back_forward_list_go_backward (self->back_forward_list);
+}
+
+static void
+ide_layout_stack_actions_show_list (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  g_signal_emit_by_name (self->views_button, "activate");
+}
+
+static const GActionEntry gbViewStackActions[] = {
+  { "close", ide_layout_stack_actions_close },
+  { "go-forward", ide_layout_stack_actions_go_forward },
+  { "go-backward", ide_layout_stack_actions_go_backward },
+  { "move-left", ide_layout_stack_actions_move_left },
+  { "move-right", ide_layout_stack_actions_move_right },
+  { "next-view", ide_layout_stack_actions_next_view },
+  { "previous-view", ide_layout_stack_actions_previous_view },
+  { "show-list", ide_layout_stack_actions_show_list },
+  { "split-down", NULL, NULL, "false", ide_layout_stack_actions_split_down },
+  { "split-left", ide_layout_stack_actions_split_left },
+  { "split-right", ide_layout_stack_actions_split_right },
+};
+
+void
+_ide_layout_stack_actions_init (IdeLayoutStack *self)
+{
+  GSimpleActionGroup *actions;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions), gbViewStackActions,
+                                   G_N_ELEMENTS (gbViewStackActions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "view-stack", G_ACTION_GROUP (actions));
+}
diff --git a/libide/ide-layout-stack-actions.h b/libide/ide-layout-stack-actions.h
new file mode 100644
index 0000000..3ecbc2b
--- /dev/null
+++ b/libide/ide-layout-stack-actions.h
@@ -0,0 +1,30 @@
+/* ide-layout-stack-actions.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_STACK_ACTIONS_H
+#define IDE_LAYOUT_STACK_ACTIONS_H
+
+#include "ide-layout-stack.h"
+
+G_BEGIN_DECLS
+
+void _ide_layout_stack_actions_init (IdeLayoutStack *self);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_STACK_ACTIONS_H */
diff --git a/libide/ide-layout-stack-private.h b/libide/ide-layout-stack-private.h
new file mode 100644
index 0000000..46cc00e
--- /dev/null
+++ b/libide/ide-layout-stack-private.h
@@ -0,0 +1,66 @@
+/* ide-layout-stack-private.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_STACK_PRIVATE_H
+#define IDE_LAYOUT_STACK_PRIVATE_H
+
+#include <gtk/gtk.h>
+
+#include "ide-context.h"
+#include "ide-back-forward-list.h"
+
+G_BEGIN_DECLS
+
+struct _IdeLayoutStack
+{
+  GtkBin              parent_instance;
+
+  GList              *focus_history;
+  IdeBackForwardList *back_forward_list;
+  GtkGesture         *swipe_gesture;
+
+  /* Weak references */
+  GtkWidget          *active_view;
+  IdeContext         *context;
+  GBinding           *modified_binding;
+  GBinding           *title_binding;
+
+  /* Template references */
+  GtkBox             *controls;
+  GtkButton          *close_button;
+  GtkMenuButton      *document_button;
+  GtkButton          *go_backward;
+  GtkButton          *go_forward;
+  GtkEventBox        *header_event_box;
+  GtkLabel           *modified_label;
+  GtkStack           *stack;
+  GtkLabel           *title_label;
+  GtkListBox         *views_button;
+  GtkListBox         *views_listbox;
+  GtkPopover         *views_popover;
+
+  guint               destroyed : 1;
+  guint               focused : 1;
+};
+
+void ide_layout_stack_add (GtkContainer *container,
+                           GtkWidget    *child);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_STACK_PRIVATE_H */
diff --git a/libide/ide-layout-stack-split.h b/libide/ide-layout-stack-split.h
new file mode 100644
index 0000000..b223a2a
--- /dev/null
+++ b/libide/ide-layout-stack-split.h
@@ -0,0 +1,44 @@
+/* ide-layout-stack-split.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_STACK_SPLIT_H
+#define IDE_LAYOUT_STACK_SPLIT_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * IdeLayoutGridSplit:
+ * %IDE_LAYOUT_GRID_SPLIT_LEFT:
+ * %IDE_LAYOUT_GRID_SPLIT_RIGHT:
+ * %IDE_LAYOUT_GRID_SPLIT_MOVE_LEFT:
+ * %IDE_LAYOUT_GRID_SPLIT_MOVE_RIGHT:
+ *
+ */
+typedef enum
+{
+  IDE_LAYOUT_GRID_SPLIT_LEFT = 1,
+  IDE_LAYOUT_GRID_SPLIT_RIGHT = 2,
+  IDE_LAYOUT_GRID_SPLIT_MOVE_LEFT = 3,
+  IDE_LAYOUT_GRID_SPLIT_MOVE_RIGHT = 4,
+} IdeLayoutGridSplit;
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_STACK_SPLIT_H */
diff --git a/libide/ide-layout-stack.c b/libide/ide-layout-stack.c
new file mode 100644
index 0000000..d87e2eb
--- /dev/null
+++ b/libide/ide-layout-stack.c
@@ -0,0 +1,755 @@
+/* ide-layout-view-stack.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-back-forward-item.h"
+#include "ide-buffer.h"
+#include "ide-buffer-manager.h"
+#include "ide-enums.h"
+#include "ide-file.h"
+#include "ide-gtk.h"
+#include "ide-layout-view.h"
+#include "ide-layout-grid.h"
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-actions.h"
+#include "ide-layout-stack-private.h"
+#include "ide-layout-stack-split.h"
+#include "ide-workbench.h"
+
+G_DEFINE_TYPE (IdeLayoutStack, ide_layout_stack, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_ACTIVE_VIEW,
+  LAST_PROP
+};
+
+enum {
+  EMPTY,
+  SPLIT,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint       signals [LAST_SIGNAL];
+
+static void
+ide_layout_stack_add_list_row (IdeLayoutStack *self,
+                               IdeLayoutView  *child)
+{
+  GtkWidget *row;
+  GtkWidget *label;
+  GtkWidget *box;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (child));
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data (G_OBJECT (row), "IDE_LAYOUT_VIEW", child);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (row), box);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "margin-bottom", 3,
+                        "margin-end", 6,
+                        "margin-start", 6,
+                        "margin-top", 3,
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  g_object_bind_property (child, "title", label, "label", G_BINDING_SYNC_CREATE);
+  gtk_container_add (GTK_CONTAINER (box), label);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "visible", FALSE,
+                        "label", "•",
+                        "margin-start", 3,
+                        "margin-end", 3,
+                        NULL);
+  g_object_bind_property (child, "modified", label, "visible", G_BINDING_SYNC_CREATE);
+  gtk_container_add (GTK_CONTAINER (box), label);
+
+  gtk_container_add (GTK_CONTAINER (self->views_listbox), row);
+}
+
+static void
+ide_layout_stack_remove_list_row (IdeLayoutStack *self,
+                                  IdeLayoutView  *child)
+{
+  GList *children;
+  GList *iter;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (child));
+
+  children = gtk_container_get_children (GTK_CONTAINER (self->views_listbox));
+
+  for (iter = children; iter; iter = iter->next)
+    {
+      IdeLayoutView *view = g_object_get_data (iter->data, "IDE_LAYOUT_VIEW");
+
+      if (view == child)
+        {
+          gtk_container_remove (GTK_CONTAINER (self->views_listbox), iter->data);
+          break;
+        }
+    }
+
+  g_list_free (children);
+}
+
+static void
+ide_layout_stack_move_top_list_row (IdeLayoutStack *self,
+                                    IdeLayoutView  *view)
+{
+  GList *children;
+  GList *iter;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+
+  children = gtk_container_get_children (GTK_CONTAINER (self->views_listbox));
+
+  for (iter = children; iter; iter = iter->next)
+    {
+      GtkWidget *row = iter->data;
+      IdeLayoutView *item = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+
+      if (item == view)
+        {
+          g_object_ref (row);
+          gtk_container_remove (GTK_CONTAINER (self->views_listbox), row);
+          gtk_list_box_prepend (self->views_listbox, row);
+          gtk_list_box_select_row (self->views_listbox, GTK_LIST_BOX_ROW (row));
+          g_object_unref (row);
+          break;
+        }
+    }
+
+  g_list_free (children);
+}
+
+void
+ide_layout_stack_add (GtkContainer *container,
+                      GtkWidget    *child)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)container;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (IDE_IS_LAYOUT_VIEW (child))
+    {
+      gtk_widget_set_sensitive (GTK_WIDGET (self->close_button), TRUE);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->document_button), TRUE);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->views_button), TRUE);
+
+      self->focus_history = g_list_prepend (self->focus_history, child);
+      gtk_container_add (GTK_CONTAINER (self->stack), child);
+      ide_layout_view_set_back_forward_list (IDE_LAYOUT_VIEW (child), self->back_forward_list);
+      ide_layout_stack_add_list_row (self, IDE_LAYOUT_VIEW (child));
+      gtk_stack_set_visible_child (self->stack, child);
+    }
+  else
+    {
+      GTK_CONTAINER_CLASS (ide_layout_stack_parent_class)->add (container, child);
+    }
+}
+
+void
+ide_layout_stack_remove (IdeLayoutStack *self,
+                         GtkWidget      *view)
+{
+  GtkWidget *controls;
+  GtkWidget *focus_after_close = NULL;
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+
+  focus_after_close = g_list_nth_data (self->focus_history, 1);
+  if (focus_after_close != NULL)
+    g_object_ref (focus_after_close);
+
+  ide_layout_stack_remove_list_row (self, IDE_LAYOUT_VIEW (view));
+
+  self->focus_history = g_list_remove (self->focus_history, view);
+  controls = ide_layout_view_get_controls (IDE_LAYOUT_VIEW (view));
+  if (controls)
+    gtk_container_remove (GTK_CONTAINER (self->controls), controls);
+  gtk_container_remove (GTK_CONTAINER (self->stack), view);
+
+  if (focus_after_close != NULL)
+    {
+      gtk_stack_set_visible_child (self->stack, focus_after_close);
+      gtk_widget_grab_focus (GTK_WIDGET (focus_after_close));
+      g_clear_object (&focus_after_close);
+    }
+  else
+    g_signal_emit (self, signals [EMPTY], 0);
+}
+
+static void
+ide_layout_stack_real_remove (GtkContainer *container,
+                              GtkWidget    *child)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)container;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (IDE_IS_LAYOUT_VIEW (child))
+    ide_layout_stack_remove (self, child);
+  else
+    GTK_CONTAINER_CLASS (ide_layout_stack_parent_class)->remove (container, child);
+}
+
+static void
+ide_layout_stack__notify_visible_child (IdeLayoutStack *self,
+                                        GParamSpec     *pspec,
+                                        GtkStack       *stack)
+{
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (GTK_IS_STACK (stack));
+
+  visible_child = gtk_stack_get_visible_child (stack);
+
+  ide_layout_stack_set_active_view (self, visible_child);
+}
+
+static void
+ide_layout_stack_grab_focus (GtkWidget *widget)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)widget;
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  visible_child = gtk_stack_get_visible_child (self->stack);
+  if (visible_child)
+    gtk_widget_grab_focus (visible_child);
+}
+
+static gboolean
+ide_layout_stack_is_empty (IdeLayoutStack *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), FALSE);
+
+  return (self->focus_history == NULL);
+}
+
+static void
+ide_layout_stack_real_empty (IdeLayoutStack *self)
+{
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  /* its possible for a widget to be added during "empty" emission. */
+  if (ide_layout_stack_is_empty (self) && !self->destroyed)
+    {
+      gtk_widget_set_sensitive (GTK_WIDGET (self->close_button), FALSE);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->document_button), FALSE);
+      gtk_widget_set_visible (GTK_WIDGET (self->modified_label), FALSE);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->views_button), FALSE);
+    }
+}
+
+#if 0
+static void
+navigate_to_cb (IdeLayoutStack     *self,
+                IdeBackForwardItem *item,
+                IdeBackForwardList *back_forward_list)
+{
+  IdeSourceLocation *srcloc;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_BACK_FORWARD_ITEM (item));
+  g_assert (IDE_IS_BACK_FORWARD_LIST (back_forward_list));
+
+  srcloc = ide_back_forward_item_get_location (item);
+  ide_layout_stack_focus_location (self, srcloc);
+}
+#endif
+
+static void
+ide_layout_stack_context_handler (GtkWidget  *widget,
+                                  IdeContext *context)
+{
+  IdeBackForwardList *back_forward;
+  IdeLayoutStack *self = (IdeLayoutStack *)widget;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context)
+    {
+      GList *children;
+      GList *iter;
+
+      ide_set_weak_pointer (&self->context, context);
+
+      back_forward = ide_context_get_back_forward_list (context);
+
+      g_clear_object (&self->back_forward_list);
+      self->back_forward_list = ide_back_forward_list_branch (back_forward);
+
+#if 0
+      /*
+       * TODO: We need to make BackForwardItem use IdeUri.
+       */
+      g_signal_connect_object (self->back_forward_list,
+                               "navigate-to",
+                               G_CALLBACK (navigate_to_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+#endif
+
+      g_object_bind_property (self->back_forward_list, "can-go-backward",
+                              self->go_backward, "sensitive",
+                              G_BINDING_SYNC_CREATE);
+      g_object_bind_property (self->back_forward_list, "can-go-forward",
+                              self->go_forward, "sensitive",
+                              G_BINDING_SYNC_CREATE);
+
+      children = gtk_container_get_children (GTK_CONTAINER (self->stack));
+      for (iter = children; iter; iter = iter->next)
+        ide_layout_view_set_back_forward_list (iter->data, self->back_forward_list);
+      g_list_free (children);
+    }
+}
+
+static void
+ide_layout_stack__workbench__unload (IdeWorkbench   *workbench,
+                                     IdeContext     *context,
+                                     IdeLayoutStack *self)
+{
+  IdeBackForwardList *back_forward_list;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (self->back_forward_list)
+    {
+      back_forward_list = ide_context_get_back_forward_list (context);
+      ide_back_forward_list_merge (back_forward_list, self->back_forward_list);
+    }
+}
+
+static void
+ide_layout_stack_hierarchy_changed (GtkWidget *widget,
+                                    GtkWidget *old_toplevel)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)widget;
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (IDE_IS_WORKBENCH (old_toplevel))
+    {
+      g_signal_handlers_disconnect_by_func (old_toplevel,
+                                            G_CALLBACK (ide_layout_stack__workbench__unload),
+                                            self);
+    }
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (IDE_IS_WORKBENCH (toplevel))
+    {
+      g_signal_connect (toplevel,
+                        "unload",
+                        G_CALLBACK (ide_layout_stack__workbench__unload),
+                        self);
+    }
+}
+
+static void
+ide_layout_stack__views_listbox__row_activated_cb (IdeLayoutStack *self,
+                                                   GtkListBoxRow  *row,
+                                                   GtkListBox     *list_box)
+{
+  IdeLayoutView *view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+
+  if (IDE_IS_LAYOUT_VIEW (view))
+    {
+      gtk_widget_hide (GTK_WIDGET (self->views_popover));
+      ide_layout_stack_set_active_view (self, GTK_WIDGET (view));
+      gtk_widget_grab_focus (GTK_WIDGET (view));
+    }
+}
+
+static void
+ide_layout_stack_swipe (IdeLayoutStack  *self,
+                        gdouble          velocity_x,
+                        gdouble          velocity_y,
+                        GtkGestureSwipe *gesture)
+{
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (GTK_IS_GESTURE_SWIPE (gesture));
+
+  if (ABS (velocity_x) > ABS (velocity_y))
+    {
+      if (velocity_x < 0)
+        ide_widget_action (GTK_WIDGET (self), "view-stack", "previous-view", NULL);
+      else if (velocity_x > 0)
+        ide_widget_action (GTK_WIDGET (self), "view-stack", "next-view", NULL);
+    }
+}
+
+static gboolean
+ide_layout_stack__header__button_press (IdeLayoutStack *self,
+                                        GdkEventButton *button,
+                                        GtkEventBox    *event_box)
+{
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (button != NULL);
+  g_assert (GTK_IS_EVENT_BOX (event_box));
+
+  if (button->button == GDK_BUTTON_PRIMARY)
+    {
+      gtk_widget_grab_focus (GTK_WIDGET (self));
+      return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_layout_stack_destroy (GtkWidget *widget)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)widget;
+
+  self->destroyed = TRUE;
+
+  GTK_WIDGET_CLASS (ide_layout_stack_parent_class)->destroy (widget);
+}
+
+static void
+ide_layout_stack_constructed (GObject *object)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)object;
+
+  G_OBJECT_CLASS (ide_layout_stack_parent_class)->constructed (object);
+
+  g_signal_connect_object (self->views_listbox,
+                           "row-activated",
+                           G_CALLBACK (ide_layout_stack__views_listbox__row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->header_event_box,
+                           "button-press-event",
+                           G_CALLBACK (ide_layout_stack__header__button_press),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  _ide_layout_stack_actions_init (self);
+
+  /*
+   * FIXME:
+   *
+   * https://bugzilla.gnome.org/show_bug.cgi?id=747060
+   *
+   * Setting sensitive in the template is getting changed out from under us.
+   * Likely due to the popover item being set (conflation of having a popover
+   * vs wanting sensitivity). So we will just override it here.
+   *
+   * Last tested Gtk+ was 3.17.
+   */
+  gtk_widget_set_sensitive (GTK_WIDGET (self->close_button), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->views_button), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->document_button), FALSE);
+}
+
+static void
+ide_layout_stack_finalize (GObject *object)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)object;
+
+  g_clear_pointer (&self->focus_history, g_list_free);
+  ide_clear_weak_pointer (&self->context);
+  ide_clear_weak_pointer (&self->title_binding);
+  ide_clear_weak_pointer (&self->active_view);
+  g_clear_object (&self->back_forward_list);
+  g_clear_object (&self->swipe_gesture);
+
+  G_OBJECT_CLASS (ide_layout_stack_parent_class)->finalize (object);
+}
+
+static void
+ide_layout_stack_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_VIEW:
+      g_value_set_object (value, ide_layout_stack_get_active_view (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_VIEW:
+      ide_layout_stack_set_active_view (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_class_init (IdeLayoutStackClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->constructed = ide_layout_stack_constructed;
+  object_class->finalize = ide_layout_stack_finalize;
+  object_class->get_property = ide_layout_stack_get_property;
+  object_class->set_property = ide_layout_stack_set_property;
+
+  widget_class->destroy = ide_layout_stack_destroy;
+  widget_class->grab_focus = ide_layout_stack_grab_focus;
+  widget_class->hierarchy_changed = ide_layout_stack_hierarchy_changed;
+
+  container_class->add = ide_layout_stack_add;
+  container_class->remove = ide_layout_stack_real_remove;
+
+  properties [PROP_ACTIVE_VIEW] =
+    g_param_spec_object ("active-view",
+                         "Active View",
+                         "The active view.",
+                         IDE_TYPE_LAYOUT_VIEW,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [EMPTY] =
+    g_signal_new_class_handler ("empty",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_layout_stack_real_empty),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE,
+                                0);
+
+  /**
+   * IdeLayoutStack::split:
+   * @self: A #IdeLayoutStack.
+   * @view: The #IdeLayoutView to split.
+   * @split_type: (type gint): A #IdeLayoutGridSplit.
+   *
+   * Requests a split to be performed on the view.
+   *
+   * This should only be used by #IdeLayoutGrid.
+   */
+  signals [SPLIT] = g_signal_new ("split",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_SIGNAL_RUN_LAST,
+                                   0,
+                                   NULL, NULL, NULL,
+                                   G_TYPE_NONE,
+                                   2,
+                                   IDE_TYPE_LAYOUT_VIEW,
+                                   IDE_TYPE_LAYOUT_GRID_SPLIT);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-stack.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, close_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, controls);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, document_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, go_backward);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, go_forward);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, header_event_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, modified_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, title_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, views_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, views_listbox);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStack, views_popover);
+}
+
+static void
+ide_layout_stack_init (IdeLayoutStack *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->stack,
+                           "notify::visible-child",
+                           G_CALLBACK (ide_layout_stack__notify_visible_child),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->swipe_gesture = gtk_gesture_swipe_new (GTK_WIDGET (self));
+  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->swipe_gesture), TRUE);
+  g_signal_connect_object (self->swipe_gesture,
+                           "swipe",
+                           G_CALLBACK (ide_layout_stack_swipe),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_widget_set_context_handler (self, ide_layout_stack_context_handler);
+}
+
+GtkWidget *
+ide_layout_stack_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_STACK, NULL);
+}
+
+/**
+ * ide_layout_stack_get_active_view:
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
+ */
+GtkWidget *
+ide_layout_stack_get_active_view (IdeLayoutStack *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), NULL);
+
+  return self->active_view;
+}
+
+void
+ide_layout_stack_set_active_view (IdeLayoutStack *self,
+                                  GtkWidget      *active_view)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (!active_view || IDE_IS_LAYOUT_VIEW (active_view));
+
+  if (self->destroyed)
+    return;
+
+  if (self->active_view != active_view)
+    {
+      if (self->active_view)
+        {
+          if (self->title_binding)
+            g_binding_unbind (self->title_binding);
+          ide_clear_weak_pointer (&self->title_binding);
+          if (self->modified_binding)
+            g_binding_unbind (self->modified_binding);
+          ide_clear_weak_pointer (&self->modified_binding);
+          gtk_label_set_label (self->title_label, NULL);
+          ide_clear_weak_pointer (&self->active_view);
+          gtk_widget_hide (GTK_WIDGET (self->controls));
+        }
+
+      if (active_view)
+        {
+          GtkWidget *controls;
+          GBinding *binding;
+          GActionGroup *group;
+          GMenu *menu;
+          GtkPopover *popover;
+
+          ide_set_weak_pointer (&self->active_view, active_view);
+          if (active_view != gtk_stack_get_visible_child (self->stack))
+            gtk_stack_set_visible_child (self->stack, active_view);
+
+          menu = ide_layout_view_get_menu (IDE_LAYOUT_VIEW (active_view));
+          popover = g_object_new (GTK_TYPE_POPOVER, NULL);
+          gtk_popover_bind_model (popover, G_MENU_MODEL (menu), NULL);
+          gtk_menu_button_set_popover (self->document_button, GTK_WIDGET (popover));
+
+          self->focus_history = g_list_remove (self->focus_history, active_view);
+          self->focus_history = g_list_prepend (self->focus_history, active_view);
+
+          binding = g_object_bind_property (active_view, "special-title",
+                                            self->title_label, "label",
+                                            G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+          ide_set_weak_pointer (&self->title_binding, binding);
+
+          binding = g_object_bind_property (active_view, "modified",
+                                            self->modified_label, "visible",
+                                            G_BINDING_SYNC_CREATE);
+          ide_set_weak_pointer (&self->modified_binding, binding);
+
+          controls = ide_layout_view_get_controls (IDE_LAYOUT_VIEW (active_view));
+
+          if (controls != NULL)
+            {
+              GList *children;
+              GList *iter;
+
+              children = gtk_container_get_children (GTK_CONTAINER (self->controls));
+              for (iter = children; iter; iter = iter->next)
+                gtk_container_remove (GTK_CONTAINER (self->controls), iter->data);
+              g_list_free (children);
+
+              gtk_container_add (GTK_CONTAINER (self->controls), controls);
+              gtk_widget_show (GTK_WIDGET (self->controls));
+            }
+          else
+            {
+              gtk_widget_hide (GTK_WIDGET (self->controls));
+            }
+
+          group = gtk_widget_get_action_group (active_view, "view");
+          if (group)
+            gtk_widget_insert_action_group (GTK_WIDGET (self), "view", group);
+
+          ide_layout_stack_move_top_list_row (self, IDE_LAYOUT_VIEW (active_view));
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE_VIEW]);
+    }
+}
+
+/**
+ * ide_layout_stack_foreach_view:
+ * @callback: (scope call): A callback to invoke for each view.
+ */
+void
+ide_layout_stack_foreach_view (IdeLayoutStack *self,
+                               GtkCallback     callback,
+                               gpointer        user_data)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (self->stack), callback, user_data);
+}
diff --git a/libide/ide-layout-stack.h b/libide/ide-layout-stack.h
new file mode 100644
index 0000000..7b4a3f9
--- /dev/null
+++ b/libide/ide-layout-stack.h
@@ -0,0 +1,42 @@
+/* ide-layout-stack.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_STACK_H
+#define IDE_LAYOUT_STACK_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_STACK (ide_layout_stack_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeLayoutStack, ide_layout_stack, IDE, LAYOUT_STACK, GtkBin)
+
+GtkWidget  *ide_layout_stack_new             (void);
+void        ide_layout_stack_remove          (IdeLayoutStack    *self,
+                                              GtkWidget         *view);
+GtkWidget  *ide_layout_stack_get_active_view (IdeLayoutStack    *self);
+void        ide_layout_stack_set_active_view (IdeLayoutStack    *self,
+                                              GtkWidget         *active_view);
+void        ide_layout_stack_foreach_view    (IdeLayoutStack    *self,
+                                              GtkCallback        callback,
+                                              gpointer           user_data);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_STACK_H */
diff --git a/libide/ide-layout-view.c b/libide/ide-layout-view.c
new file mode 100644
index 0000000..bf3d733
--- /dev/null
+++ b/libide/ide-layout-view.c
@@ -0,0 +1,365 @@
+/* ide-layout-view.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-layout-view.h"
+
+typedef struct
+{
+  GtkBox *controls;
+  GMenu  *menu;
+} IdeLayoutViewPrivate;
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeLayoutView, ide_layout_view, GTK_TYPE_BOX,
+                         G_ADD_PRIVATE (IdeLayoutView)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CAN_SPLIT,
+  PROP_MODIFIED,
+  PROP_SPECIAL_TITLE,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+/**
+ * ide_layout_view_get_can_preview:
+ * @self: A #IdeLayoutView.
+ *
+ * Checks if @self can create a preview view (such as html, markdown, etc).
+ *
+ * Returns: %TRUE if @self can create a preview view.
+ */
+gboolean
+ide_layout_view_get_can_preview (IdeLayoutView *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->get_can_preview)
+    return IDE_LAYOUT_VIEW_GET_CLASS (self)->get_can_preview (self);
+
+  return FALSE;
+}
+
+/**
+ * ide_layout_view_get_can_split:
+ * @self: A #IdeLayoutView.
+ *
+ * Checks if @self can create a split view. If so, %TRUE is returned. Otherwise, %FALSE.
+ *
+ * Returns: %TRUE if @self can create a split.
+ */
+gboolean
+ide_layout_view_get_can_split (IdeLayoutView *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->get_can_split)
+    return IDE_LAYOUT_VIEW_GET_CLASS (self)->get_can_split (self);
+
+  return FALSE;
+}
+
+/**
+ * ide_layout_view_create_split:
+ * @self: A #IdeLayoutView.
+ *
+ * Creates a new view similar to @self that can be displayed in a split.
+ * If the view does not support splits, %NULL will be returned.
+ *
+ * Returns: (transfer full): A #IdeLayoutView.
+ */
+IdeLayoutView *
+ide_layout_view_create_split (IdeLayoutView *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->create_split)
+    return IDE_LAYOUT_VIEW_GET_CLASS (self)->create_split (self);
+
+  return NULL;
+}
+
+/**
+ * ide_layout_view_set_split_view:
+ * @self: A #IdeLayoutView.
+ * @split_view: if the split should be enabled.
+ *
+ * Set a split view using GtkPaned style split with %GTK_ORIENTATION_VERTICAL.
+ */
+void
+ide_layout_view_set_split_view (IdeLayoutView   *self,
+                        gboolean  split_view)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->set_split_view)
+    IDE_LAYOUT_VIEW_GET_CLASS (self)->set_split_view (self, split_view);
+}
+
+/**
+ * ide_layout_view_get_controls:
+ * @self: A #IdeLayoutView.
+ *
+ * Gets the controls for the view.
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget.
+ */
+GtkWidget *
+ide_layout_view_get_controls (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  return GTK_WIDGET (priv->controls);
+}
+
+/* XXX: Make non-const */
+const gchar *
+ide_layout_view_get_title (IdeLayoutView *self)
+{
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->get_title)
+    return IDE_LAYOUT_VIEW_GET_CLASS (self)->get_title (self);
+
+  return _("untitled document");
+}
+
+void
+ide_layout_view_set_back_forward_list (IdeLayoutView      *self,
+                                       IdeBackForwardList *back_forward_list)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_BACK_FORWARD_LIST (back_forward_list));
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->set_back_forward_list)
+    IDE_LAYOUT_VIEW_GET_CLASS (self)->set_back_forward_list (self, back_forward_list);
+}
+
+void
+ide_layout_view_navigate_to (IdeLayoutView     *self,
+                             IdeSourceLocation *location)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (location != NULL);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->navigate_to)
+    IDE_LAYOUT_VIEW_GET_CLASS (self)->navigate_to (self, location);
+}
+
+gboolean
+ide_layout_view_get_modified (IdeLayoutView *self)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->get_modified)
+    return IDE_LAYOUT_VIEW_GET_CLASS (self)->get_modified (self);
+
+  return FALSE;
+}
+
+static void
+ide_layout_view_notify (GObject    *object,
+                        GParamSpec *pspec)
+{
+  /*
+   * XXX:
+   *
+   * This should get removed after 3.18 when path bar lands.
+   * This also notifies of special-title after title is emitted.
+   */
+  if (pspec == properties [PROP_TITLE])
+    g_object_notify_by_pspec (object, properties [PROP_SPECIAL_TITLE]);
+
+  if (G_OBJECT_CLASS (ide_layout_view_parent_class)->notify)
+    G_OBJECT_CLASS (ide_layout_view_parent_class)->notify (object, pspec);
+}
+
+static void
+ide_layout_view_destroy (GtkWidget *widget)
+{
+  IdeLayoutView *self = (IdeLayoutView *)widget;
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_clear_object (&priv->controls);
+
+  GTK_WIDGET_CLASS (ide_layout_view_parent_class)->destroy (widget);
+}
+
+static void
+ide_layout_view_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeLayoutView *self = IDE_LAYOUT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CAN_SPLIT:
+      g_value_set_boolean (value, ide_layout_view_get_can_split (self));
+      break;
+
+    case PROP_MODIFIED:
+      g_value_set_boolean (value, ide_layout_view_get_modified (self));
+      break;
+
+    case PROP_SPECIAL_TITLE:
+      g_value_set_string (value, ide_layout_view_get_special_title (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_layout_view_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_view_class_init (IdeLayoutViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_layout_view_get_property;
+  object_class->notify = ide_layout_view_notify;
+
+  widget_class->destroy = ide_layout_view_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-view.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutView, menu);
+
+  properties [PROP_CAN_SPLIT] =
+    g_param_spec_boolean ("can-split",
+                          "Can Split",
+                          "If the view can be split.",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODIFIED] =
+    g_param_spec_boolean ("modified",
+                          "Modified",
+                          "If the document has been modified.",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The view title.",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /*
+   * XXX:
+   *
+   * This property should be removed after 3.18 when path bar lands.
+   */
+  properties [PROP_SPECIAL_TITLE] =
+    g_param_spec_string ("special-title",
+                         "Special Title",
+                         "The special title to be displayed in the document menu button.",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_layout_view_init (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  GtkBox *controls;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  controls = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_HORIZONTAL,
+                           "visible", TRUE,
+                           NULL);
+  priv->controls = g_object_ref_sink (controls);
+}
+
+static GObject *
+ide_layout_view_get_internal_child (GtkBuildable *buildable,
+                                    GtkBuilder   *builder,
+                                    const gchar  *childname)
+{
+  IdeLayoutView *self = (IdeLayoutView *)buildable;
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_VIEW (self));
+
+  if (g_strcmp0 (childname, "controls") == 0)
+    return G_OBJECT (priv->controls);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = ide_layout_view_get_internal_child;
+}
+
+/**
+ * ide_layout_view_get_menu:
+ *
+ * Returns: (transfer none): A #GMenu that may be modified.
+ */
+GMenu *
+ide_layout_view_get_menu (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  return priv->menu;
+}
+
+/*
+ * XXX:
+ *
+ * This function is a hack in place for 3.18 until we get the path bar
+ * which will provide a better view of file paths. It should be removed
+ * after 3.18 when path bar lands. Also remove the "special-title"
+ * property.
+ */
+const gchar *
+ide_layout_view_get_special_title (IdeLayoutView *self)
+{
+  const gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  if (IDE_LAYOUT_VIEW_GET_CLASS (self)->get_special_title)
+    ret = IDE_LAYOUT_VIEW_GET_CLASS (self)->get_special_title (self);
+
+  if (ret == NULL)
+    ret = ide_layout_view_get_title (self);
+
+  return ret;
+}
diff --git a/libide/ide-layout-view.h b/libide/ide-layout-view.h
new file mode 100644
index 0000000..b8577a5
--- /dev/null
+++ b/libide/ide-layout-view.h
@@ -0,0 +1,68 @@
+/* ide-layout-view.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_LAYOUT_VIEW_H
+#define IDE_LAYOUT_VIEW_H
+
+#include <gtk/gtk.h>
+
+#include "ide-back-forward-list.h"
+#include "ide-source-location.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_VIEW (ide_layout_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeLayoutView, ide_layout_view, IDE, LAYOUT_VIEW, GtkBox)
+
+struct _IdeLayoutViewClass
+{
+  GtkBinClass parent;
+
+  gboolean       (*get_can_preview)       (IdeLayoutView             *self);
+  gboolean       (*get_can_split)         (IdeLayoutView             *self);
+  gboolean       (*get_modified)          (IdeLayoutView             *self);
+  const gchar   *(*get_title)             (IdeLayoutView             *self);
+  const gchar   *(*get_special_title)     (IdeLayoutView             *self);
+  IdeLayoutView *(*create_split)          (IdeLayoutView             *self);
+  void           (*set_split_view)        (IdeLayoutView             *self,
+                                           gboolean                   split_view);
+  void           (*set_back_forward_list) (IdeLayoutView             *self,
+                                           IdeBackForwardList        *back_forward_list);
+  void           (*navigate_to)           (IdeLayoutView             *self,
+                                           IdeSourceLocation         *location);
+};
+
+GMenu         *ide_layout_view_get_menu              (IdeLayoutView             *self);
+IdeLayoutView *ide_layout_view_create_split          (IdeLayoutView             *self);
+gboolean       ide_layout_view_get_can_preview       (IdeLayoutView             *self);
+gboolean       ide_layout_view_get_can_split         (IdeLayoutView             *self);
+const gchar   *ide_layout_view_get_title             (IdeLayoutView             *self);
+const gchar   *ide_layout_view_get_special_title     (IdeLayoutView             *self);
+GtkWidget     *ide_layout_view_get_controls          (IdeLayoutView             *self);
+gboolean       ide_layout_view_get_modified          (IdeLayoutView             *self);
+void           ide_layout_view_set_split_view        (IdeLayoutView             *self,
+                                                      gboolean                   split_view);
+void           ide_layout_view_set_back_forward_list (IdeLayoutView             *self,
+                                                      IdeBackForwardList        *back_forward_list);
+void           ide_layout_view_navigate_to           (IdeLayoutView             *self,
+                                                      IdeSourceLocation         *location);
+
+G_END_DECLS
+
+#endif /* IDE_LAYOUT_VIEW_H */
diff --git a/libide/resources/libide.gresource.xml b/libide/resources/libide.gresource.xml
index 957ad37..862ed5b 100644
--- a/libide/resources/libide.gresource.xml
+++ b/libide/resources/libide.gresource.xml
@@ -27,6 +27,8 @@
     <file alias="ui/ide-greeter-project-row.ui">../../data/ui/ide-greeter-project-row.ui</file>
     <file alias="ui/ide-layout.ui">../../data/ui/ide-layout.ui</file>
     <file alias="ui/ide-layout-pane.ui">../../data/ui/ide-layout-pane.ui</file>
+    <file alias="ui/ide-layout-stack.ui">../../data/ui/ide-layout-stack.ui</file>
+    <file alias="ui/ide-layout-view.ui">../../data/ui/ide-layout-view.ui</file>
     <file alias="ui/ide-preferences-entry.ui">../../data/ui/ide-preferences-entry.ui</file>
     <file alias="ui/ide-preferences-font-button.ui">../../data/ui/ide-preferences-font-button.ui</file>
     <file alias="ui/ide-preferences-group.ui">../../data/ui/ide-preferences-group.ui</file>


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