[gnome-builder/wip/chergert/perspective] libide: start on IdeLayout abstraction for libide
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/perspective] libide: start on IdeLayout abstraction for libide
- Date: Fri, 13 Nov 2015 09:37:01 +0000 (UTC)
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]