[gnome-builder: 45/139] libide-gui: add new libide-gui static library



commit 387cf0dc6a13519822f4d0d2b14d1d0009f7e385
Author: Christian Hergert <chergert redhat com>
Date:   Wed Jan 9 16:34:57 2019 -0800

    libide-gui: add new libide-gui static library
    
    This adds the new refactored UI code in the form of libide-gui. Some of
    the main changes to the design include the abstraction of IdeWorkbench
    from a GtkWindow to a GtkWindowGroup. It now contains multiple workbench
    IdeWorkspace (GtkWindow). This allows for multi-monitor setups so that
    users may have multiple views of code and content.
    
    The IdeContext creation also no longer needs to have a project to load
    initially. This allows for a non-project mode while still building on
    the abstractions provided by IdeContext/IdeObject.
    
    However, plugins should check to see if they're in a project mode and
    avoid accessing foundry components in that case.

 src/libide/gtk/menus.ui                            |  266 ---
 src/libide/gui/gs-markdown-private.h               |   58 +
 src/libide/{util => gui}/gs-markdown.c             |    4 +-
 src/libide/gui/gtk/menus.ui                        |   86 +
 src/libide/{util => gui}/ide-cell-renderer-fancy.c |    2 +-
 src/libide/{util => gui}/ide-cell-renderer-fancy.h |    7 +-
 src/libide/gui/ide-command-provider.c              |  103 +
 src/libide/gui/ide-command-provider.h              |   66 +
 src/libide/gui/ide-command.c                       |  153 ++
 src/libide/gui/ide-command.h                       |   66 +
 src/libide/gui/ide-config-view-addin.c             |   46 +
 src/libide/gui/ide-config-view-addin.h             |   48 +
 src/libide/gui/ide-environment-editor-row.c        |  278 +++
 src/libide/gui/ide-environment-editor-row.h        |   37 +
 src/libide/gui/ide-environment-editor-row.ui       |   49 +
 src/libide/gui/ide-environment-editor.c            |  317 +++
 src/libide/gui/ide-environment-editor.h            |   42 +
 src/libide/{util => gui}/ide-fancy-tree-view.c     |    4 +-
 src/libide/{util => gui}/ide-fancy-tree-view.h     |   12 +-
 src/libide/gui/ide-frame-actions.c                 |  429 ++++
 src/libide/gui/ide-frame-addin.c                   |  111 +
 src/libide/gui/ide-frame-addin.h                   |   65 +
 .../ide-frame-header.c}                            |  235 +-
 .../ide-frame-header.h}                            |   21 +-
 .../ide-frame-header.ui}                           |    4 +-
 .../ide-frame-shortcuts.c}                         |   43 +-
 .../ide-frame-wrapper.c}                           |   40 +-
 .../ide-frame-wrapper.h}                           |    6 +-
 .../{layout/ide-layout-stack.c => gui/ide-frame.c} |  833 +++----
 src/libide/gui/ide-frame.h                         |   84 +
 .../ide-layout-stack.ui => gui/ide-frame.ui}       |    6 +-
 .../ide-grid-actions.c}                            |   24 +-
 .../ide-grid-column-actions.c}                     |   34 +-
 .../ide-grid-column.c}                             |  188 +-
 .../ide-grid-column.h}                             |   23 +-
 .../{layout/ide-layout-grid.c => gui/ide-grid.c}   |  780 +++----
 src/libide/gui/ide-grid.h                          |   77 +
 .../{util/ide-gtk.c => gui/ide-gui-global.c}       |  218 +-
 .../{util/ide-gtk.h => gui/ide-gui-global.h}       |   31 +-
 src/libide/gui/ide-gui-private.h                   |  103 +
 src/libide/gui/ide-header-bar-shortcuts.c          |   68 +
 src/libide/gui/ide-header-bar.c                    |  469 ++++
 src/libide/gui/ide-header-bar.h                    |   67 +
 src/libide/gui/ide-header-bar.ui                   |   76 +
 src/libide/gui/ide-marked-view.c                   |  112 +
 .../ide-omni-bar.h => gui/ide-marked-view.h}       |   15 +-
 .../gui/ide-notification-list-box-row-private.h    |   38 +
 src/libide/gui/ide-notification-list-box-row.c     |  377 ++++
 src/libide/gui/ide-notification-list-box-row.ui    |  112 +
 src/libide/gui/ide-notification-stack-private.h    |   44 +
 src/libide/gui/ide-notification-stack.c            |  405 ++++
 src/libide/gui/ide-notification-view-private.h     |   37 +
 src/libide/gui/ide-notification-view.c             |  291 +++
 src/libide/gui/ide-notification-view.ui            |   63 +
 .../gui/ide-notifications-button-popover-private.h |   31 +
 src/libide/gui/ide-notifications-button-popover.c  |   51 +
 src/libide/gui/ide-notifications-button.c          |  217 ++
 src/libide/gui/ide-notifications-button.h          |   40 +
 src/libide/gui/ide-notifications-button.ui         |   32 +
 src/libide/gui/ide-omni-bar-addin.c                |   89 +
 src/libide/gui/ide-omni-bar-addin.h                |   55 +
 src/libide/gui/ide-omni-bar.c                      |  619 ++++++
 src/libide/gui/ide-omni-bar.h                      |   56 +
 src/libide/gui/ide-omni-bar.ui                     |  128 ++
 .../{layout/ide-layout-view.c => gui/ide-page.c}   |  406 ++--
 src/libide/gui/ide-page.h                          |  119 +
 src/libide/gui/ide-pane.c                          |   54 +
 src/libide/gui/ide-pane.h                          |   48 +
 .../{layout/ide-layout-pane.c => gui/ide-panel.c}  |   49 +-
 .../{layout/ide-layout-pane.h => gui/ide-panel.h}  |   22 +-
 .../ide-layout-pane.ui => gui/ide-panel.ui}        |    5 +-
 .../{preferences => gui}/ide-preferences-addin.c   |   39 +-
 .../{preferences => gui}/ide-preferences-addin.h   |    3 +-
 .../ide-preferences-builtin-private.h}             |    0
 .../{preferences => gui}/ide-preferences-builtin.c |   16 +-
 .../ide-preferences-language-row-private.h}        |    0
 .../ide-preferences-language-row.c                 |   14 +-
 .../ide-preferences-language-row.ui                |    0
 src/libide/gui/ide-preferences-surface.c           |  136 ++
 .../ide-preferences-surface.h}                     |   13 +-
 .../{preferences => gui}/ide-preferences-window.c  |    8 +-
 .../{preferences => gui}/ide-preferences-window.h  |    7 +-
 src/libide/gui/ide-preferences-window.ui           |   17 +
 src/libide/gui/ide-primary-workspace-actions.c     |  109 +
 src/libide/gui/ide-primary-workspace.c             |  141 ++
 src/libide/gui/ide-primary-workspace.h             |   38 +
 src/libide/gui/ide-primary-workspace.ui            |   62 +
 src/libide/gui/ide-run-button.c                    |  200 ++
 .../{util/ide-dnd.h => gui/ide-run-button.h}       |   10 +-
 src/libide/gui/ide-run-button.ui                   |   67 +
 src/libide/{search => gui}/ide-search-entry.c      |  175 +-
 src/libide/{search => gui}/ide-search-entry.h      |    6 +-
 src/libide/{search => gui}/ide-search-entry.ui     |    1 -
 src/libide/{session => gui}/ide-session-addin.c    |   18 +-
 src/libide/{session => gui}/ide-session-addin.h    |   13 +-
 src/libide/gui/ide-session-private.h               |   51 +
 src/libide/{session => gui}/ide-session.c          |   91 +-
 .../ide-shortcut-label-private.h}                  |    2 +-
 src/libide/{layout => gui}/ide-shortcut-label.c    |    2 +-
 src/libide/gui/ide-shortcuts-window-private.h      |   31 +
 src/libide/gui/ide-shortcuts-window.c              |   48 +
 src/libide/gui/ide-shortcuts-window.ui             |  547 +++++
 src/libide/gui/ide-surface.c                       |  259 +++
 src/libide/gui/ide-surface.h                       |   67 +
 src/libide/gui/ide-surfaces-button.c               |  107 +
 .../ide-layout.h => gui/ide-surfaces-button.h}     |   23 +-
 src/libide/{search => gui}/ide-tagged-entry.c      |    2 +
 src/libide/{search => gui}/ide-tagged-entry.h      |   11 +-
 src/libide/gui/ide-transient-sidebar.c             |  355 +++
 src/libide/gui/ide-transient-sidebar.h             |   58 +
 .../ide-window-settings-private.h}                 |    2 +-
 src/libide/{util => gui}/ide-window-settings.c     |    8 +-
 src/libide/gui/ide-workbench-addin.c               |  402 ++++
 src/libide/gui/ide-workbench-addin.h               |  159 ++
 src/libide/gui/ide-workbench.c                     | 2299 ++++++++++++++++++++
 src/libide/gui/ide-workbench.h                     |  144 ++
 src/libide/{workers => gui}/ide-worker-manager.c   |    8 +-
 src/libide/{workers => gui}/ide-worker-manager.h   |    0
 src/libide/{workers => gui}/ide-worker-process.c   |    9 +-
 src/libide/{workers => gui}/ide-worker-process.h   |    0
 src/libide/{workers => gui}/ide-worker.c           |    4 +-
 src/libide/{workers => gui}/ide-worker.h           |    4 +-
 src/libide/gui/ide-workspace-actions.c             |   92 +
 src/libide/gui/ide-workspace-addin.c               |  118 +
 src/libide/gui/ide-workspace-addin.h               |   54 +
 src/libide/gui/ide-workspace.c                     |  971 +++++++++
 src/libide/gui/ide-workspace.h                     |   96 +
 src/libide/gui/ide-workspace.ui                    |   23 +
 src/libide/gui/libide-gui.gresource.xml            |   24 +
 src/libide/gui/libide-gui.h                        |   70 +
 src/libide/gui/meson.build                         |  212 ++
 src/libide/layout/ide-layout-grid.h                |   80 -
 src/libide/layout/ide-layout-private.h             |   71 -
 src/libide/layout/ide-layout-stack-actions.c       |  418 ----
 src/libide/layout/ide-layout-stack-addin.c         |  130 --
 src/libide/layout/ide-layout-stack-addin.h         |   62 -
 src/libide/layout/ide-layout-stack.h               |   88 -
 src/libide/layout/ide-layout-transient-sidebar.c   |  357 ---
 src/libide/layout/ide-layout-transient-sidebar.h   |   61 -
 src/libide/layout/ide-layout-view.h                |  125 --
 src/libide/layout/ide-layout.c                     |   55 -
 src/libide/layout/meson.build                      |   41 -
 .../preferences/ide-preferences-perspective.c      |  163 --
 src/libide/preferences/ide-preferences-window.ui   |   26 -
 src/libide/preferences/meson.build                 |   24 -
 src/libide/search/ide-search-engine.c              |   72 +-
 src/libide/session/ide-session.h                   |   53 -
 src/libide/session/meson.build                     |   14 -
 src/libide/util/gs-markdown.h                      |   58 -
 src/libide/util/ide-dnd.c                          |   46 -
 src/libide/util/ide-progress.c                     |  293 ---
 src/libide/util/ide-progress.h                     |   58 -
 src/libide/workbench/ide-omni-bar.c                |  881 --------
 src/libide/workbench/ide-omni-bar.ui               |  613 ------
 src/libide/workbench/ide-omni-pausable-row.c       |  187 --
 src/libide/workbench/ide-omni-pausable-row.h       |   36 -
 src/libide/workbench/ide-omni-pausable-row.ui      |   57 -
 src/libide/workbench/ide-perspective.c             |  314 ---
 src/libide/workbench/ide-perspective.h             |   80 -
 src/libide/workbench/ide-workbench-actions.c       |  362 ---
 src/libide/workbench/ide-workbench-addin.c         |  262 ---
 src/libide/workbench/ide-workbench-addin.h         |   96 -
 src/libide/workbench/ide-workbench-header-bar.c    |  337 ---
 src/libide/workbench/ide-workbench-header-bar.h    |   73 -
 src/libide/workbench/ide-workbench-header-bar.ui   |  119 -
 src/libide/workbench/ide-workbench-message.c       |  226 --
 src/libide/workbench/ide-workbench-message.h       |   56 -
 src/libide/workbench/ide-workbench-message.ui      |   43 -
 src/libide/workbench/ide-workbench-open.c          |  539 -----
 src/libide/workbench/ide-workbench-private.h       |   68 -
 src/libide/workbench/ide-workbench-shortcuts.c     |  147 --
 src/libide/workbench/ide-workbench.c               | 1143 ----------
 src/libide/workbench/ide-workbench.h               |  151 --
 src/libide/workbench/ide-workbench.ui              |   64 -
 src/libide/workbench/meson.build                   |   32 -
 src/libide/workers/meson.build                     |   20 -
 176 files changed, 14777 insertions(+), 10064 deletions(-)
---
diff --git a/src/libide/gui/gs-markdown-private.h b/src/libide/gui/gs-markdown-private.h
new file mode 100644
index 000000000..aae18a6aa
--- /dev/null
+++ b/src/libide/gui/gs-markdown-private.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright 2008-2013 Richard Hughes <richard hughsie com>
+ * Copyright 2015 Kalev Lember <klember redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_MARKDOWN_H
+#define __GS_MARKDOWN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_MARKDOWN (gs_markdown_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsMarkdown, gs_markdown, GS, MARKDOWN, GObject)
+
+typedef enum {
+        GS_MARKDOWN_OUTPUT_TEXT,
+        GS_MARKDOWN_OUTPUT_PANGO,
+        GS_MARKDOWN_OUTPUT_HTML,
+        GS_MARKDOWN_OUTPUT_LAST
+} GsMarkdownOutputKind;
+
+GsMarkdown      *gs_markdown_new                        (GsMarkdownOutputKind    output);
+void             gs_markdown_set_max_lines              (GsMarkdown             *self,
+                                                         gint                    max_lines);
+void             gs_markdown_set_smart_quoting          (GsMarkdown             *self,
+                                                         gboolean                smart_quoting);
+void             gs_markdown_set_escape                 (GsMarkdown             *self,
+                                                         gboolean                escape);
+void             gs_markdown_set_autocode               (GsMarkdown             *self,
+                                                         gboolean                autocode);
+void             gs_markdown_set_autolinkify            (GsMarkdown             *self,
+                                                         gboolean                autolinkify);
+gchar           *gs_markdown_parse                      (GsMarkdown             *self,
+                                                         const gchar            *text);
+
+G_END_DECLS
+
+#endif /* __GS_MARKDOWN_H */
+
diff --git a/src/libide/util/gs-markdown.c b/src/libide/gui/gs-markdown.c
similarity index 99%
rename from src/libide/util/gs-markdown.c
rename to src/libide/gui/gs-markdown.c
index 168989099..ed8eb30d7 100644
--- a/src/libide/util/gs-markdown.c
+++ b/src/libide/gui/gs-markdown.c
@@ -25,7 +25,7 @@
 #include <string.h>
 #include <glib.h>
 
-#include "gs-markdown.h"
+#include "gs-markdown-private.h"
 
 /*******************************************************************************
  *
@@ -46,6 +46,8 @@
  * been run against any conformance tests. The parsing is single pass, with
  * a simple enumerated interpretor mode and a single line back-memory.
  *
+ *
+ * Since: 3.32
  ******************************************************************************/
 
 typedef enum {
diff --git a/src/libide/gui/gtk/menus.ui b/src/libide/gui/gtk/menus.ui
new file mode 100644
index 000000000..99e8390ef
--- /dev/null
+++ b/src/libide/gui/gtk/menus.ui
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
+      <attribute name="label" translatable="yes">Switch Surface</attribute>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-projects-section"/>
+    <section id="ide-primary-workspace-menu-placeholder1"/>
+    <section id="ide-primary-workspace-menu-open-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+        <attribute name="accel">&lt;primary&gt;o</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-placeholder2"/>
+    <section id="ide-primary-workspace-menu-close-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-close-project</attribute>
+        <attribute name="label" translatable="yes">Close Project</attribute>
+        <attribute name="action">workbench.close</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-placeholder3"/>
+    <section id="ide-primary-workspace-menu-app-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-preferences</attribute>
+        <attribute name="label" translatable="yes">Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+        <attribute name="accel">&lt;primary&gt;comma</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-shortcuts</attribute>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">app.shortcuts</attribute>
+        <attribute name="accel">&lt;primary&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-help</attribute>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-about</attribute>
+        <attribute name="label" translatable="yes">About Builder</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-quit-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-quit</attribute>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-new-menu">
+    <section id="new-document-section">
+    </section>
+    <section id="open-document-section">
+      <item>
+        <attribute name="id">open-file</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="run-menu">
+    <section id="run-menu-section">
+      <attribute name="label" translatable="yes">Run Options</attribute>
+      <item>
+        <attribute name="id">default-run-handler</attribute>
+        <attribute name="action">run-manager.run-with-handler</attribute>
+        <attribute name="target">run</attribute>
+        <attribute name="label" translatable="yes">Run</attribute>
+        <attribute name="verb-icon-name">media-playback-start-symbolic</attribute>
+        <attribute name="accel">&lt;Control&gt;F5</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
+
diff --git a/src/libide/util/ide-cell-renderer-fancy.c b/src/libide/gui/ide-cell-renderer-fancy.c
similarity index 99%
rename from src/libide/util/ide-cell-renderer-fancy.c
rename to src/libide/gui/ide-cell-renderer-fancy.c
index 5c7dc33a0..212cb58f0 100644
--- a/src/libide/util/ide-cell-renderer-fancy.c
+++ b/src/libide/gui/ide-cell-renderer-fancy.c
@@ -22,7 +22,7 @@
 
 #include "config.h"
 
-#include "util/ide-cell-renderer-fancy.h"
+#include "ide-cell-renderer-fancy.h"
 
 #define TITLE_SPACING 3
 
diff --git a/src/libide/util/ide-cell-renderer-fancy.h b/src/libide/gui/ide-cell-renderer-fancy.h
similarity index 92%
rename from src/libide/util/ide-cell-renderer-fancy.h
rename to src/libide/gui/ide-cell-renderer-fancy.h
index 5d6e6bd6b..2d768612f 100644
--- a/src/libide/util/ide-cell-renderer-fancy.h
+++ b/src/libide/gui/ide-cell-renderer-fancy.h
@@ -20,9 +20,12 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gtk/gtk.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/gui/ide-command-provider.c b/src/libide/gui/ide-command-provider.c
new file mode 100644
index 000000000..8b0177ab9
--- /dev/null
+++ b/src/libide/gui/ide-command-provider.c
@@ -0,0 +1,103 @@
+/* ide-command-provider.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-command-provider"
+
+#include "config.h"
+
+#include "ide-command-provider.h"
+
+G_DEFINE_INTERFACE (IdeCommandProvider, ide_command_provider, G_TYPE_OBJECT)
+
+static void
+ide_command_provider_real_query_async (IdeCommandProvider  *self,
+                                       IdeWorkspace        *workspace,
+                                       const gchar         *typed_text,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_command_provider_real_query_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Querying is not supported by this provider");
+}
+
+static GPtrArray *
+ide_command_provider_real_query_finish (IdeCommandProvider  *self,
+                                        GAsyncResult        *result,
+                                        GError             **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_command_provider_default_init (IdeCommandProviderInterface *iface)
+{
+  iface->query_async = ide_command_provider_real_query_async;
+  iface->query_finish = ide_command_provider_real_query_finish;
+}
+
+void
+ide_command_provider_query_async (IdeCommandProvider  *self,
+                                  IdeWorkspace        *workspace,
+                                  const gchar         *typed_text,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_COMMAND_PROVIDER (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+  g_return_if_fail (typed_text != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_COMMAND_PROVIDER_GET_IFACE (self)->query_async (self,
+                                                      workspace,
+                                                      typed_text,
+                                                      cancellable,
+                                                      callback,
+                                                      user_data);
+}
+
+/**
+ * ide_command_provider_query_finish:
+ * @self: a #IdeCommandProvider
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to locate all the commands matching the
+ * users typed text.
+ *
+ * Returns: (transfer full) (element-type IdeCommand): a #GPtrArray of
+ *   #IdeCommand, or %NULL.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_command_provider_query_finish (IdeCommandProvider  *self,
+                                   GAsyncResult        *result,
+                                   GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND_PROVIDER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_COMMAND_PROVIDER_GET_IFACE (self)->query_finish (self, result, error);
+}
diff --git a/src/libide/gui/ide-command-provider.h b/src/libide/gui/ide-command-provider.h
new file mode 100644
index 000000000..5fc08f06b
--- /dev/null
+++ b/src/libide/gui/ide-command-provider.h
@@ -0,0 +1,66 @@
+/* ide-command-provider.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-command.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMMAND_PROVIDER (ide_command_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCommandProvider, ide_command_provider, IDE, COMMAND_PROVIDER, GObject)
+
+struct _IdeCommandProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void       (*query_async)  (IdeCommandProvider   *self,
+                              IdeWorkspace         *workspace,
+                              const gchar          *typed_text,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  GPtrArray *(*query_finish) (IdeCommandProvider   *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void       ide_command_provider_query_async  (IdeCommandProvider   *self,
+                                              IdeWorkspace         *workspace,
+                                              const gchar          *typed_text,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_command_provider_query_finish (IdeCommandProvider   *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-command.c b/src/libide/gui/ide-command.c
new file mode 100644
index 000000000..8d99d7638
--- /dev/null
+++ b/src/libide/gui/ide-command.c
@@ -0,0 +1,153 @@
+/* ide-command.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-command"
+
+#include "config.h"
+
+#include "ide-command.h"
+
+G_DEFINE_INTERFACE (IdeCommand, ide_command, IDE_TYPE_OBJECT)
+
+static void
+ide_command_real_run_async (IdeCommand          *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_COMMAND (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_command_real_run_async);
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "The operation is not supported");
+}
+
+static gboolean
+ide_command_real_run_finish (IdeCommand    *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_command_default_init (IdeCommandInterface *iface)
+{
+  iface->run_async = ide_command_real_run_async;
+  iface->run_finish = ide_command_real_run_finish;
+}
+
+/**
+ * ide_command_run_async:
+ * @self: an #IdeCommand
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Runs the command, asynchronously.
+ *
+ * Use ide_command_run_finish() to get the result of the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_command_run_async (IdeCommand          *self,
+                       GCancellable        *cancellable,
+                       GAsyncReadyCallback  callback,
+                       gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_COMMAND (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_COMMAND_GET_IFACE (self)->run_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_command_run_finish:
+ * @self: an #IdeCommand
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if the command was successful; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_command_run_finish (IdeCommand    *self,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_COMMAND_GET_IFACE (self)->run_finish (self, result, error);
+}
+
+/**
+ * ide_command_get_title:
+ * @self: a #IdeCommand
+ *
+ * Gets the title for the command.
+ *
+ * Returns: a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_command_get_title (IdeCommand *self)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), NULL);
+
+  if (IDE_COMMAND_GET_IFACE (self)->get_title)
+    return IDE_COMMAND_GET_IFACE (self)->get_title (self);
+
+  return NULL;
+}
+
+/**
+ * ide_command_get_subtitle:
+ * @self: a #IdeCommand
+ *
+ * Gets the subtitle for the command.
+ *
+ * Returns: a string containing the subtitle
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_command_get_subtitle (IdeCommand *self)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), NULL);
+
+  if (IDE_COMMAND_GET_IFACE (self)->get_subtitle)
+    return IDE_COMMAND_GET_IFACE (self)->get_subtitle (self);
+
+  return NULL;
+}
diff --git a/src/libide/gui/ide-command.h b/src/libide/gui/ide-command.h
new file mode 100644
index 000000000..0b9f389c7
--- /dev/null
+++ b/src/libide/gui/ide-command.h
@@ -0,0 +1,66 @@
+/* ide-command.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMMAND (ide_command_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCommand, ide_command, IDE, COMMAND, IdeObject)
+
+struct _IdeCommandInterface
+{
+  GTypeInterface parent_iface;
+
+  gchar    *(*get_title)    (IdeCommand           *self);
+  gchar    *(*get_subtitle) (IdeCommand           *self);
+  void      (*run_async)    (IdeCommand           *self,
+                             GCancellable         *cancellable,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data);
+  gboolean  (*run_finish)   (IdeCommand           *self,
+                             GAsyncResult         *result,
+                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_command_get_title    (IdeCommand           *self);
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_command_get_subtitle (IdeCommand           *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_command_run_async    (IdeCommand           *self,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_command_run_finish   (IdeCommand           *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-config-view-addin.c b/src/libide/gui/ide-config-view-addin.c
new file mode 100644
index 000000000..eefc65ec1
--- /dev/null
+++ b/src/libide/gui/ide-config-view-addin.c
@@ -0,0 +1,46 @@
+/* ide-config-view-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-config-view-addin"
+
+#include "config.h"
+
+#include "ide-config-view-addin.h"
+
+G_DEFINE_INTERFACE (IdeConfigViewAddin, ide_config_view_addin, G_TYPE_OBJECT)
+
+static void
+ide_config_view_addin_default_init (IdeConfigViewAddinInterface *iface)
+{
+}
+
+void
+ide_config_view_addin_load (IdeConfigViewAddin *self,
+                            DzlPreferences     *preferences,
+                            IdeConfiguration   *configuration)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIG_VIEW_ADDIN (self));
+  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+  g_return_if_fail (IDE_IS_CONFIGURATION (configuration));
+
+  if (IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load)
+    IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load (self, preferences, configuration);
+}
diff --git a/src/libide/gui/ide-config-view-addin.h b/src/libide/gui/ide-config-view-addin.h
new file mode 100644
index 000000000..8786c80e6
--- /dev/null
+++ b/src/libide/gui/ide-config-view-addin.h
@@ -0,0 +1,48 @@
+/* ide-config-view-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONFIG_VIEW_ADDIN (ide_config_view_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeConfigViewAddin, ide_config_view_addin, IDE, CONFIG_VIEW_ADDIN, GObject)
+
+struct _IdeConfigViewAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load) (IdeConfigViewAddin *self,
+                DzlPreferences     *preferences,
+                IdeConfiguration   *configuration);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_config_view_addin_load (IdeConfigViewAddin *self,
+                                 DzlPreferences     *preferences,
+                                 IdeConfiguration   *configuration);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-environment-editor-row.c b/src/libide/gui/ide-environment-editor-row.c
new file mode 100644
index 000000000..5e39f9da1
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor-row.c
@@ -0,0 +1,278 @@
+/* ide-environment-editor-row.c
+ *
+ * Copyright 2016-2019 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-environment-editor-row"
+
+#include "config.h"
+
+#include "ide-environment-editor-row.h"
+
+struct _IdeEnvironmentEditorRow
+{
+  GtkListBoxRow           parent_instance;
+
+  IdeEnvironmentVariable *variable;
+
+  GtkEntry               *key_entry;
+  GtkEntry               *value_entry;
+  GtkButton              *delete_button;
+
+  GBinding               *key_binding;
+  GBinding               *value_binding;
+};
+
+enum {
+  PROP_0,
+  PROP_VARIABLE,
+  LAST_PROP
+};
+
+enum {
+  DELETE,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE (IdeEnvironmentEditorRow, ide_environment_editor_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static gboolean
+null_safe_mapping (GBinding     *binding,
+                   const GValue *from_value,
+                   GValue       *to_value,
+                   gpointer      user_data)
+{
+  const gchar *str = g_value_get_string (from_value);
+  g_value_set_string (to_value, str ?: "");
+  return TRUE;
+}
+
+static void
+ide_environment_editor_row_connect (IdeEnvironmentEditorRow *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (self->variable));
+
+  self->key_binding =
+    g_object_bind_property_full (self->variable, "key", self->key_entry, "text",
+                                 G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                                 null_safe_mapping, NULL, NULL, NULL);
+
+  self->value_binding =
+    g_object_bind_property_full (self->variable, "value", self->value_entry, "text",
+                                 G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                                 null_safe_mapping, NULL, NULL, NULL);
+}
+
+static void
+ide_environment_editor_row_disconnect (IdeEnvironmentEditorRow *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (self->variable));
+
+  g_clear_pointer (&self->key_binding, g_binding_unbind);
+  g_clear_pointer (&self->value_binding, g_binding_unbind);
+}
+
+static void
+delete_button_clicked (GtkButton               *button,
+                       IdeEnvironmentEditorRow *self)
+{
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  g_signal_emit (self, signals [DELETE], 0);
+}
+
+static void
+key_entry_activate (GtkWidget               *entry,
+                    IdeEnvironmentEditorRow *self)
+{
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->value_entry));
+}
+
+static void
+value_entry_activate (GtkWidget               *entry,
+                      IdeEnvironmentEditorRow *self)
+{
+  GtkWidget *parent;
+
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self));
+  parent = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_LIST_BOX);
+  g_signal_emit_by_name (parent, "move-cursor", GTK_MOVEMENT_DISPLAY_LINES, 1);
+}
+
+static void
+ide_environment_editor_row_destroy (GtkWidget *widget)
+{
+  IdeEnvironmentEditorRow *self = (IdeEnvironmentEditorRow *)widget;
+
+  if (self->variable != NULL)
+    {
+      ide_environment_editor_row_disconnect (self);
+      g_clear_object (&self->variable);
+    }
+
+  GTK_WIDGET_CLASS (ide_environment_editor_row_parent_class)->destroy (widget);
+}
+
+static void
+ide_environment_editor_row_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  IdeEnvironmentEditorRow *self = IDE_ENVIRONMENT_EDITOR_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_VARIABLE:
+      g_value_set_object (value, ide_environment_editor_row_get_variable (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_row_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  IdeEnvironmentEditorRow *self = IDE_ENVIRONMENT_EDITOR_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_VARIABLE:
+      ide_environment_editor_row_set_variable (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_row_class_init (IdeEnvironmentEditorRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_environment_editor_row_get_property;
+  object_class->set_property = ide_environment_editor_row_set_property;
+
+  widget_class->destroy = ide_environment_editor_row_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-environment-editor-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, delete_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, key_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, value_entry);
+
+  properties [PROP_VARIABLE] =
+    g_param_spec_object ("variable",
+                         "Variable",
+                         "Variable",
+                         IDE_TYPE_ENVIRONMENT_VARIABLE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [DELETE] =
+    g_signal_new ("delete",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_environment_editor_row_init (IdeEnvironmentEditorRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect (self->delete_button,
+                    "clicked",
+                    G_CALLBACK (delete_button_clicked),
+                    self);
+
+  g_signal_connect (self->key_entry,
+                    "activate",
+                    G_CALLBACK (key_entry_activate),
+                    self);
+
+  g_signal_connect (self->value_entry,
+                    "activate",
+                    G_CALLBACK (value_entry_activate),
+                    self);
+}
+
+/**
+ * ide_environment_editor_row_get_variable:
+ *
+ * Returns: (transfer none) (nullable): An #IdeEnvironmentVariable.
+ */
+IdeEnvironmentVariable *
+ide_environment_editor_row_get_variable (IdeEnvironmentEditorRow *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self), NULL);
+
+  return self->variable;
+}
+
+void
+ide_environment_editor_row_set_variable (IdeEnvironmentEditorRow *self,
+                                         IdeEnvironmentVariable  *variable)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_return_if_fail (!variable || IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  if (variable != self->variable)
+    {
+      if (self->variable != NULL)
+        {
+          ide_environment_editor_row_disconnect (self);
+          g_clear_object (&self->variable);
+        }
+
+      if (variable != NULL)
+        {
+          self->variable = g_object_ref (variable);
+          ide_environment_editor_row_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VARIABLE]);
+    }
+}
+
+void
+ide_environment_editor_row_start_editing (IdeEnvironmentEditorRow *self)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->key_entry));
+}
diff --git a/src/libide/gui/ide-environment-editor-row.h b/src/libide/gui/ide-environment-editor-row.h
new file mode 100644
index 000000000..01ae2b83e
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor-row.h
@@ -0,0 +1,37 @@
+/* ide-environment-editor-row.h
+ *
+ * Copyright 2016-2019 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT_EDITOR_ROW (ide_environment_editor_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEnvironmentEditorRow, ide_environment_editor_row, IDE, ENVIRONMENT_EDITOR_ROW, 
GtkListBoxRow)
+
+IdeEnvironmentVariable *ide_environment_editor_row_get_variable  (IdeEnvironmentEditorRow *self);
+void                    ide_environment_editor_row_set_variable  (IdeEnvironmentEditorRow *self,
+                                                                  IdeEnvironmentVariable  *variable);
+void                    ide_environment_editor_row_start_editing (IdeEnvironmentEditorRow *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-environment-editor-row.ui b/src/libide/gui/ide-environment-editor-row.ui
new file mode 100644
index 000000000..56b176a2f
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor-row.ui
@@ -0,0 +1,49 @@
+<interface>
+  <template class="IdeEnvironmentEditorRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkEntry" id="key_entry">
+            <property name="has-frame">false</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="eq_label">
+            <property name="label">=</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="value_entry">
+            <property name="hexpand">true</property>
+            <property name="has-frame">false</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="delete_button">
+            <property name="visible">true</property>
+            <property name="tooltip-text" translatable="yes">Remove environment variable</property>
+            <style>
+              <class name="image-button"/>
+              <class name="flat"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">list-remove-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-environment-editor.c b/src/libide/gui/ide-environment-editor.c
new file mode 100644
index 000000000..31c4e5b27
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor.c
@@ -0,0 +1,317 @@
+/* ide-environment-editor.c
+ *
+ * Copyright 2016-2019 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-environment-editor"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-environment-editor.h"
+#include "ide-environment-editor-row.h"
+
+struct _IdeEnvironmentEditor
+{
+  GtkListBox      parent_instance;
+  IdeEnvironment *environment;
+  GtkWidget      *dummy_row;
+
+  IdeEnvironmentVariable *dummy;
+};
+
+G_DEFINE_TYPE (IdeEnvironmentEditor, ide_environment_editor, GTK_TYPE_LIST_BOX)
+
+enum {
+  PROP_0,
+  PROP_ENVIRONMENT,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_environment_editor_delete_row (IdeEnvironmentEditor    *self,
+                                   IdeEnvironmentEditorRow *row)
+{
+  IdeEnvironmentVariable *variable;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (row));
+
+  variable = ide_environment_editor_row_get_variable (row);
+  ide_environment_remove (self->environment, variable);
+}
+
+static GtkWidget *
+ide_environment_editor_create_dummy_row (IdeEnvironmentEditor *self)
+{
+  GtkWidget *row;
+  GtkWidget *label;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", _("New variable…"),
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "child", label,
+                      "visible", TRUE,
+                      NULL);
+
+  return row;
+}
+
+static GtkWidget *
+ide_environment_editor_create_row (gpointer item,
+                                   gpointer user_data)
+{
+  IdeEnvironmentVariable *variable = item;
+  IdeEnvironmentEditor *self = user_data;
+  IdeEnvironmentEditorRow *row;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  row = g_object_new (IDE_TYPE_ENVIRONMENT_EDITOR_ROW,
+                      "variable", variable,
+                      "visible", TRUE,
+                      NULL);
+
+  g_signal_connect_object (row,
+                           "delete",
+                           G_CALLBACK (ide_environment_editor_delete_row),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  return GTK_WIDGET (row);
+}
+
+static void
+ide_environment_editor_disconnect (IdeEnvironmentEditor *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT (self->environment));
+
+  gtk_list_box_bind_model (GTK_LIST_BOX (self), NULL, NULL, NULL, NULL);
+
+  g_clear_object (&self->dummy);
+}
+
+static void
+ide_environment_editor_connect (IdeEnvironmentEditor *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT (self->environment));
+
+  gtk_list_box_bind_model (GTK_LIST_BOX (self),
+                           G_LIST_MODEL (self->environment),
+                           ide_environment_editor_create_row, self, NULL);
+
+  self->dummy_row = ide_environment_editor_create_dummy_row (self);
+  gtk_container_add (GTK_CONTAINER (self), self->dummy_row);
+}
+
+static void
+find_row_cb (GtkWidget *widget,
+             gpointer   data)
+{
+  struct {
+    IdeEnvironmentVariable  *variable;
+    IdeEnvironmentEditorRow *row;
+  } *lookup = data;
+
+  g_assert (lookup != NULL);
+  g_assert (GTK_IS_LIST_BOX_ROW (widget));
+
+  if (IDE_IS_ENVIRONMENT_EDITOR_ROW (widget))
+    {
+      IdeEnvironmentVariable *variable;
+
+      variable = ide_environment_editor_row_get_variable (IDE_ENVIRONMENT_EDITOR_ROW (widget));
+
+      if (variable == lookup->variable)
+        lookup->row = IDE_ENVIRONMENT_EDITOR_ROW (widget);
+    }
+}
+
+static IdeEnvironmentEditorRow *
+find_row (IdeEnvironmentEditor   *self,
+          IdeEnvironmentVariable *variable)
+{
+  struct {
+    IdeEnvironmentVariable  *variable;
+    IdeEnvironmentEditorRow *row;
+  } lookup = { variable, NULL };
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  gtk_container_foreach (GTK_CONTAINER (self), find_row_cb, &lookup);
+
+  return lookup.row;
+}
+
+static void
+ide_environment_editor_row_activated (GtkListBox    *list_box,
+                                      GtkListBoxRow *row)
+{
+  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)list_box;
+
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+
+  if (self->environment == NULL)
+    return;
+
+  if (self->dummy_row == GTK_WIDGET (row))
+    {
+      g_autoptr(IdeEnvironmentVariable) variable = NULL;
+
+      variable = ide_environment_variable_new (NULL, NULL);
+      ide_environment_append (self->environment, variable);
+      ide_environment_editor_row_start_editing (find_row (self, variable));
+    }
+}
+
+static void
+ide_environment_editor_destroy (GtkWidget *widget)
+{
+  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)widget;
+
+  GTK_WIDGET_CLASS (ide_environment_editor_parent_class)->destroy (widget);
+
+  g_clear_object (&self->environment);
+}
+
+static void
+ide_environment_editor_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeEnvironmentEditor *self = IDE_ENVIRONMENT_EDITOR(object);
+
+  switch (prop_id)
+    {
+    case PROP_ENVIRONMENT:
+      g_value_set_object (value, ide_environment_editor_get_environment (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeEnvironmentEditor *self = IDE_ENVIRONMENT_EDITOR(object);
+
+  switch (prop_id)
+    {
+    case PROP_ENVIRONMENT:
+      ide_environment_editor_set_environment (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_class_init (IdeEnvironmentEditorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkListBoxClass *list_box_class = GTK_LIST_BOX_CLASS (klass);
+
+  object_class->get_property = ide_environment_editor_get_property;
+  object_class->set_property = ide_environment_editor_set_property;
+
+  widget_class->destroy = ide_environment_editor_destroy;
+
+  list_box_class->row_activated = ide_environment_editor_row_activated;
+
+  properties [PROP_ENVIRONMENT] =
+    g_param_spec_object ("environment",
+                         "Environment",
+                         "Environment",
+                         IDE_TYPE_ENVIRONMENT,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_environment_editor_init (IdeEnvironmentEditor *self)
+{
+  gtk_list_box_set_selection_mode (GTK_LIST_BOX (self), GTK_SELECTION_NONE);
+}
+
+GtkWidget *
+ide_environment_editor_new (void)
+{
+  return g_object_new (IDE_TYPE_ENVIRONMENT_EDITOR, NULL);
+}
+
+void
+ide_environment_editor_set_environment (IdeEnvironmentEditor *self,
+                                        IdeEnvironment       *environment)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_return_if_fail (IDE_IS_ENVIRONMENT (environment));
+
+  if (self->environment != environment)
+    {
+      if (self->environment != NULL)
+        {
+          ide_environment_editor_disconnect (self);
+          g_clear_object (&self->environment);
+        }
+
+      if (environment != NULL)
+        {
+          self->environment = g_object_ref (environment);
+          ide_environment_editor_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENVIRONMENT]);
+    }
+}
+
+/**
+ * ide_environment_editor_get_environment:
+ *
+ * Returns: (nullable) (transfer none): An #IdeEnvironment or %NULL.
+ */
+IdeEnvironment *
+ide_environment_editor_get_environment (IdeEnvironmentEditor *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_EDITOR (self), NULL);
+
+  return self->environment;
+}
diff --git a/src/libide/gui/ide-environment-editor.h b/src/libide/gui/ide-environment-editor.h
new file mode 100644
index 000000000..2a5731da2
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor.h
@@ -0,0 +1,42 @@
+/* ide-environment-editor.h
+ *
+ * Copyright 2016-2019 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT_EDITOR (ide_environment_editor_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, IDE, ENVIRONMENT_EDITOR, GtkListBox)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget      *ide_environment_editor_new             (void);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment *ide_environment_editor_get_environment (IdeEnvironmentEditor *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_editor_set_environment (IdeEnvironmentEditor *self,
+                                                        IdeEnvironment       *environment);
+
+G_END_DECLS
diff --git a/src/libide/util/ide-fancy-tree-view.c b/src/libide/gui/ide-fancy-tree-view.c
similarity index 98%
rename from src/libide/util/ide-fancy-tree-view.c
rename to src/libide/gui/ide-fancy-tree-view.c
index 83584f939..30cf656c7 100644
--- a/src/libide/util/ide-fancy-tree-view.c
+++ b/src/libide/gui/ide-fancy-tree-view.c
@@ -24,8 +24,8 @@
 
 #include <dazzle.h>
 
-#include "util/ide-cell-renderer-fancy.h"
-#include "util/ide-fancy-tree-view.h"
+#include "ide-cell-renderer-fancy.h"
+#include "ide-fancy-tree-view.h"
 
 /**
  * SECTION:ide-fancy-tree-view:
diff --git a/src/libide/util/ide-fancy-tree-view.h b/src/libide/gui/ide-fancy-tree-view.h
similarity index 89%
rename from src/libide/util/ide-fancy-tree-view.h
rename to src/libide/gui/ide-fancy-tree-view.h
index 73965daa5..e8c7caf0b 100644
--- a/src/libide/util/ide-fancy-tree-view.h
+++ b/src/libide/gui/ide-fancy-tree-view.h
@@ -20,9 +20,12 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gtk/gtk.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -36,10 +39,7 @@ struct _IdeFancyTreeViewClass
   GtkTreeViewClass parent_class;
 
   /*< private >*/
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
+  gpointer _reserved[8];
 };
 
 IDE_AVAILABLE_IN_3_32
diff --git a/src/libide/gui/ide-frame-actions.c b/src/libide/gui/ide-frame-actions.c
new file mode 100644
index 000000000..ea2af0f3e
--- /dev/null
+++ b/src/libide/gui/ide-frame-actions.c
@@ -0,0 +1,429 @@
+/* ide-frame-actions.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-frame-actions"
+
+#include "config.h"
+
+#include "ide-frame.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workbench.h"
+
+static void
+ide_frame_actions_next_page (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  g_signal_emit_by_name (self, "change-current-page", 1);
+}
+
+static void
+ide_frame_actions_previous_page (GSimpleAction *action,
+                                 GVariant      *variant,
+                                 gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  g_signal_emit_by_name (self, "change-current-page", -1);
+}
+
+static void
+ide_frame_actions_close_page (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  IdePage *page;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  page = ide_frame_get_visible_child (self);
+  if (page != NULL)
+    _ide_frame_request_close (self, page);
+}
+
+static void
+ide_frame_actions_move (IdeFrame *self,
+                        gint      direction)
+{
+  IdePage *page;
+  IdeFrame *dest;
+  GtkWidget *grid;
+  GtkWidget *column;
+  gint index = 0;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (direction == 1 || direction == -1);
+
+  page = ide_frame_get_visible_child (self);
+
+  g_return_if_fail (page != NULL);
+  g_return_if_fail (IDE_IS_PAGE (page));
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_GRID (grid));
+
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
+  g_return_if_fail (column != NULL);
+  g_return_if_fail (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (dest != self);
+  g_return_if_fail (IDE_IS_FRAME (dest));
+
+  _ide_frame_transfer (self, dest, page);
+}
+
+static void
+ide_frame_actions_move_right (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_actions_move (self, 1);
+}
+
+static void
+ide_frame_actions_move_left (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_actions_move (self, -1);
+}
+
+static void
+ide_frame_actions_split_page (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  g_autoptr(GFile) file = NULL;
+  IdeBufferManager *bufmgr;
+  GObjectClass *klass;
+  const gchar *path;
+  GParamSpec *pspec;
+  IdeContext *context;
+  IdeBuffer *buffer;
+  GtkWidget *column;
+  GtkWidget *grid;
+  IdeFrame *dest;
+  IdePage *page;
+  IdePage *split_page;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  column = gtk_widget_get_parent (GTK_WIDGET (self));
+
+  if (column == NULL || !IDE_IS_GRID_COLUMN (column))
+    {
+      g_warning ("Failed to locate ancestor grid column");
+      return;
+    }
+
+  if (!(grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID)))
+    {
+      g_warning ("Failed to locate ancestor grid");
+      return;
+    }
+
+  if (!(page = ide_frame_get_visible_child (self)))
+    {
+      g_warning ("No page available to split");
+      return;
+    }
+
+  if ((path = g_variant_get_string (variant, NULL)) &&
+      !ide_str_empty0 (path) &&
+      (context = ide_widget_get_context (GTK_WIDGET (self))) &&
+      (bufmgr = ide_buffer_manager_from_context (context)) &&
+      (file = g_file_new_for_path (path)) &&
+      (buffer = ide_buffer_manager_find_buffer (bufmgr, file)) &&
+      (klass = G_OBJECT_GET_CLASS (page)) &&
+      (pspec = g_object_class_find_property (klass, "buffer")) &&
+      g_type_is_a (pspec->value_type, IDE_TYPE_BUFFER))
+    {
+      split_page = g_object_new (G_OBJECT_TYPE (page),
+                                 "buffer", buffer,
+                                 "visible", TRUE,
+                                 NULL);
+    }
+  else
+    {
+      if (!ide_page_get_can_split (page))
+        {
+          g_warning ("Attempt to split a page that cannot be split");
+          return;
+        }
+
+      if (!(split_page = ide_page_create_split (page)))
+        {
+          g_warning ("%s failed to create a split",
+                     G_OBJECT_TYPE_NAME (page));
+          return;
+        }
+    }
+
+  g_assert (IDE_IS_PAGE (split_page));
+  g_assert (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (self),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack_for_column (IDE_GRID (grid),
+                                             IDE_GRID_COLUMN (column),
+                                             ++index);
+
+  g_assert (IDE_IS_FRAME (dest));
+
+  gtk_container_add (GTK_CONTAINER (dest), GTK_WIDGET (split_page));
+}
+
+static void
+ide_frame_actions_open_in_new_frame (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  const gchar *filepath;
+  GtkWidget *grid;
+  GtkWidget *column;
+  IdeFrame *dest;
+  IdePage *page;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  filepath = g_variant_get_string (variant, NULL);
+  page = ide_frame_get_visible_child (self);
+
+  g_return_if_fail (page != NULL);
+  g_return_if_fail (IDE_IS_PAGE (page));
+
+  if (!ide_str_empty0 (filepath))
+    {
+      g_autoptr (GFile) file = NULL;
+      IdeBufferManager *buffer_manager;
+      IdeContext *context;
+      IdeBuffer *buffer;
+
+      context = ide_widget_get_context (GTK_WIDGET (self));
+      buffer_manager = ide_buffer_manager_from_context (context);
+      file = g_file_new_for_path (filepath);
+
+      if ((buffer = ide_buffer_manager_find_buffer (buffer_manager, file)))
+        page = g_object_new (G_OBJECT_TYPE (page),
+                             "buffer", buffer,
+                             "visible", TRUE,
+                             NULL);
+      else
+        return;
+    }
+  else
+    {
+      g_return_if_fail (ide_page_get_can_split (page));
+
+      page = ide_page_create_split (page);
+    }
+
+  if (page == NULL)
+    {
+      g_warning ("Requested split page but NULL was returned");
+      return;
+    }
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_GRID (grid));
+
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
+
+  g_return_if_fail (column != NULL);
+  g_return_if_fail (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), ++index);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (IDE_IS_FRAME (dest));
+
+  gtk_container_add (GTK_CONTAINER (dest), GTK_WIDGET (page));
+}
+
+static void
+ide_frame_actions_close_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeFrame *self = (IdeFrame *)object;
+  GtkWidget *parent;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_frame_agree_to_close_finish (self, result, NULL))
+    return;
+
+  /* Things might have changed during the async op */
+  parent = gtk_widget_get_parent (GTK_WIDGET (self));
+  if (!IDE_IS_GRID_COLUMN (parent))
+    return;
+
+  /* Make sure there is still more than a single stack */
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (parent)) > 1)
+    gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+ide_frame_actions_close_stack (GSimpleAction *action,
+                               GVariant      *variant,
+                               gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_agree_to_close_async (self,
+                                         NULL,
+                                         ide_frame_actions_close_cb,
+                                         NULL);
+}
+
+static void
+ide_frame_actions_show_list (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  IdeFrameHeader *header;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  header = IDE_FRAME_HEADER (ide_frame_get_titlebar (self));
+  _ide_frame_header_focus_list (header);
+}
+
+static const GActionEntry actions[] = {
+  { "open-in-new-frame", ide_frame_actions_open_in_new_frame, "s" },
+  { "close-stack",       ide_frame_actions_close_stack },
+  { "close-page",        ide_frame_actions_close_page },
+  { "next-page",         ide_frame_actions_next_page },
+  { "previous-page",     ide_frame_actions_previous_page },
+  { "move-right",        ide_frame_actions_move_right },
+  { "move-left",         ide_frame_actions_move_left },
+  { "split-page",        ide_frame_actions_split_page, "s" },
+  { "show-list",         ide_frame_actions_show_list },
+};
+
+void
+_ide_frame_update_actions (IdeFrame *self)
+{
+  IdePage *page;
+  GtkWidget *parent;
+  gboolean has_page = FALSE;
+  gboolean can_split_page = FALSE;
+  gboolean can_close_stack = FALSE;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+
+  page = ide_frame_get_visible_child (self);
+
+  if (page != NULL)
+    {
+      has_page = TRUE;
+      can_split_page = ide_page_get_can_split (page);
+    }
+
+  /* If there is more than one stack in the column, then we can close
+   * this stack directly without involving the column.
+   */
+  parent = gtk_widget_get_parent (GTK_WIDGET (self));
+  if (IDE_IS_GRID_COLUMN (parent))
+    can_close_stack = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (parent)) > 1;
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "move-right",
+                             "enabled", has_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "move-left",
+                             "enabled", has_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "open-in-new-frame",
+                             "enabled", can_split_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "split-page",
+                             "enabled", can_split_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "close-stack",
+                             "enabled", can_close_stack,
+                             NULL);
+}
+
+void
+_ide_frame_init_actions (IdeFrame *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "frame",
+                                  G_ACTION_GROUP (group));
+
+  _ide_frame_update_actions (self);
+}
diff --git a/src/libide/gui/ide-frame-addin.c b/src/libide/gui/ide-frame-addin.c
new file mode 100644
index 000000000..81893a80a
--- /dev/null
+++ b/src/libide/gui/ide-frame-addin.c
@@ -0,0 +1,111 @@
+/* ide-frame-addin.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-frame-addin"
+
+#include "config.h"
+
+#include "ide-frame-addin.h"
+
+/**
+ * SECTION:ide-frame-addin
+ * @title: IdeFrameAddin
+ * @short_description: addins created for every #IdeFrame
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeFrameAddin, ide_frame_addin, G_TYPE_OBJECT)
+
+static void
+ide_frame_addin_default_init (IdeFrameAddinInterface *iface)
+{
+}
+
+/**
+ * ide_frame_addin_load:
+ * @self: An #IdeFrameAddin
+ * @frame: An #IdeFrame
+ *
+ * This function should be implemented by #IdeFrameAddin plugins
+ * in #IdeFrameAddinInterface.
+ *
+ * This virtual method is called when the plugin should load itself.
+ * A new instance of the plugin is created for every #IdeFrame
+ * that is created in Builder.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_load (IdeFrameAddin *self,
+                      IdeFrame      *frame)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (IDE_IS_FRAME (frame));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->load)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->load (self, frame);
+}
+
+/**
+ * ide_frame_addin_unload:
+ * @self: An #IdeFrameAddin
+ * @frame: An #IdeFrame
+ *
+ * This function should be implemented by #IdeFrameAddin plugins
+ * in #IdeFrameAddinInterface.
+ *
+ * This virtual method is called when the plugin should unload itself.
+ * It should revert anything performed via ide_frame_addin_load().
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_unload (IdeFrameAddin *self,
+                        IdeFrame      *frame)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (IDE_IS_FRAME (frame));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->unload)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->unload (self, frame);
+}
+
+/**
+ * ide_frame_addin_set_page:
+ * @self: an #IdeFrameAddin
+ * @page: (nullable): An #IdePage or %NULL.
+ *
+ * This virtual method is called whenever the active page changes
+ * in the #IdePage. Plugins may want to alter what controls
+ * are displayed on the frame based on the current page.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_set_page (IdeFrameAddin *self,
+                          IdePage       *page)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->set_page)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->set_page (self, page);
+}
diff --git a/src/libide/gui/ide-frame-addin.h b/src/libide/gui/ide-frame-addin.h
new file mode 100644
index 000000000..819b9c551
--- /dev/null
+++ b/src/libide/gui/ide-frame-addin.h
@@ -0,0 +1,65 @@
+/* ide-frame-addin.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-frame.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME_ADDIN (ide_frame_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeFrameAddin, ide_frame_addin, IDE, FRAME_ADDIN, GObject)
+
+struct _IdeFrameAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)     (IdeFrameAddin *self,
+                    IdeFrame      *frame);
+  void (*unload)   (IdeFrameAddin *self,
+                    IdeFrame      *frame);
+  void (*set_page) (IdeFrameAddin *self,
+                    IdePage       *page);
+};
+
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_load          (IdeFrameAddin *self,
+                                                    IdeFrame      *frame);
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_unload        (IdeFrameAddin *self,
+                                                    IdeFrame      *frame);
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_set_page      (IdeFrameAddin *self,
+                                                    IdePage       *page);
+IDE_AVAILABLE_IN_3_32
+IdeFrameAddin *ide_frame_addin_find_by_module_name (IdeFrame      *frame,
+                                                    const gchar   *module_name);
+
+G_END_DECLS
diff --git a/src/libide/layout/ide-layout-stack-header.c b/src/libide/gui/ide-frame-header.c
similarity index 72%
rename from src/libide/layout/ide-layout-stack-header.c
rename to src/libide/gui/ide-frame-header.c
index 06f39c81f..9943fd10e 100644
--- a/src/libide/layout/ide-layout-stack-header.c
+++ b/src/libide/gui/ide-frame-header.c
@@ -1,6 +1,6 @@
-/* ide-layout-stack-header.c
+/* ide-frame-header.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,23 +18,23 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-stack-header"
+#define G_LOG_DOMAIN "ide-frame-header"
 
 #include "config.h"
 
 #include <glib/gi18n.h>
 
-#include "layout/ide-layout-private.h"
-#include "layout/ide-layout-stack-header.h"
+#include "ide-gui-private.h"
+#include "ide-frame-header.h"
 
 #define CSS_PROVIDER_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 100)
 
 /**
- * SECTION:ide-layout-stack-header
- * @title: IdeLayoutStackHeader
+ * SECTION:ide-frame-header
+ * @title: IdeFrameHeader
  * @short_description: The header above document stacks
  *
- * The IdeLayoutStackHeader is the titlebar widget above stacks of documents.
+ * The IdeFrameHeader is the titlebar widget above stacks of documents.
  * It is used to add state when a given document is in view.
  *
  * It can also track the primary color of the content and update it's
@@ -43,7 +43,7 @@
  * Since: 3.32
  */
 
-struct _IdeLayoutStackHeader
+struct _IdeFrameHeader
 {
   DzlPriorityBox  parent_instance;
 
@@ -79,27 +79,27 @@ enum {
   N_PROPS
 };
 
-G_DEFINE_TYPE (IdeLayoutStackHeader, ide_layout_stack_header, DZL_TYPE_PRIORITY_BOX)
+G_DEFINE_TYPE (IdeFrameHeader, ide_frame_header, DZL_TYPE_PRIORITY_BOX)
 
 static GParamSpec *properties [N_PROPS];
 
 void
-_ide_layout_stack_header_focus_list (IdeLayoutStackHeader *self)
+_ide_frame_header_focus_list (IdeFrameHeader *self)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
 
   gtk_popover_popup (self->title_popover);
   gtk_widget_grab_focus (GTK_WIDGET (self->title_list_box));
 }
 
 void
-_ide_layout_stack_header_hide (IdeLayoutStackHeader *self)
+_ide_frame_header_hide (IdeFrameHeader *self)
 {
   GtkPopover *popover;
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
 
-  /* This is like _ide_layout_stack_header_popdown() but we hide the
+  /* This is like _ide_frame_header_popdown() but we hide the
    * popovers immediately without performing the popdown animation.
    */
 
@@ -111,11 +111,11 @@ _ide_layout_stack_header_hide (IdeLayoutStackHeader *self)
 }
 
 void
-_ide_layout_stack_header_popdown (IdeLayoutStackHeader *self)
+_ide_frame_header_popdown (IdeFrameHeader *self)
 {
   GtkPopover *popover;
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
 
   popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->document_button));
   if (popover != NULL)
@@ -125,13 +125,13 @@ _ide_layout_stack_header_popdown (IdeLayoutStackHeader *self)
 }
 
 void
-_ide_layout_stack_header_update (IdeLayoutStackHeader *self,
-                                 IdeLayoutView        *view)
+_ide_frame_header_update (IdeFrameHeader *self,
+                                 IdePage        *view)
 {
-  const gchar *action = "layoutstack.close-view";
+  const gchar *action = "frame.close-page";
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (!view || IDE_IS_PAGE (view));
 
   /*
    * Update our menus for the document to include the menu type needed for the
@@ -144,7 +144,7 @@ _ide_layout_stack_header_update (IdeLayoutStackHeader *self,
 
   if (view != NULL)
     {
-      const gchar *menu_id = ide_layout_view_get_menu_id (view);
+      const gchar *menu_id = ide_page_get_menu_id (view);
 
       if (menu_id != NULL)
         {
@@ -173,14 +173,14 @@ _ide_layout_stack_header_update (IdeLayoutStackHeader *self,
       GtkWidget *stack;
       GtkWidget *column;
 
-      action = "layoutgridcolumn.close";
-      stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_STACK);
-      column = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_LAYOUT_GRID_COLUMN);
+      action = "gridcolumn.close";
+      stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+      column = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_GRID_COLUMN);
 
       if (stack != NULL && column != NULL)
         {
           if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) > 1)
-            action = "layoutstack.close-stack";
+            action = "frame.close-stack";
         }
     }
 
@@ -192,48 +192,48 @@ _ide_layout_stack_header_update (IdeLayoutStackHeader *self,
    * (inidicated by NULL view).
    */
   if (view == NULL)
-    _ide_layout_stack_header_popdown (self);
+    _ide_frame_header_popdown (self);
 }
 
 static void
 close_view_cb (GtkButton            *button,
-               IdeLayoutStackHeader *self)
+               IdeFrameHeader *self)
 {
   GtkWidget *stack;
   GtkWidget *row;
   GtkWidget *view;
 
   g_assert (GTK_IS_BUTTON (button));
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW);
   if (row == NULL)
     return;
 
-  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+  view = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
   if (view == NULL)
     return;
 
-  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_STACK);
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
   if (stack == NULL)
     return;
 
-  _ide_layout_stack_request_close (IDE_LAYOUT_STACK (stack), IDE_LAYOUT_VIEW (view));
+  _ide_frame_request_close (IDE_FRAME (stack), IDE_PAGE (view));
 }
 
 static GtkWidget *
 create_document_row (gpointer item,
                      gpointer user_data)
 {
-  IdeLayoutStackHeader *self = user_data;
+  IdeFrameHeader *self = user_data;
   GtkListBoxRow *row;
   GtkButton *close_button;
   GtkLabel *label;
   GtkImage *image;
   GtkBox *box;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (item));
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_PAGE (item));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
                       "visible", TRUE,
@@ -266,7 +266,7 @@ create_document_row (gpointer item,
   g_object_bind_property (item, "icon-name", image, "icon-name", G_BINDING_SYNC_CREATE);
   g_object_bind_property (item, "modified", label, "bold", G_BINDING_SYNC_CREATE);
   g_object_bind_property (item, "title", label, "label", G_BINDING_SYNC_CREATE);
-  g_object_set_data (G_OBJECT (row), "IDE_LAYOUT_VIEW", item);
+  g_object_set_data (G_OBJECT (row), "IDE_PAGE", item);
 
   gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
   gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
@@ -277,10 +277,10 @@ create_document_row (gpointer item,
 }
 
 void
-_ide_layout_stack_header_set_views (IdeLayoutStackHeader *self,
+_ide_frame_header_set_pages (IdeFrameHeader *self,
                                     GListModel           *model)
 {
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
   g_assert (!model || G_IS_LIST_MODEL (model));
 
   gtk_list_box_bind_model (self->title_list_box,
@@ -290,37 +290,36 @@ _ide_layout_stack_header_set_views (IdeLayoutStackHeader *self,
 }
 
 static void
-ide_layout_stack_header_view_row_activated (GtkListBox           *list_box,
+ide_frame_header_view_row_activated (GtkListBox           *list_box,
                                             GtkListBoxRow        *row,
-                                            IdeLayoutStackHeader *self)
+                                            IdeFrameHeader *self)
 {
   GtkWidget *stack;
-  GtkWidget *view;
+  GtkWidget *page;
 
   g_assert (GTK_IS_LIST_BOX (list_box));
   g_assert (GTK_IS_LIST_BOX_ROW (row));
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
-  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_STACK);
-  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+  page = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
 
-  if (stack != NULL && view != NULL)
+  if (stack != NULL && page != NULL)
     {
-      ide_layout_stack_set_visible_child (IDE_LAYOUT_STACK (stack),
-                                          IDE_LAYOUT_VIEW (view));
-      gtk_widget_grab_focus (view);
+      ide_frame_set_visible_child (IDE_FRAME (stack), IDE_PAGE (page));
+      gtk_widget_grab_focus (page);
     }
 
-  _ide_layout_stack_header_popdown (self);
+  _ide_frame_header_popdown (self);
 }
 
 static gboolean
-ide_layout_stack_header_update_css (IdeLayoutStackHeader *self)
+ide_frame_header_update_css (IdeFrameHeader *self)
 {
   g_autoptr(GString) str = NULL;
   g_autoptr(GError) error = NULL;
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
   g_assert (self->css_provider != NULL);
   g_assert (GTK_IS_CSS_PROVIDER (self->css_provider));
 
@@ -336,7 +335,7 @@ ide_layout_stack_header_update_css (IdeLayoutStackHeader *self)
     {
       g_autofree gchar *bgstr = gdk_rgba_to_string (&self->background_rgba);
 
-      g_string_append        (str, "idelayoutstackheader {\n");
+      g_string_append        (str, "ideframeheader {\n");
       g_string_append        (str, "  background: none;\n");
       g_string_append_printf (str, "  background-color: %s;\n", bgstr);
       g_string_append        (str, "  transition: background-color 400ms;\n");
@@ -375,26 +374,26 @@ ide_layout_stack_header_update_css (IdeLayoutStackHeader *self)
 }
 
 static void
-ide_layout_stack_header_queue_update_css (IdeLayoutStackHeader *self)
+ide_frame_header_queue_update_css (IdeFrameHeader *self)
 {
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   if (self->update_css_handler == 0)
     self->update_css_handler =
       gdk_threads_add_idle_full (G_PRIORITY_HIGH,
-                                 (GSourceFunc) ide_layout_stack_header_update_css,
+                                 (GSourceFunc) ide_frame_header_update_css,
                                  g_object_ref (self),
                                  g_object_unref);
 }
 
 void
-_ide_layout_stack_header_set_background_rgba (IdeLayoutStackHeader *self,
+_ide_frame_header_set_background_rgba (IdeFrameHeader *self,
                                               const GdkRGBA        *background_rgba)
 {
   GdkRGBA old;
   gboolean old_set;
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   old_set = self->background_rgba_set;
   old = self->background_rgba;
@@ -405,17 +404,17 @@ _ide_layout_stack_header_set_background_rgba (IdeLayoutStackHeader *self,
   self->background_rgba_set = !!background_rgba;
 
   if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->background_rgba, &old))
-    ide_layout_stack_header_queue_update_css (self);
+    ide_frame_header_queue_update_css (self);
 }
 
 void
-_ide_layout_stack_header_set_foreground_rgba (IdeLayoutStackHeader *self,
+_ide_frame_header_set_foreground_rgba (IdeFrameHeader *self,
                                               const GdkRGBA        *foreground_rgba)
 {
   GdkRGBA old;
   gboolean old_set;
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   old_set = self->foreground_rgba_set;
   old = self->foreground_rgba;
@@ -426,14 +425,14 @@ _ide_layout_stack_header_set_foreground_rgba (IdeLayoutStackHeader *self,
   self->foreground_rgba_set = !!foreground_rgba;
 
   if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->foreground_rgba, &old))
-    ide_layout_stack_header_queue_update_css (self);
+    ide_frame_header_queue_update_css (self);
 }
 
 static void
 update_widget_providers (GtkWidget            *widget,
-                         IdeLayoutStackHeader *self)
+                         IdeFrameHeader *self)
 {
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
   g_assert (GTK_IS_WIDGET (widget));
 
   /*
@@ -464,29 +463,29 @@ update_widget_providers (GtkWidget            *widget,
 }
 
 static void
-ide_layout_stack_header_add (GtkContainer *container,
+ide_frame_header_add (GtkContainer *container,
                              GtkWidget    *widget)
 {
-  IdeLayoutStackHeader *self = (IdeLayoutStackHeader *)container;
+  IdeFrameHeader *self = (IdeFrameHeader *)container;
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
   g_assert (GTK_IS_WIDGET (widget));
 
-  GTK_CONTAINER_CLASS (ide_layout_stack_header_parent_class)->add (container, widget);
+  GTK_CONTAINER_CLASS (ide_frame_header_parent_class)->add (container, widget);
 
   update_widget_providers (widget, self);
 }
 
 static void
-ide_layout_stack_header_get_preferred_width (GtkWidget *widget,
+ide_frame_header_get_preferred_width (GtkWidget *widget,
                                              gint      *min_width,
                                              gint      *nat_width)
 {
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (widget));
+  g_assert (IDE_IS_FRAME_HEADER (widget));
   g_assert (min_width != NULL);
   g_assert (nat_width != NULL);
 
-  GTK_WIDGET_CLASS (ide_layout_stack_header_parent_class)->get_preferred_width (widget, min_width, 
nat_width);
+  GTK_WIDGET_CLASS (ide_frame_header_parent_class)->get_preferred_width (widget, min_width, nat_width);
 
   /*
    * We don't want changes to the natural width to influence our positioning of
@@ -497,11 +496,11 @@ ide_layout_stack_header_get_preferred_width (GtkWidget *widget,
 }
 
 static void
-ide_layout_stack_header_destroy (GtkWidget *widget)
+ide_frame_header_destroy (GtkWidget *widget)
 {
-  IdeLayoutStackHeader *self = (IdeLayoutStackHeader *)widget;
+  IdeFrameHeader *self = (IdeFrameHeader *)widget;
 
-  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (IDE_IS_FRAME_HEADER (self));
 
   dzl_clear_source (&self->update_css_handler);
   g_clear_object (&self->css_provider);
@@ -511,16 +510,16 @@ ide_layout_stack_header_destroy (GtkWidget *widget)
 
   g_clear_object (&self->menu);
 
-  GTK_WIDGET_CLASS (ide_layout_stack_header_parent_class)->destroy (widget);
+  GTK_WIDGET_CLASS (ide_frame_header_parent_class)->destroy (widget);
 }
 
 static void
-ide_layout_stack_header_get_property (GObject    *object,
+ide_frame_header_get_property (GObject    *object,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
 {
-  IdeLayoutStackHeader *self = IDE_LAYOUT_STACK_HEADER (object);
+  IdeFrameHeader *self = IDE_FRAME_HEADER (object);
 
   switch (prop_id)
     {
@@ -542,25 +541,25 @@ ide_layout_stack_header_get_property (GObject    *object,
 }
 
 static void
-ide_layout_stack_header_set_property (GObject      *object,
+ide_frame_header_set_property (GObject      *object,
                                       guint         prop_id,
                                       const GValue *value,
                                       GParamSpec   *pspec)
 {
-  IdeLayoutStackHeader *self = IDE_LAYOUT_STACK_HEADER (object);
+  IdeFrameHeader *self = IDE_FRAME_HEADER (object);
 
   switch (prop_id)
     {
     case PROP_BACKGROUND_RGBA:
-      _ide_layout_stack_header_set_background_rgba (self, g_value_get_boxed (value));
+      _ide_frame_header_set_background_rgba (self, g_value_get_boxed (value));
       break;
 
     case PROP_FOREGROUND_RGBA:
-      _ide_layout_stack_header_set_foreground_rgba (self, g_value_get_boxed (value));
+      _ide_frame_header_set_foreground_rgba (self, g_value_get_boxed (value));
       break;
 
     case PROP_MODIFIED:
-      _ide_layout_stack_header_set_modified (self, g_value_get_boolean (value));
+      _ide_frame_header_set_modified (self, g_value_get_boolean (value));
       break;
 
     case PROP_SHOW_CLOSE_BUTTON:
@@ -568,7 +567,7 @@ ide_layout_stack_header_set_property (GObject      *object,
       break;
 
     case PROP_TITLE:
-      _ide_layout_stack_header_set_title (self, g_value_get_string (value));
+      _ide_frame_header_set_title (self, g_value_get_string (value));
       break;
 
     default:
@@ -577,26 +576,26 @@ ide_layout_stack_header_set_property (GObject      *object,
 }
 
 static void
-ide_layout_stack_header_class_init (IdeLayoutStackHeaderClass *klass)
+ide_frame_header_class_init (IdeFrameHeaderClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
 
-  object_class->get_property = ide_layout_stack_header_get_property;
-  object_class->set_property = ide_layout_stack_header_set_property;
+  object_class->get_property = ide_frame_header_get_property;
+  object_class->set_property = ide_frame_header_set_property;
 
-  widget_class->destroy = ide_layout_stack_header_destroy;
-  widget_class->get_preferred_width = ide_layout_stack_header_get_preferred_width;
+  widget_class->destroy = ide_frame_header_destroy;
+  widget_class->get_preferred_width = ide_frame_header_get_preferred_width;
 
-  container_class->add = ide_layout_stack_header_add;
+  container_class->add = ide_frame_header_add;
 
   /**
-   * IdeLayoutStackHeader:background-rgba:
+   * IdeFrameHeader:background-rgba:
    *
    * The "background-rgba" property can be used to set the background
    * color of the header. This should be set to the
-   * #IdeLayoutView:primary-color of the active view.
+   * #IdePage:primary-color of the active view.
    *
    * Set to %NULL to unset the primary-color.
    *
@@ -610,10 +609,10 @@ ide_layout_stack_header_class_init (IdeLayoutStackHeaderClass *klass)
                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
 
   /**
-   * IdeLayoutStackHeader:foreground-rgba:
+   * IdeFrameHeader:foreground-rgba:
    *
    * Sets the foreground color to use when
-   * #IdeLayoutStackHeader:background-rgba is used for the background.
+   * #IdeFrameHeader:background-rgba is used for the background.
    *
    * Since: 3.32
    */
@@ -647,21 +646,21 @@ ide_layout_stack_header_class_init (IdeLayoutStackHeaderClass *klass)
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_css_name (widget_class, "idelayoutstackheader");
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-layout-stack-header.ui");
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, close_button);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, document_button);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_box);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_button);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_label);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_list_box);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_modified);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_popover);
-  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_views_box);
+  gtk_widget_class_set_css_name (widget_class, "ideframeheader");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame-header.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, close_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, document_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_modified);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_views_box);
 }
 
 static void
-ide_layout_stack_header_init (IdeLayoutStackHeader *self)
+ide_frame_header_init (IdeFrameHeader *self)
 {
   GtkStyleContext *style_context;
   GMenu *frame_section;
@@ -692,7 +691,7 @@ ide_layout_stack_header_init (IdeLayoutStackHeader *self)
   self->menu = dzl_joined_menu_new ();
   dzl_menu_button_set_model (self->document_button, G_MENU_MODEL (self->menu));
   frame_section = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT,
-                                                  "ide-layout-stack-frame-menu");
+                                                  "ide-frame-menu");
   dzl_joined_menu_append_menu (self->menu, G_MENU_MODEL (frame_section));
 
   /*
@@ -702,7 +701,7 @@ ide_layout_stack_header_init (IdeLayoutStackHeader *self)
 
   g_signal_connect_object (self->title_list_box,
                            "row-activated",
-                           G_CALLBACK (ide_layout_stack_header_view_row_activated),
+                           G_CALLBACK (ide_frame_header_view_row_activated),
                            self, 0);
 
   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
@@ -711,21 +710,21 @@ ide_layout_stack_header_init (IdeLayoutStackHeader *self)
 }
 
 GtkWidget *
-ide_layout_stack_header_new (void)
+ide_frame_header_new (void)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_STACK_HEADER, NULL);
+  return g_object_new (IDE_TYPE_FRAME_HEADER, NULL);
 }
 
 /**
- * ide_layout_stack_header_add_custom_title:
- * @self: a #IdeLayoutStackHeader
+ * ide_frame_header_add_custom_title:
+ * @self: a #IdeFrameHeader
  * @widget: a #GtkWidget
  * @priority: the sort priority
  *
  * This will add @widget to the title area with @priority determining the
  * sort order of the child.
  *
- * All "title" widgets in the #IdeLayoutStackHeader are expanded to the
+ * All "title" widgets in the #IdeFrameHeader are expanded to the
  * same size. So if you don't need that, you should just use the normal
  * gtk_container_add_with_properties() API to specify your widget with
  * a given priority.
@@ -733,11 +732,11 @@ ide_layout_stack_header_new (void)
  * Since: 3.32
  */
 void
-ide_layout_stack_header_add_custom_title (IdeLayoutStackHeader *self,
+ide_frame_header_add_custom_title (IdeFrameHeader *self,
                                           GtkWidget            *widget,
                                           gint                  priority)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
   g_return_if_fail (GTK_IS_WIDGET (widget));
 
   gtk_container_add_with_properties (GTK_CONTAINER (self->title_box), widget,
@@ -748,20 +747,20 @@ ide_layout_stack_header_add_custom_title (IdeLayoutStackHeader *self,
 }
 
 void
-_ide_layout_stack_header_set_title (IdeLayoutStackHeader *self,
+_ide_frame_header_set_title (IdeFrameHeader *self,
                                     const gchar          *title)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
 
   gtk_label_set_label (GTK_LABEL (self->title_label), title);
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
 }
 
 void
-_ide_layout_stack_header_set_modified (IdeLayoutStackHeader *self,
+_ide_frame_header_set_modified (IdeFrameHeader *self,
                                        gboolean              modified)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
 
   gtk_widget_set_visible (GTK_WIDGET (self->title_modified), modified);
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
diff --git a/src/libide/layout/ide-layout-stack-header.h b/src/libide/gui/ide-frame-header.h
similarity index 60%
rename from src/libide/layout/ide-layout-stack-header.h
rename to src/libide/gui/ide-frame-header.h
index b33050257..26419db07 100644
--- a/src/libide/layout/ide-layout-stack-header.h
+++ b/src/libide/gui/ide-frame-header.h
@@ -1,4 +1,4 @@
-/* ide-layout-stack-header.h
+/* ide-frame-header.h
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -20,22 +20,25 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <dazzle.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT_STACK_HEADER (ide_layout_stack_header_get_type())
+#define IDE_TYPE_FRAME_HEADER (ide_frame_header_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdeLayoutStackHeader, ide_layout_stack_header, IDE, LAYOUT_STACK_HEADER, 
DzlPriorityBox)
+G_DECLARE_FINAL_TYPE (IdeFrameHeader, ide_frame_header, IDE, FRAME_HEADER, DzlPriorityBox)
 
 IDE_AVAILABLE_IN_3_32
-GtkWidget *ide_layout_stack_header_new              (void);
+GtkWidget *ide_frame_header_new              (void);
 IDE_AVAILABLE_IN_3_32
-void       ide_layout_stack_header_add_custom_title (IdeLayoutStackHeader *self,
-                                                     GtkWidget            *widget,
-                                                     gint                  priority);
+void       ide_frame_header_add_custom_title (IdeFrameHeader *self,
+                                              GtkWidget      *widget,
+                                              gint            priority);
 
 G_END_DECLS
diff --git a/src/libide/layout/ide-layout-stack-header.ui b/src/libide/gui/ide-frame-header.ui
similarity index 98%
rename from src/libide/layout/ide-layout-stack-header.ui
rename to src/libide/gui/ide-frame-header.ui
index ba8f583ac..a9a8b776d 100644
--- a/src/libide/layout/ide-layout-stack-header.ui
+++ b/src/libide/gui/ide-frame-header.ui
@@ -112,7 +112,7 @@
       </object>
     </child>
   </object>
-  <template class="IdeLayoutStackHeader" parent="DzlPriorityBox">
+  <template class="IdeFrameHeader" parent="DzlPriorityBox">
     <child>
       <object class="DzlPriorityBox" id="title_box">
         <property name="hexpand">true</property>
@@ -153,7 +153,7 @@
     </child>
     <child>
       <object class="GtkButton" id="close_button">
-        <property name="action-name">layoutgridcolumn.close</property>
+        <property name="action-name">gridcolumn.close</property>
         <property name="visible">true</property>
         <child>
           <object class="GtkImage">
diff --git a/src/libide/layout/ide-layout-stack-shortcuts.c b/src/libide/gui/ide-frame-shortcuts.c
similarity index 78%
rename from src/libide/layout/ide-layout-stack-shortcuts.c
rename to src/libide/gui/ide-frame-shortcuts.c
index da1c3a1ef..eef3655b6 100644
--- a/src/libide/layout/ide-layout-stack-shortcuts.c
+++ b/src/libide/gui/ide-frame-shortcuts.c
@@ -1,4 +1,4 @@
-/* ide-layout-stack-shortcuts.c
+/* ide-frame-shortcuts.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,45 +18,46 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+
 #include "config.h"
 
 #include <glib/gi18n.h>
 
-#include "layout/ide-layout-stack.h"
-#include "layout/ide-layout-private.h"
+#include "ide-frame.h"
+#include "ide-gui-private.h"
 
 #define I_(s) g_intern_static_string(s)
 
-static const DzlShortcutEntry stack_shortcuts[] = {
-  { "org.gnome.builder.layoutstack.move-right",
+static const DzlShortcutEntry frame_shortcuts[] = {
+  { "org.gnome.builder.frame.move-right",
     DZL_SHORTCUT_PHASE_CAPTURE,
     NULL,
     NC_("shortcut window", "Editor shortcuts"),
     NC_("shortcut window", "Files"),
     NC_("shortcut window", "Move document to the right") },
 
-  { "org.gnome.builder.layoutstack.move-left",
+  { "org.gnome.builder.frame.move-left",
     DZL_SHORTCUT_PHASE_CAPTURE,
     NULL,
     NC_("shortcut window", "Editor shortcuts"),
     NC_("shortcut window", "Files"),
     NC_("shortcut window", "Move document to the left") },
 
-  { "org.gnome.builder.layoutstack.previous-document",
+  { "org.gnome.builder.frame.previous-document",
     DZL_SHORTCUT_PHASE_CAPTURE,
     NULL,
     NC_("shortcut window", "Editor shortcuts"),
     NC_("shortcut window", "Files"),
     NC_("shortcut window", "Switch to the previous document") },
 
-  { "org.gnome.builder.layoutstack.next-document",
+  { "org.gnome.builder.frame.next-document",
     DZL_SHORTCUT_PHASE_CAPTURE,
     NULL,
     NC_("shortcut window", "Editor shortcuts"),
     NC_("shortcut window", "Files"),
     NC_("shortcut window", "Switch to the next document") },
 
-  { "org.gnome.builder.layoutstack.close-view",
+  { "org.gnome.builder.frame.close-page",
     DZL_SHORTCUT_PHASE_BUBBLE,
     NULL,
     NC_("shortcut window", "Editor shortcuts"),
@@ -65,48 +66,48 @@ static const DzlShortcutEntry stack_shortcuts[] = {
 };
 
 void
-_ide_layout_stack_init_shortcuts (IdeLayoutStack *self)
+_ide_frame_init_shortcuts (IdeFrame *self)
 {
   DzlShortcutController *controller;
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_FRAME (self));
 
   dzl_shortcut_manager_add_shortcut_entries (NULL,
-                                             stack_shortcuts,
-                                             G_N_ELEMENTS (stack_shortcuts),
+                                             frame_shortcuts,
+                                             G_N_ELEMENTS (frame_shortcuts),
                                              GETTEXT_PACKAGE);
 
   controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
 
   dzl_shortcut_controller_add_command_action (controller,
-                                              I_("org.gnome.builder.layoutstack.move-right"),
+                                              I_("org.gnome.builder.frame.move-right"),
                                               I_("<Primary><Alt>Page_Down"),
                                               DZL_SHORTCUT_PHASE_BUBBLE,
-                                              I_("layoutstack.move-right"));
+                                              I_("frame.move-right"));
 
   dzl_shortcut_controller_add_command_action (controller,
-                                              I_("org.gnome.builder.layoutstack.move-left"),
+                                              I_("org.gnome.builder.frame.move-left"),
                                               I_("<Primary><Alt>Page_Up"),
                                               DZL_SHORTCUT_PHASE_BUBBLE,
-                                              I_("layoutstack.move-left"));
+                                              I_("frame.move-left"));
 
   dzl_shortcut_controller_add_command_signal (controller,
-                                              I_("org.gnome.builder.layoutstack.next-document"),
+                                              I_("org.gnome.builder.frame.next-document"),
                                               I_("<Primary><Shift>Page_Down"),
                                               DZL_SHORTCUT_PHASE_BUBBLE,
                                               I_("change-current-page"),
                                               1, G_TYPE_INT, 1);
 
   dzl_shortcut_controller_add_command_signal (controller,
-                                              I_("org.gnome.builder.layoutstack.previous-document"),
+                                              I_("org.gnome.builder.frame.previous-document"),
                                               I_("<Primary><Shift>Page_Up"),
                                               DZL_SHORTCUT_PHASE_BUBBLE,
                                               I_("change-current-page"),
                                               1, G_TYPE_INT, -1);
 
   dzl_shortcut_controller_add_command_action (controller,
-                                              I_("org.gnome.builder.layoutstack.close-view"),
+                                              I_("org.gnome.builder.frame.close-page"),
                                               I_("<Primary>w"),
                                               DZL_SHORTCUT_PHASE_BUBBLE,
-                                              I_("layoutstack.close-view"));
+                                              I_("frame.close-page"));
 }
diff --git a/src/libide/layout/ide-layout-stack-wrapper.c b/src/libide/gui/ide-frame-wrapper.c
similarity index 68%
rename from src/libide/layout/ide-layout-stack-wrapper.c
rename to src/libide/gui/ide-frame-wrapper.c
index 45e42ec04..aacd241f5 100644
--- a/src/libide/layout/ide-layout-stack-wrapper.c
+++ b/src/libide/gui/ide-frame-wrapper.c
@@ -1,4 +1,4 @@
-/* ide-layout-stack-wrapper.c
+/* ide-frame-wrapper.c
  *
  * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,11 +18,11 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-stack-wrapper"
+#define G_LOG_DOMAIN "ide-frame-wrapper"
 
 #include "config.h"
 
-#include "ide-layout-stack-wrapper.h"
+#include "ide-frame-wrapper.h"
 
 /*
  * This is just a GtkStack wrapper that allows us to override
@@ -30,21 +30,21 @@
  * focused child first.
  */
 
-struct _IdeLayoutStackWrapper
+struct _IdeFrameWrapper
 {
   GtkStack parent_instance;
   GQueue   history;
 };
 
-G_DEFINE_TYPE (IdeLayoutStackWrapper, ide_layout_stack_wrapper, GTK_TYPE_STACK)
+G_DEFINE_TYPE (IdeFrameWrapper, ide_frame_wrapper, GTK_TYPE_STACK)
 
 static void
-ide_layout_stack_wrapper_add (GtkContainer *container,
+ide_frame_wrapper_add (GtkContainer *container,
                               GtkWidget    *widget)
 {
-  IdeLayoutStackWrapper *self = (IdeLayoutStackWrapper *)container;
+  IdeFrameWrapper *self = (IdeFrameWrapper *)container;
 
-  g_assert (IDE_IS_LAYOUT_STACK_WRAPPER (container));
+  g_assert (IDE_IS_FRAME_WRAPPER (container));
   g_assert (GTK_IS_WIDGET (widget));
 
   if (gtk_widget_get_visible (widget))
@@ -52,16 +52,16 @@ ide_layout_stack_wrapper_add (GtkContainer *container,
   else
     g_queue_push_tail (&self->history, widget);
 
-  GTK_CONTAINER_CLASS (ide_layout_stack_wrapper_parent_class)->add (container, widget);
+  GTK_CONTAINER_CLASS (ide_frame_wrapper_parent_class)->add (container, widget);
 }
 
 static void
-ide_layout_stack_wrapper_remove (GtkContainer *container,
+ide_frame_wrapper_remove (GtkContainer *container,
                                  GtkWidget    *widget)
 {
-  IdeLayoutStackWrapper *self = (IdeLayoutStackWrapper *)container;
+  IdeFrameWrapper *self = (IdeFrameWrapper *)container;
 
-  g_assert (IDE_IS_LAYOUT_STACK_WRAPPER (container));
+  g_assert (IDE_IS_FRAME_WRAPPER (container));
   g_assert (GTK_IS_WIDGET (widget));
 
   /* Remove the widget from our history chain, and then see if we need to
@@ -79,16 +79,16 @@ ide_layout_stack_wrapper_remove (GtkContainer *container,
         gtk_stack_set_visible_child (GTK_STACK (self), new_fg);
     }
 
-  GTK_CONTAINER_CLASS (ide_layout_stack_wrapper_parent_class)->remove (container, widget);
+  GTK_CONTAINER_CLASS (ide_frame_wrapper_parent_class)->remove (container, widget);
 }
 
 static void
-ide_layout_stack_wrapper_notify_visible_child (IdeLayoutStackWrapper *self,
+ide_frame_wrapper_notify_visible_child (IdeFrameWrapper *self,
                                                GParamSpec            *pspec)
 {
   GtkWidget *visible_child;
 
-  g_assert (IDE_IS_LAYOUT_STACK_WRAPPER (self));
+  g_assert (IDE_IS_FRAME_WRAPPER (self));
   g_assert (pspec != NULL);
 
   if ((visible_child = gtk_stack_get_visible_child (GTK_STACK (self))))
@@ -106,19 +106,19 @@ ide_layout_stack_wrapper_notify_visible_child (IdeLayoutStackWrapper *self,
 }
 
 static void
-ide_layout_stack_wrapper_class_init (IdeLayoutStackWrapperClass *klass)
+ide_frame_wrapper_class_init (IdeFrameWrapperClass *klass)
 {
   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
 
-  container_class->add = ide_layout_stack_wrapper_add;
-  container_class->remove = ide_layout_stack_wrapper_remove;
+  container_class->add = ide_frame_wrapper_add;
+  container_class->remove = ide_frame_wrapper_remove;
 }
 
 static void
-ide_layout_stack_wrapper_init (IdeLayoutStackWrapper *self)
+ide_frame_wrapper_init (IdeFrameWrapper *self)
 {
   g_signal_connect (self,
                     "notify::visible-child",
-                    G_CALLBACK (ide_layout_stack_wrapper_notify_visible_child),
+                    G_CALLBACK (ide_frame_wrapper_notify_visible_child),
                     NULL);
 }
diff --git a/src/libide/layout/ide-layout-stack-wrapper.h b/src/libide/gui/ide-frame-wrapper.h
similarity index 79%
rename from src/libide/layout/ide-layout-stack-wrapper.h
rename to src/libide/gui/ide-frame-wrapper.h
index 07cb26746..093aaa780 100644
--- a/src/libide/layout/ide-layout-stack-wrapper.h
+++ b/src/libide/gui/ide-frame-wrapper.h
@@ -1,4 +1,4 @@
-/* ide-layout-stack-wrapper.h
+/* ide-frame-wrapper.h
  *
  * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
@@ -24,8 +24,8 @@
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT_STACK_WRAPPER (ide_layout_stack_wrapper_get_type())
+#define IDE_TYPE_FRAME_WRAPPER (ide_frame_wrapper_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeLayoutStackWrapper, ide_layout_stack_wrapper, IDE, LAYOUT_STACK_WRAPPER, GtkStack)
+G_DECLARE_FINAL_TYPE (IdeFrameWrapper, ide_frame_wrapper, IDE, FRAME_WRAPPER, GtkStack)
 
 G_END_DECLS
diff --git a/src/libide/layout/ide-layout-stack.c b/src/libide/gui/ide-frame.c
similarity index 54%
rename from src/libide/layout/ide-layout-stack.c
rename to src/libide/gui/ide-frame.c
index 857f4c785..15f477930 100644
--- a/src/libide/layout/ide-layout-stack.c
+++ b/src/libide/gui/ide-frame.c
@@ -1,4 +1,4 @@
-/* ide-layout-stack.c
+/* ide-frame.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,41 +18,41 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-stack"
+
+
+#define G_LOG_DOMAIN "ide-frame"
 
 #include "config.h"
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-threading.h>
 #include <libpeas/peas.h>
 
-#include "ide-debug.h"
-
-#include "layout/ide-layout-stack.h"
-#include "layout/ide-layout-stack-addin.h"
-#include "layout/ide-layout-stack-header.h"
-#include "layout/ide-layout-stack-wrapper.h"
-#include "layout/ide-layout-private.h"
-#include "layout/ide-shortcut-label.h"
-#include "threading/ide-task.h"
+#include "ide-frame.h"
+#include "ide-frame-addin.h"
+#include "ide-frame-header.h"
+#include "ide-frame-wrapper.h"
+#include "ide-gui-private.h"
 
 #define TRANSITION_DURATION 300
 #define DISTANCE_THRESHOLD(alloc) (MIN(250, (gint)((alloc)->width * .333)))
 
 /**
- * SECTION:ide-layout-stack
- * @title: IdeLayoutStack
- * @short_description: A stack of #IdeLayoutView
+ * SECTION:ide-frame
+ * @title: IdeFrame
+ * @short_description: A stack of #IdePage
  *
- * This widget is used to represent a stack of #IdeLayoutView widgets.  it
- * includes an #IdeLayoutStackHeader at the top, and then a stack of views
+ * This widget is used to represent a stack of #IdePage widgets.  it
+ * includes an #IdeFrameHeader at the top, and then a stack of pages
  * below.
  *
- * If there are no #IdeLayoutView visibile, then an empty state widget is
+ * If there are no #IdePage visibile, then an empty state widget is
  * displayed with some common information for the user.
  *
- * To simplify integration with other systems, #IdeLayoutStack implements
- * the #GListModel interface for each of the #IdeLayoutView.
+ * To simplify integration with other systems, #IdeFrame implements
+ * the #GListModel interface for each of the #IdePage.
  *
  * Since: 3.32
  */
@@ -61,7 +61,7 @@ typedef struct
 {
   DzlBindingGroup      *bindings;
   DzlSignalGroup       *signals;
-  GPtrArray            *views;
+  GPtrArray            *pages;
   GPtrArray            *in_transition;
   PeasExtensionSet     *addins;
 
@@ -76,22 +76,22 @@ typedef struct
   GtkGesture           *dummy;
   GtkGesture           *pan;
   DzlBoxTheatric       *pan_theatric;
-  IdeLayoutView        *pan_view;
+  IdePage              *pan_page;
 
   /* Template references */
   DzlBox               *empty_state;
   DzlEmptyState        *failed_state;
-  IdeLayoutStackHeader *header;
+  IdeFrameHeader       *header;
   GtkStack             *stack;
   GtkStack             *top_stack;
   GtkEventBox          *event_box;
-} IdeLayoutStackPrivate;
+} IdeFramePrivate;
 
 typedef struct
 {
-  IdeLayoutStack *source;
-  IdeLayoutStack *dest;
-  IdeLayoutView  *view;
+  IdeFrame *source;
+  IdeFrame *dest;
+  IdePage  *page;
   DzlBoxTheatric *theatric;
 } AnimationState;
 
@@ -110,8 +110,8 @@ enum {
 static void list_model_iface_init    (GListModelInterface *iface);
 static void animation_state_complete (gpointer             data);
 
-G_DEFINE_TYPE_WITH_CODE (IdeLayoutStack, ide_layout_stack, GTK_TYPE_BOX,
-                         G_ADD_PRIVATE (IdeLayoutStack)
+G_DEFINE_TYPE_WITH_CODE (IdeFrame, ide_frame, GTK_TYPE_BOX,
+                         G_ADD_PRIVATE (IdeFrame)
                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
 static GParamSpec *properties [N_PROPS];
@@ -125,14 +125,14 @@ is_uninitialized (GtkAllocation *alloc)
 }
 
 static void
-ide_layout_stack_set_cursor (IdeLayoutStack *self,
-                             const gchar    *name)
+ide_frame_set_cursor (IdeFrame    *self,
+                      const gchar *name)
 {
   GdkWindow *window;
   GdkDisplay *display;
   GdkCursor *cursor;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (name != NULL);
 
   window = gtk_widget_get_window (GTK_WIDGET (self));
@@ -145,70 +145,70 @@ ide_layout_stack_set_cursor (IdeLayoutStack *self,
 }
 
 static void
-ide_layout_stack_view_failed (IdeLayoutStack *self,
-                              GParamSpec     *pspec,
-                              IdeLayoutView  *view)
+ide_frame_page_failed (IdeFrame   *self,
+                       GParamSpec *pspec,
+                       IdePage    *page)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
 
-  if (ide_layout_view_get_failed (view))
+  if (ide_page_get_failed (page))
     gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
   else
     gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
 }
 
 static void
-ide_layout_stack_bindings_notify_source (IdeLayoutStack  *self,
-                                         GParamSpec      *pspec,
-                                         DzlBindingGroup *bindings)
+ide_frame_bindings_notify_source (IdeFrame        *self,
+                                  GParamSpec      *pspec,
+                                  DzlBindingGroup *bindings)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   GObject *source;
 
   g_assert (DZL_IS_BINDING_GROUP (bindings));
   g_assert (pspec != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
   source = dzl_binding_group_get_source (bindings);
 
   if (source == NULL)
     {
-      _ide_layout_stack_header_set_title (priv->header, _("No Open Pages"));
-      _ide_layout_stack_header_set_modified (priv->header, FALSE);
-      _ide_layout_stack_header_set_background_rgba (priv->header, NULL);
-      _ide_layout_stack_header_set_foreground_rgba (priv->header, NULL);
+      _ide_frame_header_set_title (priv->header, _("No Open Pages"));
+      _ide_frame_header_set_modified (priv->header, FALSE);
+      _ide_frame_header_set_background_rgba (priv->header, NULL);
+      _ide_frame_header_set_foreground_rgba (priv->header, NULL);
     }
 }
 
 static void
-ide_layout_stack_notify_addin_of_view (PeasExtensionSet *set,
-                                       PeasPluginInfo   *plugin_info,
-                                       PeasExtension    *exten,
-                                       gpointer          user_data)
+ide_frame_notify_addin_of_page (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
 {
-  IdeLayoutStackAddin *addin = (IdeLayoutStackAddin *)exten;
-  IdeLayoutView *view = user_data;
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdePage *page = user_data;
 
   g_assert (PEAS_IS_EXTENSION_SET (set));
   g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK_ADDIN (addin));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
+  g_assert (!page || IDE_IS_PAGE (page));
 
-  ide_layout_stack_addin_set_view (addin, view);
+  ide_frame_addin_set_page (addin, page);
 }
 
 static void
-ide_layout_stack_notify_visible_child (IdeLayoutStack *self,
-                                       GParamSpec     *pspec,
-                                       GtkStack       *stack)
+ide_frame_notify_visible_child (IdeFrame   *self,
+                                GParamSpec *pspec,
+                                GtkStack   *stack)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   GtkWidget *visible_child;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (GTK_IS_STACK (stack));
 
   if (gtk_widget_in_destruction (GTK_WIDGET (self)))
@@ -225,15 +225,15 @@ ide_layout_stack_notify_visible_child (IdeLayoutStack *self,
    * from the header bar without any weirdness by the View.
    */
   dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child,
-                                    "IDE_LAYOUT_STACK_MUXED_ACTION");
+                                    "IDE_FRAME_MUXED_ACTION");
 
   /* Update our bindings targets */
   dzl_binding_group_set_source (priv->bindings, visible_child);
   dzl_signal_group_set_target (priv->signals, visible_child);
 
-  /* Show either the empty state, failed state, or actual view */
+  /* Show either the empty state, failed state, or actual page */
   if (visible_child != NULL &&
-      ide_layout_view_get_failed (IDE_LAYOUT_VIEW (visible_child)))
+      ide_page_get_failed (IDE_PAGE (visible_child)))
     gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
   else if (visible_child != NULL)
     gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
@@ -241,14 +241,15 @@ ide_layout_stack_notify_visible_child (IdeLayoutStack *self,
     gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_state));
 
   /* Allow the header to update settings */
-  _ide_layout_stack_header_update (priv->header, IDE_LAYOUT_VIEW (visible_child));
+  _ide_frame_header_update (priv->header, IDE_PAGE (visible_child));
 
   /* Ensure action state is up to date */
-  _ide_layout_stack_update_actions (self);
+  _ide_frame_update_actions (self);
 
-  peas_extension_set_foreach (priv->addins,
-                              ide_layout_stack_notify_addin_of_view,
-                              visible_child);
+  if (priv->addins != NULL)
+    peas_extension_set_foreach (priv->addins,
+                                ide_frame_notify_addin_of_page,
+                                visible_child);
 
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_CHILD]);
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_VIEW]);
@@ -262,15 +263,15 @@ collect_widgets (GtkWidget *widget,
 }
 
 static void
-ide_layout_stack_change_current_page (IdeLayoutStack *self,
-                                      gint            direction)
+ide_frame_change_current_page (IdeFrame *self,
+                               gint      direction)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   g_autoptr(GPtrArray) ar = NULL;
   GtkWidget *visible_child;
   gint position = 0;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
   visible_child = gtk_stack_get_visible_child (priv->stack);
 
@@ -291,85 +292,85 @@ ide_layout_stack_change_current_page (IdeLayoutStack *self,
 }
 
 static void
-ide_layout_stack_add (GtkContainer *container,
-                      GtkWidget    *widget)
+ide_frame_add (GtkContainer *container,
+               GtkWidget    *widget)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)container;
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)container;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_FRAME (self));
   g_return_if_fail (GTK_IS_WIDGET (widget));
 
-  if (IDE_IS_LAYOUT_VIEW (widget))
+  if (IDE_IS_PAGE (widget))
     gtk_container_add (GTK_CONTAINER (priv->stack), widget);
   else
-    GTK_CONTAINER_CLASS (ide_layout_stack_parent_class)->add (container, widget);
+    GTK_CONTAINER_CLASS (ide_frame_parent_class)->add (container, widget);
 
   gtk_widget_queue_resize (GTK_WIDGET (self));
 }
 
 static void
-ide_layout_stack_view_added (IdeLayoutStack *self,
-                             IdeLayoutView  *view)
+ide_frame_page_added (IdeFrame *self,
+                      IdePage  *page)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   guint position;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
 
   /*
    * Make sure that the header has dismissed all of the popovers immediately.
    * We don't want them lingering while we do other UI work which might want to
    * grab focus, etc.
    */
-  _ide_layout_stack_header_popdown (priv->header);
+  _ide_frame_header_popdown (priv->header);
 
-  /* Notify GListModel consumers of the new view and it's position within
-   * our stack of view widgets.
+  /* Notify GListModel consumers of the new page and it's position within
+   * our stack of page widgets.
    */
-  position = priv->views->len;
-  g_ptr_array_add (priv->views, view);
+  position = priv->pages->len;
+  g_ptr_array_add (priv->pages, page);
   g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
 
   /*
-   * Now ensure that the view is displayed and focus the widget so the
+   * Now ensure that the page is displayed and focus the widget so the
    * user can immediately start typing.
    */
-  ide_layout_stack_set_visible_child (self, view);
-  gtk_widget_grab_focus (GTK_WIDGET (view));
+  ide_frame_set_visible_child (self, page);
+  gtk_widget_grab_focus (GTK_WIDGET (page));
 }
 
 static void
-ide_layout_stack_view_removed (IdeLayoutStack *self,
-                               IdeLayoutView  *view)
+ide_frame_page_removed (IdeFrame *self,
+                        IdePage  *page)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
 
-  if (priv->views != NULL)
+  if (priv->pages != NULL)
     {
       guint position = 0;
 
-      /* If this is the last view, hide the popdown now.  We use our hide
+      /* If this is the last page, hide the popdown now.  We use our hide
        * variant instead of popdown so that we don't have jittery animations.
        */
-      if (priv->views->len == 1)
-        _ide_layout_stack_header_hide (priv->header);
+      if (priv->pages->len == 1)
+        _ide_frame_header_hide (priv->header);
 
       /*
-       * Only remove the view if it is not in transition. We hold onto the
-       * view during the transition so that we keep the list stable.
+       * Only remove the page if it is not in transition. We hold onto the
+       * page during the transition so that we keep the list stable.
        */
-      if (!g_ptr_array_find_with_equal_func (priv->in_transition, view, NULL, &position))
+      if (!g_ptr_array_find_with_equal_func (priv->in_transition, page, NULL, &position))
         {
-          for (guint i = 0; i < priv->views->len; i++)
+          for (guint i = 0; i < priv->pages->len; i++)
             {
-              if ((gpointer)view == g_ptr_array_index (priv->views, i))
+              if ((gpointer)page == g_ptr_array_index (priv->pages, i))
                 {
-                  g_ptr_array_remove_index (priv->views, i);
+                  g_ptr_array_remove_index (priv->pages, i);
                   g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
                 }
             }
@@ -378,83 +379,83 @@ ide_layout_stack_view_removed (IdeLayoutStack *self,
 }
 
 static void
-ide_layout_stack_real_agree_to_close_async (IdeLayoutStack      *self,
-                                            GCancellable        *cancellable,
-                                            GAsyncReadyCallback  callback,
-                                            gpointer             user_data)
+ide_frame_real_agree_to_close_async (IdeFrame            *self,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
 {
   g_autoptr(IdeTask) task = NULL;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_source_tag (task, ide_layout_stack_real_agree_to_close_async);
+  ide_task_set_source_tag (task, ide_frame_real_agree_to_close_async);
   ide_task_set_priority (task, G_PRIORITY_LOW);
   ide_task_return_boolean (task, TRUE);
 }
 
 static gboolean
-ide_layout_stack_real_agree_to_close_finish (IdeLayoutStack *self,
-                                             GAsyncResult   *result,
-                                             GError        **error)
+ide_frame_real_agree_to_close_finish (IdeFrame      *self,
+                                      GAsyncResult  *result,
+                                      GError       **error)
 {
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (IDE_IS_TASK (result));
 
   return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
 
 static void
-ide_layout_stack_addin_added (PeasExtensionSet *set,
-                              PeasPluginInfo   *plugin_info,
-                              PeasExtension    *exten,
-                              gpointer          user_data)
+ide_frame_addin_added (PeasExtensionSet *set,
+                       PeasPluginInfo   *plugin_info,
+                       PeasExtension    *exten,
+                       gpointer          user_data)
 {
-  IdeLayoutStackAddin *addin = (IdeLayoutStackAddin *)exten;
-  IdeLayoutStack *self = user_data;
-  IdeLayoutView *visible_child;
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdeFrame *self = user_data;
+  IdePage *visible_child;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (PEAS_IS_EXTENSION_SET (set));
   g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK_ADDIN (addin));
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
 
-  ide_layout_stack_addin_load (addin, self);
+  ide_frame_addin_load (addin, self);
 
-  visible_child = ide_layout_stack_get_visible_child (self);
+  visible_child = ide_frame_get_visible_child (self);
 
   if (visible_child != NULL)
-    ide_layout_stack_addin_set_view (addin, visible_child);
+    ide_frame_addin_set_page (addin, visible_child);
 }
 
 static void
-ide_layout_stack_addin_removed (PeasExtensionSet *set,
-                                PeasPluginInfo   *plugin_info,
-                                PeasExtension    *exten,
-                                gpointer          user_data)
+ide_frame_addin_removed (PeasExtensionSet *set,
+                         PeasPluginInfo   *plugin_info,
+                         PeasExtension    *exten,
+                         gpointer          user_data)
 {
-  IdeLayoutStackAddin *addin = (IdeLayoutStackAddin *)exten;
-  IdeLayoutStack *self = user_data;
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdeFrame *self = user_data;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (PEAS_IS_EXTENSION_SET (set));
   g_assert (plugin_info != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK_ADDIN (addin));
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
 
-  ide_layout_stack_addin_set_view (addin, NULL);
-  ide_layout_stack_addin_unload (addin, self);
+  ide_frame_addin_set_page (addin, NULL);
+  ide_frame_addin_unload (addin, self);
 }
 
 static gboolean
-ide_layout_stack_pan_begin (IdeLayoutStack   *self,
-                            GdkEventSequence *sequence,
-                            GtkGesturePan    *gesture)
+ide_frame_pan_begin (IdeFrame         *self,
+                     GdkEventSequence *sequence,
+                     GtkGesturePan    *gesture)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   GtkAllocation alloc;
   cairo_surface_t *surface = NULL;
-  IdeLayoutView *view;
+  IdePage *page;
   GdkWindow *window;
   GtkWidget *grid;
   cairo_t *cr;
@@ -463,23 +464,23 @@ ide_layout_stack_pan_begin (IdeLayoutStack   *self,
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (GTK_IS_GESTURE_PAN (gesture));
   g_assert (priv->pan_theatric == NULL);
 
-  view = ide_layout_stack_get_visible_child (self);
-  if (view != NULL)
-    gtk_widget_get_allocation (GTK_WIDGET (view), &alloc);
+  page = ide_frame_get_visible_child (self);
+  if (page != NULL)
+    gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
 
   g_object_get (gtk_settings_get_default (),
                 "gtk-enable-animations", &enable_animations,
                 NULL);
 
   if (sequence != NULL ||
-      view == NULL ||
+      page == NULL ||
       !enable_animations ||
       is_uninitialized (&alloc) ||
-      NULL == (window = gtk_widget_get_window (GTK_WIDGET (view))) ||
+      NULL == (window = gtk_widget_get_window (GTK_WIDGET (page))) ||
       NULL == (surface = gdk_window_create_similar_surface (window,
                                                             CAIRO_CONTENT_COLOR,
                                                             alloc.width,
@@ -493,14 +494,14 @@ ide_layout_stack_pan_begin (IdeLayoutStack   *self,
   gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
 
   cr = cairo_create (surface);
-  gtk_widget_draw (GTK_WIDGET (view), cr);
+  gtk_widget_draw (GTK_WIDGET (page), cr);
   cairo_destroy (cr);
 
-  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
   gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
                                     &alloc.x, &alloc.y);
 
-  priv->pan_view = g_object_ref (view);
+  priv->pan_page = g_object_ref (page);
   priv->pan_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
                                      "surface", surface,
                                      "target", grid,
@@ -512,34 +513,34 @@ ide_layout_stack_pan_begin (IdeLayoutStack   *self,
 
   g_clear_pointer (&surface, cairo_surface_destroy);
 
-  /* Hide the view while we begin the possible transition to another
+  /* Hide the page while we begin the possible transition to another
    * layout stack.
    */
-  gtk_widget_hide (GTK_WIDGET (priv->pan_view));
+  gtk_widget_hide (GTK_WIDGET (priv->pan_page));
 
   /*
-   * Hide the mouse cursor until ide_layout_stack_pan_end() is called.
+   * Hide the mouse cursor until ide_frame_pan_end() is called.
    * It can be distracting otherwise (and we want to warp it to the new
    * grid column too).
    */
-  ide_layout_stack_set_cursor (self, "none");
+  ide_frame_set_cursor (self, "none");
 
   IDE_RETURN (TRUE);
 }
 
 static void
-ide_layout_stack_pan_update (IdeLayoutStack   *self,
-                             GdkEventSequence *sequence,
-                             GtkGestureSwipe  *gesture)
+ide_frame_pan_update (IdeFrame         *self,
+                      GdkEventSequence *sequence,
+                      GtkGestureSwipe  *gesture)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
   GtkAllocation alloc;
   GtkWidget *grid;
   gdouble x, y;
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (GTK_IS_GESTURE_PAN (gesture));
   g_assert (!priv->pan_theatric || DZL_IS_BOX_THEATRIC (priv->pan_theatric));
 
@@ -553,7 +554,7 @@ ide_layout_stack_pan_update (IdeLayoutStack   *self,
   gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
   gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
 
-  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
   gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
                                     &alloc.x, &alloc.y);
 
@@ -565,13 +566,13 @@ ide_layout_stack_pan_update (IdeLayoutStack   *self,
 }
 
 static void
-ide_layout_stack_pan_end (IdeLayoutStack   *self,
-                          GdkEventSequence *sequence,
-                          GtkGesturePan    *gesture)
+ide_frame_pan_end (IdeFrame         *self,
+                   GdkEventSequence *sequence,
+                   GtkGesturePan    *gesture)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
-  IdeLayoutStackPrivate *dest_priv;
-  IdeLayoutStack *dest;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdeFramePrivate *dest_priv;
+  IdeFrame *dest;
   GtkAllocation alloc;
   GtkWidget *grid;
   GtkWidget *column;
@@ -581,10 +582,10 @@ ide_layout_stack_pan_end (IdeLayoutStack   *self,
 
   IDE_ENTRY;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
   g_assert (GTK_IS_GESTURE_PAN (gesture));
 
-  if (priv->pan_theatric == NULL || priv->pan_view == NULL)
+  if (priv->pan_theatric == NULL || priv->pan_page == NULL)
     IDE_GOTO (cleanup);
 
   gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
@@ -598,22 +599,22 @@ ide_layout_stack_pan_end (IdeLayoutStack   *self,
   else
     direction = 0;
 
-  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
   g_assert (grid != NULL);
-  g_assert (IDE_IS_LAYOUT_GRID (grid));
+  g_assert (IDE_IS_GRID (grid));
 
-  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID_COLUMN);
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
   g_assert (column != NULL);
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (column));
+  g_assert (IDE_IS_GRID_COLUMN (column));
 
   gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
                            "index", &index,
                            NULL);
 
-  dest = _ide_layout_grid_get_nth_stack (IDE_LAYOUT_GRID (grid), index + direction);
-  dest_priv = ide_layout_stack_get_instance_private (dest);
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction);
+  dest_priv = ide_frame_get_instance_private (dest);
   g_assert (dest != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK (dest));
+  g_assert (IDE_IS_FRAME (dest));
 
   gtk_widget_get_allocation (GTK_WIDGET (dest), &alloc);
 
@@ -624,7 +625,7 @@ ide_layout_stack_pan_end (IdeLayoutStack   *self,
       state = g_slice_new0 (AnimationState);
       state->source = g_object_ref (self);
       state->dest = g_object_ref (dest);
-      state->view = g_object_ref (priv->pan_view);
+      state->page = g_object_ref (priv->pan_page);
       state->theatric = priv->pan_theatric;
 
       gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->top_stack), grid, 0, 0,
@@ -646,8 +647,8 @@ ide_layout_stack_pan_end (IdeLayoutStack   *self,
 
       if (dest != self)
         {
-          g_ptr_array_add (priv->in_transition, g_object_ref (priv->pan_view));
-          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (priv->pan_view));
+          g_ptr_array_add (priv->in_transition, g_object_ref (priv->pan_page));
+          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (priv->pan_page));
         }
 
       IDE_TRACE_MSG ("Animating transition to %s column",
@@ -655,52 +656,52 @@ ide_layout_stack_pan_end (IdeLayoutStack   *self,
     }
   else
     {
-      g_autoptr(IdeLayoutView) view = g_object_ref (priv->pan_view);
+      g_autoptr(IdePage) page = g_object_ref (priv->pan_page);
 
-      IDE_TRACE_MSG ("Moving view to a previously non-existant column");
+      IDE_TRACE_MSG ("Moving page to a previously non-existant column");
 
-      gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (view));
-      gtk_widget_show (GTK_WIDGET (view));
-      gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (view));
+      gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
+      gtk_widget_show (GTK_WIDGET (page));
+      gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
     }
 
 cleanup:
   g_clear_object (&priv->pan_theatric);
-  g_clear_object (&priv->pan_view);
+  g_clear_object (&priv->pan_page);
 
   gtk_widget_queue_draw (gtk_widget_get_toplevel (GTK_WIDGET (self)));
 
-  ide_layout_stack_set_cursor (self, "arrow");
+  ide_frame_set_cursor (self, "arrow");
 
   IDE_EXIT;
 }
 
 static void
-ide_layout_stack_constructed (GObject *object)
+ide_frame_constructed (GObject *object)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)object;
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)object;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
-  G_OBJECT_CLASS (ide_layout_stack_parent_class)->constructed (object);
+  G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object);
 
   priv->addins = peas_extension_set_new (peas_engine_get_default (),
-                                         IDE_TYPE_LAYOUT_STACK_ADDIN,
+                                         IDE_TYPE_FRAME_ADDIN,
                                          NULL);
 
   g_signal_connect (priv->addins,
                     "extension-added",
-                    G_CALLBACK (ide_layout_stack_addin_added),
+                    G_CALLBACK (ide_frame_addin_added),
                     self);
 
   g_signal_connect (priv->addins,
                     "extension-removed",
-                    G_CALLBACK (ide_layout_stack_addin_removed),
+                    G_CALLBACK (ide_frame_addin_removed),
                     self);
 
   peas_extension_set_foreach (priv->addins,
-                              ide_layout_stack_addin_added,
+                              ide_frame_addin_added,
                               self);
 
   gtk_widget_add_events (GTK_WIDGET (priv->event_box), GDK_TOUCH_MASK);
@@ -711,15 +712,15 @@ ide_layout_stack_constructed (GObject *object)
                             NULL);
   g_signal_connect_swapped (priv->pan,
                             "begin",
-                            G_CALLBACK (ide_layout_stack_pan_begin),
+                            G_CALLBACK (ide_frame_pan_begin),
                             self);
   g_signal_connect_swapped (priv->pan,
                             "update",
-                            G_CALLBACK (ide_layout_stack_pan_update),
+                            G_CALLBACK (ide_frame_pan_update),
                             self);
   g_signal_connect_swapped (priv->pan,
                             "end",
-                            G_CALLBACK (ide_layout_stack_pan_end),
+                            G_CALLBACK (ide_frame_pan_end),
                             self);
   gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->pan),
                                               GTK_PHASE_BUBBLE);
@@ -736,39 +737,39 @@ ide_layout_stack_constructed (GObject *object)
 }
 
 static void
-ide_layout_stack_grab_focus (GtkWidget *widget)
+ide_frame_grab_focus (GtkWidget *widget)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)widget;
-  IdeLayoutView *child;
+  IdeFrame *self = (IdeFrame *)widget;
+  IdePage *child;
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
-  child = ide_layout_stack_get_visible_child (self);
+  child = ide_frame_get_visible_child (self);
 
   if (child != NULL)
     gtk_widget_grab_focus (GTK_WIDGET (child));
   else
-    GTK_WIDGET_CLASS (ide_layout_stack_parent_class)->grab_focus (widget);
+    GTK_WIDGET_CLASS (ide_frame_parent_class)->grab_focus (widget);
 }
 
 static void
-ide_layout_stack_destroy (GtkWidget *widget)
+ide_frame_destroy (GtkWidget *widget)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)widget;
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)widget;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
+
+  g_clear_object (&priv->addins);
 
   g_clear_pointer (&priv->in_transition, g_ptr_array_unref);
 
-  if (priv->views != NULL)
+  if (priv->pages != NULL)
     {
-      g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->views->len, 0);
-      g_clear_pointer (&priv->views, g_ptr_array_unref);
+      g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->pages->len, 0);
+      g_clear_pointer (&priv->pages, g_ptr_array_unref);
     }
 
-  g_clear_object (&priv->addins);
-
   if (priv->bindings != NULL)
     {
       dzl_binding_group_set_source (priv->bindings, NULL);
@@ -783,25 +784,25 @@ ide_layout_stack_destroy (GtkWidget *widget)
 
   g_clear_object (&priv->pan);
 
-  GTK_WIDGET_CLASS (ide_layout_stack_parent_class)->destroy (widget);
+  GTK_WIDGET_CLASS (ide_frame_parent_class)->destroy (widget);
 }
 
 static void
-ide_layout_stack_get_property (GObject    *object,
-                               guint       prop_id,
-                               GValue     *value,
-                               GParamSpec *pspec)
+ide_frame_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
 {
-  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+  IdeFrame *self = IDE_FRAME (object);
 
   switch (prop_id)
     {
     case PROP_HAS_VIEW:
-      g_value_set_boolean (value, ide_layout_stack_get_has_view (self));
+      g_value_set_boolean (value, ide_frame_get_has_page (self));
       break;
 
     case PROP_VISIBLE_CHILD:
-      g_value_set_object (value, ide_layout_stack_get_visible_child (self));
+      g_value_set_object (value, ide_frame_get_visible_child (self));
       break;
 
     default:
@@ -810,17 +811,17 @@ ide_layout_stack_get_property (GObject    *object,
 }
 
 static void
-ide_layout_stack_set_property (GObject      *object,
-                               guint         prop_id,
-                               const GValue *value,
-                               GParamSpec   *pspec)
+ide_frame_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
 {
-  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+  IdeFrame *self = IDE_FRAME (object);
 
   switch (prop_id)
     {
     case PROP_VISIBLE_CHILD:
-      ide_layout_stack_set_visible_child (self, g_value_get_object (value));
+      ide_frame_set_visible_child (self, g_value_get_object (value));
       break;
 
     default:
@@ -829,34 +830,34 @@ ide_layout_stack_set_property (GObject      *object,
 }
 
 static void
-ide_layout_stack_class_init (IdeLayoutStackClass *klass)
+ide_frame_class_init (IdeFrameClass *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->get_property = ide_layout_stack_get_property;
-  object_class->set_property = ide_layout_stack_set_property;
+  object_class->constructed = ide_frame_constructed;
+  object_class->get_property = ide_frame_get_property;
+  object_class->set_property = ide_frame_set_property;
 
-  widget_class->destroy = ide_layout_stack_destroy;
-  widget_class->grab_focus = ide_layout_stack_grab_focus;
+  widget_class->destroy = ide_frame_destroy;
+  widget_class->grab_focus = ide_frame_grab_focus;
 
-  container_class->add = ide_layout_stack_add;
+  container_class->add = ide_frame_add;
 
-  klass->agree_to_close_async = ide_layout_stack_real_agree_to_close_async;
-  klass->agree_to_close_finish = ide_layout_stack_real_agree_to_close_finish;
+  klass->agree_to_close_async = ide_frame_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_frame_real_agree_to_close_finish;
 
   properties [PROP_HAS_VIEW] =
-    g_param_spec_boolean ("has-view", NULL, NULL,
+    g_param_spec_boolean ("has-page", NULL, NULL,
                           FALSE,
                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_VISIBLE_CHILD] =
     g_param_spec_object ("visible-child",
                          "Visible Child",
-                         "The current view to be displayed",
-                         IDE_TYPE_LAYOUT_VIEW,
+                         "The current page to be displayed",
+                         IDE_TYPE_PAGE,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
@@ -865,50 +866,50 @@ ide_layout_stack_class_init (IdeLayoutStackClass *klass)
     g_signal_new_class_handler ("change-current-page",
                                 G_TYPE_FROM_CLASS (klass),
                                 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
-                                G_CALLBACK (ide_layout_stack_change_current_page),
+                                G_CALLBACK (ide_frame_change_current_page),
                                 NULL, NULL,
                                 g_cclosure_marshal_VOID__INT,
                                 G_TYPE_NONE, 1, G_TYPE_INT);
 
-  gtk_widget_class_set_css_name (widget_class, "idelayoutstack");
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-stack.ui");
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, empty_state);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, failed_state);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, header);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, stack);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, top_stack);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, event_box);
-
-  g_type_ensure (IDE_TYPE_LAYOUT_STACK_HEADER);
-  g_type_ensure (IDE_TYPE_LAYOUT_STACK_WRAPPER);
+  gtk_widget_class_set_css_name (widget_class, "ideframe");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, empty_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, failed_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, header);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, top_stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, event_box);
+
+  g_type_ensure (IDE_TYPE_FRAME_HEADER);
+  g_type_ensure (IDE_TYPE_FRAME_WRAPPER);
   g_type_ensure (IDE_TYPE_SHORTCUT_LABEL);
 }
 
 static void
-ide_layout_stack_init (IdeLayoutStack *self)
+ide_frame_init (IdeFrame *self)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  _ide_layout_stack_init_actions (self);
-  _ide_layout_stack_init_shortcuts (self);
+  _ide_frame_init_actions (self);
+  _ide_frame_init_shortcuts (self);
 
-  priv->views = g_ptr_array_new ();
+  priv->pages = g_ptr_array_new ();
   priv->in_transition = g_ptr_array_new_with_free_func (g_object_unref);
 
-  priv->signals = dzl_signal_group_new (IDE_TYPE_LAYOUT_VIEW);
+  priv->signals = dzl_signal_group_new (IDE_TYPE_PAGE);
 
   dzl_signal_group_connect_swapped (priv->signals,
                                     "notify::failed",
-                                    G_CALLBACK (ide_layout_stack_view_failed),
+                                    G_CALLBACK (ide_frame_page_failed),
                                     self);
 
   priv->bindings = dzl_binding_group_new ();
 
   g_signal_connect_object (priv->bindings,
                            "notify::source",
-                           G_CALLBACK (ide_layout_stack_bindings_notify_source),
+                           G_CALLBACK (ide_frame_bindings_notify_source),
                            self,
                            G_CONNECT_SWAPPED);
 
@@ -930,135 +931,135 @@ ide_layout_stack_init (IdeLayoutStack *self)
 
   g_signal_connect_object (priv->stack,
                            "notify::visible-child",
-                           G_CALLBACK (ide_layout_stack_notify_visible_child),
+                           G_CALLBACK (ide_frame_notify_visible_child),
                            self,
                            G_CONNECT_SWAPPED);
 
   g_signal_connect_object (priv->stack,
                            "add",
-                           G_CALLBACK (ide_layout_stack_view_added),
+                           G_CALLBACK (ide_frame_page_added),
                            self,
                            G_CONNECT_SWAPPED | G_CONNECT_AFTER);
 
   g_signal_connect_object (priv->stack,
                            "remove",
-                           G_CALLBACK (ide_layout_stack_view_removed),
+                           G_CALLBACK (ide_frame_page_removed),
                            self,
                            G_CONNECT_SWAPPED);
 
-  _ide_layout_stack_header_set_views (priv->header, G_LIST_MODEL (self));
-  _ide_layout_stack_header_update (priv->header, NULL);
+  _ide_frame_header_set_pages (priv->header, G_LIST_MODEL (self));
+  _ide_frame_header_update (priv->header, NULL);
 }
 
 GtkWidget *
-ide_layout_stack_new (void)
+ide_frame_new (void)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_STACK, NULL);
+  return g_object_new (IDE_TYPE_FRAME, NULL);
 }
 
 /**
- * ide_layout_stack_set_visible_child:
- * @self: a #IdeLayoutStack
+ * ide_frame_set_visible_child:
+ * @self: a #IdeFrame
  *
- * Sets the current view for the stack.
+ * Sets the current page for the stack.
  *
  * Since: 3.32
  */
 void
-ide_layout_stack_set_visible_child (IdeLayoutStack *self,
-                                    IdeLayoutView  *view)
+ide_frame_set_visible_child (IdeFrame *self,
+                             IdePage  *page)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
-  g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (view)) == (GtkWidget *)priv->stack);
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_PAGE (page));
+  g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (page)) == (GtkWidget *)priv->stack);
 
-  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (view));
+  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (page));
 }
 
 /**
- * ide_layout_stack_get_visible_child:
- * @self: a #IdeLayoutStack
+ * ide_frame_get_visible_child:
+ * @self: a #IdeFrame
  *
- * Gets the visible #IdeLayoutView if there is one; otherwise %NULL.
+ * Gets the visible #IdePage if there is one; otherwise %NULL.
  *
- * Returns: (nullable) (transfer none): An #IdeLayoutView or %NULL
+ * Returns: (nullable) (transfer none): An #IdePage or %NULL
  *
  * Since: 3.32
  */
-IdeLayoutView *
-ide_layout_stack_get_visible_child (IdeLayoutStack *self)
+IdePage *
+ide_frame_get_visible_child (IdeFrame *self)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), NULL);
+  g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
 
-  return IDE_LAYOUT_VIEW (gtk_stack_get_visible_child (priv->stack));
+  return IDE_PAGE (gtk_stack_get_visible_child (priv->stack));
 }
 
 /**
- * ide_layout_stack_get_titlebar:
- * @self: a #IdeLayoutStack
+ * ide_frame_get_titlebar:
+ * @self: a #IdeFrame
  *
- * Gets the #IdeLayoutStackHeader header that is at the top of the stack.
+ * Gets the #IdeFrameHeader header that is at the top of the stack.
  *
- * Returns: (transfer none) (type Ide.LayoutStackHeader): The layout stack header.
+ * Returns: (transfer none) (type IdeFrameHeader): The layout stack header.
  *
  * Since: 3.32
  */
 GtkWidget *
-ide_layout_stack_get_titlebar (IdeLayoutStack *self)
+ide_frame_get_titlebar (IdeFrame *self)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), NULL);
+  g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
 
   return GTK_WIDGET (priv->header);
 }
 
 /**
- * ide_layout_stack_get_has_view:
- * @self: an #IdeLayoutStack
+ * ide_frame_get_has_page:
+ * @self: an #IdeFrame
  *
- * Gets the "has-view" property.
+ * Gets the "has-page" property.
  *
  * This property is a convenience to allow widgets to easily bind
- * properties based on whether or not a view is visible in the stack.
+ * properties based on whether or not a page is visible in the stack.
  *
- * Returns: %TRUE if the stack has a view
+ * Returns: %TRUE if the stack has a page
  *
  * Since: 3.32
  */
 gboolean
-ide_layout_stack_get_has_view (IdeLayoutStack *self)
+ide_frame_get_has_page (IdeFrame *self)
 {
-  IdeLayoutView *visible_child;
+  IdePage *visible_child;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), FALSE);
+  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
 
-  visible_child = ide_layout_stack_get_visible_child (self);
+  visible_child = ide_frame_get_visible_child (self);
 
   return visible_child != NULL;
 }
 
 static void
-ide_layout_stack_close_view_cb (GObject      *object,
-                                GAsyncResult *result,
-                                gpointer      user_data)
+ide_frame_close_page_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
 {
-  IdeLayoutView *view = (IdeLayoutView *)object;
-  g_autoptr(IdeLayoutStack) self = user_data;
+  IdePage *page = (IdePage *)object;
+  g_autoptr(IdeFrame) self = user_data;
   g_autoptr(GError) error = NULL;
   GtkWidget *toplevel;
   GtkWidget *focus;
   gboolean had_focus = FALSE;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_PAGE (page));
   g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
-  if (!ide_layout_view_agree_to_close_finish (view, result, &error))
+  if (!ide_page_agree_to_close_finish (page, result, &error))
     {
       g_message ("%s", error->message);
       return;
@@ -1067,15 +1068,15 @@ ide_layout_stack_close_view_cb (GObject      *object,
   /* Keep track of whether or not the widget had focus (which
    * would happen if we were activated from a keybinding.
    */
-  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page));
   if (GTK_IS_WINDOW (toplevel) &&
       NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) &&
-      (focus == GTK_WIDGET (view) ||
-       gtk_widget_is_ancestor (focus, GTK_WIDGET (view))))
+      (focus == GTK_WIDGET (page) ||
+       gtk_widget_is_ancestor (focus, GTK_WIDGET (page))))
     had_focus = TRUE;
 
   /* Now we can destroy the child */
-  gtk_widget_destroy (GTK_WIDGET (view));
+  gtk_widget_destroy (GTK_WIDGET (page));
 
   /* We don't want to leave the widget focus in an indeterminate
    * state so we immediately focus the next child in the stack.
@@ -1083,7 +1084,7 @@ ide_layout_stack_close_view_cb (GObject      *object,
    */
   if (had_focus)
     {
-      IdeLayoutView *visible_child = ide_layout_stack_get_visible_child (self);
+      IdePage *visible_child = ide_frame_get_visible_child (self);
 
       if (visible_child != NULL)
         gtk_widget_grab_focus (GTK_WIDGET (visible_child));
@@ -1091,108 +1092,108 @@ ide_layout_stack_close_view_cb (GObject      *object,
 }
 
 void
-_ide_layout_stack_request_close (IdeLayoutStack *self,
-                                 IdeLayoutView  *view)
+_ide_frame_request_close (IdeFrame *self,
+                          IdePage  *page)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_PAGE (page));
 
-  ide_layout_view_agree_to_close_async (view,
+  ide_page_agree_to_close_async (page,
                                         NULL,
-                                        ide_layout_stack_close_view_cb,
+                                        ide_frame_close_page_cb,
                                         g_object_ref (self));
 }
 
 static GType
-ide_layout_stack_get_item_type (GListModel *model)
+ide_frame_get_item_type (GListModel *model)
 {
-  return IDE_TYPE_LAYOUT_VIEW;
+  return IDE_TYPE_PAGE;
 }
 
 static guint
-ide_layout_stack_get_n_items (GListModel *model)
+ide_frame_get_n_items (GListModel *model)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)model;
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)model;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_FRAME (self));
 
-  return priv->views ? priv->views->len : 0;
+  return priv->pages ? priv->pages->len : 0;
 }
 
 static gpointer
-ide_layout_stack_get_item (GListModel *model,
-                           guint       position)
+ide_frame_get_item (GListModel *model,
+                    guint       position)
 {
-  IdeLayoutStack *self = (IdeLayoutStack *)model;
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)model;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_STACK (self));
-  g_assert (position < priv->views->len);
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (position < priv->pages->len);
 
-  return g_object_ref (g_ptr_array_index (priv->views, position));
+  return g_object_ref (g_ptr_array_index (priv->pages, position));
 }
 
 static void
 list_model_iface_init (GListModelInterface *iface)
 {
-  iface->get_n_items = ide_layout_stack_get_n_items;
-  iface->get_item = ide_layout_stack_get_item;
-  iface->get_item_type = ide_layout_stack_get_item_type;
+  iface->get_n_items = ide_frame_get_n_items;
+  iface->get_item = ide_frame_get_item;
+  iface->get_item_type = ide_frame_get_item_type;
 }
 
 void
-ide_layout_stack_agree_to_close_async (IdeLayoutStack      *self,
-                                       GCancellable        *cancellable,
-                                       GAsyncReadyCallback  callback,
-                                       gpointer             user_data)
+ide_frame_agree_to_close_async (IdeFrame            *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_FRAME (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  IDE_LAYOUT_STACK_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+  IDE_FRAME_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
 }
 
 gboolean
-ide_layout_stack_agree_to_close_finish (IdeLayoutStack *self,
-                                        GAsyncResult   *result,
-                                        GError        **error)
+ide_frame_agree_to_close_finish (IdeFrame      *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
 {
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), FALSE);
+  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
   g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
 
-  return IDE_LAYOUT_STACK_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+  return IDE_FRAME_GET_CLASS (self)->agree_to_close_finish (self, result, error);
 }
 
 static void
 animation_state_complete (gpointer data)
 {
-  IdeLayoutStackPrivate *priv;
+  IdeFramePrivate *priv;
   AnimationState *state = data;
 
   g_assert (state != NULL);
-  g_assert (IDE_IS_LAYOUT_STACK (state->source));
-  g_assert (IDE_IS_LAYOUT_STACK (state->dest));
-  g_assert (IDE_IS_LAYOUT_VIEW (state->view));
+  g_assert (IDE_IS_FRAME (state->source));
+  g_assert (IDE_IS_FRAME (state->dest));
+  g_assert (IDE_IS_PAGE (state->page));
 
   /* Add the widget to the new stack */
   if (state->dest != state->source)
     {
-      gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->view));
+      gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->page));
 
       /* Now remove it from our temporary transition. Be careful in case we were
        * destroyed in the mean time.
        */
-      priv = ide_layout_stack_get_instance_private (state->source);
+      priv = ide_frame_get_instance_private (state->source);
 
       if (priv->in_transition != NULL)
         {
           guint position = 0;
 
-          if (g_ptr_array_find_with_equal_func (priv->views, state->view, NULL, &position))
+          if (g_ptr_array_find_with_equal_func (priv->pages, state->page, NULL, &position))
             {
-              g_ptr_array_remove (priv->in_transition, state->view);
-              g_ptr_array_remove_index (priv->views, position);
+              g_ptr_array_remove (priv->in_transition, state->page);
+              g_ptr_array_remove_index (priv->pages, position);
               g_list_model_items_changed (G_LIST_MODEL (state->source), position, 1, 0);
             }
         }
@@ -1200,33 +1201,33 @@ animation_state_complete (gpointer data)
 
   /*
    * We might need to reshow the widget in cases where we are in a
-   * three-finger-swipe of the view. There is also a chance that we
+   * three-finger-swipe of the page. There is also a chance that we
    * aren't the proper visible child and that needs to be restored now.
    */
-  gtk_widget_show (GTK_WIDGET (state->view));
-  ide_layout_stack_set_visible_child (state->dest, state->view);
+  gtk_widget_show (GTK_WIDGET (state->page));
+  ide_frame_set_visible_child (state->dest, state->page);
 
   g_clear_object (&state->source);
   g_clear_object (&state->dest);
-  g_clear_object (&state->view);
+  g_clear_object (&state->page);
   g_clear_object (&state->theatric);
   g_slice_free (AnimationState, state);
 }
 
 void
-_ide_layout_stack_transfer (IdeLayoutStack *self,
-                            IdeLayoutStack *dest,
-                            IdeLayoutView  *view)
+_ide_frame_transfer (IdeFrame *self,
+                     IdeFrame *dest,
+                     IdePage  *page)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
-  IdeLayoutStackPrivate *dest_priv = ide_layout_stack_get_instance_private (dest);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdeFramePrivate *dest_priv = ide_frame_get_instance_private (dest);
   const GdkRGBA *fg;
   const GdkRGBA *bg;
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (dest));
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
-  g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (view)));
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_FRAME (dest));
+  g_return_if_fail (IDE_IS_PAGE (page));
+  g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (page)));
 
   /*
    * Inform the destination stack about our new primary colors so that it can
@@ -1235,20 +1236,20 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
    * transitions.
    */
 
-  fg = ide_layout_view_get_primary_color_fg (view);
-  bg = ide_layout_view_get_primary_color_bg (view);
-  _ide_layout_stack_header_set_foreground_rgba (dest_priv->header, fg);
-  _ide_layout_stack_header_set_background_rgba (dest_priv->header, bg);
+  fg = ide_page_get_primary_color_fg (page);
+  bg = ide_page_get_primary_color_bg (page);
+  _ide_frame_header_set_foreground_rgba (dest_priv->header, fg);
+  _ide_frame_header_set_background_rgba (dest_priv->header, bg);
 
   /*
    * If both the old and the new stacks are mapped, we can animate
-   * between them using a snapshot of the view. Well, we also need
+   * between them using a snapshot of the page. Well, we also need
    * to be sure they have a valid allocation, but that check is done
    * slightly after this because it makes things easier.
    */
   if (gtk_widget_get_mapped (GTK_WIDGET (self)) &&
       gtk_widget_get_mapped (GTK_WIDGET (dest)) &&
-      gtk_widget_get_mapped (GTK_WIDGET (view)))
+      gtk_widget_get_mapped (GTK_WIDGET (page)))
     {
       GtkAllocation alloc, dest_alloc;
       cairo_surface_t *surface = NULL;
@@ -1256,9 +1257,9 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
       GtkWidget *grid;
       gboolean enable_animations;
 
-      grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+      grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
 
-      gtk_widget_get_allocation (GTK_WIDGET (view), &alloc);
+      gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
       gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc);
 
       g_object_get (gtk_settings_get_default (),
@@ -1270,7 +1271,7 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
           !is_uninitialized (&alloc) &&
           !is_uninitialized (&dest_alloc) &&
           dest_alloc.width > 0 && dest_alloc.height > 0 &&
-          NULL != (window = gtk_widget_get_window (GTK_WIDGET (view))) &&
+          NULL != (window = gtk_widget_get_window (GTK_WIDGET (page))) &&
           NULL != (surface = gdk_window_create_similar_surface (window,
                                                                 CAIRO_CONTENT_COLOR,
                                                                 alloc.width,
@@ -1281,7 +1282,7 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
           cairo_t *cr;
 
           cr = cairo_create (surface);
-          gtk_widget_draw (GTK_WIDGET (view), cr);
+          gtk_widget_draw (GTK_WIDGET (page), cr);
           cairo_destroy (cr);
 
           gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0,
@@ -1301,7 +1302,7 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
           state = g_slice_new0 (AnimationState);
           state->source = g_object_ref (self);
           state->dest = g_object_ref (dest);
-          state->view = g_object_ref (view);
+          state->page = g_object_ref (page);
           state->theatric = theatric;
 
           dzl_object_animate_full (theatric,
@@ -1317,11 +1318,11 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
                                    NULL);
 
           /*
-           * Mark the view as in-transition so that when we remove it
+           * Mark the page as in-transition so that when we remove it
            * we can ignore the items-changed until the animation completes.
            */
-          g_ptr_array_add (priv->in_transition, g_object_ref (view));
-          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (view));
+          g_ptr_array_add (priv->in_transition, g_object_ref (page));
+          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
 
           cairo_surface_destroy (surface);
 
@@ -1329,60 +1330,60 @@ _ide_layout_stack_transfer (IdeLayoutStack *self,
         }
     }
 
-  g_object_ref (view);
-  gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (view));
-  gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (view));
-  g_object_unref (view);
+  g_object_ref (page);
+  gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
+  gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
+  g_object_unref (page);
 }
 
 /**
- * ide_layout_stack_foreach_view:
- * @self: a #IdeLayoutStack
- * @callback: (scope call) (closure user_data): A callback for each view
+ * ide_frame_foreach_page:
+ * @self: a #IdeFrame
+ * @callback: (scope call) (closure user_data): A callback for each page
  * @user_data: user data for @callback
  *
- * This function will call @callback for every view found in @self.
+ * This function will call @callback for every page found in @self.
  *
  * Since: 3.32
  */
 void
-ide_layout_stack_foreach_view (IdeLayoutStack *self,
-                               GtkCallback     callback,
-                               gpointer        user_data)
+ide_frame_foreach_page (IdeFrame    *self,
+                        GtkCallback  callback,
+                        gpointer     user_data)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_FRAME (self));
   g_return_if_fail (callback != NULL);
 
   gtk_container_foreach (GTK_CONTAINER (priv->stack), callback, user_data);
 }
 
 /**
- * ide_layout_stack_addin_find_by_module_name:
- * @stack: An #IdeLayoutStack
+ * ide_frame_addin_find_by_module_name:
+ * @frame: An #IdeFrame
  * @module_name: the module name which provides the addin
  *
- * This function will locate the #IdeLayoutStackAddin that was registered by
+ * This function will locate the #IdeFrameAddin that was registered by
  * the plugin named @module_name (which should match the "Module" field
  * provided in the .plugin file).
  *
  * If no module was found or that module does not implement the
- * #IdeLayoutStackAddinInterface, then %NULL is returned.
+ * #IdeFrameAddinInterface, then %NULL is returned.
  *
- * Returns: (transfer none) (nullable): An #IdeLayoutStackAddin or %NULL
+ * Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL
  *
  * Since: 3.32
  */
-IdeLayoutStackAddin *
-ide_layout_stack_addin_find_by_module_name (IdeLayoutStack *stack,
-                                            const gchar    *module_name)
+IdeFrameAddin *
+ide_frame_addin_find_by_module_name (IdeFrame    *frame,
+                                     const gchar *module_name)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (stack);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (frame);
   PeasExtension *ret = NULL;
   PeasPluginInfo *plugin_info;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (stack), NULL);
+  g_return_val_if_fail (IDE_IS_FRAME (frame), NULL);
   g_return_val_if_fail (priv->addins != NULL, NULL);
   g_return_val_if_fail (module_name != NULL, NULL);
 
@@ -1393,17 +1394,17 @@ ide_layout_stack_addin_find_by_module_name (IdeLayoutStack *stack,
   else
     g_warning ("No addin could be found matching module \"%s\"", module_name);
 
-  return ret ? IDE_LAYOUT_STACK_ADDIN (ret) : NULL;
+  return ret ? IDE_FRAME_ADDIN (ret) : NULL;
 }
 
 void
-ide_layout_stack_add_with_depth (IdeLayoutStack *self,
-                                 GtkWidget      *widget,
-                                 guint           position)
+ide_frame_add_with_depth (IdeFrame  *self,
+                          GtkWidget *widget,
+                          guint      position)
 {
-  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_FRAME (self));
   g_return_if_fail (GTK_IS_WIDGET (widget));
 
   gtk_container_add_with_properties (GTK_CONTAINER (priv->stack), widget,
diff --git a/src/libide/gui/ide-frame.h b/src/libide/gui/ide-frame.h
new file mode 100644
index 000000000..36e586d7c
--- /dev/null
+++ b/src/libide/gui/ide-frame.h
@@ -0,0 +1,84 @@
+/* ide-frame.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME (ide_frame_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeFrame, ide_frame, IDE, FRAME, GtkBox)
+
+struct _IdeFrameClass
+{
+  GtkBoxClass parent_class;
+
+  void     (*agree_to_close_async)  (IdeFrame             *stack,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*agree_to_close_finish) (IdeFrame             *stack,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_frame_new                   (void);
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_frame_get_titlebar          (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+IdePage   *ide_frame_get_visible_child     (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_set_visible_child     (IdeFrame             *self,
+                                            IdePage              *page);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_frame_get_has_page          (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_agree_to_close_async  (IdeFrame             *self,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_frame_agree_to_close_finish (IdeFrame             *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_foreach_page          (IdeFrame             *self,
+                                            GtkCallback           callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_add_with_depth        (IdeFrame             *self,
+                                            GtkWidget            *widget,
+                                            guint                 position);
+
+G_END_DECLS
diff --git a/src/libide/layout/ide-layout-stack.ui b/src/libide/gui/ide-frame.ui
similarity index 97%
rename from src/libide/layout/ide-layout-stack.ui
rename to src/libide/gui/ide-frame.ui
index fa7cf2d70..3dc728752 100644
--- a/src/libide/layout/ide-layout-stack.ui
+++ b/src/libide/gui/ide-frame.ui
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <template class="IdeLayoutStack" parent="GtkBox">
+  <template class="IdeFrame" parent="GtkBox">
     <property name="orientation">vertical</property>
     <child>
-      <object class="IdeLayoutStackHeader" id="header">
+      <object class="IdeFrameHeader" id="header">
         <property name="show-close-button">true</property>
         <property name="title" translatable="yes">No Open Pages</property>
         <property name="visible">true</property>
@@ -127,7 +127,7 @@
               </object>
             </child>
             <child>
-              <object class="IdeLayoutStackWrapper" id="stack">
+              <object class="IdeFrameWrapper" id="stack">
                 <property name="expand">true</property>
                 <property name="homogeneous">false</property>
                 <property name="interpolate-size">false</property>
diff --git a/src/libide/layout/ide-layout-grid-actions.c b/src/libide/gui/ide-grid-actions.c
similarity index 73%
rename from src/libide/layout/ide-layout-grid-actions.c
rename to src/libide/gui/ide-grid-actions.c
index 1d14d24e2..472f9e4d8 100644
--- a/src/libide/layout/ide-layout-grid-actions.c
+++ b/src/libide/gui/ide-grid-actions.c
@@ -1,4 +1,4 @@
-/* ide-layout-grid-actions.c
+/* ide-grid-actions.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,25 +18,25 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-grid"
+#define G_LOG_DOMAIN "ide-grid"
 
 #include "config.h"
 
-#include "layout/ide-layout-grid.h"
-#include "layout/ide-layout-private.h"
+#include "ide-grid.h"
+#include "ide-gui-private.h"
 
 static void
-ide_layout_grid_actions_focus_neighbor (GSimpleAction *action,
+ide_grid_actions_focus_neighbor (GSimpleAction *action,
                                         GVariant      *variant,
                                         gpointer       user_data)
 {
-  IdeLayoutGrid *self = user_data;
+  IdeGrid *self = user_data;
   GtkDirectionType dir;
 
   g_return_if_fail (G_IS_SIMPLE_ACTION (action));
   g_return_if_fail (variant != NULL);
   g_return_if_fail (g_variant_is_of_type (variant, G_VARIANT_TYPE_INT32));
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_GRID (self));
 
   dir = (GtkDirectionType)g_variant_get_int32 (variant);
 
@@ -48,7 +48,7 @@ ide_layout_grid_actions_focus_neighbor (GSimpleAction *action,
     case GTK_DIR_DOWN:
     case GTK_DIR_LEFT:
     case GTK_DIR_RIGHT:
-      ide_layout_grid_focus_neighbor (self, dir);
+      ide_grid_focus_neighbor (self, dir);
       break;
 
     default:
@@ -57,17 +57,17 @@ ide_layout_grid_actions_focus_neighbor (GSimpleAction *action,
 }
 
 static const GActionEntry actions[] = {
-  { "focus-neighbor", ide_layout_grid_actions_focus_neighbor, "i" },
+  { "focus-neighbor", ide_grid_actions_focus_neighbor, "i" },
 };
 
 void
-_ide_layout_grid_init_actions (IdeLayoutGrid *self)
+_ide_grid_init_actions (IdeGrid *self)
 {
   g_autoptr(GSimpleActionGroup) group = NULL;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_GRID (self));
 
   group = g_simple_action_group_new ();
   g_action_map_add_action_entries (G_ACTION_MAP (group), actions, G_N_ELEMENTS (actions), self);
-  gtk_widget_insert_action_group (GTK_WIDGET (self), "layoutgrid", G_ACTION_GROUP (group));
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "grid", G_ACTION_GROUP (group));
 }
diff --git a/src/libide/layout/ide-layout-grid-column-actions.c b/src/libide/gui/ide-grid-column-actions.c
similarity index 65%
rename from src/libide/layout/ide-layout-grid-column-actions.c
rename to src/libide/gui/ide-grid-column-actions.c
index 5dd83e66c..54786ae8b 100644
--- a/src/libide/layout/ide-layout-grid-column-actions.c
+++ b/src/libide/gui/ide-grid-column-actions.c
@@ -1,4 +1,4 @@
-/* ide-layout-grid-column-actions.c
+/* ide-grid-column-actions.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,57 +18,57 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-grid-column-actions"
+#define G_LOG_DOMAIN "ide-grid-column-actions"
 
 #include "config.h"
 
-#include "layout/ide-layout-private.h"
+#include "ide-gui-private.h"
 
 static void
-ide_layout_grid_column_actions_close (GSimpleAction *action,
-                                      GVariant      *variant,
-                                      gpointer       user_data)
+ide_grid_column_actions_close (GSimpleAction *action,
+                               GVariant      *variant,
+                               gpointer       user_data)
 {
-  IdeLayoutGridColumn *self = user_data;
+  IdeGridColumn *self = user_data;
 
   g_assert (G_IS_SIMPLE_ACTION (action));
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_assert (IDE_IS_GRID_COLUMN (self));
 
-  _ide_layout_grid_column_try_close (self);
+  _ide_grid_column_try_close (self);
 }
 
 static const GActionEntry grid_column_actions[] = {
-  { "close", ide_layout_grid_column_actions_close },
+  { "close", ide_grid_column_actions_close },
 };
 
 void
-_ide_layout_grid_column_update_actions (IdeLayoutGridColumn *self)
+_ide_grid_column_update_actions (IdeGridColumn *self)
 {
   GtkWidget *grid;
   gboolean can_close;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_assert (IDE_IS_GRID_COLUMN (self));
 
   grid = gtk_widget_get_parent (GTK_WIDGET (self));
 
-  if (grid == NULL || !IDE_IS_LAYOUT_GRID (grid))
+  if (grid == NULL || !IDE_IS_GRID (grid))
     {
       g_warning ("Attempt to update actions in unowned grid column");
       return;
     }
 
   can_close = (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (grid)) > 1);
-  dzl_gtk_widget_action_set (GTK_WIDGET (self), "layoutgridcolumn", "close",
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "gridcolumn", "close",
                              "enabled", can_close,
                              NULL);
 }
 
 void
-_ide_layout_grid_column_init_actions (IdeLayoutGridColumn *self)
+_ide_grid_column_init_actions (IdeGridColumn *self)
 {
   g_autoptr(GSimpleActionGroup) group = NULL;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_assert (IDE_IS_GRID_COLUMN (self));
 
   group = g_simple_action_group_new ();
   g_action_map_add_action_entries (G_ACTION_MAP (group),
@@ -76,6 +76,6 @@ _ide_layout_grid_column_init_actions (IdeLayoutGridColumn *self)
                                   G_N_ELEMENTS (grid_column_actions),
                                   self);
   gtk_widget_insert_action_group (GTK_WIDGET (self),
-                                  "layoutgridcolumn",
+                                  "gridcolumn",
                                   G_ACTION_GROUP (group));
 }
diff --git a/src/libide/layout/ide-layout-grid-column.c b/src/libide/gui/ide-grid-column.c
similarity index 53%
rename from src/libide/layout/ide-layout-grid-column.c
rename to src/libide/gui/ide-grid-column.c
index e1b8ad4a6..1fbee766a 100644
--- a/src/libide/layout/ide-layout-grid-column.c
+++ b/src/libide/gui/ide-grid-column.c
@@ -1,4 +1,4 @@
-/* ide-layout-grid-column.c
+/* ide-grid-column.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,16 +18,18 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-grid-column"
+#define G_LOG_DOMAIN "ide-grid-column"
 
 #include "config.h"
 
-#include "layout/ide-layout-grid-column.h"
-#include "layout/ide-layout-private.h"
-#include "layout/ide-layout-view.h"
-#include "threading/ide-task.h"
+#include <libide-core.h>
+#include <libide-threading.h>
 
-struct _IdeLayoutGridColumn
+#include "ide-grid-column.h"
+#include "ide-gui-private.h"
+#include "ide-page.h"
+
+struct _IdeGridColumn
 {
   DzlMultiPaned parent_instance;
   GQueue        focus_stack;
@@ -39,9 +41,9 @@ typedef struct
   IdeTask *backpointer;
 } TryCloseState;
 
-G_DEFINE_TYPE (IdeLayoutGridColumn, ide_layout_grid_column, DZL_TYPE_MULTI_PANED)
+G_DEFINE_TYPE (IdeGridColumn, ide_grid_column, DZL_TYPE_MULTI_PANED)
 
-static void ide_layout_grid_column_try_close_pump (IdeTask *task);
+static void ide_grid_column_try_close_pump (IdeTask *task);
 
 enum {
   PROP_0,
@@ -66,14 +68,14 @@ try_close_state_free (gpointer data)
 }
 
 static void
-ide_layout_grid_column_add (GtkContainer *container,
-                            GtkWidget    *widget)
+ide_grid_column_add (GtkContainer *container,
+                     GtkWidget    *widget)
 {
-  IdeLayoutGridColumn *self = (IdeLayoutGridColumn *)container;
+  IdeGridColumn *self = (IdeGridColumn *)container;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_assert (IDE_IS_GRID_COLUMN (self));
 
-  if (IDE_IS_LAYOUT_VIEW (widget))
+  if (IDE_IS_PAGE (widget))
     {
       GtkWidget *child;
 
@@ -82,84 +84,84 @@ ide_layout_grid_column_add (GtkContainer *container,
       child = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (container), 0);
       gtk_container_add (GTK_CONTAINER (child), widget);
     }
-  else if (IDE_IS_LAYOUT_STACK (widget))
+  else if (IDE_IS_FRAME (widget))
     {
       GtkWidget *grid;
 
       g_queue_push_head (&self->focus_stack, widget);
-      GTK_CONTAINER_CLASS (ide_layout_grid_column_parent_class)->add (container, widget);
+      GTK_CONTAINER_CLASS (ide_grid_column_parent_class)->add (container, widget);
 
-      if (IDE_IS_LAYOUT_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
-        _ide_layout_grid_stack_added (IDE_LAYOUT_GRID (grid), IDE_LAYOUT_STACK (widget));
+      if (IDE_IS_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
+        _ide_grid_stack_added (IDE_GRID (grid), IDE_FRAME (widget));
     }
   else
     {
-      g_warning ("%s only supports adding IdeLayoutView or IdeLayoutStack",
+      g_warning ("%s only supports adding IdePage or IdeFrame",
                  G_OBJECT_TYPE_NAME (self));
       return;
     }
 }
 
 static void
-ide_layout_grid_column_remove (GtkContainer *container,
-                               GtkWidget    *widget)
+ide_grid_column_remove (GtkContainer *container,
+                        GtkWidget    *widget)
 {
-  IdeLayoutGridColumn *self = (IdeLayoutGridColumn *)container;
+  IdeGridColumn *self = (IdeGridColumn *)container;
   GtkWidget *grid;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
-  g_assert (IDE_IS_LAYOUT_STACK (widget));
+  g_assert (IDE_IS_GRID_COLUMN (self));
+  g_assert (IDE_IS_FRAME (widget));
 
-  if (IDE_IS_LAYOUT_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
-    _ide_layout_grid_stack_removed (IDE_LAYOUT_GRID (grid), IDE_LAYOUT_STACK (widget));
+  if (IDE_IS_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
+    _ide_grid_stack_removed (IDE_GRID (grid), IDE_FRAME (widget));
 
   g_queue_remove (&self->focus_stack, widget);
 
-  GTK_CONTAINER_CLASS (ide_layout_grid_column_parent_class)->remove (container, widget);
+  GTK_CONTAINER_CLASS (ide_grid_column_parent_class)->remove (container, widget);
 }
 
 static void
-ide_layout_grid_column_grab_focus (GtkWidget *widget)
+ide_grid_column_grab_focus (GtkWidget *widget)
 {
-  IdeLayoutGridColumn *self = (IdeLayoutGridColumn *)widget;
-  IdeLayoutStack *stack;
+  IdeGridColumn *self = (IdeGridColumn *)widget;
+  IdeFrame *stack;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_assert (IDE_IS_GRID_COLUMN (self));
 
-  stack = ide_layout_grid_column_get_current_stack (self);
+  stack = ide_grid_column_get_current_stack (self);
 
   if (stack != NULL)
     gtk_widget_grab_focus (GTK_WIDGET (stack));
   else
-    GTK_WIDGET_CLASS (ide_layout_grid_column_parent_class)->grab_focus (widget);
+    GTK_WIDGET_CLASS (ide_grid_column_parent_class)->grab_focus (widget);
 }
 
 static void
-ide_layout_grid_column_finalize (GObject *object)
+ide_grid_column_finalize (GObject *object)
 {
 #ifndef G_DISABLE_ASSERT
-  IdeLayoutGridColumn *self = (IdeLayoutGridColumn *)object;
+  IdeGridColumn *self = (IdeGridColumn *)object;
 
   g_assert (self->focus_stack.head == NULL);
   g_assert (self->focus_stack.tail == NULL);
   g_assert (self->focus_stack.length == 0);
 #endif
 
-  G_OBJECT_CLASS (ide_layout_grid_column_parent_class)->finalize (object);
+  G_OBJECT_CLASS (ide_grid_column_parent_class)->finalize (object);
 }
 
 static void
-ide_layout_grid_column_get_property (GObject    *object,
-                                     guint       prop_id,
-                                     GValue     *value,
-                                     GParamSpec *pspec)
+ide_grid_column_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
 {
-  IdeLayoutGridColumn *self = IDE_LAYOUT_GRID_COLUMN (object);
+  IdeGridColumn *self = IDE_GRID_COLUMN (object);
 
   switch (prop_id)
     {
     case PROP_CURRENT_STACK:
-      g_value_set_object (value, ide_layout_grid_column_get_current_stack (self));
+      g_value_set_object (value, ide_grid_column_get_current_stack (self));
       break;
 
     default:
@@ -168,17 +170,17 @@ ide_layout_grid_column_get_property (GObject    *object,
 }
 
 static void
-ide_layout_grid_column_set_property (GObject      *object,
-                                     guint         prop_id,
-                                     const GValue *value,
-                                     GParamSpec   *pspec)
+ide_grid_column_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
 {
-  IdeLayoutGridColumn *self = IDE_LAYOUT_GRID_COLUMN (object);
+  IdeGridColumn *self = IDE_GRID_COLUMN (object);
 
   switch (prop_id)
     {
     case PROP_CURRENT_STACK:
-      ide_layout_grid_column_set_current_stack (self, g_value_get_object (value));
+      ide_grid_column_set_current_stack (self, g_value_get_object (value));
       break;
 
     default:
@@ -187,59 +189,59 @@ ide_layout_grid_column_set_property (GObject      *object,
 }
 
 static void
-ide_layout_grid_column_class_init (IdeLayoutGridColumnClass *klass)
+ide_grid_column_class_init (IdeGridColumnClass *klass)
 {
   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-  object_class->finalize = ide_layout_grid_column_finalize;
-  object_class->get_property = ide_layout_grid_column_get_property;
-  object_class->set_property = ide_layout_grid_column_set_property;
+  object_class->finalize = ide_grid_column_finalize;
+  object_class->get_property = ide_grid_column_get_property;
+  object_class->set_property = ide_grid_column_set_property;
 
-  widget_class->grab_focus = ide_layout_grid_column_grab_focus;
+  widget_class->grab_focus = ide_grid_column_grab_focus;
 
-  container_class->add = ide_layout_grid_column_add;
-  container_class->remove = ide_layout_grid_column_remove;
+  container_class->add = ide_grid_column_add;
+  container_class->remove = ide_grid_column_remove;
 
   properties [PROP_CURRENT_STACK] =
     g_param_spec_object ("current-stack",
                          "Current Stack",
                          "The most recently focused stack within the column",
-                         IDE_TYPE_LAYOUT_STACK,
+                         IDE_TYPE_FRAME,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_css_name (widget_class, "idelayoutgridcolumn");
+  gtk_widget_class_set_css_name (widget_class, "idegridcolumn");
 }
 
 static void
-ide_layout_grid_column_init (IdeLayoutGridColumn *self)
+ide_grid_column_init (IdeGridColumn *self)
 {
-  _ide_layout_grid_column_init_actions (self);
+  _ide_grid_column_init_actions (self);
 }
 
 GtkWidget *
-ide_layout_grid_column_new (void)
+ide_grid_column_new (void)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_GRID_COLUMN, NULL);
+  return g_object_new (IDE_TYPE_GRID_COLUMN, NULL);
 }
 
 static void
-ide_layout_grid_column_try_close_cb (GObject      *object,
-                                     GAsyncResult *result,
-                                     gpointer      user_data)
+ide_grid_column_try_close_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
 {
-  IdeLayoutStack *stack = (IdeLayoutStack *)object;
+  IdeFrame *stack = (IdeFrame *)object;
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
 
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_FRAME (stack));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  if (!ide_layout_stack_agree_to_close_finish (stack, result, &error))
+  if (!ide_frame_agree_to_close_finish (stack, result, &error))
     {
       g_debug ("Cannot close stack now due to: %s", error->message);
       gtk_widget_grab_focus (GTK_WIDGET (stack));
@@ -249,14 +251,14 @@ ide_layout_grid_column_try_close_cb (GObject      *object,
 
   gtk_widget_destroy (GTK_WIDGET (stack));
 
-  ide_layout_grid_column_try_close_pump (g_steal_pointer (&task));
+  ide_grid_column_try_close_pump (g_steal_pointer (&task));
 }
 
 static void
-ide_layout_grid_column_try_close_pump (IdeTask *_task)
+ide_grid_column_try_close_pump (IdeTask *_task)
 {
   g_autoptr(IdeTask) task = _task;
-  g_autoptr(IdeLayoutStack) stack = NULL;
+  g_autoptr(IdeFrame) stack = NULL;
   TryCloseState *state;
   GCancellable *cancellable;
 
@@ -268,9 +270,9 @@ ide_layout_grid_column_try_close_pump (IdeTask *_task)
 
   if (state->stacks == NULL)
     {
-      IdeLayoutGridColumn *self = ide_task_get_source_object (task);
+      IdeGridColumn *self = ide_task_get_source_object (task);
 
-      g_assert (IDE_IS_LAYOUT_GRID_COLUMN (self));
+      g_assert (IDE_IS_GRID_COLUMN (self));
       gtk_widget_destroy (GTK_WIDGET (self));
       ide_task_return_boolean (task, TRUE);
       return;
@@ -278,24 +280,24 @@ ide_layout_grid_column_try_close_pump (IdeTask *_task)
 
   stack = state->stacks->data;
   state->stacks = g_list_remove (state->stacks, stack);
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_FRAME (stack));
 
   cancellable = ide_task_get_cancellable (task);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  ide_layout_stack_agree_to_close_async (stack,
+  ide_frame_agree_to_close_async (stack,
                                          cancellable,
-                                         ide_layout_grid_column_try_close_cb,
+                                         ide_grid_column_try_close_cb,
                                          g_steal_pointer (&task));
 }
 
 void
-_ide_layout_grid_column_try_close (IdeLayoutGridColumn *self)
+_ide_grid_column_try_close (IdeGridColumn *self)
 {
   TryCloseState state = { 0 };
   g_autoptr(IdeTask) task = NULL;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (self));
+  g_return_if_fail (IDE_IS_GRID_COLUMN (self));
 
   state.stacks = gtk_container_get_children (GTK_CONTAINER (self));
 
@@ -309,20 +311,20 @@ _ide_layout_grid_column_try_close (IdeLayoutGridColumn *self)
     }
 
   task = ide_task_new (self, NULL, NULL, NULL);
-  ide_task_set_source_tag (task, _ide_layout_grid_column_try_close);
+  ide_task_set_source_tag (task, _ide_grid_column_try_close);
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   g_list_foreach (state.stacks, (GFunc)g_object_ref, NULL);
   state.backpointer = task;
   ide_task_set_task_data (task, g_slice_dup (TryCloseState, &state), try_close_state_free);
 
-  ide_layout_grid_column_try_close_pump (g_steal_pointer (&task));
+  ide_grid_column_try_close_pump (g_steal_pointer (&task));
 }
 
 gboolean
-_ide_layout_grid_column_is_empty (IdeLayoutGridColumn *self)
+_ide_grid_column_is_empty (IdeGridColumn *self)
 {
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (self), FALSE);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (self), FALSE);
 
   /*
    * Check if we only have a single stack and it is empty.
@@ -333,41 +335,41 @@ _ide_layout_grid_column_is_empty (IdeLayoutGridColumn *self)
     {
       GtkWidget *child = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0);
 
-      g_assert (IDE_IS_LAYOUT_STACK (child));
+      g_assert (IDE_IS_FRAME (child));
 
-      return !ide_layout_stack_get_has_view (IDE_LAYOUT_STACK (child));
+      return !ide_frame_get_has_page (IDE_FRAME (child));
     }
 
   return FALSE;
 }
 
 /**
- * ide_layout_grid_column_get_current_stack:
- * @self: a #IdeLayoutGridColumn
+ * ide_grid_column_get_current_stack:
+ * @self: a #IdeGridColumn
  *
  * Gets the most recently focused stack. If no stack has been added, then
  * %NULL is returned.
  *
- * Returns: (transfer none) (nullable): an #IdeLayoutStack or %NULL.
+ * Returns: (transfer none) (nullable): an #IdeFrame or %NULL.
  *
  * Since: 3.32
  */
-IdeLayoutStack *
-ide_layout_grid_column_get_current_stack (IdeLayoutGridColumn *self)
+IdeFrame *
+ide_grid_column_get_current_stack (IdeGridColumn *self)
 {
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (self), NULL);
 
   return self->focus_stack.head ? self->focus_stack.head->data : NULL;
 }
 
 void
-ide_layout_grid_column_set_current_stack (IdeLayoutGridColumn *self,
-                                          IdeLayoutStack      *stack)
+ide_grid_column_set_current_stack (IdeGridColumn *self,
+                                   IdeFrame      *stack)
 {
   GList *iter;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (self));
-  g_return_if_fail (!stack || IDE_IS_LAYOUT_STACK (stack));
+  g_return_if_fail (IDE_IS_GRID_COLUMN (self));
+  g_return_if_fail (!stack || IDE_IS_FRAME (stack));
 
   /* If there is nothing to do, short-circuit. */
   if (stack == NULL ||
diff --git a/src/libide/layout/ide-layout-grid-column.h b/src/libide/gui/ide-grid-column.h
similarity index 60%
rename from src/libide/layout/ide-layout-grid-column.h
rename to src/libide/gui/ide-grid-column.h
index 9708a4b08..ab2810fe0 100644
--- a/src/libide/layout/ide-layout-grid-column.h
+++ b/src/libide/gui/ide-grid-column.h
@@ -1,4 +1,4 @@
-/* ide-layout-grid-column.h
+/* ide-grid-column.h
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -20,25 +20,28 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <dazzle.h>
+#include <libide-core.h>
 
-#include "layout/ide-layout-stack.h"
+#include "ide-frame.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT_GRID_COLUMN (ide_layout_grid_column_get_type())
+#define IDE_TYPE_GRID_COLUMN (ide_grid_column_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdeLayoutGridColumn, ide_layout_grid_column, IDE, LAYOUT_GRID_COLUMN, DzlMultiPaned)
+G_DECLARE_FINAL_TYPE (IdeGridColumn, ide_grid_column, IDE, GRID_COLUMN, DzlMultiPaned)
 
 IDE_AVAILABLE_IN_3_32
-GtkWidget      *ide_layout_grid_column_new               (void);
+GtkWidget *ide_grid_column_new               (void);
 IDE_AVAILABLE_IN_3_32
-IdeLayoutStack *ide_layout_grid_column_get_current_stack (IdeLayoutGridColumn *self);
+IdeFrame  *ide_grid_column_get_current_stack (IdeGridColumn *self);
 IDE_AVAILABLE_IN_3_32
-void            ide_layout_grid_column_set_current_stack (IdeLayoutGridColumn *self,
-                                                          IdeLayoutStack      *stack);
+void       ide_grid_column_set_current_stack (IdeGridColumn *self,
+                                              IdeFrame      *stack);
 
 G_END_DECLS
diff --git a/src/libide/layout/ide-layout-grid.c b/src/libide/gui/ide-grid.c
similarity index 56%
rename from src/libide/layout/ide-layout-grid.c
rename to src/libide/gui/ide-grid.c
index c5e319afd..47b8dc63a 100644
--- a/src/libide/layout/ide-layout-grid.c
+++ b/src/libide/gui/ide-grid.c
@@ -1,4 +1,4 @@
-/* ide-layout-grid.c
+/* ide-grid.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,35 +18,34 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-grid"
+
+#define G_LOG_DOMAIN "ide-grid"
 
 #include "config.h"
 
 #include <string.h>
 
-#include "ide-object.h"
-
-#include "layout/ide-layout-grid.h"
-#include "layout/ide-layout-private.h"
+#include "ide-grid.h"
+#include "ide-gui-private.h"
 
 /**
- * SECTION:ide-layout-grid
- * @title: IdeLayoutGrid
- * @short_description: A grid for #IdeLayoutView
+ * SECTION:ide-grid
+ * @title: IdeGrid
+ * @short_description: A grid for #IdePage
  *
- * The #IdeLayoutGrid provides a grid of views that the user may
+ * The #IdeGrid provides a grid of pages that the user may
  * manipulate.
  *
- * Internally, this is implemented with #IdeLayoutGrid at the top
- * containing one or more of #IdeLayoutGridColumn. Those columns
- * contain one or more #IdeLayoutStack. The stack can contain many
- * #IdeLayoutView.
+ * Internally, this is implemented with #IdeGrid at the top
+ * containing one or more of #IdeGridColumn. Those columns
+ * contain one or more #IdeFrame. The stack can contain many
+ * #IdePage.
  *
- * #IdeLayoutGrid implements the #GListModel interface to simplify
- * the process of listing (with deduplication) the views that are
- * contianed within the #IdeLayoutGrid. If you would instead like
- * to see all possible views in the stack, use the
- * ide_layout_grid_foreach_view() API.
+ * #IdeGrid implements the #GListModel interface to simplify
+ * the process of listing (with deduplication) the pages that are
+ * contianed within the #IdeGrid. If you would instead like
+ * to see all possible pages in the stack, use the
+ * ide_grid_foreach_page() API.
  *
  * Since: 3.32
  */
@@ -67,22 +66,22 @@ typedef struct
 
   /*
    * This unowned reference is simply used to compare to a new focus
-   * view to see if we have changed our current view. It is not to
+   * page to see if we have changed our current page. It is not to
    * be used directly, only for pointer comparison.
    */
-  IdeLayoutView  *_last_focused_view;
+  IdePage  *_last_focused_page;
 
   /*
    * A GSource that is used to remove empty stacks that are unnecessary
    * (after a last stack item is removed).
    */
   guint cull_source;
-} IdeLayoutGridPrivate;
+} IdeGridPrivate;
 
 typedef struct
 {
-  IdeLayoutGridColumn *column;
-  IdeLayoutStack      *stack;
+  IdeGridColumn *column;
+  IdeFrame      *stack;
   GdkRectangle         area;
   gint                 drop;
   gint                 x;
@@ -91,7 +90,7 @@ typedef struct
 
 typedef struct
 {
-  IdeLayoutStack *stack;
+  IdeFrame *stack;
   guint           len;
 } StackInfo;
 
@@ -99,7 +98,7 @@ enum {
   PROP_0,
   PROP_CURRENT_COLUMN,
   PROP_CURRENT_STACK,
-  PROP_CURRENT_VIEW,
+  PROP_CURRENT_PAGE,
   N_PROPS
 };
 
@@ -119,28 +118,28 @@ enum {
 
 static void list_model_iface_init (GListModelInterface *iface);
 
-G_DEFINE_TYPE_WITH_CODE (IdeLayoutGrid, ide_layout_grid, DZL_TYPE_MULTI_PANED,
-                         G_ADD_PRIVATE (IdeLayoutGrid)
+G_DEFINE_TYPE_WITH_CODE (IdeGrid, ide_grid, DZL_TYPE_MULTI_PANED,
+                         G_ADD_PRIVATE (IdeGrid)
                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
 static void
-ide_layout_grid_cull (IdeLayoutGrid *self)
+ide_grid_cull (IdeGrid *self)
 {
   guint n_columns;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
 
   for (guint i = n_columns; i > 0; i--)
     {
-      IdeLayoutGridColumn *column;
+      IdeGridColumn *column;
       guint n_stacks;
 
-      column = IDE_LAYOUT_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i - 1));
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i - 1));
       n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
 
       if (n_columns == 1 && n_stacks == 1)
@@ -148,10 +147,10 @@ ide_layout_grid_cull (IdeLayoutGrid *self)
 
       for (guint j = n_stacks; j > 0; j--)
         {
-          IdeLayoutStack *stack;
+          IdeFrame *stack;
           guint n_items;
 
-          stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j - 1));
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j - 1));
           n_items = g_list_model_get_n_items (G_LIST_MODEL (stack));
 
           if (n_items == 0)
@@ -164,42 +163,42 @@ ide_layout_grid_cull (IdeLayoutGrid *self)
 }
 
 static gboolean
-ide_layout_grid_do_cull (gpointer data)
+ide_grid_do_cull (gpointer data)
 {
-  IdeLayoutGrid *self = data;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = data;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   priv->cull_source = 0;
 
-  ide_layout_grid_cull (self);
+  ide_grid_cull (self);
 
   return G_SOURCE_REMOVE;
 }
 
 static void
-ide_layout_grid_queue_cull (IdeLayoutGrid *self)
+ide_grid_queue_cull (IdeGrid *self)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   if (priv->cull_source != 0)
     return;
 
   priv->cull_source = gdk_threads_add_idle_full (G_PRIORITY_HIGH,
-                                                 ide_layout_grid_do_cull,
+                                                 ide_grid_do_cull,
                                                  g_object_ref (self),
                                                  g_object_unref);
 }
 
 static void
-ide_layout_grid_update_actions (IdeLayoutGrid *self)
+ide_grid_update_actions (IdeGrid *self)
 {
   guint n_children;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
 
@@ -207,45 +206,45 @@ ide_layout_grid_update_actions (IdeLayoutGrid *self)
     {
       GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
 
-      g_assert (IDE_IS_LAYOUT_GRID_COLUMN (column));
+      g_assert (IDE_IS_GRID_COLUMN (column));
 
-      _ide_layout_grid_column_update_actions (IDE_LAYOUT_GRID_COLUMN (column));
+      _ide_grid_column_update_actions (IDE_GRID_COLUMN (column));
     }
 }
 
-static IdeLayoutStack *
-ide_layout_grid_real_create_stack (IdeLayoutGrid *self)
+static IdeFrame *
+ide_grid_real_create_frame (IdeGrid *self)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_STACK,
+  return g_object_new (IDE_TYPE_FRAME,
                        "expand", TRUE,
                        "visible", TRUE,
                        NULL);
 }
 
 static GtkWidget *
-ide_layout_grid_create_stack (IdeLayoutGrid *self)
+ide_grid_create_frame (IdeGrid *self)
 {
-  IdeLayoutStack *ret = NULL;
+  IdeFrame *ret = NULL;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   g_signal_emit (self, signals [CREATE_STACK], 0, &ret);
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (ret), NULL);
+  g_return_val_if_fail (IDE_IS_FRAME (ret), NULL);
   return GTK_WIDGET (ret);
 }
 
 static GtkWidget *
-ide_layout_grid_create_column (IdeLayoutGrid *self)
+ide_grid_create_column (IdeGrid *self)
 {
   GtkWidget *stack;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
-  stack = ide_layout_grid_create_stack (self);
+  stack = ide_grid_create_frame (self);
 
   if (stack != NULL)
     {
-      GtkWidget *column = g_object_new (IDE_TYPE_LAYOUT_GRID_COLUMN,
+      GtkWidget *column = g_object_new (IDE_TYPE_GRID_COLUMN,
                                         "visible", TRUE,
                                         NULL);
       gtk_container_add (GTK_CONTAINER (column), stack);
@@ -256,61 +255,61 @@ ide_layout_grid_create_column (IdeLayoutGrid *self)
 }
 
 static void
-ide_layout_grid_after_set_focus (IdeLayoutGrid *self,
+ide_grid_after_set_focus (IdeGrid *self,
                                  GtkWidget     *widget,
                                  GtkWidget     *toplevel)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (!widget || GTK_IS_WIDGET (widget));
   g_assert (GTK_IS_WINDOW (toplevel));
 
   if (widget != NULL)
     {
       GtkWidget *column = NULL;
-      GtkWidget *view;
+      GtkWidget *page;
 
       if (gtk_widget_is_ancestor (widget, GTK_WIDGET (self)))
         {
-          column = gtk_widget_get_ancestor (widget, IDE_TYPE_LAYOUT_GRID_COLUMN);
+          column = gtk_widget_get_ancestor (widget, IDE_TYPE_GRID_COLUMN);
 
           if (column != NULL)
-            ide_layout_grid_set_current_column (self, IDE_LAYOUT_GRID_COLUMN (column));
+            ide_grid_set_current_column (self, IDE_GRID_COLUMN (column));
         }
 
       /*
-       * self->_last_focused_view is an unowned reference, we only
+       * self->_last_focused_page is an unowned reference, we only
        * use it for pointer comparison, nothing more.
        */
-      view = gtk_widget_get_ancestor (widget, IDE_TYPE_LAYOUT_VIEW);
-      if (view != (GtkWidget *)priv->_last_focused_view)
+      page = gtk_widget_get_ancestor (widget, IDE_TYPE_PAGE);
+      if (page != (GtkWidget *)priv->_last_focused_page)
         {
-          priv->_last_focused_view = (IdeLayoutView *)view;
-          ide_object_notify_in_main (self, properties [PROP_CURRENT_VIEW]);
+          priv->_last_focused_page = (IdePage *)page;
+          ide_object_notify_in_main (self, properties [PROP_CURRENT_PAGE]);
 
-          if (view != NULL && column != NULL)
+          if (page != NULL && column != NULL)
             {
               GtkWidget *stack;
 
-              stack = gtk_widget_get_ancestor (GTK_WIDGET (view), IDE_TYPE_LAYOUT_STACK);
+              stack = gtk_widget_get_ancestor (GTK_WIDGET (page), IDE_TYPE_FRAME);
               if (stack != NULL)
-                ide_layout_grid_column_set_current_stack (IDE_LAYOUT_GRID_COLUMN (column),
-                                                          IDE_LAYOUT_STACK (stack));
+                ide_grid_column_set_current_stack (IDE_GRID_COLUMN (column),
+                                                          IDE_FRAME (stack));
             }
         }
     }
 }
 
 static void
-ide_layout_grid_hierarchy_changed (GtkWidget *widget,
+ide_grid_hierarchy_changed (GtkWidget *widget,
                                    GtkWidget *old_toplevel)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   GtkWidget *toplevel;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
 
   /*
@@ -334,54 +333,54 @@ ide_layout_grid_hierarchy_changed (GtkWidget *widget,
 
   if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (widget)) == 0)
     {
-      GtkWidget *column = ide_layout_grid_create_column (self);
+      GtkWidget *column = ide_grid_create_column (self);
       gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (column));
     }
 }
 
 static void
-ide_layout_grid_add (GtkContainer *container,
+ide_grid_add (GtkContainer *container,
                      GtkWidget    *widget)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)container;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)container;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (GTK_IS_WIDGET (widget));
 
-  if (IDE_IS_LAYOUT_GRID_COLUMN (widget))
+  if (IDE_IS_GRID_COLUMN (widget))
     {
       GList *children;
 
       /* Add our column to the grid */
       g_queue_push_head (&priv->focus_column, widget);
-      GTK_CONTAINER_CLASS (ide_layout_grid_parent_class)->add (container, widget);
-      ide_layout_grid_set_current_column (self, IDE_LAYOUT_GRID_COLUMN (widget));
-      _ide_layout_grid_column_update_actions (IDE_LAYOUT_GRID_COLUMN (widget));
+      GTK_CONTAINER_CLASS (ide_grid_parent_class)->add (container, widget);
+      ide_grid_set_current_column (self, IDE_GRID_COLUMN (widget));
+      _ide_grid_column_update_actions (IDE_GRID_COLUMN (widget));
 
-      /* Start monitoring all the stacks in the grid for views */
+      /* Start monitoring all the stacks in the grid for pages */
       children = gtk_container_get_children (GTK_CONTAINER (widget));
       for (const GList *iter = children; iter; iter = iter->next)
-        if (IDE_IS_LAYOUT_STACK (iter->data))
-          _ide_layout_grid_stack_added (self, iter->data);
+        if (IDE_IS_FRAME (iter->data))
+          _ide_grid_stack_added (self, iter->data);
       g_list_free (children);
     }
-  else if (IDE_IS_LAYOUT_STACK (widget))
+  else if (IDE_IS_FRAME (widget))
     {
-      IdeLayoutGridColumn *column;
+      IdeGridColumn *column;
 
-      column = ide_layout_grid_get_current_column (self);
+      column = ide_grid_get_current_column (self);
       gtk_container_add (GTK_CONTAINER (column), widget);
-      ide_layout_grid_set_current_column (self, column);
+      ide_grid_set_current_column (self, column);
     }
-  else if (IDE_IS_LAYOUT_VIEW (widget))
+  else if (IDE_IS_PAGE (widget))
     {
-      IdeLayoutGridColumn *column = NULL;
+      IdeGridColumn *column = NULL;
       guint n_columns;
 
       /* If we have an empty layout stack, we'll prefer to add the
-       * view to that. If we don't find an empty stack, we'll add
-       * the view to the most recently focused stack.
+       * page to that. If we don't find an empty stack, we'll add
+       * the page to the most recently focused stack.
        */
 
       n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
@@ -390,49 +389,49 @@ ide_layout_grid_add (GtkContainer *container,
         {
           GtkWidget *ele = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
 
-          g_assert (IDE_IS_LAYOUT_GRID_COLUMN (ele));
+          g_assert (IDE_IS_GRID_COLUMN (ele));
 
-          if (_ide_layout_grid_column_is_empty (IDE_LAYOUT_GRID_COLUMN (ele)))
+          if (_ide_grid_column_is_empty (IDE_GRID_COLUMN (ele)))
             {
-              column = IDE_LAYOUT_GRID_COLUMN (ele);
+              column = IDE_GRID_COLUMN (ele);
               break;
             }
         }
 
       if (column == NULL)
-        column = ide_layout_grid_get_current_column (self);
+        column = ide_grid_get_current_column (self);
 
-      g_assert (IDE_IS_LAYOUT_GRID_COLUMN (column));
+      g_assert (IDE_IS_GRID_COLUMN (column));
 
       gtk_container_add (GTK_CONTAINER (column), widget);
     }
   else
     {
-      g_warning ("%s must be one of IdeLayoutStack, IdeLayoutView, or IdeLayoutGrid",
+      g_warning ("%s must be one of IdeFrame, IdePage, or IdeGrid",
                  G_OBJECT_TYPE_NAME (self));
       return;
     }
 
-  ide_layout_grid_update_actions (self);
+  ide_grid_update_actions (self);
 }
 
 static void
-ide_layout_grid_remove (GtkContainer *container,
+ide_grid_remove (GtkContainer *container,
                         GtkWidget    *widget)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)container;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)container;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   gboolean notify = FALSE;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (widget));
+  g_assert (IDE_IS_GRID (self));
+  g_assert (IDE_IS_GRID_COLUMN (widget));
 
   notify = g_queue_peek_head (&priv->focus_column) == (gpointer)widget;
   g_queue_remove (&priv->focus_column, widget);
 
-  GTK_CONTAINER_CLASS (ide_layout_grid_parent_class)->remove (container, widget);
+  GTK_CONTAINER_CLASS (ide_grid_parent_class)->remove (container, widget);
 
-  ide_layout_grid_update_actions (self);
+  ide_grid_update_actions (self);
 
   if (notify)
     {
@@ -446,19 +445,19 @@ ide_layout_grid_remove (GtkContainer *container,
 }
 
 static gboolean
-ide_layout_grid_get_drop_area (IdeLayoutGrid        *self,
+ide_grid_get_drop_area (IdeGrid        *self,
                                gint                  x,
                                gint                  y,
                                GdkRectangle         *out_area,
-                               IdeLayoutGridColumn **out_column,
-                               IdeLayoutStack      **out_stack,
+                               IdeGridColumn **out_column,
+                               IdeFrame      **out_stack,
                                gint                 *out_drop)
 {
   GtkAllocation alloc;
   GtkWidget *column;
   GtkWidget *stack = NULL;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (out_area != NULL);
   g_assert (out_column != NULL);
   g_assert (out_stack != NULL);
@@ -482,8 +481,8 @@ ide_layout_grid_get_drop_area (IdeLayoutGrid        *self,
                                         &stack_alloc.x, &stack_alloc.y);
 
       *out_area = stack_alloc;
-      *out_column = IDE_LAYOUT_GRID_COLUMN (column);
-      *out_stack = IDE_LAYOUT_STACK (stack);
+      *out_column = IDE_GRID_COLUMN (column);
+      *out_stack = IDE_FRAME (stack);
       *out_drop = DROP_ONTO;
 
       gtk_widget_translate_coordinates (GTK_WIDGET (self), stack, x, y, &x, &y);
@@ -523,22 +522,22 @@ ide_layout_grid_get_drop_area (IdeLayoutGrid        *self,
 }
 
 static gboolean
-ide_layout_grid_drag_motion (GtkWidget      *widget,
+ide_grid_drag_motion (GtkWidget      *widget,
                              GdkDragContext *context,
                              gint            x,
                              gint            y,
                              guint           time_)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
-  IdeLayoutGridColumn *column = NULL;
-  IdeLayoutStack *stack = NULL;
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  IdeGridColumn *column = NULL;
+  IdeFrame *stack = NULL;
   DzlAnimation *drag_anim;
   GdkRectangle area = {0};
   GtkAllocation alloc;
   gint drop = DROP_ONTO;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (GDK_IS_DRAG_CONTEXT (context));
 
   if (priv->drag_anim != NULL)
@@ -549,7 +548,7 @@ ide_layout_grid_drag_motion (GtkWidget      *widget,
 
   gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
 
-  if (!ide_layout_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
+  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
     return GDK_EVENT_PROPAGATE;
 
   if (priv->drag_theatric == NULL)
@@ -583,7 +582,7 @@ ide_layout_grid_drag_motion (GtkWidget      *widget,
 }
 
 static void
-ide_layout_grid_drag_data_received (GtkWidget        *widget,
+ide_grid_drag_data_received (GtkWidget        *widget,
                                     GdkDragContext   *context,
                                     gint              x,
                                     gint              y,
@@ -591,21 +590,21 @@ ide_layout_grid_drag_data_received (GtkWidget        *widget,
                                     guint             info,
                                     guint             time_)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridColumn *column = NULL;
-  IdeLayoutStack *stack = NULL;
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridColumn *column = NULL;
+  IdeFrame *stack = NULL;
   g_auto(GStrv) uris = NULL;
   GdkRectangle area = {0};
   gint drop = DROP_ONTO;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (GDK_IS_DRAG_CONTEXT (context));
 
-  if (!ide_layout_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
+  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
     return;
 
-  g_assert (IDE_IS_LAYOUT_GRID_COLUMN (column));
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_GRID_COLUMN (column));
+  g_assert (IDE_IS_FRAME (stack));
 
   if (!(uris = gtk_selection_data_get_uris (data)))
     return;
@@ -613,15 +612,15 @@ ide_layout_grid_drag_data_received (GtkWidget        *widget,
   for (guint i = 0; uris[i] != NULL; i++)
     {
       const gchar *uri = uris[i];
-      IdeLayoutView *view = NULL;
+      IdePage *page = NULL;
       gint column_index = 0;
       gint stack_index = 0;
 
-      g_signal_emit (self, signals [CREATE_VIEW], 0, uri, &view);
+      g_signal_emit (self, signals [CREATE_VIEW], 0, uri, &page);
 
-      if (view == NULL)
+      if (page == NULL)
         {
-          g_debug ("Failed to load IdeLayoutView for \"%s\"", uri);
+          g_debug ("Failed to load IdePage for \"%s\"", uri);
           continue;
         }
 
@@ -635,39 +634,39 @@ ide_layout_grid_drag_data_received (GtkWidget        *widget,
       switch (drop)
         {
         case DROP_ONTO:
-          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (view));
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
           break;
 
         case DROP_ABOVE:
-          stack = IDE_LAYOUT_STACK (ide_layout_grid_create_stack (self));
+          stack = IDE_FRAME (ide_grid_create_frame (self));
           gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack),
                                              "index", stack_index,
                                              NULL);
-          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (view));
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
           break;
 
         case DROP_BELOW:
-          stack = IDE_LAYOUT_STACK (ide_layout_grid_create_stack (self));
+          stack = IDE_FRAME (ide_grid_create_frame (self));
           gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack),
                                              "index", stack_index + 1,
                                              NULL);
-          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (view));
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
           break;
 
         case DROP_LEFT_OF:
-          column = IDE_LAYOUT_GRID_COLUMN (ide_layout_grid_create_column (self));
+          column = IDE_GRID_COLUMN (ide_grid_create_column (self));
           gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column),
                                              "index", column_index,
                                              NULL);
-          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (view));
+          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page));
           break;
 
         case DROP_RIGHT_OF:
-          column = IDE_LAYOUT_GRID_COLUMN (ide_layout_grid_create_column (self));
+          column = IDE_GRID_COLUMN (ide_grid_create_column (self));
           gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column),
                                              "index", column_index + 1,
                                              NULL);
-          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (view));
+          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page));
           break;
 
         default:
@@ -677,14 +676,14 @@ ide_layout_grid_drag_data_received (GtkWidget        *widget,
 }
 
 static void
-ide_layout_grid_drag_leave (GtkWidget      *widget,
+ide_grid_drag_leave (GtkWidget      *widget,
                             GdkDragContext *context,
                             guint           time_)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (GDK_IS_DRAG_CONTEXT (context));
 
   if (priv->drag_anim != NULL)
@@ -698,14 +697,14 @@ ide_layout_grid_drag_leave (GtkWidget      *widget,
 }
 
 static gboolean
-ide_layout_grid_drag_failed (GtkWidget      *widget,
+ide_grid_drag_failed (GtkWidget      *widget,
                              GdkDragContext *context,
                              GtkDragResult   result)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (GDK_IS_DRAG_CONTEXT (context));
 
   if (priv->drag_anim != NULL)
@@ -721,39 +720,39 @@ ide_layout_grid_drag_failed (GtkWidget      *widget,
 }
 
 static void
-ide_layout_grid_grab_focus (GtkWidget *widget)
+ide_grid_grab_focus (GtkWidget *widget)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutStack *stack;
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeFrame *stack;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
-  stack = ide_layout_grid_get_current_stack (self);
+  stack = ide_grid_get_current_stack (self);
 
   if (stack != NULL)
     gtk_widget_grab_focus (GTK_WIDGET (stack));
   else
-    GTK_WIDGET_CLASS (ide_layout_grid_parent_class)->grab_focus (widget);
+    GTK_WIDGET_CLASS (ide_grid_parent_class)->grab_focus (widget);
 }
 
 static void
-ide_layout_grid_destroy (GtkWidget *widget)
+ide_grid_destroy (GtkWidget *widget)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
   dzl_clear_source (&priv->cull_source);
 
-  GTK_WIDGET_CLASS (ide_layout_grid_parent_class)->destroy (widget);
+  GTK_WIDGET_CLASS (ide_grid_parent_class)->destroy (widget);
 }
 
 static void
-ide_layout_grid_finalize (GObject *object)
+ide_grid_finalize (GObject *object)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)object;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)object;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
   g_assert (priv->focus_column.head == NULL);
   g_assert (priv->focus_column.tail == NULL);
   g_assert (priv->focus_column.length == 0);
@@ -761,25 +760,25 @@ ide_layout_grid_finalize (GObject *object)
   g_clear_pointer (&priv->stack_info, g_array_unref);
   g_clear_object (&priv->toplevel_signals);
 
-  G_OBJECT_CLASS (ide_layout_grid_parent_class)->finalize (object);
+  G_OBJECT_CLASS (ide_grid_parent_class)->finalize (object);
 }
 
 static void
-ide_layout_grid_get_property (GObject    *object,
-                              guint       prop_id,
-                              GValue     *value,
-                              GParamSpec *pspec)
+ide_grid_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
 {
-  IdeLayoutGrid *self = IDE_LAYOUT_GRID (object);
+  IdeGrid *self = IDE_GRID (object);
 
   switch (prop_id)
     {
     case PROP_CURRENT_COLUMN:
-      g_value_set_object (value, ide_layout_grid_get_current_column (self));
+      g_value_set_object (value, ide_grid_get_current_column (self));
       break;
 
     case PROP_CURRENT_STACK:
-      g_value_set_object (value, ide_layout_grid_get_current_stack (self));
+      g_value_set_object (value, ide_grid_get_current_stack (self));
       break;
 
     default:
@@ -788,17 +787,17 @@ ide_layout_grid_get_property (GObject    *object,
 }
 
 static void
-ide_layout_grid_set_property (GObject      *object,
-                              guint         prop_id,
-                              const GValue *value,
-                              GParamSpec   *pspec)
+ide_grid_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
 {
-  IdeLayoutGrid *self = IDE_LAYOUT_GRID (object);
+  IdeGrid *self = IDE_GRID (object);
 
   switch (prop_id)
     {
     case PROP_CURRENT_COLUMN:
-      ide_layout_grid_set_current_column (self, g_value_get_object (value));
+      ide_grid_set_current_column (self, g_value_get_object (value));
       break;
 
     default:
@@ -807,61 +806,61 @@ ide_layout_grid_set_property (GObject      *object,
 }
 
 static void
-ide_layout_grid_class_init (IdeLayoutGridClass *klass)
+ide_grid_class_init (IdeGridClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
 
-  object_class->finalize = ide_layout_grid_finalize;
-  object_class->get_property = ide_layout_grid_get_property;
-  object_class->set_property = ide_layout_grid_set_property;
+  object_class->finalize = ide_grid_finalize;
+  object_class->get_property = ide_grid_get_property;
+  object_class->set_property = ide_grid_set_property;
 
-  widget_class->destroy = ide_layout_grid_destroy;
-  widget_class->drag_data_received = ide_layout_grid_drag_data_received;
-  widget_class->drag_motion = ide_layout_grid_drag_motion;
-  widget_class->drag_leave = ide_layout_grid_drag_leave;
-  widget_class->drag_failed = ide_layout_grid_drag_failed;
-  widget_class->grab_focus = ide_layout_grid_grab_focus;
-  widget_class->hierarchy_changed = ide_layout_grid_hierarchy_changed;
+  widget_class->destroy = ide_grid_destroy;
+  widget_class->drag_data_received = ide_grid_drag_data_received;
+  widget_class->drag_motion = ide_grid_drag_motion;
+  widget_class->drag_leave = ide_grid_drag_leave;
+  widget_class->drag_failed = ide_grid_drag_failed;
+  widget_class->grab_focus = ide_grid_grab_focus;
+  widget_class->hierarchy_changed = ide_grid_hierarchy_changed;
 
-  container_class->add = ide_layout_grid_add;
-  container_class->remove = ide_layout_grid_remove;
+  container_class->add = ide_grid_add;
+  container_class->remove = ide_grid_remove;
 
-  klass->create_stack = ide_layout_grid_real_create_stack;
+  klass->create_frame = ide_grid_real_create_frame;
 
   properties [PROP_CURRENT_COLUMN] =
     g_param_spec_object ("current-column",
                          "Current Column",
                          "The most recently focused grid column",
-                         IDE_TYPE_LAYOUT_GRID_COLUMN,
+                         IDE_TYPE_GRID_COLUMN,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_CURRENT_STACK] =
     g_param_spec_object ("current-stack",
                          "Current Stack",
-                         "The most recently focused IdeLayoutStack",
-                         IDE_TYPE_LAYOUT_STACK,
+                         "The most recently focused IdeFrame",
+                         IDE_TYPE_FRAME,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  properties [PROP_CURRENT_VIEW] =
-    g_param_spec_object ("current-view",
+  properties [PROP_CURRENT_PAGE] =
+    g_param_spec_object ("current-page",
                          "Current View",
-                         "The most recently focused IdeLayoutView",
-                         IDE_TYPE_LAYOUT_VIEW,
+                         "The most recently focused IdePage",
+                         IDE_TYPE_PAGE,
                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_css_name (widget_class, "idelayoutgrid");
+  gtk_widget_class_set_css_name (widget_class, "idegrid");
 
   /**
-   * IdeLayoutGrid::create-stack:
-   * @self: an #IdeLayoutGrid
+   * IdeGrid::create-stack:
+   * @self: an #IdeGrid
    *
    * Creates a new stack to be added to the grid.
    *
-   * Returns: (transfer full): A newly created #IdeLayoutStack
+   * Returns: (transfer full): A newly created #IdeFrame
    *
    * Since: 3.32
    */
@@ -869,40 +868,43 @@ ide_layout_grid_class_init (IdeLayoutGridClass *klass)
     g_signal_new (g_intern_static_string ("create-stack"),
                   G_TYPE_FROM_CLASS (klass),
                   G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (IdeLayoutGridClass, create_stack),
+                  G_STRUCT_OFFSET (IdeGridClass, create_frame),
                   g_signal_accumulator_first_wins, NULL, NULL,
-                  IDE_TYPE_LAYOUT_STACK, 0);
+                  IDE_TYPE_FRAME, 0);
 
   /**
-   * IdeLayoutGrid::create-view:
-   * @self: an #IdeLayoutGrid
+   * IdeGrid::create-page:
+   * @self: an #IdeGrid
    * @uri: the URI to open
    *
-   * Creates a new view for @uri to be added to the grid.
+   * Creates a new page for @uri to be added to the grid.
    *
-   * Returns: (transfer full): A newly created #IdeLayoutView
+   * Returns: (transfer full): A newly created #IdePage
    *
    * Since: 3.32
    */
   signals [CREATE_VIEW] =
-    g_signal_new (g_intern_static_string ("create-view"),
+    g_signal_new (g_intern_static_string ("create-page"),
                   G_TYPE_FROM_CLASS (klass),
                   G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (IdeLayoutGridClass, create_view),
+                  G_STRUCT_OFFSET (IdeGridClass, create_page),
                   g_signal_accumulator_first_wins, NULL, NULL,
-                  IDE_TYPE_LAYOUT_VIEW,
+                  IDE_TYPE_PAGE,
                   1,
                   G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
 }
 
 static void
-ide_layout_grid_init (IdeLayoutGrid *self)
+ide_grid_init (IdeGrid *self)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   static const GtkTargetEntry target_entries[] = {
     { (gchar *)"text/uri-list", 0, 0 },
   };
 
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self),
+                                  GTK_ORIENTATION_HORIZONTAL);
+
   gtk_drag_dest_set (GTK_WIDGET (self),
                      GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
                      target_entries,
@@ -915,56 +917,56 @@ ide_layout_grid_init (IdeLayoutGrid *self)
 
   dzl_signal_group_connect_object (priv->toplevel_signals,
                                    "set-focus",
-                                   G_CALLBACK (ide_layout_grid_after_set_focus),
+                                   G_CALLBACK (ide_grid_after_set_focus),
                                    self,
                                    G_CONNECT_SWAPPED | G_CONNECT_AFTER);
 
-  _ide_layout_grid_init_actions (self);
+  _ide_grid_init_actions (self);
 }
 
 /**
- * ide_layout_grid_new:
+ * ide_grid_new:
  *
- * Creates a new #IdeLayoutGrid.
+ * Creates a new #IdeGrid.
  *
- * Returns: (transfer full): A newly created #IdeLayoutGrid
+ * Returns: (transfer full): A newly created #IdeGrid
  *
  * Since: 3.32
  */
 GtkWidget *
-ide_layout_grid_new (void)
+ide_grid_new (void)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_GRID, NULL);
+  return g_object_new (IDE_TYPE_GRID, NULL);
 }
 
 /**
- * ide_layout_grid_get_current_stack:
- * @self: a #IdeLayoutGrid
+ * ide_grid_get_current_stack:
+ * @self: a #IdeGrid
  *
  * Gets the most recently focused stack. This is useful when you want to open
  * a document on the stack the user last focused.
  *
- * Returns: (transfer none) (nullable): an #IdeLayoutStack or %NULL.
+ * Returns: (transfer none) (nullable): an #IdeFrame or %NULL.
  *
  * Since: 3.32
  */
-IdeLayoutStack *
-ide_layout_grid_get_current_stack (IdeLayoutGrid *self)
+IdeFrame *
+ide_grid_get_current_stack (IdeGrid *self)
 {
-  IdeLayoutGridColumn *column;
+  IdeGridColumn *column;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
 
-  column = ide_layout_grid_get_current_column (self);
+  column = ide_grid_get_current_column (self);
   if (column != NULL)
-    return ide_layout_grid_column_get_current_stack (column);
+    return ide_grid_column_get_current_stack (column);
 
   return NULL;
 }
 
 /**
- * ide_layout_grid_get_nth_column:
- * @self: a #IdeLayoutGrid
+ * ide_grid_get_nth_column:
+ * @self: a #IdeGrid
  * @nth: the index of the column, or -1
  *
  * Gets the @nth column from the grid.
@@ -975,28 +977,28 @@ ide_layout_grid_get_current_stack (IdeLayoutGrid *self)
  * If @nth is >= the number of columns in the grid, then a new
  * column at the end of the grid is created.
  *
- * Returns: (transfer none): An #IdeLayoutGridColumn.
+ * Returns: (transfer none): An #IdeGridColumn.
  *
  * Since: 3.32
  */
-IdeLayoutGridColumn *
-ide_layout_grid_get_nth_column (IdeLayoutGrid *self,
+IdeGridColumn *
+ide_grid_get_nth_column (IdeGrid *self,
                                 gint           nth)
 {
   GtkWidget *column;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
 
   if (nth < 0)
     {
-      column = ide_layout_grid_create_column (self);
+      column = ide_grid_create_column (self);
       gtk_container_add_with_properties (GTK_CONTAINER (self), column,
                                          "index", 0,
                                          NULL);
     }
   else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)))
     {
-      column = ide_layout_grid_create_column (self);
+      column = ide_grid_create_column (self);
       gtk_container_add (GTK_CONTAINER (self), column);
     }
   else
@@ -1004,13 +1006,13 @@ ide_layout_grid_get_nth_column (IdeLayoutGrid *self,
       column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), nth);
     }
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (column), NULL);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL);
 
-  return IDE_LAYOUT_GRID_COLUMN (column);
+  return IDE_GRID_COLUMN (column);
 }
 
 /*
- * _ide_layout_grid_get_nth_stack:
+ * _ide_grid_get_nth_stack:
  *
  * This will get the @nth stack. If it does not yet exist,
  * it will be created.
@@ -1020,29 +1022,29 @@ ide_layout_grid_get_nth_column (IdeLayoutGrid *self,
  * If nth >= the number of stacks, a new stack will be created
  * at the end of the grid.
  *
- * Returns: (not nullable) (transfer none): An #IdeLayoutStack.
+ * Returns: (not nullable) (transfer none): An #IdeFrame.
  */
-IdeLayoutStack *
-_ide_layout_grid_get_nth_stack (IdeLayoutGrid *self,
+IdeFrame *
+_ide_grid_get_nth_stack (IdeGrid *self,
                                 gint           nth)
 {
-  IdeLayoutGridColumn *column;
-  IdeLayoutStack *stack;
+  IdeGridColumn *column;
+  IdeFrame *stack;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
 
-  column = ide_layout_grid_get_nth_column (self, nth);
-  stack = ide_layout_grid_column_get_current_stack (IDE_LAYOUT_GRID_COLUMN (column));
+  column = ide_grid_get_nth_column (self, nth);
+  stack = ide_grid_column_get_current_stack (IDE_GRID_COLUMN (column));
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (stack), NULL);
+  g_return_val_if_fail (IDE_IS_FRAME (stack), NULL);
 
   return stack;
 }
 
 /**
- * _ide_layout_grid_get_nth_stack_for_column:
- * @self: an #IdeLayoutGrid
- * @column: an #IdeLayoutGridColumn
+ * _ide_grid_get_nth_stack_for_column:
+ * @self: an #IdeGrid
+ * @column: an #IdeGridColumn
  * @nth: the index of the column, between -1 and G_MAXINT
  *
  * This will get the @nth stack within @column. If a matching stack
@@ -1053,31 +1055,31 @@ _ide_layout_grid_get_nth_stack (IdeLayoutGrid *self,
  * If @nth is greater-than the number of stacks, then a new stack
  * will be created at the bottom.
  *
- * Returns: (not nullable) (transfer none): An #IdeLayoutStack.
+ * Returns: (not nullable) (transfer none): An #IdeFrame.
  *
  * Since: 3.32
  */
-IdeLayoutStack *
-_ide_layout_grid_get_nth_stack_for_column (IdeLayoutGrid       *self,
-                                           IdeLayoutGridColumn *column,
+IdeFrame *
+_ide_grid_get_nth_stack_for_column (IdeGrid       *self,
+                                           IdeGridColumn *column,
                                            gint                 nth)
 {
   GtkWidget *stack;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (column), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL);
   g_return_val_if_fail (gtk_widget_get_parent (GTK_WIDGET (column)) == GTK_WIDGET (self), NULL);
 
   if (nth < 0)
     {
-      stack = ide_layout_grid_create_stack (self);
+      stack = ide_grid_create_frame (self);
       gtk_container_add_with_properties (GTK_CONTAINER (column), stack,
                                          "index", 0,
                                          NULL);
     }
   else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)))
     {
-      stack = ide_layout_grid_create_stack (self);
+      stack = ide_grid_create_frame (self);
       gtk_container_add (GTK_CONTAINER (self), stack);
     }
   else
@@ -1085,28 +1087,28 @@ _ide_layout_grid_get_nth_stack_for_column (IdeLayoutGrid       *self,
       stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), nth);
     }
 
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_FRAME (stack));
 
-  return IDE_LAYOUT_STACK (stack);
+  return IDE_FRAME (stack);
 }
 
 /**
- * ide_layout_grid_get_current_column:
- * @self: a #IdeLayoutGrid
+ * ide_grid_get_current_column:
+ * @self: a #IdeGrid
  *
  * Gets the most recently focused column of the grid.
  *
- * Returns: (transfer none) (not nullable): An #IdeLayoutGridColumn
+ * Returns: (transfer none) (not nullable): An #IdeGridColumn
  *
  * Since: 3.32
  */
-IdeLayoutGridColumn *
-ide_layout_grid_get_current_column (IdeLayoutGrid *self)
+IdeGridColumn *
+ide_grid_get_current_column (IdeGrid *self)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   GtkWidget *ret = NULL;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
 
   if (priv->focus_column.head != NULL)
     ret = priv->focus_column.head->data;
@@ -1115,19 +1117,19 @@ ide_layout_grid_get_current_column (IdeLayoutGrid *self)
 
   if (ret == NULL)
     {
-      ret = ide_layout_grid_create_column (self);
+      ret = ide_grid_create_column (self);
       gtk_container_add (GTK_CONTAINER (self), ret);
     }
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID_COLUMN (ret), NULL);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (ret), NULL);
 
-  return IDE_LAYOUT_GRID_COLUMN (ret);
+  return IDE_GRID_COLUMN (ret);
 }
 
 /**
- * ide_layout_grid_set_current_column:
- * @self: an #IdeLayoutGrid
- * @column: (nullable): an #IdeLayoutGridColumn or %NULL
+ * ide_grid_set_current_column:
+ * @self: an #IdeGrid
+ * @column: (nullable): an #IdeGridColumn or %NULL
  *
  * Sets the current column for the grid. Generally this is automatically
  * updated for you when the focus changes within the workbench.
@@ -1137,14 +1139,14 @@ ide_layout_grid_get_current_column (IdeLayoutGrid *self)
  * Since: 3.32
  */
 void
-ide_layout_grid_set_current_column (IdeLayoutGrid       *self,
-                                    IdeLayoutGridColumn *column)
+ide_grid_set_current_column (IdeGrid       *self,
+                                    IdeGridColumn *column)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   GList *iter;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
-  g_return_if_fail (!column || IDE_IS_LAYOUT_GRID_COLUMN (column));
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (!column || IDE_IS_GRID_COLUMN (column));
 
   if (column == NULL)
     return;
@@ -1160,7 +1162,7 @@ ide_layout_grid_set_current_column (IdeLayoutGrid       *self,
       g_queue_unlink (&priv->focus_column, iter);
       g_queue_push_head_link (&priv->focus_column, iter);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]);
-      ide_layout_grid_update_actions (self);
+      ide_grid_update_actions (self);
       return;
     }
 
@@ -1169,61 +1171,61 @@ ide_layout_grid_set_current_column (IdeLayoutGrid       *self,
 }
 
 /**
- * ide_layout_grid_get_current_view:
- * @self: a #IdeLayoutGrid
+ * ide_grid_get_current_page:
+ * @self: a #IdeGrid
  *
- * Gets the most recent view used by the user as determined by tracking
+ * Gets the most recent page used by the user as determined by tracking
  * the window focus.
  *
- * Returns: (transfer none): An #IdeLayoutView or %NULL
+ * Returns: (transfer none): An #IdePage or %NULL
  *
  * Since: 3.32
  */
-IdeLayoutView *
-ide_layout_grid_get_current_view (IdeLayoutGrid *self)
+IdePage *
+ide_grid_get_current_page (IdeGrid *self)
 {
-  IdeLayoutStack *stack;
+  IdeFrame *stack;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
 
-  stack = ide_layout_grid_get_current_stack (self);
+  stack = ide_grid_get_current_stack (self);
 
   if (stack != NULL)
-    return ide_layout_stack_get_visible_child (stack);
+    return ide_frame_get_visible_child (stack);
 
   return NULL;
 }
 
 static void
-collect_views (GtkWidget *widget,
+collect_pages (GtkWidget *widget,
                GPtrArray *ar)
 {
-  if (IDE_IS_LAYOUT_VIEW (widget))
+  if (IDE_IS_PAGE (widget))
     g_ptr_array_add (ar, widget);
 }
 
 /**
- * ide_layout_grid_foreach_view:
- * @self: a #IdeLayoutGrid
- * @callback: (scope call) (closure user_data): A callback for each view
+ * ide_grid_foreach_page:
+ * @self: a #IdeGrid
+ * @callback: (scope call) (closure user_data): A callback for each page
  * @user_data: user data for @callback
  *
- * This function will call @callback for every view found in @self.
+ * This function will call @callback for every page found in @self.
  *
  * Since: 3.32
  */
 void
-ide_layout_grid_foreach_view (IdeLayoutGrid *self,
-                              GtkCallback    callback,
-                              gpointer       user_data)
+ide_grid_foreach_page (IdeGrid     *self,
+                       GtkCallback  callback,
+                       gpointer     user_data)
 {
-  g_autoptr(GPtrArray) views = NULL;
+  g_autoptr(GPtrArray) pages = NULL;
   guint n_columns;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (IDE_IS_GRID (self));
   g_return_if_fail (callback != NULL);
 
-  views = g_ptr_array_new ();
+  pages = g_ptr_array_new ();
 
   n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
 
@@ -1232,7 +1234,7 @@ ide_layout_grid_foreach_view (IdeLayoutGrid *self,
       GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
       guint n_stacks;
 
-      g_assert (IDE_IS_LAYOUT_GRID_COLUMN (column));
+      g_assert (IDE_IS_GRID_COLUMN (column));
 
       n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
 
@@ -1240,32 +1242,32 @@ ide_layout_grid_foreach_view (IdeLayoutGrid *self,
         {
           GtkWidget *stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j);
 
-          g_assert (IDE_IS_LAYOUT_STACK (stack));
+          g_assert (IDE_IS_FRAME (stack));
 
-          ide_layout_stack_foreach_view (IDE_LAYOUT_STACK (stack),
-                                         (GtkCallback) collect_views,
-                                         views);
+          ide_frame_foreach_page (IDE_FRAME (stack),
+                                  (GtkCallback) collect_pages,
+                                  pages);
         }
     }
 
-  for (guint i = 0; i < views->len; i++)
-    callback (g_ptr_array_index (views, i), user_data);
+  for (guint i = 0; i < pages->len; i++)
+    callback (g_ptr_array_index (pages, i), user_data);
 }
 
 static GType
-ide_layout_grid_get_item_type (GListModel *model)
+ide_grid_get_item_type (GListModel *model)
 {
-  return IDE_TYPE_LAYOUT_VIEW;
+  return IDE_TYPE_PAGE;
 }
 
 static guint
-ide_layout_grid_get_n_items (GListModel *model)
+ide_grid_get_n_items (GListModel *model)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)model;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)model;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   guint n_items = 0;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (IDE_IS_GRID (self));
 
   for (guint i = 0; i < priv->stack_info->len; i++)
     n_items += g_array_index (priv->stack_info, StackInfo, i).len;
@@ -1274,14 +1276,14 @@ ide_layout_grid_get_n_items (GListModel *model)
 }
 
 static gpointer
-ide_layout_grid_get_item (GListModel *model,
+ide_grid_get_item (GListModel *model,
                           guint       position)
 {
-  IdeLayoutGrid *self = (IdeLayoutGrid *)model;
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGrid *self = (IdeGrid *)model;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
-  g_assert (position < ide_layout_grid_get_n_items (model));
+  g_assert (IDE_IS_GRID (self));
+  g_assert (position < ide_grid_get_n_items (model));
 
   for (guint i = 0; i < priv->stack_info->len; i++)
     {
@@ -1305,23 +1307,23 @@ ide_layout_grid_get_item (GListModel *model,
 static void
 list_model_iface_init (GListModelInterface *iface)
 {
-  iface->get_item_type = ide_layout_grid_get_item_type;
-  iface->get_n_items = ide_layout_grid_get_n_items;
-  iface->get_item = ide_layout_grid_get_item;
+  iface->get_item_type = ide_grid_get_item_type;
+  iface->get_n_items = ide_grid_get_n_items;
+  iface->get_item = ide_grid_get_item;
 }
 
 static void
-ide_layout_grid_stack_items_changed (IdeLayoutGrid  *self,
+ide_grid_stack_items_changed (IdeGrid  *self,
                                      guint           position,
                                      guint           removed,
                                      guint           added,
-                                     IdeLayoutStack *stack)
+                                     IdeFrame *stack)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   guint real_position = 0;
 
-  g_assert (IDE_IS_LAYOUT_GRID (self));
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_GRID (self));
+  g_assert (IDE_IS_FRAME (stack));
 
   for (guint i = 0; i < priv->stack_info->len; i++)
     {
@@ -1337,9 +1339,9 @@ ide_layout_grid_stack_items_changed (IdeLayoutGrid  *self,
                                       removed,
                                       added);
 
-          ide_object_notify_in_main (G_OBJECT (self), properties [PROP_CURRENT_VIEW]);
+          ide_object_notify_in_main (G_OBJECT (self), properties [PROP_CURRENT_PAGE]);
 
-          ide_layout_grid_queue_cull (self);
+          ide_grid_queue_cull (self);
 
           return;
         }
@@ -1352,15 +1354,15 @@ ide_layout_grid_stack_items_changed (IdeLayoutGrid  *self,
 }
 
 void
-_ide_layout_grid_stack_added (IdeLayoutGrid  *self,
-                              IdeLayoutStack *stack)
+_ide_grid_stack_added (IdeGrid  *self,
+                              IdeFrame *stack)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   StackInfo info = { 0 };
   guint n_items;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (stack));
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (IDE_IS_FRAME (stack));
   g_return_if_fail (G_IS_LIST_MODEL (stack));
 
   info.stack = stack;
@@ -1370,26 +1372,26 @@ _ide_layout_grid_stack_added (IdeLayoutGrid  *self,
 
   g_signal_connect_object (stack,
                            "items-changed",
-                           G_CALLBACK (ide_layout_grid_stack_items_changed),
+                           G_CALLBACK (ide_grid_stack_items_changed),
                            self,
                            G_CONNECT_SWAPPED);
 
   n_items = g_list_model_get_n_items (G_LIST_MODEL (stack));
-  ide_layout_grid_stack_items_changed (self, 0, 0, n_items, stack);
+  ide_grid_stack_items_changed (self, 0, 0, n_items, stack);
 }
 
 void
-_ide_layout_grid_stack_removed (IdeLayoutGrid  *self,
-                                IdeLayoutStack *stack)
+_ide_grid_stack_removed (IdeGrid  *self,
+                                IdeFrame *stack)
 {
-  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
   guint position = 0;
 
-  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
-  g_return_if_fail (IDE_IS_LAYOUT_STACK (stack));
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (IDE_IS_FRAME (stack));
 
   g_signal_handlers_disconnect_by_func (stack,
-                                        G_CALLBACK (ide_layout_grid_stack_items_changed),
+                                        G_CALLBACK (ide_grid_stack_items_changed),
                                         self);
 
   for (guint i = 0; i < priv->stack_info->len; i++)
@@ -1406,55 +1408,55 @@ _ide_layout_grid_stack_removed (IdeLayoutGrid  *self,
 }
 
 static void
-count_views_cb (GtkWidget *widget,
+count_pages_cb (GtkWidget *widget,
                 gpointer   data)
 {
   (*(guint *)data)++;
 }
 
 guint
-ide_layout_grid_count_views (IdeLayoutGrid *self)
+ide_grid_count_pages (IdeGrid *self)
 {
   guint count = 0;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), 0);
+  g_return_val_if_fail (IDE_IS_GRID (self), 0);
 
-  ide_layout_grid_foreach_view (self, count_views_cb, &count);
+  ide_grid_foreach_page (self, count_pages_cb, &count);
 
   return count;
 }
 
 /**
- * ide_layout_grid_focus_neighbor:
- * @self: An #IdeLayoutGrid
+ * ide_grid_focus_neighbor:
+ * @self: An #IdeGrid
  * @dir: the direction for the focus change
  *
- * Attempts to focus a neighbor #IdeLayoutView in the grid based on
+ * Attempts to focus a neighbor #IdePage in the grid based on
  * the direction requested.
  *
- * If an #IdeLayoutView was focused, it will be returned to the caller.
+ * If an #IdePage was focused, it will be returned to the caller.
  *
- * Returns: (transfer none) (nullable): An #IdeLayoutView or %NULL
+ * Returns: (transfer none) (nullable): An #IdePage or %NULL
  *
  * Since: 3.32
  */
-IdeLayoutView *
-ide_layout_grid_focus_neighbor (IdeLayoutGrid    *self,
+IdePage *
+ide_grid_focus_neighbor (IdeGrid    *self,
                                 GtkDirectionType  dir)
 {
-  IdeLayoutGridColumn *column;
-  IdeLayoutStack *stack;
-  IdeLayoutView *view = NULL;
+  IdeGridColumn *column;
+  IdeFrame *stack;
+  IdePage *page = NULL;
   guint stack_pos = 0;
   guint column_pos = 0;
   guint n_children;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
   g_return_val_if_fail (dir <= GTK_DIR_RIGHT, NULL);
 
-  /* Make sure we have a current view and stack */
-  if (NULL == (stack = ide_layout_grid_get_current_stack (self)) ||
-      NULL == (column = ide_layout_grid_get_current_column (self)))
+  /* Make sure we have a current page and stack */
+  if (NULL == (stack = ide_grid_get_current_stack (self)) ||
+      NULL == (column = ide_grid_get_current_column (self)))
     return NULL;
 
   gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column),
@@ -1471,52 +1473,52 @@ ide_layout_grid_focus_neighbor (IdeLayoutGrid    *self,
       n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
       if (n_children - stack_pos == 1)
         return NULL;
-      stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos + 1));
-      view = ide_layout_stack_get_visible_child (stack);
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos + 1));
+      page = ide_frame_get_visible_child (stack);
       break;
 
     case GTK_DIR_RIGHT:
       n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
       if (n_children - column_pos == 1)
         return NULL;
-      column = IDE_LAYOUT_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos + 
1));
-      stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
-      view = ide_layout_stack_get_visible_child (stack);
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos + 1));
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+      page = ide_frame_get_visible_child (stack);
       break;
 
     case GTK_DIR_UP:
       if (stack_pos == 0)
         return NULL;
-      stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos - 1));
-      view = ide_layout_stack_get_visible_child (stack);
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos - 1));
+      page = ide_frame_get_visible_child (stack);
       break;
 
     case GTK_DIR_LEFT:
       if (column_pos == 0)
         return NULL;
-      column = IDE_LAYOUT_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos - 
1));
-      stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
-      view = ide_layout_stack_get_visible_child (stack);
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos - 1));
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+      page = ide_frame_get_visible_child (stack);
       break;
 
     case GTK_DIR_TAB_FORWARD:
-      if (!ide_layout_grid_focus_neighbor (self, GTK_DIR_DOWN) &&
-          !ide_layout_grid_focus_neighbor (self, GTK_DIR_RIGHT))
+      if (!ide_grid_focus_neighbor (self, GTK_DIR_DOWN) &&
+          !ide_grid_focus_neighbor (self, GTK_DIR_RIGHT))
         {
-          column = IDE_LAYOUT_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0));
-          stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
-          view = ide_layout_stack_get_visible_child (stack);
+          column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0));
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+          page = ide_frame_get_visible_child (stack);
         }
       break;
 
     case GTK_DIR_TAB_BACKWARD:
-      if (!ide_layout_grid_focus_neighbor (self, GTK_DIR_UP) &&
-          !ide_layout_grid_focus_neighbor (self, GTK_DIR_LEFT))
+      if (!ide_grid_focus_neighbor (self, GTK_DIR_UP) &&
+          !ide_grid_focus_neighbor (self, GTK_DIR_LEFT))
         {
           n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
-          column = IDE_LAYOUT_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), n_children 
- 1));
-          stack = IDE_LAYOUT_STACK (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
-          view = ide_layout_stack_get_visible_child (stack);
+          column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), n_children - 1));
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+          page = ide_frame_get_visible_child (stack);
         }
       break;
 
@@ -1524,8 +1526,8 @@ ide_layout_grid_focus_neighbor (IdeLayoutGrid    *self,
       g_assert_not_reached ();
     }
 
-  if (view != NULL)
-    gtk_widget_child_focus (GTK_WIDGET (view), GTK_DIR_TAB_FORWARD);
+  if (page != NULL)
+    gtk_widget_child_focus (GTK_WIDGET (page), GTK_DIR_TAB_FORWARD);
 
-  return view;
+  return page;
 }
diff --git a/src/libide/gui/ide-grid.h b/src/libide/gui/ide-grid.h
new file mode 100644
index 000000000..04516e6b5
--- /dev/null
+++ b/src/libide/gui/ide-grid.h
@@ -0,0 +1,77 @@
+/* ide-grid.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-grid-column.h"
+#include "ide-frame.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GRID (ide_grid_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeGrid, ide_grid, IDE, GRID, DzlMultiPaned)
+
+struct _IdeGridClass
+{
+  DzlMultiPanedClass parent_class;
+
+  IdeFrame *(*create_frame) (IdeGrid     *self);
+  IdePage  *(*create_page)  (IdeGrid     *self,
+                             const gchar *uri);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget     *ide_grid_new                (void);
+IDE_AVAILABLE_IN_3_32
+IdeGridColumn *ide_grid_get_nth_column     (IdeGrid          *self,
+                                            gint              nth);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_grid_focus_neighbor     (IdeGrid          *self,
+                                            GtkDirectionType  dir);
+IDE_AVAILABLE_IN_3_32
+IdeGridColumn *ide_grid_get_current_column (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_grid_set_current_column (IdeGrid          *self,
+                                            IdeGridColumn    *column);
+IDE_AVAILABLE_IN_3_32
+IdeFrame      *ide_grid_get_current_stack  (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_grid_get_current_page   (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_grid_count_pages        (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_grid_foreach_page       (IdeGrid          *self,
+                                            GtkCallback       callback,
+                                            gpointer          user_data);
+
+G_END_DECLS
diff --git a/src/libide/util/ide-gtk.c b/src/libide/gui/ide-gui-global.c
similarity index 52%
rename from src/libide/util/ide-gtk.c
rename to src/libide/gui/ide-gui-global.c
index 03c42c60b..bf4063367 100644
--- a/src/libide/util/ide-gtk.c
+++ b/src/libide/gui/ide-gui-global.c
@@ -1,6 +1,6 @@
-/* ide-gtk.c
+/* ide-gui-global.c
  *
- * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,21 +18,27 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-gtk"
+#define G_LOG_DOMAIN "ide-gui-global"
 
 #include "config.h"
 
-#include <gtk/gtk.h>
+#include <dazzle.h>
+#include <libide-threading.h>
 
-#include "application/ide-application.h"
-#include "subprocess/ide-subprocess.h"
-#include "subprocess/ide-subprocess-launcher.h"
-#include "util/ide-flatpak.h"
-#include "util/ide-gtk.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workspace.h"
 
 static GQuark quark_handler;
 static GQuark quark_where_context_was;
 
+static void ide_widget_notify_context    (GtkWidget  *toplevel,
+                                          GParamSpec *pspec,
+                                          GtkWidget  *widget);
+static void ide_widget_hierarchy_changed (GtkWidget  *widget,
+                                          GtkWidget  *previous_toplevel,
+                                          gpointer    user_data);
+
 static void
 ide_widget_notify_context (GtkWidget  *toplevel,
                            GParamSpec *pspec,
@@ -58,6 +64,13 @@ ide_widget_notify_context (GtkWidget  *toplevel,
 
   g_object_set_qdata (G_OBJECT (widget), quark_where_context_was, context);
 
+  g_signal_handlers_disconnect_by_func (toplevel,
+                                        G_CALLBACK (ide_widget_notify_context),
+                                        widget);
+  g_signal_handlers_disconnect_by_func (widget,
+                                        G_CALLBACK (ide_widget_hierarchy_changed),
+                                        NULL);
+
   handler (widget, context);
 }
 
@@ -119,6 +132,7 @@ ide_widget_set_context_handler (gpointer                widget,
   /* Ensure we have our quarks for quick key lookup */
   if G_UNLIKELY (quark_handler == 0)
     quark_handler = g_quark_from_static_string ("IDE_CONTEXT_HANDLER");
+
   if G_UNLIKELY (quark_where_context_was == 0)
     quark_where_context_was = g_quark_from_static_string ("IDE_CONTEXT");
 
@@ -136,113 +150,136 @@ ide_widget_set_context_handler (gpointer                widget,
 }
 
 /**
- * ide_widget_get_workbench:
+ * ide_widget_get_context:
+ * @widget: a #GtkWidget
  *
- * Gets the workbench @widget is associated with, if any.
+ * Gets the context for the widget.
  *
- * If no workbench is associated, NULL is returned.
+ * Returns: (nullable) (transfer none): an #IdeContext, or %NULL
  *
- * Returns: (transfer none) (nullable): An #IdeWorkbench
+ * Since: 3.32
+ */
+IdeContext *
+ide_widget_get_context (GtkWidget *widget)
+{
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (IDE_IS_WORKSPACE (toplevel))
+    return ide_workspace_get_context (IDE_WORKSPACE (toplevel));
+
+  return NULL;
+}
+
+/**
+ * ide_widget_get_workbench:
+ * @widget: a #GtkWidget
+ *
+ * Gets the #IdeWorkbench that contains @widget.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL
  *
  * Since: 3.32
  */
 IdeWorkbench *
 ide_widget_get_workbench (GtkWidget *widget)
 {
-  GtkWidget *ancestor;
+  GtkWidget *toplevel;
 
   g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
 
-  if (IDE_IS_WORKBENCH (widget))
-    return IDE_WORKBENCH (widget);
+  toplevel = gtk_widget_get_toplevel (widget);
 
-  ancestor = gtk_widget_get_ancestor (widget, IDE_TYPE_WORKBENCH);
-  if (IDE_IS_WORKBENCH (ancestor))
-    return IDE_WORKBENCH (ancestor);
+  if (GTK_IS_WINDOW (toplevel))
+    {
+      GtkWindowGroup *group = gtk_window_get_group (GTK_WINDOW (toplevel));
 
-  /*
-   * TODO: Add "IDE_WORKBENCH" gdata for popout windows.
-   */
+      if (IDE_IS_WORKBENCH (group))
+        return IDE_WORKBENCH (group);
+    }
 
   return NULL;
 }
 
 /**
- * ide_widget_get_context: (skip)
+ * ide_widget_get_workspace:
+ * @widget: a #GtkWidget
  *
- * Returns: (nullable) (transfer none): An #IdeContext or %NULL.
+ * Gets the #IdeWorkspace containing @widget.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
  *
  * Since: 3.32
  */
-IdeContext *
-ide_widget_get_context (GtkWidget *widget)
+IdeWorkspace *
+ide_widget_get_workspace (GtkWidget *widget)
 {
-  IdeWorkbench *workbench;
-
   g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
 
-  workbench = ide_widget_get_workbench (widget);
+  return (IdeWorkspace *)dzl_gtk_widget_get_relative (widget, IDE_TYPE_WORKSPACE);
+}
+
+static gboolean
+ide_gtk_progress_bar_tick_cb (gpointer data)
+{
+  GtkProgressBar *progress = data;
+
+  g_assert (GTK_IS_PROGRESS_BAR (progress));
 
-  if (workbench == NULL)
-    return NULL;
+  gtk_progress_bar_pulse (progress);
+  gtk_widget_queue_draw (GTK_WIDGET (progress));
 
-  return ide_workbench_get_context (workbench);
+  return G_SOURCE_CONTINUE;
 }
 
 void
-ide_widget_message (gpointer     instance,
-                    const gchar *format,
-                    ...)
+_ide_gtk_progress_bar_stop_pulsing (GtkProgressBar *progress)
 {
-  g_autofree gchar *str = NULL;
-  IdeContext *context = NULL;
-  va_list args;
+  guint tick_id;
 
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
-  g_return_if_fail (!instance || GTK_IS_WIDGET (instance));
+  g_return_if_fail (GTK_IS_PROGRESS_BAR (progress));
 
-  va_start (args, format);
-  str = g_strdup_vprintf (format, args);
-  va_end (args);
+  tick_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (progress), "PULSE_ID"));
 
-  if (instance != NULL)
-    context = ide_widget_get_context (instance);
+  if (tick_id != 0)
+    {
+      g_source_remove (tick_id);
+      g_object_set_data (G_OBJECT (progress), "PULSE_ID", NULL);
+    }
 
-  if (context != NULL)
-    ide_context_emit_log (context, G_LOG_LEVEL_MESSAGE, str, -1);
-  else
-    g_message ("%s", str);
+  gtk_progress_bar_set_fraction (progress, 0.0);
 }
 
 void
-ide_widget_warning (gpointer     instance,
-                    const gchar *format,
-                    ...)
+_ide_gtk_progress_bar_start_pulsing (GtkProgressBar *progress)
 {
-  g_autofree gchar *str = NULL;
-  IdeContext *context = NULL;
-  va_list args;
-
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
-  g_return_if_fail (!instance || GTK_IS_WIDGET (instance));
+  guint tick_id;
 
-  va_start (args, format);
-  str = g_strdup_vprintf (format, args);
-  va_end (args);
+  g_return_if_fail (GTK_IS_PROGRESS_BAR (progress));
 
-  if (instance != NULL)
-    context = ide_widget_get_context (instance);
+  if (g_object_get_data (G_OBJECT (progress), "PULSE_ID"))
+    return;
 
-  if (context != NULL)
-    ide_context_emit_log (context, G_LOG_LEVEL_WARNING, str, -1);
-  else
-    g_warning ("%s", str);
+  gtk_progress_bar_set_fraction (progress, 0.0);
+  gtk_progress_bar_set_pulse_step (progress, .5);
+
+  /* We want lower than the frame rate, because that is all that is needed */
+  tick_id = dzl_frame_source_add_full (G_PRIORITY_DEFAULT,
+                                       2,
+                                       ide_gtk_progress_bar_tick_cb,
+                                       g_object_ref (progress),
+                                       g_object_unref);
+  g_object_set_data (G_OBJECT (progress), "PULSE_ID", GUINT_TO_POINTER (tick_id));
+  ide_gtk_progress_bar_tick_cb (progress);
 }
 
 gboolean
 ide_gtk_show_uri_on_window (GtkWindow    *window,
                             const gchar  *uri,
-                            guint32       timestamp,
+                            gint64        timestamp,
                             GError      **error)
 {
   g_return_val_if_fail (!window || GTK_IS_WINDOW (window), FALSE);
@@ -270,9 +307,52 @@ ide_gtk_show_uri_on_window (GtkWindow    *window,
     }
   else
     {
-      if (!gtk_show_uri_on_window (window, uri, gtk_get_current_event_time (), error))
+      /* XXX: Workaround for wayland timestamp issue */
+      if (!gtk_show_uri_on_window (window, uri, timestamp / 1000L, error))
         return FALSE;
     }
 
   return TRUE;
 }
+
+static void
+show_parents (GtkWidget *widget)
+{
+  GtkWidget *workspace;
+  GtkWidget *parent;
+
+  g_assert (GTK_IS_WIDGET (widget));
+
+  workspace = gtk_widget_get_ancestor (widget, IDE_TYPE_WORKSPACE);
+  parent = gtk_widget_get_parent (widget);
+
+  if (DZL_IS_DOCK_REVEALER (widget))
+    dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (widget), TRUE);
+
+  if (IDE_IS_SURFACE (widget))
+    ide_workspace_set_visible_surface (IDE_WORKSPACE (workspace), IDE_SURFACE (widget));
+
+  if (GTK_IS_STACK (parent))
+    gtk_stack_set_visible_child (GTK_STACK (parent), widget);
+
+  if (parent != NULL)
+    show_parents (parent);
+}
+
+void
+ide_widget_reveal_and_grab (GtkWidget *widget)
+{
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  show_parents (widget);
+  gtk_widget_grab_focus (widget);
+}
+
+void
+ide_gtk_window_present (GtkWindow *window)
+{
+  /* TODO: We need the last event time to do this properly. Until then,
+   * we'll just fake some timing info to workaround wayland issues.
+   */
+  gtk_window_present_with_time (window, g_get_monotonic_time () / 1000L);
+}
diff --git a/src/libide/util/ide-gtk.h b/src/libide/gui/ide-gui-global.h
similarity index 64%
rename from src/libide/util/ide-gtk.h
rename to src/libide/gui/ide-gui-global.h
index f78709b9f..399d9f769 100644
--- a/src/libide/util/ide-gtk.h
+++ b/src/libide/gui/ide-gui-global.h
@@ -1,6 +1,6 @@
-/* ide-gtk.h
+/* ide-gui-global.h
  *
- * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,15 +21,18 @@
 #pragma once
 
 #include <gtk/gtk.h>
+#include <libide-core.h>
 
-#include "ide-version-macros.h"
-
-#include "ide-context.h"
-
-#include "workbench/ide-workbench.h"
+#include "ide-workbench.h"
 
 G_BEGIN_DECLS
 
+#define ide_widget_warning(instance, format, ...)                                                   \
+  G_STMT_START {                                                                                    \
+    IdeContext *context = ide_widget_get_context (GTK_WIDGET (instance));                           \
+    ide_context_log (context, G_LOG_LEVEL_WARNING, G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__); \
+  } G_STMT_END
+
 typedef void (*IdeWidgetContextHandler) (GtkWidget  *widget,
                                          IdeContext *context);
 
@@ -39,19 +42,17 @@ void          ide_widget_set_context_handler (gpointer                 widget,
 IDE_AVAILABLE_IN_3_32
 IdeContext   *ide_widget_get_context         (GtkWidget               *widget);
 IDE_AVAILABLE_IN_3_32
-IdeWorkbench *ide_widget_get_workbench       (GtkWidget               *widget);
+void          ide_widget_reveal_and_grab     (GtkWidget               *widget);
 IDE_AVAILABLE_IN_3_32
-void          ide_widget_message             (gpointer                 instance,
-                                              const gchar             *format,
-                                              ...) G_GNUC_PRINTF (2, 3);
+IdeWorkbench *ide_widget_get_workbench       (GtkWidget               *widget);
 IDE_AVAILABLE_IN_3_32
-void          ide_widget_warning             (gpointer                 instance,
-                                              const gchar             *format,
-                                              ...) G_GNUC_PRINTF (2, 3);
+IdeWorkspace *ide_widget_get_workspace       (GtkWidget               *widget);
 IDE_AVAILABLE_IN_3_32
 gboolean      ide_gtk_show_uri_on_window     (GtkWindow               *window,
                                               const gchar             *uri,
-                                              guint32                  timestamp,
+                                              gint64                   timestamp,
                                               GError                 **error);
+IDE_AVAILABLE_IN_3_32
+void          ide_gtk_window_present         (GtkWindow               *window);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-gui-private.h b/src/libide/gui/ide-gui-private.h
new file mode 100644
index 000000000..634312a3d
--- /dev/null
+++ b/src/libide/gui/ide-gui-private.h
@@ -0,0 +1,103 @@
+/* ide-gui-private.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <dazzle.h>
+#include <gtk/gtk.h>
+#include <libpeas/peas.h>
+#include <libpeas/peas-autocleanups.h>
+#include <libide-core.h>
+#include <libide-projects.h>
+
+#include "ide-frame.h"
+#include "ide-frame-header.h"
+#include "ide-grid.h"
+#include "ide-grid-column.h"
+#include "ide-header-bar.h"
+#include "ide-notification-list-box-row-private.h"
+#include "ide-notification-stack-private.h"
+#include "ide-notification-view-private.h"
+#include "ide-page.h"
+#include "ide-primary-workspace.h"
+#include "ide-shortcut-label-private.h"
+#include "ide-workbench.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+void      _ide_frame_init_actions               (IdeFrame            *self);
+void      _ide_frame_init_shortcuts             (IdeFrame            *self);
+void      _ide_frame_update_actions             (IdeFrame            *self);
+void      _ide_frame_transfer                   (IdeFrame            *self,
+                                                 IdeFrame            *dest,
+                                                 IdePage             *view);
+void      _ide_grid_column_init_actions         (IdeGridColumn       *self);
+void      _ide_grid_column_update_actions       (IdeGridColumn       *self);
+gboolean  _ide_grid_column_is_empty             (IdeGridColumn       *self);
+void      _ide_grid_column_try_close            (IdeGridColumn       *self);
+IdeFrame *_ide_grid_get_nth_stack               (IdeGrid             *self,
+                                                 gint                 nth);
+IdeFrame *_ide_grid_get_nth_stack_for_column    (IdeGrid             *self,
+                                                 IdeGridColumn       *column,
+                                                 gint                 nth);
+void      _ide_grid_init_actions                (IdeGrid             *self);
+void      _ide_grid_stack_added                 (IdeGrid             *self,
+                                                 IdeFrame            *stack);
+void      _ide_grid_stack_removed               (IdeGrid             *self,
+                                                 IdeFrame            *stack);
+void      _ide_frame_request_close              (IdeFrame            *stack,
+                                                 IdePage             *view);
+void      _ide_frame_header_update              (IdeFrameHeader      *self,
+                                                 IdePage             *view);
+void      _ide_frame_header_focus_list          (IdeFrameHeader      *self);
+void      _ide_frame_header_hide                (IdeFrameHeader      *self);
+void      _ide_frame_header_popdown             (IdeFrameHeader      *self);
+void      _ide_frame_header_set_pages           (IdeFrameHeader      *self,
+                                                 GListModel          *model);
+void      _ide_frame_header_set_title           (IdeFrameHeader      *self,
+                                                 const gchar         *title);
+void      _ide_frame_header_set_modified        (IdeFrameHeader      *self,
+                                                 gboolean             modified);
+void      _ide_frame_header_set_background_rgba (IdeFrameHeader      *self,
+                                                 const GdkRGBA       *background_rgba);
+void      _ide_frame_header_set_foreground_rgba (IdeFrameHeader      *self,
+                                                 const GdkRGBA       *foreground_rgba);
+void      _ide_primary_workspace_init_actions   (IdePrimaryWorkspace *self);
+void      _ide_workspace_init_actions           (IdeWorkspace        *self);
+GList    *_ide_workspace_get_mru_link           (IdeWorkspace        *self);
+void      _ide_workspace_add_page_mru           (IdeWorkspace        *self,
+                                                 GList               *mru_link);
+void      _ide_workspace_remove_page_mru        (IdeWorkspace        *self,
+                                                 GList               *mru_link);
+void      _ide_workspace_move_front_page_mru    (IdeWorkspace        *workspace,
+                                                 GList               *mru_link);
+void      _ide_workspace_set_context            (IdeWorkspace        *workspace,
+                                                 IdeContext          *context);
+gboolean  _ide_workbench_is_last_workspace      (IdeWorkbench        *self,
+                                                 IdeWorkspace        *workspace);
+void      _ide_header_bar_init_shortcuts        (IdeHeaderBar        *self);
+void      _ide_header_bar_show_menu             (IdeHeaderBar        *self);
+void      _ide_gtk_progress_bar_start_pulsing   (GtkProgressBar      *progress);
+void      _ide_gtk_progress_bar_stop_pulsing    (GtkProgressBar      *progress);
+void      _ide_surface_set_fullscreen           (IdeSurface          *self,
+                                                 gboolean             fullscreen);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-header-bar-shortcuts.c b/src/libide/gui/ide-header-bar-shortcuts.c
new file mode 100644
index 000000000..a0a3230ec
--- /dev/null
+++ b/src/libide/gui/ide-header-bar-shortcuts.c
@@ -0,0 +1,68 @@
+/* ide-header-bar-shortcuts.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-header-bar-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-gui-private.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+static DzlShortcutEntry workspace_shortcuts[] = {
+  { "org.gnome.builder.workspace.show-menu",
+    0, NULL,
+    NC_("shortcut window", "Window shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Show window menu") },
+
+  { "org.gnome.builder.workspace.fullscreen",
+    0, NULL,
+    NC_("shortcut window", "Window shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Toggle window to fullscreen") },
+};
+
+void
+_ide_header_bar_init_shortcuts (IdeHeaderBar *self)
+{
+  DzlShortcutController *controller;
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.workspace.show-menu"),
+                                              "F10",
+                                              DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("win.show-menu"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.workspace.fullscreen"),
+                                              "F11",
+                                              DZL_SHORTCUT_PHASE_DISPATCH | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("win.fullscreen"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             workspace_shortcuts,
+                                             G_N_ELEMENTS (workspace_shortcuts),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/libide/gui/ide-header-bar.c b/src/libide/gui/ide-header-bar.c
new file mode 100644
index 000000000..245eb5c5e
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.c
@@ -0,0 +1,469 @@
+/* ide-header-bar.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-header-bar"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-gui-private.h"
+#include "ide-header-bar.h"
+
+typedef struct
+{
+  gchar              *menu_id;
+
+  GtkToggleButton    *fullscreen_button;
+  GtkImage           *fullscreen_image;
+  DzlShortcutTooltip *fullscreen_tooltip;
+  DzlMenuButton      *menu_button;
+  DzlShortcutTooltip *menu_tooltip;
+  GtkBox             *primary;
+  GtkBox             *secondary;
+
+  guint               show_fullscreen_button : 1;
+} IdeHeaderBarPrivate;
+
+enum {
+  PROP_0,
+  PROP_MENU_ID,
+  PROP_SHOW_FULLSCREEN_BUTTON,
+  N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeHeaderBar, ide_header_bar, GTK_TYPE_HEADER_BAR,
+                         G_ADD_PRIVATE (IdeHeaderBar)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GParamSpec        *properties [N_PROPS];
+static GtkBuildableIface *buildable_parent;
+
+static void
+on_fullscreen_toggled_cb (GtkToggleButton *button,
+                          GParamSpec      *pspec,
+                          GtkImage        *image)
+{
+  const gchar *icon_name;
+
+  g_assert (GTK_IS_TOGGLE_BUTTON (button));
+  g_assert (GTK_IS_IMAGE (image));
+
+  if (gtk_toggle_button_get_active (button))
+    icon_name = "view-restore-symbolic";
+  else
+    icon_name = "view-fullscreen-symbolic";
+
+  g_object_set (image, "icon-name", icon_name, NULL);
+}
+
+static void
+ide_header_bar_finalize (GObject *object)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)object;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_clear_pointer (&priv->menu_id, g_free);
+
+  G_OBJECT_CLASS (ide_header_bar_parent_class)->finalize (object);
+}
+
+static void
+ide_header_bar_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeHeaderBar *self = IDE_HEADER_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MENU_ID:
+      g_value_set_string (value, ide_header_bar_get_menu_id (self));
+      break;
+
+    case PROP_SHOW_FULLSCREEN_BUTTON:
+      g_value_set_boolean (value, ide_header_bar_get_show_fullscreen_button (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_header_bar_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeHeaderBar *self = IDE_HEADER_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MENU_ID:
+      ide_header_bar_set_menu_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_SHOW_FULLSCREEN_BUTTON:
+      ide_header_bar_set_show_fullscreen_button (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_header_bar_class_init (IdeHeaderBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_header_bar_finalize;
+  object_class->get_property = ide_header_bar_get_property;
+  object_class->set_property = ide_header_bar_set_property;
+
+  properties [PROP_SHOW_FULLSCREEN_BUTTON] =
+    g_param_spec_boolean ("show-fullscreen-button",
+                          "Show Fullscreen Button",
+                          "If the fullscreen button should be shown",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU_ID] =
+    g_param_spec_string ("menu-id",
+                         "Menu ID",
+                         "The id of the menu to display with the window",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-header-bar.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_button);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_image);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_tooltip);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, menu_button);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, menu_tooltip);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, primary);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, secondary);
+
+  g_type_ensure (DZL_TYPE_PRIORITY_BOX);
+  g_type_ensure (DZL_TYPE_SHORTCUT_TOOLTIP);
+}
+
+static void
+ide_header_bar_init (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (priv->fullscreen_button,
+                           "notify::active",
+                           G_CALLBACK (on_fullscreen_toggled_cb),
+                           priv->fullscreen_image,
+                           0);
+
+  _ide_header_bar_init_shortcuts (self);
+}
+
+GtkWidget *
+ide_header_bar_new (void)
+{
+  return g_object_new (IDE_TYPE_HEADER_BAR, NULL);
+}
+
+/**
+ * ide_header_bar_get_show_fullscreen_button:
+ * @self: a #IdeHeaderBar
+ *
+ * Gets if the fullscreen button should be displayed in the header bar.
+ *
+ * Returns: %TRUE if it should be displayed
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_header_bar_get_show_fullscreen_button (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_HEADER_BAR (self), FALSE);
+
+  return priv->show_fullscreen_button;
+}
+
+/**
+ * ide_header_bar_set_show_fullscreen_button:
+ * @self: a #IdeHeaderBar
+ * @show_fullscreen_button: if the fullscreen button should be displayed
+ *
+ * Changes the visibility of the fullscreen button.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_set_show_fullscreen_button (IdeHeaderBar *self,
+                                           gboolean      show_fullscreen_button)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  show_fullscreen_button = !!show_fullscreen_button;
+
+  if (show_fullscreen_button != priv->show_fullscreen_button)
+    {
+      const gchar *session;
+
+      priv->show_fullscreen_button = show_fullscreen_button;
+
+      session = g_getenv ("DESKTOP_SESSION");
+      if (ide_str_equal0 (session, "pantheon"))
+        show_fullscreen_button = FALSE;
+
+      gtk_widget_set_visible (GTK_WIDGET (priv->fullscreen_button), show_fullscreen_button);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_FULLSCREEN_BUTTON]);
+    }
+}
+
+/**
+ * ide_header_bar_get_menu_id:
+ * @self: a #IdeHeaderBar
+ *
+ * Gets the menu-id to show in the workspace window.
+ *
+ * Returns: (nullable): a string containing the menu-id, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_header_bar_get_menu_id (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_HEADER_BAR (self), NULL);
+
+  return priv->menu_id;
+}
+
+/**
+ * ide_header_bar_set_menu_id:
+ * @self: a #IdeHeaderBar
+ *
+ * Sets the menu-id to display in the window.
+ *
+ * Set to %NULL to hide the workspace menu.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_set_menu_id (IdeHeaderBar *self,
+                            const gchar  *menu_id)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  if (!ide_str_equal0 (menu_id, priv->menu_id))
+    {
+      g_free (priv->menu_id);
+      priv->menu_id = g_strdup (menu_id);
+      g_object_set (priv->menu_button, "menu-id", menu_id, NULL);
+      gtk_widget_set_visible (GTK_WIDGET (priv->menu_button), !ide_str_empty0 (menu_id));
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
+    }
+}
+
+/**
+ * ide_header_bar_add_primary:
+ * @self: a #IdeHeaderBar
+ *
+ * Adds a widget to the primary button section of the workspace header.
+ * This is the left, for LTR languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_add_primary (IdeHeaderBar *self,
+                            GtkWidget    *widget)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add (GTK_CONTAINER (priv->primary), widget);
+}
+
+void
+ide_header_bar_add_center_left (IdeHeaderBar *self,
+                                GtkWidget    *child)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), child,
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+}
+
+/**
+ * ide_header_bar_add_secondary:
+ * @self: a #IdeHeaderBar
+ *
+ * Adds a widget to the secondary button section of the workspace header.
+ * This is the right, for LTR languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_add_secondary (IdeHeaderBar *self,
+                              GtkWidget    *widget)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add (GTK_CONTAINER (priv->secondary), widget);
+}
+
+void
+_ide_header_bar_show_menu (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  gtk_widget_activate (GTK_WIDGET (priv->menu_button));
+}
+
+static void
+ide_header_bar_add_child (GtkBuildable  *buildable,
+                          GtkBuilder    *builder,
+                          GObject       *child,
+                          const gchar   *type)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)buildable;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_assert (IDE_IS_HEADER_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (G_IS_OBJECT (child));
+
+  if (ide_str_equal0 (type, "left-of-center"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "left") || ide_str_equal0 (type, "primary"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_START,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "right-of-center"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->secondary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_START,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "right") || ide_str_equal0 (type, "secondary"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->secondary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  buildable_parent->add_child (buildable, builder, child, type);
+
+  return;
+
+warning:
+  g_warning ("'%s' child type must be a GtkWidget, not %s",
+             type, G_OBJECT_TYPE_NAME (child));
+}
+
+static GObject *
+ide_header_bar_get_internal_child (GtkBuildable *buildable,
+                                   GtkBuilder   *builder,
+                                   const gchar  *child_name)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)buildable;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_assert (IDE_IS_HEADER_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+
+  if (ide_str_equal0 (child_name, "primary"))
+    return G_OBJECT (priv->primary);
+
+  if (ide_str_equal0 (child_name, "secondary"))
+    return G_OBJECT (priv->secondary);
+
+  if (buildable_parent->get_internal_child)
+    return buildable_parent->get_internal_child (buildable, builder, child_name);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  buildable_parent = g_type_interface_peek_parent (iface);
+  iface->add_child = ide_header_bar_add_child;
+  iface->get_internal_child = ide_header_bar_get_internal_child;
+}
diff --git a/src/libide/gui/ide-header-bar.h b/src/libide/gui/ide-header-bar.h
new file mode 100644
index 000000000..ec77fbd39
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.h
@@ -0,0 +1,67 @@
+/* ide-header-bar.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HEADER_BAR (ide_header_bar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeHeaderBar, ide_header_bar, IDE, HEADER_BAR, GtkHeaderBar)
+
+struct _IdeHeaderBarClass
+{
+  GtkHeaderBarClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget   *ide_header_bar_new                       (void);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_primary               (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_center_left           (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_secondary             (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_header_bar_get_menu_id               (IdeHeaderBar *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_set_menu_id               (IdeHeaderBar *self,
+                                                       const gchar  *menu_id);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_header_bar_get_show_fullscreen_button (IdeHeaderBar *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_header_bar_set_show_fullscreen_button (IdeHeaderBar *self,
+                                                       gboolean      show_fullscreen_button);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-header-bar.ui b/src/libide/gui/ide-header-bar.ui
new file mode 100644
index 000000000..e55beb724
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.ui
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeHeaderBar" parent="GtkHeaderBar">
+    <child>
+      <object class="DzlPriorityBox" id="primary">
+        <property name="hexpand">true</property>
+        <property name="margin-end">6</property>
+        <property name="orientation">horizontal</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+      </object>
+      <packing>
+        <property name="pack-type">start</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="DzlPriorityBox" id="secondary">
+        <property name="hexpand">true</property>
+        <property name="margin-start">6</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkToggleButton" id="fullscreen_button">
+                <property name="action-name">win.fullscreen</property>
+                <property name="focus-on-click">false</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage" id="fullscreen_image">
+                    <property name="icon-name">view-fullscreen-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="DzlMenuButton" id="menu_button">
+                <property name="icon-name">open-menu-symbolic</property>
+                <property name="show-accels">true</property>
+                <property name="show-icons">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+            <property name="priority">-1000000</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+  </template>
+  <object class="DzlShortcutTooltip" id="fullscreen_tooltip">
+    <property name="command-id">org.gnome.builder.workspace.fullscreen</property>
+    <property name="widget">fullscreen_button</property>
+  </object>
+  <object class="DzlShortcutTooltip" id="menu_tooltip">
+    <property name="command-id">org.gnome.builder.workspace.show-menu</property>
+    <property name="widget">menu_button</property>
+  </object>
+</interface>
+
diff --git a/src/libide/gui/ide-marked-view.c b/src/libide/gui/ide-marked-view.c
new file mode 100644
index 000000000..0edfa8f1f
--- /dev/null
+++ b/src/libide/gui/ide-marked-view.c
@@ -0,0 +1,112 @@
+/* ide-marked-view.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-marked-view"
+
+#include "config.h"
+
+#include <webkit2/webkit2.h>
+
+#include "gs-markdown-private.h"
+#include "ide-marked-view.h"
+
+struct _IdeMarkedView
+{
+  GtkBin parent_instance;
+};
+
+G_DEFINE_TYPE (IdeMarkedView, ide_marked_view, GTK_TYPE_BIN)
+
+static void
+ide_marked_view_class_init (IdeMarkedViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_css_name (widget_class, "markedview");
+}
+
+static void
+ide_marked_view_init (IdeMarkedView *self)
+{
+}
+
+GtkWidget *
+ide_marked_view_new (IdeMarkedContent *content)
+{
+  g_autofree gchar *markup = NULL;
+  GtkWidget *child = NULL;
+  IdeMarkedView *self;
+  IdeMarkedKind kind;
+
+  g_return_val_if_fail (content != NULL, NULL);
+
+  self = g_object_new (IDE_TYPE_MARKED_VIEW, NULL);
+  kind = ide_marked_content_get_kind (content);
+  markup = ide_marked_content_as_string (content);
+
+  switch (kind)
+    {
+    default:
+    case IDE_MARKED_KIND_PLAINTEXT:
+    case IDE_MARKED_KIND_PANGO:
+      child = g_object_new (GTK_TYPE_LABEL,
+                            "max-width-chars", 80,
+                            "wrap", TRUE,
+                            "xalign", 0.0f,
+                            "visible", TRUE,
+                            "use-markup", kind == IDE_MARKED_KIND_PANGO,
+                            "label", markup,
+                            NULL);
+      break;
+
+    case IDE_MARKED_KIND_HTML:
+      child = g_object_new (WEBKIT_TYPE_WEB_VIEW,
+                            "visible", TRUE,
+                            NULL);
+      webkit_web_view_load_html (WEBKIT_WEB_VIEW (child), markup, NULL);
+      break;
+
+    case IDE_MARKED_KIND_MARKDOWN:
+      {
+        g_autoptr(GsMarkdown) md = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO);
+        g_autofree gchar *parsed = NULL;
+
+        gs_markdown_set_smart_quoting (md, TRUE);
+        gs_markdown_set_autocode (md, TRUE);
+        gs_markdown_set_autolinkify (md, TRUE);
+
+        if ((parsed = gs_markdown_parse (md, markup)))
+          child = g_object_new (GTK_TYPE_LABEL,
+                                "max-width-chars", 80,
+                                "wrap", TRUE,
+                                "xalign", 0.0f,
+                                "visible", TRUE,
+                                "use-markup", TRUE,
+                                "label", parsed,
+                                NULL);
+      }
+      break;
+    }
+
+  if (child != NULL)
+    gtk_container_add (GTK_CONTAINER (self), child);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/libide/workbench/ide-omni-bar.h b/src/libide/gui/ide-marked-view.h
similarity index 71%
rename from src/libide/workbench/ide-omni-bar.h
rename to src/libide/gui/ide-marked-view.h
index bc8bbd87b..b424b868a 100644
--- a/src/libide/workbench/ide-omni-bar.h
+++ b/src/libide/gui/ide-marked-view.h
@@ -1,6 +1,6 @@
-/* ide-omni-bar.h
+/* ide-marked-view.h
  *
- * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,18 +21,17 @@
 #pragma once
 
 #include <gtk/gtk.h>
-
-#include "ide-types.h"
-#include "ide-version-macros.h"
+#include <libide-core.h>
+#include <libide-io.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_OMNI_BAR (ide_omni_bar_get_type())
+#define IDE_TYPE_MARKED_VIEW (ide_marked_view_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdeOmniBar, ide_omni_bar, IDE, OMNI_BAR, GtkBox)
+G_DECLARE_FINAL_TYPE (IdeMarkedView, ide_marked_view, IDE, MARKED_VIEW, GtkBin)
 
 IDE_AVAILABLE_IN_3_32
-GtkWidget *ide_omni_bar_new (void);
+GtkWidget *ide_marked_view_new (IdeMarkedContent *content);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-notification-list-box-row-private.h 
b/src/libide/gui/ide-notification-list-box-row-private.h
new file mode 100644
index 000000000..b0a603ff5
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row-private.h
@@ -0,0 +1,38 @@
+/* ide-notification-list-box-row-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_LIST_BOX_ROW (ide_notification_list_box_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationListBoxRow, ide_notification_list_box_row, IDE, 
NOTIFICATION_LIST_BOX_ROW, GtkListBoxRow)
+
+GtkWidget       *ide_notification_list_box_row_new              (IdeNotification           *notification);
+IdeNotification *ide_notification_list_box_row_get_notification (IdeNotificationListBoxRow *self);
+void             ide_notification_list_box_row_set_compact      (IdeNotificationListBoxRow *self,
+                                                                 gboolean                   compact);
+gboolean         ide_notification_list_box_row_get_compact      (IdeNotificationListBoxRow *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-list-box-row.c b/src/libide/gui/ide-notification-list-box-row.c
new file mode 100644
index 000000000..8bc0ca5fe
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row.c
@@ -0,0 +1,377 @@
+/* ide-notification-list-box-row.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notification-list-box-row"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-gui-private.h"
+#include "ide-notification-list-box-row-private.h"
+
+struct _IdeNotificationListBoxRow
+{
+  GtkListBoxRow    parent_instance;
+
+  IdeNotification *notification;
+
+  GtkLabel        *body;
+  GtkLabel        *title;
+  GtkBox          *lower_button_area;
+  GtkBox          *side_button_area;
+  GtkBox          *buttons;
+  GtkProgressBar  *progress;
+
+  guint            compact : 1;
+};
+
+G_DEFINE_TYPE (IdeNotificationListBoxRow, ide_notification_list_box_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_COMPACT,
+  PROP_NOTIFICATION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+setup_buttons_locked (IdeNotificationListBoxRow *self)
+{
+  g_autofree gchar *body = NULL;
+  g_autofree gchar *title = NULL;
+  guint n_buttons;
+
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+  g_assert (self->notification != NULL);
+
+  title = ide_notification_dup_title (self->notification);
+  body = ide_notification_dup_body (self->notification);
+
+  n_buttons = ide_notification_get_n_buttons (self->notification);
+
+  for (guint i = 0; i < n_buttons; i++)
+    {
+      g_autofree gchar *action = NULL;
+      g_autofree gchar *label = NULL;
+      g_autoptr(GIcon) icon = NULL;
+      g_autoptr(GVariant) target = NULL;
+
+      if (ide_notification_get_button (self->notification, i, &label, &icon, &action, &target))
+        {
+          GtkButton *button;
+          GtkWidget *child = NULL;
+
+          if (action == NULL || (label == NULL && icon == NULL))
+            continue;
+
+          if (label != NULL && (!self->compact || icon == NULL))
+            child = g_object_new (GTK_TYPE_LABEL,
+                                  "label", label,
+                                  "visible", TRUE,
+                                  NULL);
+          else if (icon != NULL)
+            child = g_object_new (GTK_TYPE_IMAGE,
+                                  "icon-size", GTK_ICON_SIZE_MENU,
+                                  "gicon", icon,
+                                  "visible", TRUE,
+                                  NULL);
+
+          g_assert (GTK_IS_WIDGET (child));
+
+          button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                                 "child", child,
+                                 "action-name", action,
+                                 "action-target", target,
+                                 "visible", TRUE,
+                                 NULL);
+
+          if (!self->compact)
+            dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "suggested-action");
+          else
+            dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "circular");
+
+          g_assert (GTK_IS_WIDGET (button));
+
+          gtk_container_add_with_properties (GTK_CONTAINER (self->buttons), GTK_WIDGET (button),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+        }
+    }
+
+  /* Always show labels when compact+buttons for alignment. */
+  gtk_widget_set_visible (GTK_WIDGET (self->body),
+                          !ide_str_empty0 (body) || (self->compact && n_buttons > 0));
+  gtk_widget_set_visible (GTK_WIDGET (self->title),
+                          !ide_str_empty0 (title) || (self->compact && n_buttons > 0));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->buttons), n_buttons > 0);
+}
+
+/**
+ * ide_notification_list_box_row_new:
+ *
+ * Create a new #IdeNotificationListBoxRow.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationListBoxRow
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notification_list_box_row_new (IdeNotification *notification)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (notification), NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "notification", notification,
+                       NULL);
+}
+
+static void
+ide_notification_list_box_row_constructed (GObject *object)
+{
+  IdeNotificationListBoxRow *self = (IdeNotificationListBoxRow *)object;
+  g_autofree gchar *body = NULL;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+
+  if (self->notification == NULL)
+    {
+      g_warning ("%s created without an IdeNotification!",
+                 G_OBJECT_TYPE_NAME (self));
+      goto chain_up;
+    }
+
+  ide_object_lock (IDE_OBJECT (self->notification));
+
+  body = ide_notification_dup_body (self->notification);
+  title = ide_notification_dup_title (self->notification);
+
+  g_object_bind_property (self->notification, "title", self->title, "label", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self->notification, "body", self->body, "label", G_BINDING_SYNC_CREATE);
+
+  /* Always show labels when compact+buttons for alignment. */
+  gtk_widget_set_visible (GTK_WIDGET (self->body),
+                          !ide_str_empty0 (body) ||
+                          (self->compact && ide_notification_get_n_buttons (self->notification)));
+  gtk_widget_set_visible (GTK_WIDGET (self->title),
+                          !ide_str_empty0 (title) ||
+                          (self->compact && ide_notification_get_n_buttons (self->notification)));
+
+  if (ide_notification_get_urgent (self->notification))
+    dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "needs-attention");
+
+  gtk_widget_set_visible (GTK_WIDGET (self->progress),
+                          ide_notification_get_has_progress (self->notification));
+  g_object_bind_property (self->notification, "progress",
+                          self->progress, "fraction",
+                          G_BINDING_SYNC_CREATE);
+
+  setup_buttons_locked (self);
+
+  if (ide_notification_get_progress_is_imprecise (self->notification))
+    _ide_gtk_progress_bar_start_pulsing (self->progress);
+
+  ide_object_unlock (IDE_OBJECT (self->notification));
+
+chain_up:
+  G_OBJECT_CLASS (ide_notification_list_box_row_parent_class)->constructed (object);
+}
+
+static void
+ide_notification_list_box_row_destroy (GtkWidget *widget)
+{
+  IdeNotificationListBoxRow *self = (IdeNotificationListBoxRow *)widget;
+
+  if (self->progress != NULL)
+    _ide_gtk_progress_bar_stop_pulsing (self->progress);
+
+  g_clear_object (&self->notification);
+
+  GTK_WIDGET_CLASS (ide_notification_list_box_row_parent_class)->destroy (widget);
+}
+
+static void
+ide_notification_list_box_row_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  IdeNotificationListBoxRow *self = IDE_NOTIFICATION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPACT:
+      g_value_set_boolean (value, ide_notification_list_box_row_get_compact (self));
+      break;
+
+    case PROP_NOTIFICATION:
+      g_value_set_object (value, self->notification);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_list_box_row_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  IdeNotificationListBoxRow *self = IDE_NOTIFICATION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPACT:
+      ide_notification_list_box_row_set_compact (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_NOTIFICATION:
+      self->notification = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_list_box_row_class_init (IdeNotificationListBoxRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_notification_list_box_row_constructed;
+  object_class->get_property = ide_notification_list_box_row_get_property;
+  object_class->set_property = ide_notification_list_box_row_set_property;
+
+  widget_class->destroy = ide_notification_list_box_row_destroy;
+
+  properties [PROP_COMPACT] =
+    g_param_spec_boolean ("compact",
+                          "Compact",
+                          "If the compact button mode should be used",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NOTIFICATION] =
+    g_param_spec_object ("notification",
+                         "Notification",
+                         "The notification to display",
+                         IDE_TYPE_NOTIFICATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notification-list-box-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, body);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, buttons);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, lower_button_area);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, progress);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, side_button_area);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, title);
+}
+
+static void
+ide_notification_list_box_row_init (IdeNotificationListBoxRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * ide_notification_list_box_row_get_notification:
+ * @self: a #IdeNotificationListBoxRow
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_list_box_row_get_notification (IdeNotificationListBoxRow *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self), NULL);
+
+  return self->notification;
+}
+
+gboolean
+ide_notification_list_box_row_get_compact (IdeNotificationListBoxRow *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self), FALSE);
+
+  return self->compact;
+}
+
+void
+ide_notification_list_box_row_set_compact (IdeNotificationListBoxRow *self,
+                                           gboolean                   compact)
+{
+  GtkBox *parent;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+
+  if (self->compact != compact)
+    {
+      self->compact = compact;
+
+      g_object_ref (self->buttons);
+
+      gtk_container_foreach (GTK_CONTAINER (self->buttons),
+                             (GtkCallback)gtk_widget_destroy,
+                             NULL);
+
+      parent = GTK_BOX (gtk_widget_get_parent (GTK_WIDGET (self->buttons)));
+      gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (self->buttons));
+      gtk_widget_hide (GTK_WIDGET (parent));
+
+      if (compact)
+        parent = self->side_button_area;
+      else
+        parent = self->lower_button_area;
+
+      gtk_container_add_with_properties (GTK_CONTAINER (parent), GTK_WIDGET (self->buttons),
+                                         "pack-type", GTK_PACK_END,
+                                         NULL);
+
+      g_object_unref (self->buttons);
+
+      gtk_label_set_width_chars (self->title, self->compact ? 35 : 50);
+      gtk_label_set_max_width_chars (self->title, self->compact ? 35 : 50);
+
+      gtk_label_set_width_chars (self->body, self->compact ? 35 : 50);
+      gtk_label_set_max_width_chars (self->body, self->compact ? 35 : 50);
+
+      if (self->notification != NULL)
+        {
+          ide_object_lock (IDE_OBJECT (self->notification));
+          setup_buttons_locked (self);
+          gtk_widget_set_visible (GTK_WIDGET (parent),
+                                  ide_notification_get_n_buttons (self->notification) > 0);
+          ide_object_unlock (IDE_OBJECT (self->notification));
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPACT]);
+    }
+}
diff --git a/src/libide/gui/ide-notification-list-box-row.ui b/src/libide/gui/ide-notification-list-box-row.ui
new file mode 100644
index 000000000..b74f8eaea
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row.ui
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="IdeNotificationListBoxRow" parent="GtkListBoxRow">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_start">12</property>
+        <property name="margin_end">12</property>
+        <property name="margin_top">12</property>
+        <property name="margin_bottom">12</property>
+        <property name="row_spacing">6</property>
+        <property name="column_spacing">12</property>
+        <property name="baseline_row">2</property>
+        <child>
+          <object class="GtkProgressBar" id="progress">
+            <property name="name">progress</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">baseline</property>
+            <property name="hexpand">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="body">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="wrap">True</property>
+            <property name="width_chars">50</property>
+            <property name="max_width_chars">50</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="body"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="lower_button_area">
+            <property name="margin_top">6</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkBox" id="buttons">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <child>
+                  <placeholder/>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">3</property>
+            <property name="width">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="side_button_area">
+            <property name="can_focus">False</property>
+            <property name="valign">start</property>
+            <property name="margin_top">10</property>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="height">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <style>
+      <class name="notification"/>
+    </style>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-notification-stack-private.h b/src/libide/gui/ide-notification-stack-private.h
new file mode 100644
index 000000000..df9f2e0ca
--- /dev/null
+++ b/src/libide/gui/ide-notification-stack-private.h
@@ -0,0 +1,44 @@
+/* ide-notification-stack-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_STACK (ide_notification_stack_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationStack, ide_notification_stack, IDE, NOTIFICATION_STACK, GtkStack)
+
+GtkWidget       *ide_notification_stack_new           (void);
+void             ide_notification_stack_bind_model    (IdeNotificationStack *self,
+                                                       GListModel           *notifications);
+gboolean         ide_notification_stack_is_empty      (IdeNotificationStack *self);
+gboolean         ide_notification_stack_get_can_move  (IdeNotificationStack *self);
+void             ide_notification_stack_move_next     (IdeNotificationStack *self);
+void             ide_notification_stack_move_previous (IdeNotificationStack *self);
+IdeNotification *ide_notification_stack_get_visible   (IdeNotificationStack *self);
+gdouble          ide_notification_stack_get_progress  (IdeNotificationStack *self);
+void             ide_notification_stack_set_progress  (IdeNotificationStack *self,
+                                                       gdouble               progress);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-stack.c b/src/libide/gui/ide-notification-stack.c
new file mode 100644
index 000000000..9d33b6f47
--- /dev/null
+++ b/src/libide/gui/ide-notification-stack.c
@@ -0,0 +1,405 @@
+/* ide-notification-stack.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notification-stack"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-notification-stack-private.h"
+#include "ide-notification-view-private.h"
+
+#define CAROUSEL_TIMEOUT_SECS 5
+#define TRANSITION_DURATION   500
+
+struct _IdeNotificationStack
+{
+  GtkStack         parent_instance;
+  DzlSignalGroup  *signals;
+  DzlBindingGroup *bindings;
+  GListModel      *model;
+  gdouble          progress;
+  guint            carousel_source;
+  guint            in_carousel : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_PROGRESS,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeNotificationStack, ide_notification_stack, GTK_TYPE_STACK)
+
+static guint signals [N_SIGNALS];
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notification_stack_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeNotificationStack *self = IDE_NOTIFICATION_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notification_stack_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_stack_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeNotificationStack *self = IDE_NOTIFICATION_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      ide_notification_stack_set_progress (self, g_value_get_double (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static gboolean
+ide_notification_stack_carousel_cb (gpointer data)
+{
+  IdeNotificationStack *self = data;
+
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  self->in_carousel = TRUE;
+  ide_notification_stack_move_next (self);
+  self->in_carousel = FALSE;
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_notification_stack_items_changed_cb (IdeNotificationStack *self,
+                                         guint                 position,
+                                         guint                 removed,
+                                         guint                 added,
+                                         GListModel           *model)
+{
+  GtkWidget *urgent = NULL;
+  GList *children;
+  GList *iter;
+
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  children = gtk_container_get_children (GTK_CONTAINER (self));
+  iter = g_list_nth (children, position);
+
+  for (guint i = 0; i < removed; i++, iter = iter->next)
+    {
+      GtkWidget *child = iter->data;
+      gtk_widget_destroy (child);
+    }
+
+  g_list_free (children);
+
+  for (guint i = 0; i < added; i++)
+    {
+      g_autoptr(IdeNotification) notif = g_list_model_get_item (model, position + i);
+      GtkWidget *view = g_object_new (IDE_TYPE_NOTIFICATION_VIEW,
+                                      "notification", notif,
+                                      "visible", TRUE,
+                                      NULL);
+
+      gtk_container_add_with_properties (GTK_CONTAINER (self), view,
+                                         "position", position + i,
+                                         NULL);
+
+      if (!urgent && ide_notification_get_urgent (notif))
+        urgent = view;
+    }
+
+  if (urgent != NULL)
+    {
+      gtk_stack_set_visible_child (GTK_STACK (self), urgent);
+      g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+
+  if (self->carousel_source == 0 && g_list_model_get_n_items (model))
+    self->carousel_source = g_timeout_add_seconds (CAROUSEL_TIMEOUT_SECS,
+                                                   ide_notification_stack_carousel_cb,
+                                                   self);
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+ide_notification_stack_notify_visible_child (IdeNotificationStack *self)
+{
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  self->progress = 0.0;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+  dzl_binding_group_set_source (self->bindings,
+                                ide_notification_stack_get_visible (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+ide_notification_stack_destroy (GtkWidget *widget)
+{
+  IdeNotificationStack *self = (IdeNotificationStack *)widget;
+
+  if (self->signals != NULL)
+    dzl_signal_group_set_target (self->signals, NULL);
+
+  if (self->bindings != NULL)
+    dzl_binding_group_set_source (self->bindings, NULL);
+
+  g_clear_object (&self->bindings);
+  g_clear_object (&self->signals);
+  g_clear_handle_id (&self->carousel_source, g_source_remove);
+
+  GTK_WIDGET_CLASS (ide_notification_stack_parent_class)->destroy (widget);
+}
+
+static void
+ide_notification_stack_class_init (IdeNotificationStackClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_notification_stack_get_property;
+  object_class->set_property = ide_notification_stack_set_property;
+
+  widget_class->destroy = ide_notification_stack_destroy;
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress of the current item",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_css_name (widget_class, "notificationstack");
+}
+
+static void
+ide_notification_stack_init (IdeNotificationStack *self)
+{
+  self->signals = dzl_signal_group_new (G_TYPE_LIST_MODEL);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "items-changed",
+                                   G_CALLBACK (ide_notification_stack_items_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "progress", self, "progress",
+                          G_BINDING_SYNC_CREATE);
+
+  gtk_stack_set_transition_duration (GTK_STACK (self), TRANSITION_DURATION);
+  gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+  g_signal_connect (self,
+                    "notify::visible-child",
+                    G_CALLBACK (ide_notification_stack_notify_visible_child),
+                    NULL);
+}
+
+void
+ide_notification_stack_bind_model (IdeNotificationStack *self,
+                                   GListModel           *model)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+  g_return_if_fail (!model || G_IS_LIST_MODEL (model));
+  g_return_if_fail (!model ||
+                    g_type_is_a (g_list_model_get_item_type (model), IDE_TYPE_NOTIFICATION));
+
+  if (g_set_object (&self->model, model))
+    {
+      guint n_items = 0;
+
+      if (model != NULL)
+        n_items = g_list_model_get_n_items (model);
+
+      gtk_container_foreach (GTK_CONTAINER (self), (GtkCallback)gtk_widget_destroy, NULL);
+      dzl_signal_group_set_target (self->signals, model);
+
+      if (n_items > 0)
+        ide_notification_stack_items_changed_cb (self, 0, 0, n_items, model);
+    }
+}
+
+gboolean
+ide_notification_stack_get_can_move (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), FALSE);
+
+  if (self->model != NULL)
+    return g_list_model_get_n_items (self->model) > 1;
+  else
+    return FALSE;
+}
+
+void
+ide_notification_stack_move_next (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+  gint position;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      GList *children;
+
+      gtk_container_child_get (GTK_CONTAINER (self), child,
+                               "position", &position,
+                               NULL);
+      children = gtk_container_get_children (GTK_CONTAINER (self));
+      if (!(child = g_list_nth_data (children, position + 1)))
+        child = children->data;
+      g_list_free (children);
+
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN);
+      gtk_stack_set_visible_child (GTK_STACK (self), child);
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+      if (!self->in_carousel)
+        g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+}
+
+void
+ide_notification_stack_move_previous (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+  gint position;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      GList *children;
+
+      gtk_container_child_get (GTK_CONTAINER (self), child,
+                               "position", &position,
+                               NULL);
+      children = gtk_container_get_children (GTK_CONTAINER (self));
+      if (position == 0)
+        child = g_list_last (children)->data;
+      else
+        child = g_list_nth_data (children, position - 1);
+      g_list_free (children);
+
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP);
+      gtk_stack_set_visible_child (GTK_STACK (self), child);
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+      if (!self->in_carousel)
+        g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+}
+
+/**
+ * ide_notification_stack_get_visible:
+ * @self: a #IdeNotificationStack
+ *
+ * Gets the visible notification in the stack.
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_stack_get_visible (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), NULL);
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      if (IDE_IS_NOTIFICATION_VIEW (child))
+        return ide_notification_view_get_notification (IDE_NOTIFICATION_VIEW (child));
+    }
+
+  return NULL;
+}
+
+gdouble
+ide_notification_stack_get_progress (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), 0.0);
+
+  return self->progress;
+}
+
+void
+ide_notification_stack_set_progress (IdeNotificationStack *self,
+                                     gdouble               progress)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  progress = CLAMP (progress, 0.0, 1.0);
+
+  if (progress != self->progress)
+    {
+      self->progress = progress;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+}
+
+gboolean
+ide_notification_stack_is_empty (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), FALSE);
+
+  return self->model == NULL || g_list_model_get_n_items (self->model) == 0;
+}
diff --git a/src/libide/gui/ide-notification-view-private.h b/src/libide/gui/ide-notification-view-private.h
new file mode 100644
index 000000000..017a83fa5
--- /dev/null
+++ b/src/libide/gui/ide-notification-view-private.h
@@ -0,0 +1,37 @@
+/* ide-notification-view-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_VIEW (ide_notification_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationView, ide_notification_view, IDE, NOTIFICATION_VIEW, GtkBin)
+
+GtkWidget       *ide_notification_view_new              (void);
+IdeNotification *ide_notification_view_get_notification (IdeNotificationView *self);
+void             ide_notification_view_set_notification (IdeNotificationView *self,
+                                                         IdeNotification     *notification);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-view.c b/src/libide/gui/ide-notification-view.c
new file mode 100644
index 000000000..8b160aee9
--- /dev/null
+++ b/src/libide/gui/ide-notification-view.c
@@ -0,0 +1,291 @@
+/* ide-notification-view.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notification-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-notification-view-private.h"
+
+struct _IdeNotificationView
+{
+  GtkBin           parent_instance;
+
+  IdeNotification *notification;
+  DzlBindingGroup *bindings;
+
+  GtkLabel        *label;
+  GtkBox          *buttons;
+  GtkButton       *default_button;
+  GtkImage        *default_button_image;
+};
+
+G_DEFINE_TYPE (IdeNotificationView, ide_notification_view, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_NOTIFICATION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notification_view_notify_icon (IdeNotificationView *self,
+                                   GParamSpec          *pspec,
+                                   IdeNotification     *notif)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATION_VIEW (self));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  icon = ide_notification_ref_icon (notif);
+  gtk_image_set_from_gicon (self->default_button_image, icon, GTK_ICON_SIZE_MENU);
+  gtk_widget_set_visible (GTK_WIDGET (self->default_button), icon != NULL);
+}
+
+static void
+connect_notification (IdeNotificationView *self,
+                      IdeNotification     *notification)
+{
+  g_autofree gchar *action_name = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autoptr(GIcon) icon = NULL;
+  guint n_buttons;
+
+  g_assert (IDE_IS_NOTIFICATION_VIEW (self));
+  g_assert (!notification || IDE_IS_NOTIFICATION (notification));
+
+  gtk_container_foreach (GTK_CONTAINER (self->buttons), (GtkCallback)gtk_widget_destroy, NULL);
+
+  if (notification == NULL)
+    {
+      gtk_widget_hide (GTK_WIDGET (self->label));
+      gtk_widget_hide (GTK_WIDGET (self->default_button));
+      gtk_widget_hide (GTK_WIDGET (self->buttons));
+      return;
+    }
+
+  g_signal_connect_object (notification,
+                           "notify::icon",
+                           G_CALLBACK (ide_notification_view_notify_icon),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_notification_view_notify_icon (self, NULL, notification);
+
+  /*
+   * Setup the default action button (which is shown right after the label
+   * containing notification title).
+   */
+
+  if (ide_notification_get_default_action (notification, &action_name, &target_value))
+    {
+      gtk_actionable_set_action_name (GTK_ACTIONABLE (self->default_button), action_name);
+      gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->default_button), target_value);
+    }
+
+  /*
+   * Now add all of the buttons requested by the notification.
+   */
+
+  ide_object_lock (IDE_OBJECT (notification));
+
+  n_buttons = ide_notification_get_n_buttons (notification);
+
+  for (guint i = 0; i < n_buttons; i++)
+    {
+      g_autofree gchar *action = NULL;
+      g_autofree gchar *label = NULL;
+      g_autoptr(GIcon) button_icon = NULL;
+      g_autoptr(GVariant) target = NULL;
+
+      if (ide_notification_get_button (notification, i, &label, &button_icon, &action, &target) &&
+          button_icon != NULL &&
+          action_name != NULL)
+        {
+          GtkButton *button;
+
+          button = g_object_new (GTK_TYPE_BUTTON,
+                                 "child", g_object_new (GTK_TYPE_IMAGE,
+                                                        "gicon", button_icon,
+                                                        "visible", TRUE,
+                                                        NULL),
+                                 "action-name", action,
+                                 "action-target", target,
+                                 "has-tooltip", TRUE,
+                                 "tooltip-text", label,
+                                 "visible", TRUE,
+                                 NULL);
+          gtk_container_add (GTK_CONTAINER (self->buttons), GTK_WIDGET (button));
+        }
+    }
+
+  ide_object_unlock (IDE_OBJECT (notification));
+}
+
+static void
+ide_notification_view_finalize (GObject *object)
+{
+  IdeNotificationView *self = (IdeNotificationView *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_object (&self->bindings);
+  g_clear_object (&self->notification);
+
+  G_OBJECT_CLASS (ide_notification_view_parent_class)->finalize (object);
+}
+
+static void
+ide_notification_view_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeNotificationView *self = IDE_NOTIFICATION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NOTIFICATION:
+      g_value_set_object (value, ide_notification_view_get_notification (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_view_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeNotificationView *self = IDE_NOTIFICATION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NOTIFICATION:
+      ide_notification_view_set_notification (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_view_class_init (IdeNotificationViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_notification_view_finalize;
+  object_class->get_property = ide_notification_view_get_property;
+  object_class->set_property = ide_notification_view_set_property;
+
+  /**
+   * IdeNotificationView:notification:
+   *
+   * The "notification" property is the #IdeNotification to be displayed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_NOTIFICATION] =
+    g_param_spec_object ("notification",
+                         "Notification",
+                         "The IdeNotification to be viewed",
+                         IDE_TYPE_NOTIFICATION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notification-view.ui");
+  gtk_widget_class_set_css_name (widget_class, "notification");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, label);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, buttons);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, default_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, default_button_image);
+}
+
+static void
+ide_notification_view_init (IdeNotificationView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "title", self->label, "label", G_BINDING_SYNC_CREATE);
+}
+
+/**
+ * ide_notification_view_new:
+ *
+ * Create a new #IdeNotificationView to visualize a notification within
+ * the #IdeOmniBar.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationView
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notification_view_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATION_VIEW, NULL);
+}
+
+/**
+ * ide_notification_view_get_notification:
+ * @self: an #IdeNotificationView
+ *
+ * Gets the #IdeNotification that is being viewed.
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_view_get_notification (IdeNotificationView *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_VIEW (self), NULL);
+
+  return self->notification;
+}
+
+void
+ide_notification_view_set_notification (IdeNotificationView *self,
+                                        IdeNotification     *notification)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_NOTIFICATION_VIEW (self));
+  g_return_if_fail (!notification || IDE_IS_NOTIFICATION (notification));
+
+  if (g_set_object (&self->notification, notification))
+    {
+      dzl_binding_group_set_source (self->bindings, notification);
+      connect_notification (self, notification);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NOTIFICATION]);
+    }
+}
diff --git a/src/libide/gui/ide-notification-view.ui b/src/libide/gui/ide-notification-view.ui
new file mode 100644
index 000000000..bc7b177cf
--- /dev/null
+++ b/src/libide/gui/ide-notification-view.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="IdeNotificationView" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="ellipsize">end</property>
+            <property name="margin-start">6</property>
+            <property name="visible">True</property>
+            <property name="width_chars">5</property>
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="default_button">
+            <property name="can_focus">True</property>
+            <property name="focus_on_click">False</property>
+            <property name="receives_default">True</property>
+            <child>
+              <object class="GtkImage" id="default_button_image">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="stock">gtk-missing-image</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="buttons">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">end</property>
+            <property name="hexpand">True</property>
+            <property name="spacing">6</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/src/libide/gui/ide-notifications-button-popover-private.h 
b/src/libide/gui/ide-notifications-button-popover-private.h
new file mode 100644
index 000000000..180506cfc
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button-popover-private.h
@@ -0,0 +1,31 @@
+/* ide-notifications-button-popover-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS_BUTTON_POPOVER (ide_notifications_button_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationsButtonPopover, ide_notifications_button_popover, IDE, 
NOTIFICATIONS_BUTTON_POPOVER, GtkPopover)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notifications-button-popover.c 
b/src/libide/gui/ide-notifications-button-popover.c
new file mode 100644
index 000000000..b89e45be9
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button-popover.c
@@ -0,0 +1,51 @@
+/* ide-notifications-button-popover.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notifications-button-popover"
+
+#include "config.h"
+
+#include "ide-notifications-button-popover-private.h"
+
+struct _IdeNotificationsButtonPopover
+{
+  GtkPopover parent_instance;
+};
+
+G_DEFINE_TYPE (IdeNotificationsButtonPopover, ide_notifications_button_popover, GTK_TYPE_POPOVER)
+
+static GtkSizeRequestMode
+ide_notifications_button_popover_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+ide_notifications_button_popover_class_init (IdeNotificationsButtonPopoverClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->get_request_mode = ide_notifications_button_popover_get_request_mode;
+}
+
+static void
+ide_notifications_button_popover_init (IdeNotificationsButtonPopover *self)
+{
+}
diff --git a/src/libide/gui/ide-notifications-button.c b/src/libide/gui/ide-notifications-button.c
new file mode 100644
index 000000000..a9e47e41e
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.c
@@ -0,0 +1,217 @@
+/* ide-notifications-button.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notifications-button"
+
+#include "config.h"
+
+#include "ide-notifications-button.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+
+/**
+ * SECTION:ide-notifications-button:
+ * @title: IdeNotificationsButton
+ * @short_description: a popover menu button containing progress notifications
+ *
+ * The #IdeNotificationsButton shows ongoing notifications that have progress.
+ * The individual notifications are displayed in a popover with appropriate
+ * progress show for each.
+ *
+ * The button itself will show a "combined" progress of all the active
+ * notifications.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeNotificationsButton
+{
+  DzlProgressMenuButton  parent_instance;
+
+  GListModel            *model;
+  DzlListModelFilter    *filter;
+
+  /* Template widgets */
+  GtkPopover            *popover;
+  GtkListBox            *list_box;
+};
+
+G_DEFINE_TYPE (IdeNotificationsButton, ide_notifications_button, DZL_TYPE_PROGRESS_MENU_BUTTON)
+
+static GtkWidget *
+create_notification_row (gpointer item,
+                         gpointer user_data)
+{
+  IdeNotification *notif = item;
+  gboolean has_default;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (user_data));
+
+  has_default = ide_notification_get_default_action (notif, NULL, NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "activatable", has_default,
+                       "compact", TRUE,
+                       "notification", item,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static gboolean
+filter_by_has_progress (GObject  *object,
+                        gpointer  user_data)
+{
+  IdeNotification *notif = (IdeNotification *)object;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (user_data == NULL);
+
+  return ide_notification_get_has_progress (notif);
+}
+
+static void
+ide_notifications_button_bind_model (IdeNotificationsButton *self,
+                                     GListModel             *model)
+{
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  if (g_set_object (&self->model, model))
+    {
+      g_clear_object (&self->filter);
+
+      self->filter = dzl_list_model_filter_new (model);
+      dzl_list_model_filter_set_filter_func (self->filter,
+                                             filter_by_has_progress,
+                                             NULL, NULL);
+
+      gtk_list_box_bind_model (self->list_box,
+                               G_LIST_MODEL (self->filter),
+                               create_notification_row,
+                               self, NULL);
+    }
+}
+
+static void
+ide_notifications_button_context_set_cb (GtkWidget  *widget,
+                                         IdeContext *context)
+{
+  IdeNotificationsButton *self = (IdeNotificationsButton *)widget;
+  g_autoptr(IdeNotifications) notifications = NULL;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
+  ide_notifications_button_bind_model (self, G_LIST_MODEL (notifications));
+
+  g_object_bind_property (notifications, "progress", self, "progress",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (notifications, "has-progress", self, "visible",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (notifications, "progress-is-imprecise", self, "show-progress",
+                          G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
+}
+
+static void
+ide_notifications_button_row_activated (IdeNotificationsButton    *self,
+                                        IdeNotificationListBoxRow *row,
+                                        GtkListBox                *list_box)
+{
+  g_autofree gchar *default_action = NULL;
+  g_autoptr(GVariant) default_target = NULL;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  notif = ide_notification_list_box_row_get_notification (row);
+
+  if (ide_notification_get_default_action (notif, &default_action, &default_target))
+    {
+      gchar *name = strchr (default_action, '.');
+      gchar *group = default_action;
+
+      if (name != NULL)
+        {
+          *name = '\0';
+          name++;
+        }
+      else
+        {
+          group = NULL;
+          name = default_action;
+        }
+
+      dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
+    }
+}
+
+static void
+ide_notifications_button_destroy (GtkWidget *widget)
+{
+  IdeNotificationsButton *self = (IdeNotificationsButton *)widget;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+
+  g_clear_object (&self->filter);
+  g_clear_object (&self->model);
+
+  GTK_WIDGET_CLASS (ide_notifications_button_parent_class)->destroy (widget);
+}
+
+static void
+ide_notifications_button_class_init (IdeNotificationsButtonClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_notifications_button_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notifications-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationsButton, list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationsButton, popover);
+  gtk_widget_class_bind_template_callback (widget_class, ide_notifications_button_row_activated);
+}
+
+static void
+ide_notifications_button_init (IdeNotificationsButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_widget_set_context_handler (GTK_WIDGET (self),
+                                  ide_notifications_button_context_set_cb);
+}
+
+/**
+ * ide_notifications_button_new:
+ *
+ * Create a new #IdeNotificationsButton.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationsButton
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notifications_button_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATIONS_BUTTON, NULL);
+}
diff --git a/src/libide/gui/ide-notifications-button.h b/src/libide/gui/ide-notifications-button.h
new file mode 100644
index 000000000..703d63f4b
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.h
@@ -0,0 +1,40 @@
+/* ide-notifications-button.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS_BUTTON (ide_notifications_button_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeNotificationsButton, ide_notifications_button, IDE, NOTIFICATIONS_BUTTON, 
DzlProgressMenuButton)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_notifications_button_new (void);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notifications-button.ui b/src/libide/gui/ide-notifications-button.ui
new file mode 100644
index 000000000..ebf209a45
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeNotificationsButton" parent="DzlProgressMenuButton">
+    <property name="show-progress">false</property>
+    <property name="popover">popover</property>
+  </template>
+  <object class="GtkPopover" id="popover">
+    <style>
+      <class name="notificationsbutton"/>
+    </style>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">true</property>
+        <property name="max-content-width">400</property>
+        <property name="min-content-width">400</property>
+        <property name="hscrollbar-policy">never</property>
+        <property name="propagate-natural-width">false</property>
+        <property name="propagate-natural-height">true</property>
+        <child>
+          <object class="GtkListBox" id="list_box">
+            <signal name="row-activated" handler="ide_notifications_button_row_activated" swapped="true" 
object="IdeNotificationsButton"/>
+            <property name="selection-mode">none</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+
+
+
diff --git a/src/libide/gui/ide-omni-bar-addin.c b/src/libide/gui/ide-omni-bar-addin.c
new file mode 100644
index 000000000..49d6a3b30
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar-addin.c
@@ -0,0 +1,89 @@
+/* ide-omni-bar-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-omni-bar-addin"
+
+#include "config.h"
+
+#include "ide-omni-bar-addin.h"
+
+/**
+ * SECTION:ide-omni-bar-addin
+ * @title: IdeOmniBarAddin
+ * @short_description: addins to extend the #IdeOmniBar
+ *
+ * The #IdeOmniBarAddin allows plugins to extend how the #IdeOmniBar
+ * works. They can add additional components such as buttons, or more
+ * information to the popover.
+ *
+ * See #IdeOmniBar for information about what you can alter.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeOmniBarAddin, ide_omni_bar_addin, G_TYPE_OBJECT)
+
+static void
+ide_omni_bar_addin_default_init (IdeOmniBarAddinInterface *iface)
+{
+}
+
+/**
+ * ide_omni_bar_addin_load:
+ * @self: an #IdeOmniBarAddin
+ * @omni_bar: an #IdeOmniBar
+ *
+ * Requests that the #IdeOmniBarAddin initialize, possibly modifying
+ * @omni_bar as necessary.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_addin_load (IdeOmniBarAddin *self,
+                         IdeOmniBar      *omni_bar)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR_ADDIN (self));
+  g_return_if_fail (IDE_IS_OMNI_BAR (omni_bar));
+
+  if (IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->load)
+    IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->load (self, omni_bar);
+}
+
+/**
+ * ide_omni_bar_addin_unload:
+ * @self: an #IdeOmniBarAddin
+ * @omni_bar: an #IdeOmniBar
+ *
+ * Requests that the #IdeOmniBarAddin shutdown, possibly modifying
+ * @omni_bar as necessary to return it to the original state before
+ * the addin was loaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_addin_unload (IdeOmniBarAddin *self,
+                           IdeOmniBar      *omni_bar)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR_ADDIN (self));
+  g_return_if_fail (IDE_IS_OMNI_BAR (omni_bar));
+
+  if (IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->unload)
+    IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->unload (self, omni_bar);
+}
diff --git a/src/libide/gui/ide-omni-bar-addin.h b/src/libide/gui/ide-omni-bar-addin.h
new file mode 100644
index 000000000..0b2290d39
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar-addin.h
@@ -0,0 +1,55 @@
+/* ide-omni-bar-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-omni-bar.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OMNI_BAR_ADDIN (ide_omni_bar_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeOmniBarAddin, ide_omni_bar_addin, IDE, OMNI_BAR_ADDIN, GObject)
+
+struct _IdeOmniBarAddinInterface
+{
+  GTypeInterface parent;
+
+  void (*load)   (IdeOmniBarAddin *self,
+                  IdeOmniBar      *omni_bar);
+  void (*unload) (IdeOmniBarAddin *self,
+                  IdeOmniBar      *omni_bar);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_omni_bar_addin_load   (IdeOmniBarAddin *self,
+                                IdeOmniBar      *omni_bar);
+IDE_AVAILABLE_IN_3_32
+void ide_omni_bar_addin_unload (IdeOmniBarAddin *self,
+                                IdeOmniBar      *omni_bar);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-omni-bar.c b/src/libide/gui/ide-omni-bar.c
new file mode 100644
index 000000000..fbf8c6399
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.c
@@ -0,0 +1,619 @@
+/* ide-omni-bar.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-omni-bar"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <dazzle.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-notification-list-box-row-private.h"
+#include "ide-notification-stack-private.h"
+#include "ide-omni-bar-addin.h"
+#include "ide-omni-bar.h"
+
+struct _IdeOmniBar
+{
+  GtkEventBox           parent_instance;
+
+  PeasExtensionSet     *addins;
+  GtkGesture           *gesture;
+  GtkEventController   *motion;
+
+  GtkStack             *top_stack;
+  GtkPopover           *popover;
+  DzlEntryBox          *entry_box;
+  IdeNotificationStack *notification_stack;
+  GtkListBox           *notifications_list_box;
+  DzlPriorityBox       *inner_box;
+  DzlPriorityBox       *outer_box;
+  GtkProgressBar       *progress;
+  GtkWidget            *placeholder;
+  DzlPriorityBox       *sections_box;
+};
+
+static void ide_omni_bar_move_next     (IdeOmniBar        *self,
+                                        GVariant          *param);
+static void ide_omni_bar_move_previous (IdeOmniBar        *self,
+                                        GVariant          *param);
+static void buildable_iface_init       (GtkBuildableIface *iface);
+
+DZL_DEFINE_ACTION_GROUP (IdeOmniBar, ide_omni_bar, {
+  { "move-next", ide_omni_bar_move_next },
+  { "move-previous", ide_omni_bar_move_previous },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeOmniBar, ide_omni_bar, GTK_TYPE_EVENT_BOX,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_omni_bar_init_action_group)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static void
+ide_omni_bar_popover_closed_cb (IdeOmniBar *self,
+                                GtkPopover *popover)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_POPOVER (popover));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  state_flags &= ~GTK_STATE_FLAG_ACTIVE;
+  state_flags &= ~GTK_STATE_FLAG_PRELIGHT;
+
+  gtk_style_context_set_state (style_context, state_flags);
+}
+
+static void
+multipress_pressed_cb (IdeOmniBar           *self,
+                       guint                 n_press,
+                       gdouble               x,
+                       gdouble               y,
+                       GtkGestureMultiPress *gesture)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
+
+  gtk_popover_popup (self->popover);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+  gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_ACTIVE);
+
+  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+ide_omni_bar_notification_stack_changed_cb (IdeOmniBar           *self,
+                                            IdeNotificationStack *stack)
+{
+  IdeNotification *notif;
+  gboolean enabled;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_NOTIFICATION_STACK (stack));
+
+  enabled = ide_notification_stack_get_can_move (stack);
+
+  ide_omni_bar_set_action_enabled (self, "move-previous", enabled);
+  ide_omni_bar_set_action_enabled (self, "move-next", enabled);
+
+  _ide_gtk_progress_bar_stop_pulsing (self->progress);
+  gtk_widget_hide (GTK_WIDGET (self->progress));
+
+  if ((notif = ide_notification_stack_get_visible (stack)))
+    {
+      if (ide_notification_get_has_progress (notif))
+        {
+          if (ide_notification_get_progress_is_imprecise (notif))
+            _ide_gtk_progress_bar_start_pulsing (self->progress);
+          gtk_widget_show (GTK_WIDGET (self->progress));
+        }
+    }
+
+  if (ide_notification_stack_is_empty (stack))
+    gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
+  else
+    gtk_stack_set_visible_child_name (self->top_stack, "notifications");
+}
+
+static void
+ide_omni_bar_extension_added_cb (PeasExtensionSet *set,
+                                 PeasPluginInfo   *plugin_info,
+                                 PeasExtension    *exten,
+                                 gpointer          user_data)
+{
+  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
+  IdeOmniBar *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  ide_omni_bar_addin_load (addin, self);
+}
+
+static void
+ide_omni_bar_extension_removed_cb (PeasExtensionSet *set,
+                                   PeasPluginInfo   *plugin_info,
+                                   PeasExtension    *exten,
+                                   gpointer          user_data)
+{
+  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
+  IdeOmniBar *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  ide_omni_bar_addin_unload (addin, self);
+}
+
+static GtkWidget *
+create_notification_row (gpointer item,
+                         gpointer user_data)
+{
+  IdeNotification *notif = item;
+  gboolean has_default;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  has_default = ide_notification_get_default_action (notif, NULL, NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "activatable", has_default,
+                       "notification", notif,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static gboolean
+filter_for_popover (GObject  *object,
+                    gpointer  user_data)
+{
+  IdeNotification *notif = (IdeNotification *)object;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (user_data == NULL);
+
+  return !ide_notification_get_has_progress (notif) &&
+         ide_notification_get_urgent (notif);
+}
+
+static void
+ide_omni_bar_context_set_cb (GtkWidget  *widget,
+                             IdeContext *context)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+  g_autoptr(IdeObject) notifications = NULL;
+  g_autoptr(DzlListModelFilter) filter = NULL;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (self->addins == NULL);
+
+  notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
+  ide_notification_stack_bind_model (self->notification_stack, G_LIST_MODEL (notifications));
+
+  filter = dzl_list_model_filter_new (G_LIST_MODEL (notifications));
+  dzl_list_model_filter_set_filter_func (filter, filter_for_popover, NULL, NULL);
+  gtk_list_box_bind_model (self->notifications_list_box,
+                           G_LIST_MODEL (filter),
+                           create_notification_row,
+                           NULL, NULL);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_OMNI_BAR_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_omni_bar_extension_added_cb),
+                    self);
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_omni_bar_extension_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_omni_bar_extension_added_cb,
+                              self);
+}
+
+static void
+ide_omni_bar_motion_enter_cb (IdeOmniBar               *self,
+                              gdouble                   x,
+                              gdouble                   y,
+                              GtkEventControllerMotion *motion)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  if ((state_flags & GTK_STATE_FLAG_PRELIGHT) == 0)
+    gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_PRELIGHT);
+}
+
+static void
+ide_omni_bar_motion_leave_cb (IdeOmniBar               *self,
+                              GtkEventControllerMotion *motion)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  if (state_flags & GTK_STATE_FLAG_PRELIGHT)
+    gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_PRELIGHT);
+}
+
+static void
+ide_omni_bar_motion_cb (IdeOmniBar               *self,
+                        gdouble                   x,
+                        gdouble                   y,
+                        GtkEventControllerMotion *motion)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  /*
+   * Because of how crossing-events work with Gtk 3, we don't get reliable
+   * crossing events for the motion controller. So every motion (which we do
+   * seem to get semi-reliably), just re-run the enter-notify path to ensure
+   * we get proper state set.
+   */
+
+  ide_omni_bar_motion_enter_cb (self, x, y, motion);
+}
+
+static gboolean
+ide_omni_bar_query_tooltip (GtkWidget  *widget,
+                            gint        x,
+                            gint        y,
+                            gboolean    keyboard_mode,
+                            GtkTooltip *tooltip)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+
+  if ((notif = ide_notification_stack_get_visible (self->notification_stack)))
+    {
+      g_autofree gchar *body = ide_notification_dup_body (notif);
+
+      if (body != NULL)
+        {
+          gtk_tooltip_set_text (tooltip, body);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+ide_omni_bar_notification_row_activated (IdeOmniBar                *self,
+                                         IdeNotificationListBoxRow *row,
+                                         GtkListBox                *list_box)
+{
+  g_autofree gchar *default_action = NULL;
+  g_autoptr(GVariant) default_target = NULL;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  notif = ide_notification_list_box_row_get_notification (row);
+
+  if (ide_notification_get_default_action (notif, &default_action, &default_target))
+    {
+      gchar *name = strchr (default_action, '.');
+      gchar *group = default_action;
+
+      if (name != NULL)
+        {
+          *name = '\0';
+          name++;
+        }
+      else
+        {
+          group = NULL;
+          name = default_action;
+        }
+
+      dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
+    }
+}
+
+static void
+ide_omni_bar_destroy (GtkWidget *widget)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  if (self->progress != NULL)
+    _ide_gtk_progress_bar_stop_pulsing (self->progress);
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->gesture);
+  g_clear_object (&self->motion);
+
+  GTK_WIDGET_CLASS (ide_omni_bar_parent_class)->destroy (widget);
+}
+
+static void
+ide_omni_bar_class_init (IdeOmniBarClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_omni_bar_destroy;
+  widget_class->query_tooltip = ide_omni_bar_query_tooltip;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-omni-bar.ui");
+  gtk_widget_class_set_css_name (widget_class, "omnibar");
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, inner_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notification_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notifications_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, outer_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, progress);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, sections_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, top_stack);
+  gtk_widget_class_bind_template_callback (widget_class, ide_omni_bar_notification_row_activated);
+
+  g_type_ensure (DZL_TYPE_ENTRY_BOX);
+  g_type_ensure (IDE_TYPE_NOTIFICATION_STACK);
+}
+
+static void
+ide_omni_bar_init (IdeOmniBar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);
+
+  gtk_widget_add_events (GTK_WIDGET (self),
+                         (GDK_POINTER_MOTION_MASK |
+                          GDK_ENTER_NOTIFY_MASK |
+                          GDK_LEAVE_NOTIFY_MASK));
+
+  self->motion = gtk_event_controller_motion_new (GTK_WIDGET (self));
+  gtk_event_controller_set_propagation_phase (self->motion, GTK_PHASE_CAPTURE);
+
+  g_signal_connect_swapped (self->motion,
+                            "enter",
+                            G_CALLBACK (ide_omni_bar_motion_enter_cb),
+                            self);
+
+  g_signal_connect_swapped (self->motion,
+                            "motion",
+                            G_CALLBACK (ide_omni_bar_motion_cb),
+                            self);
+
+  g_signal_connect_swapped (self->motion,
+                            "leave",
+                            G_CALLBACK (ide_omni_bar_motion_leave_cb),
+                            self);
+
+  self->gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self));
+
+  g_signal_connect_swapped (self->gesture,
+                            "pressed",
+                            G_CALLBACK (multipress_pressed_cb),
+                            self);
+
+  g_signal_connect_object (self->notification_stack,
+                           "changed",
+                           G_CALLBACK (ide_omni_bar_notification_stack_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->popover,
+                           "closed",
+                           G_CALLBACK (ide_omni_bar_popover_closed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "omnibar", G_ACTION_GROUP (self));
+
+  ide_widget_set_context_handler (GTK_WIDGET (self), ide_omni_bar_context_set_cb);
+}
+
+GtkWidget *
+ide_omni_bar_new (void)
+{
+  return g_object_new (IDE_TYPE_OMNI_BAR, NULL);
+}
+
+static void
+ide_omni_bar_move_next (IdeOmniBar *self,
+                        GVariant   *param)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (param == NULL);
+
+  ide_notification_stack_move_next (self->notification_stack);
+}
+
+static void
+ide_omni_bar_move_previous (IdeOmniBar *self,
+                            GVariant   *param)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (param == NULL);
+
+  ide_notification_stack_move_previous (self->notification_stack);
+}
+
+/**
+ * ide_omni_bar_add_status_icon:
+ * @self: a #IdeOmniBar
+ * @widget: the #GtkWidget to add
+ * @priority: the sort priority for @widget
+ *
+ * Adds a status-icon style widget to the end of the omnibar. Generally,
+ * you'll want this to be either a GtkButton, GtkLabel, or something simple.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_add_status_icon (IdeOmniBar *self,
+                              GtkWidget  *widget,
+                              gint        priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->inner_box), widget,
+                                     "pack-type", GTK_PACK_END,
+                                     "priority", priority,
+                                     NULL);
+}
+
+void
+ide_omni_bar_add_button (IdeOmniBar  *self,
+                         GtkWidget   *widget,
+                         GtkPackType  pack_type,
+                         gint         priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+  g_return_if_fail (pack_type == GTK_PACK_START ||
+                    pack_type == GTK_PACK_END);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->outer_box), widget,
+                                     "pack-type", pack_type,
+                                     "priority", priority,
+                                     NULL);
+}
+
+void
+ide_omni_bar_set_placeholder (IdeOmniBar *self,
+                              GtkWidget  *widget)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (!widget || GTK_IS_WIDGET (widget));
+
+  if (self->placeholder == widget)
+    return;
+
+  if (self->placeholder)
+    gtk_widget_destroy (self->placeholder);
+
+  self->placeholder = widget;
+
+  if (self->placeholder)
+    {
+      g_signal_connect (self->placeholder,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        self->placeholder);
+      gtk_container_add_with_properties (GTK_CONTAINER (self->top_stack), self->placeholder,
+                                         "name", "placeholder",
+                                         NULL);
+      if (self->notification_stack == NULL ||
+          ide_notification_stack_is_empty (self->notification_stack))
+        gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
+    }
+}
+
+static void
+ide_omni_bar_add_child (GtkBuildable *buildable,
+                          GtkBuilder   *builder,
+                        GObject      *child,
+                        const gchar  *type)
+{
+  IdeOmniBar *self = (IdeOmniBar *)buildable;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (G_IS_OBJECT (child));
+
+  if (ide_str_equal0 (type, "start") && GTK_IS_WIDGET (child))
+    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
+                             GTK_WIDGET (child),
+                             GTK_PACK_START,
+                             0);
+  else if (ide_str_equal0 (type, "end") && GTK_IS_WIDGET (child))
+    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
+                             GTK_WIDGET (child),
+                             GTK_PACK_END,
+                             0);
+  else if (ide_str_equal0 (type, "placeholder") && GTK_IS_WIDGET (child))
+    ide_omni_bar_set_placeholder (IDE_OMNI_BAR (self), GTK_WIDGET (child));
+  else
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+  iface->add_child = ide_omni_bar_add_child;
+}
+
+/**
+ * ide_omni_bar_add_popover_section:
+ * @self: an #IdeOmniBar
+ * @widget: a #GtkWidget
+ * @priority: sort priority for the section
+ *
+ * Adds @widget to the omnibar popover, sorted by @priority
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_add_popover_section (IdeOmniBar *self,
+                                  GtkWidget  *widget,
+                                  gint        priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->sections_box), widget,
+                                     "priority", priority,
+                                     NULL);
+}
diff --git a/src/libide/gui/ide-omni-bar.h b/src/libide/gui/ide-omni-bar.h
new file mode 100644
index 000000000..ef4a9e484
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.h
@@ -0,0 +1,56 @@
+/* ide-omni-bar.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OMNI_BAR (ide_omni_bar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeOmniBar, ide_omni_bar, IDE, OMNI_BAR, GtkEventBox)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_omni_bar_new                 (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_status_icon     (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             gint         priority);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_button          (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             GtkPackType  pack_type,
+                                             gint         priority);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_set_placeholder     (IdeOmniBar  *self,
+                                             GtkWidget   *placeholder);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_popover_section (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             gint         priority);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-omni-bar.ui b/src/libide/gui/ide-omni-bar.ui
new file mode 100644
index 000000000..53591ff36
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.ui
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeOmniBar" parent="GtkEventBox">
+    <child>
+      <object class="DzlPriorityBox" id="outer_box">
+        <property name="hexpand">true</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="linked"/>
+        </style>
+        <child type="center">
+          <object class="DzlEntryBox" id="entry_box">
+            <property name="max-width-chars">40</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkOverlay" id="overlay">
+                <property name="visible">true</property>
+                <child type="overlay">
+                  <object class="GtkProgressBar" id="progress">
+                    <property name="valign">end</property>
+                    <property name="hexpand">true</property>
+                    <property name="fraction" bind-source="notification_stack" bind-property="progress"/>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="osd"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="DzlPriorityBox" id="inner_box">
+                    <property name="margin-top">1</property>
+                    <property name="spacing">3</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="hexpand">false</property>
+                        <property name="vexpand">false</property>
+                        <property name="valign">center</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="pan"/>>
+                        </style>
+                        <child>
+                          <object class="GtkButton">
+                            <property name="action-name">omnibar.move-previous</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">pan-up-symbolic</property>
+                                <property name="pixel-size">12</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton">
+                            <property name="action-name">omnibar.move-next</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">pan-down-symbolic</property>
+                                <property name="pixel-size">12</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkStack" id="top_stack">
+                        <property name="margin-start">3</property>
+                        <property name="margin-end">3</property>
+                        <property name="hexpand">true</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="IdeNotificationStack" id="notification_stack">
+                            <property name="visible">true</property>
+                          </object>
+                          <packing>
+                            <property name="name">notifications</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">true</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkPopover" id="popover">
+    <property name="width-request">500</property>
+    <property name="relative-to">IdeOmniBar</property>
+    <property name="position">top</property>
+    <style>
+      <class name="omnibar"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlPriorityBox" id="sections_box">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkListBox" id="notifications_list_box">
+            <signal name="row-activated" swapped="true" object="IdeOmniBar" 
handler="ide_omni_bar_notification_row_activated"/>
+            <property name="selection-mode">none</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/libide/layout/ide-layout-view.c b/src/libide/gui/ide-page.c
similarity index 57%
rename from src/libide/layout/ide-layout-view.c
rename to src/libide/gui/ide-page.c
index d33946fb9..6e6c40925 100644
--- a/src/libide/layout/ide-layout-view.c
+++ b/src/libide/gui/ide-page.c
@@ -1,4 +1,4 @@
-/* ide-layout-view.c
+/* ide-page.c
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
@@ -18,17 +18,22 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-view"
+#define G_LOG_DOMAIN "ide-page"
 
 #include "config.h"
 
+#include <libide-threading.h>
 #include <string.h>
 
-#include "layout/ide-layout-view.h"
-#include "threading/ide-task.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-page.h"
+#include "ide-workspace.h"
 
 typedef struct
 {
+  GList        mru_link;
+
   const gchar *menu_id;
   const gchar *icon_name;
   gchar       *title;
@@ -42,7 +47,7 @@ typedef struct
   guint        can_split : 1;
   guint        primary_color_bg_set : 1;
   guint        primary_color_fg_set : 1;
-} IdeLayoutViewPrivate;
+} IdePagePrivate;
 
 enum {
   PROP_0,
@@ -59,38 +64,38 @@ enum {
 };
 
 enum {
-  CREATE_SPLIT_VIEW,
+  CREATE_SPLIT,
   N_SIGNALS
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (IdeLayoutView, ide_layout_view, GTK_TYPE_BOX)
+G_DEFINE_TYPE_WITH_PRIVATE (IdePage, ide_page, GTK_TYPE_BOX)
 
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
 static void
-ide_layout_view_real_agree_to_close_async (IdeLayoutView       *self,
-                                           GCancellable        *cancellable,
-                                           GAsyncReadyCallback  callback,
-                                           gpointer             user_data)
+ide_page_real_agree_to_close_async (IdePage             *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
 {
   g_autoptr(IdeTask) task = NULL;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (self));
+  g_assert (IDE_IS_PAGE (self));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_priority (task, G_PRIORITY_LOW);
-  ide_task_set_source_tag (task, ide_layout_view_agree_to_close_async);
+  ide_task_set_source_tag (task, ide_page_agree_to_close_async);
   ide_task_return_boolean (task, TRUE);
 }
 
 static gboolean
-ide_layout_view_real_agree_to_close_finish (IdeLayoutView  *self,
-                                            GAsyncResult   *result,
-                                            GError        **error)
+ide_page_real_agree_to_close_finish (IdePage       *self,
+                                     GAsyncResult  *result,
+                                     GError       **error)
 {
-  g_assert (IDE_IS_LAYOUT_VIEW (self));
+  g_assert (IDE_IS_PAGE (self));
   g_assert (IDE_IS_TASK (result));
 
   return ide_task_propagate_boolean (IDE_TASK (result), error);
@@ -105,11 +110,11 @@ find_focus_child (GtkWidget *widget,
 }
 
 static void
-ide_layout_view_grab_focus (GtkWidget *widget)
+ide_page_grab_focus (GtkWidget *widget)
 {
   gboolean handled = FALSE;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (widget));
+  g_assert (IDE_IS_PAGE (widget));
 
   /*
    * This default grab_focus override just looks for the first child (generally
@@ -122,61 +127,107 @@ ide_layout_view_grab_focus (GtkWidget *widget)
 }
 
 static void
-ide_layout_view_finalize (GObject *object)
+ide_page_hierarchy_changed (GtkWidget *widget,
+                            GtkWidget *previous_toplevel)
+{
+  IdePage *self = (IdePage *)widget;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_PAGE (self));
+  g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
+
+  if (IDE_IS_WORKSPACE (previous_toplevel))
+    _ide_workspace_remove_page_mru (IDE_WORKSPACE (previous_toplevel), &priv->mru_link);
+
+  if (GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed)
+    GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed (widget, previous_toplevel);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (IDE_IS_WORKSPACE (toplevel))
+    _ide_workspace_add_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
+}
+
+/**
+ * ide_page_mark_used:
+ * @self: a #IdePage
+ *
+ * This function marks the page as used by updating it's position in the
+ * workspaces MRU (most-recently-used) queue.
+ *
+ * Pages should call this when their contents have been focused.
+ *
+ * Since: 3.32
+ */
+void
+ide_page_mark_used (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  IdeWorkspace *workspace;
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  if ((workspace = ide_widget_get_workspace (GTK_WIDGET (self))))
+    _ide_workspace_move_front_page_mru (workspace, &priv->mru_link);
+}
+
+static void
+ide_page_finalize (GObject *object)
 {
-  IdeLayoutView *self = (IdeLayoutView *)object;
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePage *self = (IdePage *)object;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
   g_clear_pointer (&priv->title, g_free);
   g_clear_object (&priv->icon);
 
-  G_OBJECT_CLASS (ide_layout_view_parent_class)->finalize (object);
+  G_OBJECT_CLASS (ide_page_parent_class)->finalize (object);
 }
 
 static void
-ide_layout_view_get_property (GObject    *object,
-                              guint       prop_id,
-                              GValue     *value,
-                              GParamSpec *pspec)
+ide_page_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
 {
-  IdeLayoutView *self = IDE_LAYOUT_VIEW (object);
+  IdePage *self = IDE_PAGE (object);
 
   switch (prop_id)
     {
     case PROP_CAN_SPLIT:
-      g_value_set_boolean (value, ide_layout_view_get_can_split (self));
+      g_value_set_boolean (value, ide_page_get_can_split (self));
       break;
 
     case PROP_FAILED:
-      g_value_set_boolean (value, ide_layout_view_get_failed (self));
+      g_value_set_boolean (value, ide_page_get_failed (self));
       break;
 
     case PROP_ICON_NAME:
-      g_value_set_static_string (value, ide_layout_view_get_icon_name (self));
+      g_value_set_static_string (value, ide_page_get_icon_name (self));
       break;
 
     case PROP_ICON:
-      g_value_set_object (value, ide_layout_view_get_icon (self));
+      g_value_set_object (value, ide_page_get_icon (self));
       break;
 
     case PROP_MENU_ID:
-      g_value_set_static_string (value, ide_layout_view_get_menu_id (self));
+      g_value_set_static_string (value, ide_page_get_menu_id (self));
       break;
 
     case PROP_MODIFIED:
-      g_value_set_boolean (value, ide_layout_view_get_modified (self));
+      g_value_set_boolean (value, ide_page_get_modified (self));
       break;
 
     case PROP_PRIMARY_COLOR_BG:
-      g_value_set_boxed (value, ide_layout_view_get_primary_color_bg (self));
+      g_value_set_boxed (value, ide_page_get_primary_color_bg (self));
       break;
 
     case PROP_PRIMARY_COLOR_FG:
-      g_value_set_boxed (value, ide_layout_view_get_primary_color_fg (self));
+      g_value_set_boxed (value, ide_page_get_primary_color_fg (self));
       break;
 
     case PROP_TITLE:
-      g_value_set_string (value, ide_layout_view_get_title (self));
+      g_value_set_string (value, ide_page_get_title (self));
       break;
 
     default:
@@ -185,49 +236,49 @@ ide_layout_view_get_property (GObject    *object,
 }
 
 static void
-ide_layout_view_set_property (GObject      *object,
-                              guint         prop_id,
-                              const GValue *value,
-                              GParamSpec   *pspec)
+ide_page_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
 {
-  IdeLayoutView *self = IDE_LAYOUT_VIEW (object);
+  IdePage *self = IDE_PAGE (object);
 
   switch (prop_id)
     {
     case PROP_CAN_SPLIT:
-      ide_layout_view_set_can_split (self, g_value_get_boolean (value));
+      ide_page_set_can_split (self, g_value_get_boolean (value));
       break;
 
     case PROP_FAILED:
-      ide_layout_view_set_failed (self, g_value_get_boolean (value));
+      ide_page_set_failed (self, g_value_get_boolean (value));
       break;
 
     case PROP_ICON_NAME:
-      ide_layout_view_set_icon_name (self, g_value_get_string (value));
+      ide_page_set_icon_name (self, g_value_get_string (value));
       break;
 
     case PROP_ICON:
-      ide_layout_view_set_icon (self, g_value_get_object (value));
+      ide_page_set_icon (self, g_value_get_object (value));
       break;
 
     case PROP_MENU_ID:
-      ide_layout_view_set_menu_id (self, g_value_get_string (value));
+      ide_page_set_menu_id (self, g_value_get_string (value));
       break;
 
     case PROP_MODIFIED:
-      ide_layout_view_set_modified (self, g_value_get_boolean (value));
+      ide_page_set_modified (self, g_value_get_boolean (value));
       break;
 
     case PROP_PRIMARY_COLOR_BG:
-      ide_layout_view_set_primary_color_bg (self, g_value_get_boxed (value));
+      ide_page_set_primary_color_bg (self, g_value_get_boxed (value));
       break;
 
     case PROP_PRIMARY_COLOR_FG:
-      ide_layout_view_set_primary_color_fg (self, g_value_get_boxed (value));
+      ide_page_set_primary_color_fg (self, g_value_get_boxed (value));
       break;
 
     case PROP_TITLE:
-      ide_layout_view_set_title (self, g_value_get_string (value));
+      ide_page_set_title (self, g_value_get_string (value));
       break;
 
     default:
@@ -236,19 +287,20 @@ ide_layout_view_set_property (GObject      *object,
 }
 
 static void
-ide_layout_view_class_init (IdeLayoutViewClass *klass)
+ide_page_class_init (IdePageClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->finalize = ide_layout_view_finalize;
-  object_class->get_property = ide_layout_view_get_property;
-  object_class->set_property = ide_layout_view_set_property;
+  object_class->finalize = ide_page_finalize;
+  object_class->get_property = ide_page_get_property;
+  object_class->set_property = ide_page_set_property;
 
-  widget_class->grab_focus = ide_layout_view_grab_focus;
+  widget_class->grab_focus = ide_page_grab_focus;
+  widget_class->hierarchy_changed = ide_page_hierarchy_changed;
 
-  klass->agree_to_close_async = ide_layout_view_real_agree_to_close_async;
-  klass->agree_to_close_finish = ide_layout_view_real_agree_to_close_finish;
+  klass->agree_to_close_async = ide_page_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_page_real_agree_to_close_finish;
 
   properties [PROP_CAN_SPLIT] =
     g_param_spec_boolean ("can-split",
@@ -293,7 +345,7 @@ ide_layout_view_class_init (IdeLayoutViewClass *klass)
                           (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   /**
-   * IdeLayoutView:primary-color-bg:
+   * IdePage:primary-color-bg:
    *
    * The "primary-color-bg" property should describe the primary color
    * of the content of the view (if any).
@@ -311,7 +363,7 @@ ide_layout_view_class_init (IdeLayoutViewClass *klass)
                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   /**
-   * IdeLayoutView:primary-color-fg:
+   * IdePage:primary-color-fg:
    *
    * The "primary-color-fg" property should describe the foreground
    * to use for content above primary-color-bg.
@@ -338,36 +390,38 @@ ide_layout_view_class_init (IdeLayoutViewClass *klass)
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   /**
-   * IdeLayoutView::create-split-view:
+   * IdePage::create-split:
+   * @self: an #IdePage
    *
    * This signal is emitted when the view is requested to make a split
    * version of itself. This happens when the user requests that a second
    * version of the file to be displayed, often side-by-side.
    *
-   * This signal will only be emitted when #IdeLayoutView:can-split is
+   * This signal will only be emitted when #IdePage:can-split is
    * set to %TRUE. The default is %FALSE.
    *
-   * Returns: (transfer full): A newly created #IdeLayoutView
+   * Returns: (transfer full): A newly created #IdePage
    *
    * Since: 3.32
    */
-  signals [CREATE_SPLIT_VIEW] =
-    g_signal_new (g_intern_static_string ("create-split-view"),
+  signals [CREATE_SPLIT] =
+    g_signal_new (g_intern_static_string ("create-split"),
                   G_TYPE_FROM_CLASS (klass),
                   G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (IdeLayoutViewClass, create_split_view),
+                  G_STRUCT_OFFSET (IdePageClass, create_split),
                   g_signal_accumulator_first_wins, NULL,
-                  NULL, IDE_TYPE_LAYOUT_VIEW, 0);
+                  NULL, IDE_TYPE_PAGE, 0);
 }
 
 static void
-ide_layout_view_init (IdeLayoutView *self)
+ide_page_init (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
   g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new ();
 
   gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
 
+  priv->mru_link.data = self;
   priv->icon_name = g_intern_string ("text-x-generic-symbolic");
 
   /* Add an action group out of convenience to plugins that want to
@@ -377,28 +431,28 @@ ide_layout_view_init (IdeLayoutView *self)
 }
 
 GtkWidget *
-ide_layout_view_new (void)
+ide_page_new (void)
 {
-  return g_object_new (IDE_TYPE_LAYOUT_VIEW, NULL);
+  return g_object_new (IDE_TYPE_PAGE, NULL);
 }
 
 const gchar *
-ide_layout_view_get_title (IdeLayoutView *self)
+ide_page_get_title (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   return priv->title;
 }
 
 void
-ide_layout_view_set_title (IdeLayoutView *self,
-                           const gchar   *title)
+ide_page_set_title (IdePage     *self,
+                    const gchar *title)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   if (g_strcmp0 (title, priv->title) != 0)
     {
@@ -409,22 +463,22 @@ ide_layout_view_set_title (IdeLayoutView *self,
 }
 
 const gchar *
-ide_layout_view_get_menu_id (IdeLayoutView *self)
+ide_page_get_menu_id (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   return priv->menu_id;
 }
 
 void
-ide_layout_view_set_menu_id (IdeLayoutView *self,
-                             const gchar   *menu_id)
+ide_page_set_menu_id (IdePage     *self,
+                      const gchar *menu_id)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   menu_id = g_intern_string (menu_id);
 
@@ -436,45 +490,45 @@ ide_layout_view_set_menu_id (IdeLayoutView *self,
 }
 
 void
-ide_layout_view_agree_to_close_async (IdeLayoutView       *self,
-                                      GCancellable        *cancellable,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
+ide_page_agree_to_close_async (IdePage             *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
 {
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  IDE_LAYOUT_VIEW_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+  IDE_PAGE_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
 }
 
 gboolean
-ide_layout_view_agree_to_close_finish (IdeLayoutView  *self,
-                                       GAsyncResult   *result,
-                                       GError        **error)
+ide_page_agree_to_close_finish (IdePage       *self,
+                                GAsyncResult  *result,
+                                GError       **error)
 {
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
   g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
 
-  return IDE_LAYOUT_VIEW_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+  return IDE_PAGE_GET_CLASS (self)->agree_to_close_finish (self, result, error);
 }
 
 gboolean
-ide_layout_view_get_failed (IdeLayoutView *self)
+ide_page_get_failed (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
 
   return priv->failed;
 }
 
 void
-ide_layout_view_set_failed (IdeLayoutView *self,
-                            gboolean       failed)
+ide_page_set_failed (IdePage  *self,
+                     gboolean  failed)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   failed = !!failed;
 
@@ -486,22 +540,22 @@ ide_layout_view_set_failed (IdeLayoutView *self,
 }
 
 gboolean
-ide_layout_view_get_modified (IdeLayoutView *self)
+ide_page_get_modified (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
 
   return priv->modified;
 }
 
 void
-ide_layout_view_set_modified (IdeLayoutView *self,
-                              gboolean       modified)
+ide_page_set_modified (IdePage  *self,
+                       gboolean  modified)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   modified = !!modified;
 
@@ -513,8 +567,8 @@ ide_layout_view_set_modified (IdeLayoutView *self,
 }
 
 /**
- * ide_layout_view_get_icon:
- * @self: a #IdeLayoutView
+ * ide_page_get_icon:
+ * @self: a #IdePage
  *
  * Gets the #GIcon to represent the view.
  *
@@ -523,11 +577,11 @@ ide_layout_view_set_modified (IdeLayoutView *self,
  * Since: 3.32
  */
 GIcon *
-ide_layout_view_get_icon (IdeLayoutView *self)
+ide_page_get_icon (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   if (priv->icon == NULL)
     {
@@ -539,34 +593,34 @@ ide_layout_view_get_icon (IdeLayoutView *self)
 }
 
 void
-ide_layout_view_set_icon (IdeLayoutView *self,
-                          GIcon         *icon)
+ide_page_set_icon (IdePage *self,
+                   GIcon   *icon)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   if (g_set_object (&priv->icon, icon))
     g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
 }
 
 const gchar *
-ide_layout_view_get_icon_name (IdeLayoutView *self)
+ide_page_get_icon_name (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   return priv->icon_name;
 }
 
 void
-ide_layout_view_set_icon_name (IdeLayoutView *self,
-                               const gchar   *icon_name)
+ide_page_set_icon_name (IdePage     *self,
+                        const gchar *icon_name)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   icon_name = g_intern_string (icon_name);
 
@@ -579,22 +633,22 @@ ide_layout_view_set_icon_name (IdeLayoutView *self,
 }
 
 gboolean
-ide_layout_view_get_can_split (IdeLayoutView *self)
+ide_page_get_can_split (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
 
   return priv->can_split;
 }
 
 void
-ide_layout_view_set_can_split (IdeLayoutView *self,
-                               gboolean       can_split)
+ide_page_set_can_split (IdePage  *self,
+                        gboolean  can_split)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   can_split = !!can_split;
 
@@ -606,40 +660,40 @@ ide_layout_view_set_can_split (IdeLayoutView *self,
 }
 
 /**
- * ide_layout_view_create_split_view:
- * @self: an #IdeLayoutView
+ * ide_page_create_split:
+ * @self: an #IdePage
  *
- * This function requests that the #IdeLayoutView create a split version
+ * This function requests that the #IdePage create a split version
  * of itself so that the user may view the document in multiple views.
  *
  * The view should be added to an #IdeLayoutStack where appropriate.
  *
- * Returns: (nullable) (transfer full): A newly created #IdeLayoutView or %NULL.
+ * Returns: (nullable) (transfer full): A newly created #IdePage or %NULL.
  *
  * Since: 3.32
  */
-IdeLayoutView *
-ide_layout_view_create_split_view (IdeLayoutView *self)
+IdePage *
+ide_page_create_split (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
-  IdeLayoutView *ret = NULL;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  IdePage *ret = NULL;
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   if (priv->can_split)
     {
-      g_signal_emit (self, signals [CREATE_SPLIT_VIEW], 0, &ret);
-      g_return_val_if_fail (!ret || IDE_IS_LAYOUT_VIEW (ret), NULL);
+      g_signal_emit (self, signals [CREATE_SPLIT], 0, &ret);
+      g_return_val_if_fail (!ret || IDE_IS_PAGE (ret), NULL);
     }
 
   return ret;
 }
 
 /**
- * ide_layout_view_get_primary_color_bg:
- * @self: a #IdeLayoutView
+ * ide_page_get_primary_color_bg:
+ * @self: a #IdePage
  *
- * Gets the #IdeLayoutView:primary-color-bg property if it has been set.
+ * Gets the #IdePage:primary-color-bg property if it has been set.
  *
  * The primary-color-bg can be used to alter the color of the layout
  * stack header to match the document contents.
@@ -649,34 +703,34 @@ ide_layout_view_create_split_view (IdeLayoutView *self)
  * Since: 3.32
  */
 const GdkRGBA *
-ide_layout_view_get_primary_color_bg (IdeLayoutView *self)
+ide_page_get_primary_color_bg (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   return priv->primary_color_bg_set ?  &priv->primary_color_bg : NULL;
 }
 
 /**
- * ide_layout_view_set_primary_color_bg:
- * @self: a #IdeLayoutView
+ * ide_page_set_primary_color_bg:
+ * @self: a #IdePage
  * @primary_color_bg: (nullable): a #GdkRGBA or %NULL
  *
- * Sets the #IdeLayoutView:primary-color-bg property.
+ * Sets the #IdePage:primary-color-bg property.
  * If @primary_color_bg is %NULL, the property is unset.
  *
  * Since: 3.32
  */
 void
-ide_layout_view_set_primary_color_bg (IdeLayoutView *self,
-                                      const GdkRGBA *primary_color_bg)
+ide_page_set_primary_color_bg (IdePage       *self,
+                               const GdkRGBA *primary_color_bg)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
   gboolean old_set;
   GdkRGBA old;
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   old_set = priv->primary_color_bg_set;
   old = priv->primary_color_bg;
@@ -698,10 +752,10 @@ ide_layout_view_set_primary_color_bg (IdeLayoutView *self,
 }
 
 /**
- * ide_layout_view_get_primary_color_fg:
- * @self: a #IdeLayoutView
+ * ide_page_get_primary_color_fg:
+ * @self: a #IdePage
  *
- * Gets the #IdeLayoutView:primary-color-fg property if it has been set.
+ * Gets the #IdePage:primary-color-fg property if it has been set.
  *
  * The primary-color-fg can be used to alter the foreground color of the layout
  * stack header to match the document contents.
@@ -711,34 +765,34 @@ ide_layout_view_set_primary_color_bg (IdeLayoutView *self,
  * Since: 3.32
  */
 const GdkRGBA *
-ide_layout_view_get_primary_color_fg (IdeLayoutView *self)
+ide_page_get_primary_color_fg (IdePage *self)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
   return priv->primary_color_fg_set ?  &priv->primary_color_fg : NULL;
 }
 
 /**
- * ide_layout_view_set_primary_color_fg:
- * @self: a #IdeLayoutView
+ * ide_page_set_primary_color_fg:
+ * @self: a #IdePage
  * @primary_color_fg: (nullable): a #GdkRGBA or %NULL
  *
- * Sets the #IdeLayoutView:primary-color-fg property.
+ * Sets the #IdePage:primary-color-fg property.
  * If @primary_color_fg is %NULL, the property is unset.
  *
  * Since: 3.32
  */
 void
-ide_layout_view_set_primary_color_fg (IdeLayoutView *self,
-                                      const GdkRGBA *primary_color_fg)
+ide_page_set_primary_color_fg (IdePage       *self,
+                               const GdkRGBA *primary_color_fg)
 {
-  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
   gboolean old_set;
   GdkRGBA old;
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   old_set = priv->primary_color_fg_set;
   old = priv->primary_color_fg;
@@ -760,8 +814,8 @@ ide_layout_view_set_primary_color_fg (IdeLayoutView *self,
 }
 
 /**
- * ide_layout_view_report_error:
- * @self: a #IdeLayoutView
+ * ide_page_report_error:
+ * @self: a #IdePage
  * @format: a printf-style format string
  *
  * This function reports an error to the user in the layout view.
@@ -772,9 +826,9 @@ ide_layout_view_set_primary_color_fg (IdeLayoutView *self,
  * Since: 3.32
  */
 void
-ide_layout_view_report_error (IdeLayoutView *self,
-                              const gchar   *format,
-                              ...)
+ide_page_report_error (IdePage     *self,
+                       const gchar *format,
+                       ...)
 {
   g_autofree gchar *message = NULL;
   GtkInfoBar *infobar;
@@ -782,7 +836,7 @@ ide_layout_view_report_error (IdeLayoutView *self,
   GtkLabel *label;
   va_list args;
 
-  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (IDE_IS_PAGE (self));
 
   va_start (args, format);
   message = g_strdup_vprintf (format, args);
diff --git a/src/libide/gui/ide-page.h b/src/libide/gui/ide-page.h
new file mode 100644
index 000000000..3a5c34631
--- /dev/null
+++ b/src/libide/gui/ide-page.h
@@ -0,0 +1,119 @@
+/* ide-page.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PAGE (ide_page_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdePage, ide_page, IDE, PAGE, GtkBox)
+
+struct _IdePageClass
+{
+  GtkBoxClass parent_class;
+
+  void           (*agree_to_close_async)  (IdePage              *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+  gboolean       (*agree_to_close_finish) (IdePage              *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+  IdePage       *(*create_split)          (IdePage              *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget     *ide_page_new                   (void);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_can_split         (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_can_split         (IdePage              *self,
+                                               gboolean              can_split);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_page_create_split          (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_icon_name         (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_icon_name         (IdePage              *self,
+                                               const gchar          *icon_name);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_page_get_icon              (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_icon              (IdePage              *self,
+                                               GIcon                *icon);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_failed            (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_failed            (IdePage              *self,
+                                               gboolean              failed);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_menu_id           (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_menu_id           (IdePage              *self,
+                                               const gchar          *menu_id);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_modified          (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_modified          (IdePage              *self,
+                                               gboolean              modified);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_title             (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_title             (IdePage              *self,
+                                               const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_page_get_primary_color_bg  (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_primary_color_bg  (IdePage              *self,
+                                               const GdkRGBA        *primary_color_bg);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_page_get_primary_color_fg  (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_primary_color_fg  (IdePage              *self,
+                                               const GdkRGBA        *primary_color_fg);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_agree_to_close_async  (IdePage              *self,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_agree_to_close_finish (IdePage              *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_mark_used             (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_report_error          (IdePage              *self,
+                                               const gchar          *format,
+                                               ...) G_GNUC_PRINTF (2, 3);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-pane.c b/src/libide/gui/ide-pane.c
new file mode 100644
index 000000000..bb1c7ec0a
--- /dev/null
+++ b/src/libide/gui/ide-pane.c
@@ -0,0 +1,54 @@
+/* ide-pane.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-pane"
+
+#include "config.h"
+
+#include "ide-pane.h"
+
+G_DEFINE_TYPE (IdePane, ide_pane, DZL_TYPE_DOCK_WIDGET)
+
+static void
+ide_pane_class_init (IdePaneClass *klass)
+{
+}
+
+static void
+ide_pane_init (IdePane *self)
+{
+}
+
+/**
+ * ide_pane_new:
+ *
+ * Creates a new #IdePane widget.
+ *
+ * These widgets are meant to be added to #IdePanel widgets.
+ *
+ * Returns: (transfer full): a new #IdePane
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_pane_new (void)
+{
+  return g_object_new (IDE_TYPE_PANE, NULL);
+}
diff --git a/src/libide/gui/ide-pane.h b/src/libide/gui/ide-pane.h
new file mode 100644
index 000000000..0eed763d5
--- /dev/null
+++ b/src/libide/gui/ide-pane.h
@@ -0,0 +1,48 @@
+/* ide-pane.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PANE (ide_pane_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdePane, ide_pane, IDE, PANE, DzlDockWidget)
+
+struct _IdePaneClass
+{
+  DzlDockWidgetClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_pane_new (void);
+
+G_END_DECLS
diff --git a/src/libide/layout/ide-layout-pane.c b/src/libide/gui/ide-panel.c
similarity index 59%
rename from src/libide/layout/ide-layout-pane.c
rename to src/libide/gui/ide-panel.c
index 59d848e04..e3091fd85 100644
--- a/src/libide/layout/ide-layout-pane.c
+++ b/src/libide/gui/ide-panel.c
@@ -1,4 +1,4 @@
-/* ide-layout-pane.c
+/* ide-panel.c
  *
  * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
@@ -18,51 +18,68 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#define G_LOG_DOMAIN "ide-layout-pane"
+#define G_LOG_DOMAIN "ide-panel"
 
 #include "config.h"
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
 
-#include "layout/ide-layout-pane.h"
+#include "ide-panel.h"
 
 typedef struct
 {
   DzlDockStack *dock_stack;
-} IdeLayoutPanePrivate;
+} IdePanelPrivate;
 
-G_DEFINE_TYPE_WITH_PRIVATE (IdeLayoutPane, ide_layout_pane, DZL_TYPE_DOCK_BIN_EDGE)
+G_DEFINE_TYPE_WITH_PRIVATE (IdePanel, ide_panel, DZL_TYPE_DOCK_BIN_EDGE)
 
 static void
-ide_layout_pane_add (GtkContainer *container,
-                     GtkWidget    *widget)
+ide_panel_add (GtkContainer *container,
+               GtkWidget    *widget)
 {
-  IdeLayoutPane *self = (IdeLayoutPane *)container;
-  IdeLayoutPanePrivate *priv = ide_layout_pane_get_instance_private (self);
+  IdePanel *self = (IdePanel *)container;
+  IdePanelPrivate *priv = ide_panel_get_instance_private (self);
 
-  g_assert (IDE_IS_LAYOUT_PANE (self));
+  g_assert (IDE_IS_PANEL (self));
 
   if (DZL_IS_DOCK_WIDGET (widget))
     gtk_container_add (GTK_CONTAINER (priv->dock_stack), widget);
   else
-    GTK_CONTAINER_CLASS (ide_layout_pane_parent_class)->add (container, widget);
+    GTK_CONTAINER_CLASS (ide_panel_parent_class)->add (container, widget);
 }
 
 static void
-ide_layout_pane_class_init (IdeLayoutPaneClass *klass)
+ide_panel_class_init (IdePanelClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
 
-  container_class->add = ide_layout_pane_add;
+  container_class->add = ide_panel_add;
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-pane.ui");
-  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutPane, dock_stack);
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-panel.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdePanel, dock_stack);
 }
 
 static void
-ide_layout_pane_init (IdeLayoutPane *self)
+ide_panel_init (IdePanel *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
 }
+
+/**
+ * ide_panel_new:
+ *
+ * Creates a new #IdePanel widget.
+ *
+ * These are meant to be added to #IdeSurface widgets within a workspace.
+ *
+ * Returns: an #IdePanel
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_panel_new (void)
+{
+  return g_object_new (IDE_TYPE_PANEL, NULL);
+}
diff --git a/src/libide/layout/ide-layout-pane.h b/src/libide/gui/ide-panel.h
similarity index 65%
rename from src/libide/layout/ide-layout-pane.h
rename to src/libide/gui/ide-panel.h
index 82c19c057..50f99acc3 100644
--- a/src/libide/layout/ide-layout-pane.h
+++ b/src/libide/gui/ide-panel.h
@@ -1,6 +1,6 @@
-/* ide-layout-pane.h
+/* ide-panel.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,23 +20,29 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <dazzle.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT_PANE (ide_layout_pane_get_type())
+#define IDE_TYPE_PANEL (ide_panel_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_DERIVABLE_TYPE (IdeLayoutPane, ide_layout_pane, IDE, LAYOUT_PANE, DzlDockBinEdge)
+G_DECLARE_DERIVABLE_TYPE (IdePanel, ide_panel, IDE, PANEL, DzlDockBinEdge)
 
-struct _IdeLayoutPaneClass
+struct _IdePanelClass
 {
   DzlDockBinEdgeClass parent_class;
 
   /*< private >*/
-  gpointer _reserved[8];
+  gpointer _reserved[16];
 };
 
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_panel_new (void);
+
 G_END_DECLS
diff --git a/src/libide/layout/ide-layout-pane.ui b/src/libide/gui/ide-panel.ui
similarity index 73%
rename from src/libide/layout/ide-layout-pane.ui
rename to src/libide/gui/ide-panel.ui
index 83856bff3..4fd94fc62 100644
--- a/src/libide/layout/ide-layout-pane.ui
+++ b/src/libide/gui/ide-panel.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.16 -->
-  <template class="IdeLayoutPane" parent="DzlDockBinEdge">
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdePanel" parent="DzlDockBinEdge">
     <child>
       <object class="DzlDockStack" id="dock_stack">
         <property name="expand">true</property>
@@ -10,3 +10,4 @@
     </child>
   </template>
 </interface>
+
diff --git a/src/libide/preferences/ide-preferences-addin.c b/src/libide/gui/ide-preferences-addin.c
similarity index 67%
rename from src/libide/preferences/ide-preferences-addin.c
rename to src/libide/gui/ide-preferences-addin.c
index cec8ef6af..eef7dd6c2 100644
--- a/src/libide/preferences/ide-preferences-addin.c
+++ b/src/libide/gui/ide-preferences-addin.c
@@ -22,27 +22,13 @@
 
 #include "config.h"
 
-#include "preferences/ide-preferences-addin.h"
+#include "ide-preferences-addin.h"
 
 G_DEFINE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, G_TYPE_OBJECT)
 
-static void
-ide_preferences_addin_real_load (IdePreferencesAddin *self,
-                                 DzlPreferences      *preferences)
-{
-}
-
-static void
-ide_preferences_addin_real_unload (IdePreferencesAddin *self,
-                                   DzlPreferences      *preferences)
-{
-}
-
 static void
 ide_preferences_addin_default_init (IdePreferencesAddinInterface *iface)
 {
-  iface->load = ide_preferences_addin_real_load;
-  iface->unload = ide_preferences_addin_real_unload;
 }
 
 /**
@@ -50,12 +36,13 @@ ide_preferences_addin_default_init (IdePreferencesAddinInterface *iface)
  * @self: An #IdePreferencesAddin.
  * @preferences: The preferences container implementation.
  *
- * This interface method is called when a preferences addin is initialized. It could be
- * initialized from multiple preferences implementations, so consumers should use the
- * #DzlPreferences interface to add their preferences controls to the container.
+ * This interface method is called when a preferences addin is initialized. It
+ * could be initialized from multiple preferences implementations, so consumers
+ * should use the #DzlPreferences interface to add their preferences controls
+ * to the container.
  *
- * Such implementations might include a preferences dialog window, or a preferences
- * widget which could be rendered as a perspective.
+ * Such implementations might include a preferences dialog window, or a
+ * preferences widget which could be rendered as a perspective.
  *
  * Since: 3.32
  */
@@ -66,7 +53,8 @@ ide_preferences_addin_load (IdePreferencesAddin *self,
   g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
   g_return_if_fail (DZL_IS_PREFERENCES (preferences));
 
-  IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences);
+  if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load)
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences);
 }
 
 /**
@@ -74,9 +62,9 @@ ide_preferences_addin_load (IdePreferencesAddin *self,
  * @self: An #IdePreferencesAddin.
  * @preferences: The preferences container implementation.
  *
- * This interface method is called when the preferences addin should remove all controls
- * added to @preferences. This could happen during desctruction of @preferences, or when
- * the plugin is unloaded.
+ * This interface method is called when the preferences addin should remove all
+ * controls added to @preferences. This could happen during desctruction of
+ * @preferences, or when the plugin is unloaded.
  *
  * Since: 3.32
  */
@@ -87,5 +75,6 @@ ide_preferences_addin_unload (IdePreferencesAddin *self,
   g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
   g_return_if_fail (DZL_IS_PREFERENCES (preferences));
 
-  IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences);
+  if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload)
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences);
 }
diff --git a/src/libide/preferences/ide-preferences-addin.h b/src/libide/gui/ide-preferences-addin.h
similarity index 96%
rename from src/libide/preferences/ide-preferences-addin.h
rename to src/libide/gui/ide-preferences-addin.h
index 5c7d4357a..70fa8f098 100644
--- a/src/libide/preferences/ide-preferences-addin.h
+++ b/src/libide/gui/ide-preferences-addin.h
@@ -21,9 +21,8 @@
 #pragma once
 
 #include <dazzle.h>
-#include <gtk/gtk.h>
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/preferences/ide-preferences-builtin.h 
b/src/libide/gui/ide-preferences-builtin-private.h
similarity index 100%
rename from src/libide/preferences/ide-preferences-builtin.h
rename to src/libide/gui/ide-preferences-builtin-private.h
diff --git a/src/libide/preferences/ide-preferences-builtin.c b/src/libide/gui/ide-preferences-builtin.c
similarity index 97%
rename from src/libide/preferences/ide-preferences-builtin.c
rename to src/libide/gui/ide-preferences-builtin.c
index ca2876e22..1fdee4849 100644
--- a/src/libide/preferences/ide-preferences-builtin.c
+++ b/src/libide/gui/ide-preferences-builtin.c
@@ -27,10 +27,8 @@
 #include <gtksourceview/gtksource.h>
 #include <libpeas/peas.h>
 
-#include "application/ide-application-private.h"
-#include "preferences/ide-preferences-builtin.h"
-#include "preferences/ide-preferences-language-row.h"
-#include "vcs/ide-vcs-config.h"
+#include "ide-preferences-builtin-private.h"
+#include "ide-preferences-language-row-private.h"
 
 static gint
 sort_plugin_info (gconstpointer a,
@@ -142,9 +140,6 @@ ide_preferences_builtin_register_keyboard (DzlPreferences *preferences)
 
   dzl_preferences_add_list_group (preferences, "keyboard", "mode", _("Emulation"), GTK_SELECTION_NONE, 0);
   dzl_preferences_add_radio (preferences, "keyboard", "mode", "org.gnome.builder.editor", "keybindings", 
NULL, "\"default\"", _("Builder"), _("Default keybinding mode which mimics gedit"), NULL, 0);
-  dzl_preferences_add_radio (preferences, "keyboard", "mode", "org.gnome.builder.editor", "keybindings", 
NULL, "\"emacs\"", _("Emacs"), _("Emulates the Emacs text editor"), NULL, 0);
-  dzl_preferences_add_radio (preferences, "keyboard", "mode", "org.gnome.builder.editor", "keybindings", 
NULL, "\"vim\"", _("Vim"), _("Emulates the Vim text editor"), NULL, 0);
-  dzl_preferences_add_radio (preferences, "keyboard", "mode", "org.gnome.builder.editor", "keybindings", 
NULL, "\"sublime\"", _("Sublime Text"), _("Emulates the Sublime Text editor"), NULL, 0);
 
   dzl_preferences_add_list_group (preferences, "keyboard", "movements", _("Movement"), GTK_SELECTION_NONE, 
100);
   dzl_preferences_add_switch (preferences, "keyboard", "movements", "org.gnome.builder.editor", 
"smart-home-end", NULL, NULL, _("Smart Home and End"), _("Home moves to first non-whitespace character"), 
NULL, 0);
@@ -257,13 +252,10 @@ ide_preferences_builtin_register_languages (DzlPreferences *preferences)
 {
   GtkSourceLanguageManager *manager;
   const gchar * const *language_ids;
-  g_autoptr(GHashTable) sections = NULL;
   GtkSearchEntry *search;
   GtkWidget *group = NULL;
   GtkWidget *flow = NULL;
 
-  sections = g_hash_table_new (g_str_hash, g_str_equal);
-
   dzl_preferences_add_page (preferences, "languages", _("Programming Languages"), 200);
 
   manager = gtk_source_language_manager_get_default ();
@@ -426,6 +418,7 @@ ide_preferences_builtin_register_projects (DzlPreferences *preferences)
   dzl_preferences_add_switch (preferences, "projects", "directory", "org.gnome.builder", 
"restore-previous-files", NULL, NULL, _("Restore previously opened files"), _("Open previously opened files 
when loading a project"), NULL, 10);
 }
 
+#if 0
 static void
 author_changed_cb (DzlPreferencesEntry *entry,
                    const gchar         *text,
@@ -551,6 +544,7 @@ ide_preferences_builtin_register_vcs (DzlPreferences *preferences)
   peas_extension_set_foreach (extensions, vcs_configs_foreach_cb, preferences);
   g_clear_object (&extensions);
 }
+#endif
 
 static void
 ide_preferences_builtin_register_sdks (DzlPreferences *preferences)
@@ -572,6 +566,6 @@ _ide_preferences_builtin_register (DzlPreferences *preferences)
   ide_preferences_builtin_register_plugins (preferences);
   ide_preferences_builtin_register_build (preferences);
   ide_preferences_builtin_register_projects (preferences);
-  ide_preferences_builtin_register_vcs (preferences);
+  //ide_preferences_builtin_register_vcs (preferences);
   ide_preferences_builtin_register_sdks (preferences);
 }
diff --git a/src/libide/preferences/ide-preferences-language-row.h 
b/src/libide/gui/ide-preferences-language-row-private.h
similarity index 100%
rename from src/libide/preferences/ide-preferences-language-row.h
rename to src/libide/gui/ide-preferences-language-row-private.h
diff --git a/src/libide/preferences/ide-preferences-language-row.c 
b/src/libide/gui/ide-preferences-language-row.c
similarity index 94%
rename from src/libide/preferences/ide-preferences-language-row.c
rename to src/libide/gui/ide-preferences-language-row.c
index 0d2403bf5..b8844d8dc 100644
--- a/src/libide/preferences/ide-preferences-language-row.c
+++ b/src/libide/gui/ide-preferences-language-row.c
@@ -22,7 +22,7 @@
 
 #include "config.h"
 
-#include "preferences/ide-preferences-language-row.h"
+#include "ide-preferences-language-row-private.h"
 
 struct _IdePreferencesLanguageRow
 {
@@ -37,16 +37,16 @@ enum {
   PROP_0,
   PROP_ID,
   PROP_TITLE,
-  LAST_PROP
+  N_PROPS
 };
 
 enum {
   ACTIVATE,
-  LAST_SIGNAL
+  N_SIGNALS
 };
 
-static GParamSpec *properties [LAST_PROP];
-static guint signals [LAST_SIGNAL];
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
 
 static void
 ide_preferences_language_row_activate (IdePreferencesLanguageRow *self)
@@ -148,7 +148,7 @@ ide_preferences_language_row_class_init (IdePreferencesLanguageRowClass *klass)
                          NULL,
                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 
   signals [ACTIVATE] =
     g_signal_new_class_handler ("activate",
@@ -160,7 +160,7 @@ ide_preferences_language_row_class_init (IdePreferencesLanguageRowClass *klass)
 
   widget_class->activate_signal = signals [ACTIVATE];
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-preferences-language-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-preferences-language-row.ui");
   gtk_widget_class_bind_template_child (widget_class, IdePreferencesLanguageRow, title);
 }
 
diff --git a/src/libide/preferences/ide-preferences-language-row.ui 
b/src/libide/gui/ide-preferences-language-row.ui
similarity index 100%
rename from src/libide/preferences/ide-preferences-language-row.ui
rename to src/libide/gui/ide-preferences-language-row.ui
diff --git a/src/libide/gui/ide-preferences-surface.c b/src/libide/gui/ide-preferences-surface.c
new file mode 100644
index 000000000..e132b43c1
--- /dev/null
+++ b/src/libide/gui/ide-preferences-surface.c
@@ -0,0 +1,136 @@
+/* ide-preferences-surface.c
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-preferences-surface"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-preferences-addin.h"
+#include "ide-preferences-builtin-private.h"
+#include "ide-preferences-surface.h"
+#include "ide-surface.h"
+
+struct _IdePreferencesSurface
+{
+  IdeSurface          parent_instance;
+  DzlPreferencesView *view;
+  PeasExtensionSet   *extensions;
+};
+
+G_DEFINE_TYPE (IdePreferencesSurface, ide_preferences_surface, IDE_TYPE_SURFACE)
+
+static void
+ide_preferences_surface_addin_added_cb (PeasExtensionSet *set,
+                                        PeasPluginInfo   *plugin_info,
+                                        PeasExtension    *extension,
+                                        gpointer          user_data)
+{
+  IdePreferencesSurface *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_ADDIN (extension));
+  g_assert (IDE_IS_PREFERENCES_SURFACE (self));
+
+  ide_preferences_addin_load (IDE_PREFERENCES_ADDIN (extension), DZL_PREFERENCES (self->view));
+  dzl_preferences_view_reapply_filter (self->view);
+}
+
+static void
+ide_preferences_surface_addin_removed_cb (PeasExtensionSet *set,
+                                          PeasPluginInfo   *plugin_info,
+                                          PeasExtension    *extension,
+                                          gpointer          user_data)
+{
+  IdePreferencesSurface *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_ADDIN (extension));
+  g_assert (IDE_IS_PREFERENCES_SURFACE (self));
+
+  ide_preferences_addin_unload (IDE_PREFERENCES_ADDIN (extension), DZL_PREFERENCES (self->view));
+  dzl_preferences_view_reapply_filter (self->view);
+}
+
+static void
+ide_preferences_surface_destroy (GtkWidget *widget)
+{
+  IdePreferencesSurface *self = (IdePreferencesSurface *)widget;
+
+  g_clear_object (&self->extensions);
+
+  GTK_WIDGET_CLASS (ide_preferences_surface_parent_class)->destroy (widget);
+}
+
+static void
+ide_preferences_surface_constructed (GObject *object)
+{
+  IdePreferencesSurface *self = (IdePreferencesSurface *)object;
+
+  G_OBJECT_CLASS (ide_preferences_surface_parent_class)->constructed (object);
+
+  _ide_preferences_builtin_register (DZL_PREFERENCES (self->view));
+
+  self->extensions = peas_extension_set_new (peas_engine_get_default (),
+                                             IDE_TYPE_PREFERENCES_ADDIN,
+                                             NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (ide_preferences_surface_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (ide_preferences_surface_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->extensions,
+                              ide_preferences_surface_addin_added_cb,
+                              self);
+}
+
+static void
+ide_preferences_surface_class_init (IdePreferencesSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_preferences_surface_constructed;
+
+  widget_class->destroy = ide_preferences_surface_destroy;
+}
+
+static void
+ide_preferences_surface_init (IdePreferencesSurface *self)
+{
+  ide_surface_set_icon_name (IDE_SURFACE (self), "preferences-system-symbolic");
+  gtk_widget_set_name (GTK_WIDGET (self), "preferences");
+
+  self->view = g_object_new (DZL_TYPE_PREFERENCES_VIEW,
+                             "visible", TRUE,
+                             NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->view));
+}
diff --git a/src/libide/preferences/ide-preferences-perspective.h b/src/libide/gui/ide-preferences-surface.h
similarity index 69%
rename from src/libide/preferences/ide-preferences-perspective.h
rename to src/libide/gui/ide-preferences-surface.h
index a5bb4e6b3..5657af084 100644
--- a/src/libide/preferences/ide-preferences-perspective.h
+++ b/src/libide/gui/ide-preferences-surface.h
@@ -1,4 +1,4 @@
-/* ide-preferences-perspective.h
+/* ide-preferences-surface.h
  *
  * Copyright 2015-2019 Christian Hergert <chergert redhat com>
  *
@@ -20,16 +20,17 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include "ide-surface.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_PREFERENCES_PERSPECTIVE     (ide_preferences_perspective_get_type())
-#define IDE_PREFERENCES_PERSPECTIVE_PRIORITY 1000000
+#define IDE_TYPE_PREFERENCES_SURFACE (ide_preferences_surface_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdePreferencesPerspective, ide_preferences_perspective, IDE, PREFERENCES_PERSPECTIVE, 
DzlPreferencesView)
+G_DECLARE_FINAL_TYPE (IdePreferencesSurface, ide_preferences_surface, IDE, PREFERENCES_SURFACE, IdeSurface)
 
 G_END_DECLS
diff --git a/src/libide/preferences/ide-preferences-window.c b/src/libide/gui/ide-preferences-window.c
similarity index 85%
rename from src/libide/preferences/ide-preferences-window.c
rename to src/libide/gui/ide-preferences-window.c
index bad5151c6..450d5c457 100644
--- a/src/libide/preferences/ide-preferences-window.c
+++ b/src/libide/gui/ide-preferences-window.c
@@ -22,21 +22,21 @@
 
 #include "config.h"
 
-#include "preferences/ide-preferences-window.h"
+#include "ide-preferences-window.h"
 
 struct _IdePreferencesWindow
 {
-  GtkWindow parent_instance;
+  DzlApplicationWindow parent_window;
 };
 
-G_DEFINE_TYPE (IdePreferencesWindow, ide_preferences_window, GTK_TYPE_WINDOW)
+G_DEFINE_TYPE (IdePreferencesWindow, ide_preferences_window, DZL_TYPE_APPLICATION_WINDOW)
 
 static void
 ide_preferences_window_class_init (IdePreferencesWindowClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-preferences-window.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-preferences-window.ui");
 }
 
 static void
diff --git a/src/libide/preferences/ide-preferences-window.h b/src/libide/gui/ide-preferences-window.h
similarity index 91%
rename from src/libide/preferences/ide-preferences-window.h
rename to src/libide/gui/ide-preferences-window.h
index d5d9fbb4f..dc7fd2752 100644
--- a/src/libide/preferences/ide-preferences-window.h
+++ b/src/libide/gui/ide-preferences-window.h
@@ -20,15 +20,14 @@
 
 #pragma once
 
-#include <gtk/gtk.h>
-
-#include "ide-version-macros.h"
+#include <dazzle.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_PREFERENCES_WINDOW (ide_preferences_window_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, IDE, PREFERENCES_WINDOW, GtkWindow)
+G_DECLARE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, IDE, PREFERENCES_WINDOW, 
DzlApplicationWindow)
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.ui b/src/libide/gui/ide-preferences-window.ui
new file mode 100644
index 000000000..a02207197
--- /dev/null
+++ b/src/libide/gui/ide-preferences-window.ui
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdePreferencesWindow" parent="DzlApplicationWindow">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="title" translatable="yes">Preferences</property>
+        <property name="show-close-button">true</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child>
+      <object class="IdePreferencesSurface" id="surface">
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-primary-workspace-actions.c b/src/libide/gui/ide-primary-workspace-actions.c
new file mode 100644
index 000000000..e90976f4e
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace-actions.c
@@ -0,0 +1,109 @@
+/* ide-primary-workspace-actions.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-primary-workspace-actions"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libpeas/peas.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-primary-workspace.h"
+
+static void
+update_dependencies_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeDependencyUpdater *updater = (IdeDependencyUpdater *)object;
+  g_autoptr(IdePrimaryWorkspace) self = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_DEPENDENCY_UPDATER (updater));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+
+  if (!ide_dependency_updater_update_finish (updater, result, &error))
+    ide_context_warning (context, "%s", error->message);
+
+  ide_object_destroy (IDE_OBJECT (updater));
+}
+
+static void
+ide_primary_workspace_actions_update_dependencies_cb (PeasExtensionSet *set,
+                                                      PeasPluginInfo   *plugin_info,
+                                                      PeasExtension    *exten,
+                                                      gpointer          user_data)
+{
+  IdeDependencyUpdater *updater = (IdeDependencyUpdater *)exten;
+  IdePrimaryWorkspace *self = user_data;
+  IdeContext *context;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEPENDENCY_UPDATER (updater));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  ide_object_append (IDE_OBJECT (context), IDE_OBJECT (updater));
+
+  ide_dependency_updater_update_async (updater,
+                                       NULL,
+                                       update_dependencies_cb,
+                                       g_object_ref (self));
+}
+
+static void
+ide_primary_workspace_actions_update_dependencies (GSimpleAction *action,
+                                                   GVariant      *param,
+                                                   gpointer       user_data)
+{
+  IdePrimaryWorkspace *self = user_data;
+  g_autoptr(PeasExtensionSet) set = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_DEPENDENCY_UPDATER,
+                                NULL);
+  peas_extension_set_foreach (set, ide_primary_workspace_actions_update_dependencies_cb, self);
+}
+
+static const GActionEntry actions[] = {
+  { "update-dependencies", ide_primary_workspace_actions_update_dependencies },
+};
+
+void
+_ide_primary_workspace_init_actions (IdePrimaryWorkspace *self)
+{
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
diff --git a/src/libide/gui/ide-primary-workspace.c b/src/libide/gui/ide-primary-workspace.c
new file mode 100644
index 000000000..a88b3d9fd
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.c
@@ -0,0 +1,141 @@
+/* ide-primary-workspace.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-primary-workspace"
+
+#include "config.h"
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-header-bar.h"
+#include "ide-omni-bar.h"
+#include "ide-primary-workspace.h"
+#include "ide-run-button.h"
+#include "ide-search-entry.h"
+#include "ide-surface.h"
+#include "ide-window-settings-private.h"
+
+/**
+ * SECTION:ide-primary-workspace
+ * @title: IdePrimaryWorkspace
+ * @short_description: The primary IDE window
+ *
+ * The primary workspace is the main workspace window for the user. This is the
+ * "IDE experience" workspace. It is generally created by the workbench when
+ * opening a project (unless another workspace type has been requested).
+ *
+ * See ide_workbench_open_async() for how to select another workspace type
+ * when opening a project.
+ *
+ * Returns: (transfer full): an #IdePrimaryWorkspace
+ *
+ * Since: 3.32
+ */
+
+struct _IdePrimaryWorkspace
+{
+  IdeWorkspace   parent_instance;
+
+  /* Template widgets */
+  IdeHeaderBar   *header_bar;
+  DzlMenuButton  *surface_menu_button;
+  IdeRunButton   *run_button;
+  IdeSearchEntry *search_entry;
+  GtkLabel       *project_title;
+};
+
+G_DEFINE_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+ide_primary_workspace_context_set (IdeWorkspace *workspace,
+                                   IdeContext   *context)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+  IdeProjectInfo *project_info;
+  IdeWorkbench *workbench;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  IDE_WORKSPACE_CLASS (ide_primary_workspace_parent_class)->context_set (workspace, context);
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  project_info = ide_workbench_get_project_info (workbench);
+
+  if (project_info)
+    g_object_bind_property (project_info, "name", self->project_title, "label",
+                            G_BINDING_SYNC_CREATE);
+}
+
+static void
+ide_primary_workspace_surface_set (IdeWorkspace *workspace,
+                                   IdeSurface   *surface)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (DZL_IS_DOCK_ITEM (surface))
+    {
+      g_autofree gchar *icon_name = NULL;
+
+      icon_name = dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (surface));
+      g_object_set (self->surface_menu_button,
+                    "icon-name", icon_name,
+                    NULL);
+    }
+
+  IDE_WORKSPACE_CLASS (ide_primary_workspace_parent_class)->surface_set (workspace, surface);
+}
+
+static void
+ide_primary_workspace_class_init (IdePrimaryWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  ide_workspace_class_set_kind (workspace_class, "primary");
+
+  workspace_class->surface_set = ide_primary_workspace_surface_set;
+  workspace_class->context_set = ide_primary_workspace_context_set;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-primary-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, project_title);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, run_button);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, surface_menu_button);
+
+  g_type_ensure (IDE_TYPE_HEADER_BAR);
+  g_type_ensure (IDE_TYPE_OMNI_BAR);
+  g_type_ensure (IDE_TYPE_RUN_BUTTON);
+  g_type_ensure (IDE_TYPE_SEARCH_ENTRY);
+}
+
+static void
+ide_primary_workspace_init (IdePrimaryWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  _ide_primary_workspace_init_actions (self);
+  _ide_window_settings_register (GTK_WINDOW (self));
+}
diff --git a/src/libide/gui/ide-primary-workspace.h b/src/libide/gui/ide-primary-workspace.h
new file mode 100644
index 000000000..a20da72eb
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.h
@@ -0,0 +1,38 @@
+/* ide-primary-workspace.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include "ide-application.h"
+#include "ide-surface.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PRIMARY_WORKSPACE (ide_primary_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE, PRIMARY_WORKSPACE, IdeWorkspace)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-primary-workspace.ui b/src/libide/gui/ide-primary-workspace.ui
new file mode 100644
index 000000000..a374869b0
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdePrimaryWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="menu-id">ide-primary-workspace-menu</property>
+        <property name="show-close-button">true</property>
+        <property name="show-fullscreen-button">true</property>
+        <property name="visible">true</property>
+        <child type="left">
+          <object class="IdeSurfacesButton" id="surface_menu_button">
+            <property name="focus-on-click">false</property>
+            <property name="menu-id">ide-primary-workspace-surfaces-menu</property>
+            <property name="show-accels">true</property>
+            <property name="show-arrow">true</property>
+            <property name="show-icons">true</property>
+            <!-- disable transitions since they'll cause jitter with the
+                 whole surface changing below it. -->
+            <property name="transitions-enabled">false</property>
+            <property name="has-tooltip">true</property>
+            <property name="tooltip-text" translatable="yes">Change workspace surface</property>
+          </object>
+        </child>
+        <child type="title">
+          <object class="IdeOmniBar" id="omni_bar">
+            <property name="halign">center</property>
+            <property name="hexpand">false</property>
+            <property name="hexpand-set">true</property>
+            <property name="visible">true</property>
+            <child type="placeholder">
+              <object class="GtkLabel" id="project_title">
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="right-of-center">
+          <object class="IdeRunButton" id="run_button">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="IdeNotificationsButton" id="notifications_button">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="IdeSearchEntry" id="search_entry">
+            <property name="margin-start">6</property>
+            <property name="margin-end">6</property>
+            <property name="primary-icon-name">edit-find-symbolic</property>
+            <property name="placeholder-text" translatable="yes">Press Ctrl+. to search</property>
+            <property name="width-chars">5</property>
+            <property name="max-width-chars">24</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-run-button.c b/src/libide/gui/ide-run-button.c
new file mode 100644
index 000000000..6b69341ba
--- /dev/null
+++ b/src/libide/gui/ide-run-button.c
@@ -0,0 +1,200 @@
+/* ide-run-button.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-run-button"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+
+#include "ide-gui-global.h"
+#include "ide-run-button.h"
+
+#include "ide-run-manager-private.h"
+
+struct _IdeRunButton
+{
+  GtkBox                parent_instance;
+
+  GtkButton            *button;
+  GtkImage             *button_image;
+  DzlMenuButton        *menu_button;
+  GtkButton            *stop_button;
+  GtkShortcutsShortcut *run_shortcut;
+  GtkLabel             *run_tooltip_message;
+  DzlShortcutTooltip   *tooltip;
+};
+
+G_DEFINE_TYPE (IdeRunButton, ide_run_button, GTK_TYPE_BOX)
+
+static void
+ide_run_button_handler_set (IdeRunButton  *self,
+                            GParamSpec    *pspec,
+                            IdeRunManager *run_manager)
+{
+  const GList *list;
+  const GList *iter;
+  const gchar *handler;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  handler = ide_run_manager_get_handler (run_manager);
+  list = _ide_run_manager_get_handlers (run_manager);
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      const IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, handler) == 0)
+        {
+          g_object_set (self->button_image,
+                        "icon-name", info->icon_name,
+                        NULL);
+          break;
+        }
+    }
+}
+
+static void
+ide_run_button_load (IdeRunButton *self,
+                     IdeContext   *context)
+{
+  IdeRunManager *run_manager;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  run_manager = ide_run_manager_from_context (context);
+
+  g_object_bind_property (run_manager, "busy", self->button, "visible",
+                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+  g_object_bind_property (run_manager, "busy", self->stop_button, "visible",
+                          G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (run_manager,
+                           "notify::handler",
+                           G_CALLBACK (ide_run_button_handler_set),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_run_button_handler_set (self, NULL, run_manager);
+}
+
+static void
+ide_run_button_context_set (GtkWidget  *widget,
+                            IdeContext *context)
+{
+  IdeRunButton *self = (IdeRunButton *)widget;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context != NULL)
+    ide_run_button_load (self, context);
+}
+
+static gboolean
+ide_run_button_query_tooltip (IdeRunButton *self,
+                              gint          x,
+                              gint          y,
+                              gboolean      keyboard_tooltip,
+                              GtkTooltip   *tooltip,
+                              GtkButton    *button)
+{
+  IdeRunManager *run_manager;
+  const GList *list;
+  const GList *iter;
+  const gchar *handler;
+  IdeContext *context;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+  g_assert (GTK_IS_BUTTON (button));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  run_manager = ide_run_manager_from_context (context);
+  handler = ide_run_manager_get_handler (run_manager);
+  list = _ide_run_manager_get_handlers (run_manager);
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      const IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, handler) == 0)
+        {
+          gboolean enabled;
+          /* Figure out if the run action is enabled. If it
+           * is not, then we should inform the user that
+           * the project cannot be run yet because the
+           * build pipeline is not yet configured. */
+          g_action_group_query_action (G_ACTION_GROUP (run_manager),
+                                       "run",
+                                       &enabled,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       NULL);
+
+          if (!enabled)
+            {
+              gtk_tooltip_set_custom (tooltip, GTK_WIDGET (self->run_tooltip_message));
+              return TRUE;
+            }
+
+          /* The shortcut tooltip will set this up after us */
+          dzl_shortcut_tooltip_set_accel (self->tooltip, info->accel);
+          dzl_shortcut_tooltip_set_title (self->tooltip, info->title);
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+ide_run_button_class_init (IdeRunButtonClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-run-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, button_image);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, menu_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, run_shortcut);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, stop_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, run_tooltip_message);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, tooltip);
+}
+
+static void
+ide_run_button_init (IdeRunButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->button,
+                           "query-tooltip",
+                           G_CALLBACK (ide_run_button_query_tooltip),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_widget_set_context_handler (self, ide_run_button_context_set);
+}
diff --git a/src/libide/util/ide-dnd.h b/src/libide/gui/ide-run-button.h
similarity index 75%
rename from src/libide/util/ide-dnd.h
rename to src/libide/gui/ide-run-button.h
index 03d636641..f758c3adc 100644
--- a/src/libide/util/ide-dnd.h
+++ b/src/libide/gui/ide-run-button.h
@@ -1,6 +1,6 @@
-/* ide-dnd.h
+/* ide-run-button.h
  *
- * Copyright 2015 Dimitris Zenios <dimitris zenios gmail com>
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,6 +24,10 @@
 
 G_BEGIN_DECLS
 
-gchar **ide_dnd_get_uri_list (GtkSelectionData *selection_data);
+#define IDE_TYPE_RUN_BUTTON (ide_run_button_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeRunButton, ide_run_button, IDE, RUN_BUTTON, GtkBox)
+
+GtkWidget *ide_run_button_new (void);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-run-button.ui b/src/libide/gui/ide-run-button.ui
new file mode 100644
index 000000000..e68678402
--- /dev/null
+++ b/src/libide/gui/ide-run-button.ui
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkShortcutsShortcut" id="run_shortcut">
+  </object>
+  <object class="GtkLabel" id="run_tooltip_message">
+    <property name="label" translatable="yes">The project cannot be run while the build pipeline is being 
set up</property>
+    <property name="visible">true</property>
+  </object>
+  <template class="IdeRunButton" parent="GtkBox">
+    <property name="orientation">horizontal</property>
+    <style>
+      <class name="linked"/>
+    </style>
+    <child>
+      <object class="GtkButton" id="button">
+        <property name="action-name">run-manager.run</property>
+        <property name="focus-on-click">false</property>
+        <property name="has-tooltip">true</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child>
+          <object class="GtkImage" id="button_image">
+            <property name="icon-name">media-playback-start-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="stop_button">
+        <property name="action-name">run-manager.stop</property>
+        <property name="focus-on-click">false</property>
+        <property name="tooltip-text" translatable="yes">Stop running</property>
+        <style>
+          <class name="image-button"/>
+        </style>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">media-playback-stop-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="DzlMenuButton" id="menu_button">
+        <property name="focus-on-click">false</property>
+        <property name="icon-name">pan-down-symbolic</property>
+        <property name="menu-id">run-menu</property>
+        <property name="show-accels">true</property>
+        <property name="show-arrow">false</property>
+        <property name="show-icons">true</property>
+        <property name="tooltip-text" translatable="yes">Change run options</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="image-button"/>
+          <class name="run-arrow-button"/>
+        </style>
+      </object>
+    </child>
+  </template>
+  <object class="DzlShortcutTooltip" id="tooltip">
+    <property name="widget">button</property>
+  </object>
+</interface>
diff --git a/src/libide/search/ide-search-entry.c b/src/libide/gui/ide-search-entry.c
similarity index 65%
rename from src/libide/search/ide-search-entry.c
rename to src/libide/gui/ide-search-entry.c
index 9632cb4d3..8d8088ca4 100644
--- a/src/libide/search/ide-search-entry.c
+++ b/src/libide/gui/ide-search-entry.c
@@ -22,29 +22,23 @@
 
 #include "config.h"
 
-#include "ide-context.h"
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-search.h>
 
-#include "editor/ide-editor-perspective.h"
-#include "search/ide-search-engine.h"
-#include "search/ide-search-entry.h"
-#include "search/ide-search-result.h"
-#include "util/ide-gtk.h"
-#include "workbench/ide-workbench.h"
+#include "ide-gui-global.h"
+#include "ide-search-entry.h"
+#include "ide-workbench.h"
 
 #define DEFAULT_SEARCH_MAX 25
+#define I_ g_intern_string
 
 struct _IdeSearchEntry
 {
   DzlSuggestionEntry parent_instance;
-  guint max_results;
+  guint              max_results;
 };
 
-typedef struct
-{
-  IdeEditorPerspective *editor;
-  IdeSourceLocation    *location;
-} DelayedActivate;
-
 G_DEFINE_TYPE (IdeSearchEntry, ide_search_entry, DZL_TYPE_SUGGESTION_ENTRY)
 
 enum {
@@ -61,6 +55,35 @@ enum {
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
+static void
+search_popover_position_func (DzlSuggestionEntry *entry,
+                              GdkRectangle       *area,
+                              gboolean           *is_absolute,
+                              gpointer            user_data)
+{
+  gint new_width;
+
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+  g_assert (area != NULL);
+  g_assert (is_absolute != NULL);
+  g_assert (user_data == NULL);
+
+#define RIGHT_MARGIN 6
+
+  /* We want the search area to be the right 2/5ths of the window, with a bit
+   * of margin on the popover.
+   */
+
+  dzl_suggestion_entry_window_position_func (entry, area, is_absolute, NULL);
+
+  new_width = (area->width * 2 / 5);
+  area->x += area->width - new_width;
+  area->width = new_width - RIGHT_MARGIN;
+  area->y -= 3;
+
+#undef RIGHT_MARGIN
+}
+
 static void
 ide_search_entry_search_cb (GObject      *object,
                             GAsyncResult *result,
@@ -94,93 +117,35 @@ static void
 ide_search_entry_changed (IdeSearchEntry *self)
 {
   IdeSearchEngine *engine;
-  IdeContext *context;
+  IdeWorkbench *workbench;
   const gchar *typed_text;
 
   g_assert (IDE_IS_SEARCH_ENTRY (self));
 
-  if (NULL == (context = ide_widget_get_context (GTK_WIDGET (self))))
-    return;
-
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  engine = ide_workbench_get_search_engine (workbench);
   typed_text = dzl_suggestion_entry_get_typed_text (DZL_SUGGESTION_ENTRY (self));
 
   if (dzl_str_empty0 (typed_text))
-    {
-      dzl_suggestion_entry_set_model (DZL_SUGGESTION_ENTRY (self), NULL);
-      return;
-    }
-
-  engine = ide_context_get_search_engine (context);
-
-  ide_search_engine_search_async (engine,
-                                  typed_text,
-                                  self->max_results,
-                                  NULL,
-                                  ide_search_entry_search_cb,
-                                  g_object_ref (self));
-}
-
-static void
-delayed_activate_free (gpointer data)
-{
-  DelayedActivate *da = data;
-
-  g_clear_object (&da->editor);
-  g_clear_pointer (&da->location, ide_source_location_unref);
-  g_slice_free (DelayedActivate, da);
-}
-
-static gboolean
-delayed_activate_handle (gpointer data)
-{
-  DelayedActivate *da = data;
-
-  g_assert (da != NULL);
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (da->editor));
-  g_assert (da->location != NULL);
-
-  ide_editor_perspective_focus_location (da->editor, da->location);
-
-  return G_SOURCE_REMOVE;
+    dzl_suggestion_entry_set_model (DZL_SUGGESTION_ENTRY (self), NULL);
+  else
+    ide_search_engine_search_async (engine,
+                                    typed_text,
+                                    self->max_results,
+                                    NULL,
+                                    ide_search_entry_search_cb,
+                                    g_object_ref (self));
 }
 
 static void
 suggestion_activated (DzlSuggestionEntry *entry,
                       DzlSuggestion      *suggestion)
 {
-  g_autoptr(IdeSourceLocation) location = NULL;
-
   g_assert (IDE_IS_SEARCH_ENTRY (entry));
   g_assert (IDE_IS_SEARCH_RESULT (suggestion));
 
-  location = ide_search_result_get_source_location (IDE_SEARCH_RESULT (suggestion));
-
-  if (location != NULL)
-    {
-      IdeWorkbench *workbench;
-      IdePerspective *perspective;
-      DelayedActivate *da;
-
-      workbench = ide_widget_get_workbench (GTK_WIDGET (entry));
-      perspective = ide_workbench_get_perspective_by_name (workbench, "editor");
-
-      /*
-       * The goal here is to wait a short bit of time before activating the
-       * item so that our window has a chance to animate out. Otherwise, we
-       * get a jittery animation because the UI is likely doing too much work
-       * (such as creating and sink'ing the new widgetry).
-       */
-
-      da = g_slice_new0 (DelayedActivate);
-      da->editor = g_object_ref (IDE_EDITOR_PERSPECTIVE (perspective));
-      da->location = g_steal_pointer (&location);
-
-      gdk_threads_add_timeout_full (G_PRIORITY_LOW,
-                                    250,
-                                    delayed_activate_handle,
-                                    g_steal_pointer (&da),
-                                    delayed_activate_free);
-    }
+  /* TODO: Get last focus from workspace */
+  ide_search_result_activate (IDE_SEARCH_RESULT (suggestion), GTK_WIDGET (entry));
 
   /* Chain up to properly clear entry buffer */
   if (DZL_SUGGESTION_ENTRY_CLASS (ide_search_entry_parent_class)->suggestion_activated)
@@ -238,6 +203,37 @@ ide_search_entry_set_property (GObject      *object,
     }
 }
 
+static DzlShortcutEntry shortcuts[] = {
+  { "org.gnome.builder.workspace.global-search",
+    0, NULL,
+    NC_("shortcut window", "Workspace shortcuts"),
+    NC_("shortcut window", "Search"),
+    NC_("shortcut window", "Focus to the global search entry") },
+};
+
+static void
+ide_search_entry_init_shortcuts (IdeSearchEntry *self)
+{
+  DzlShortcutController *controller;
+
+  g_assert (IDE_IS_SEARCH_ENTRY (self));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                I_("org.gnome.builder.workspace.global-search"),
+                                                "<Primary>period",
+                                                DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                                (GtkCallback)gtk_widget_grab_focus,
+                                                NULL,
+                                                NULL);
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             shortcuts,
+                                             G_N_ELEMENTS (shortcuts),
+                                             GETTEXT_PACKAGE);
+}
+
 static void
 ide_search_entry_class_init (IdeSearchEntryClass *klass)
 {
@@ -269,7 +265,7 @@ ide_search_entry_class_init (IdeSearchEntryClass *klass)
                                 G_CALLBACK (ide_search_entry_unfocus),
                                 NULL, NULL, NULL, G_TYPE_NONE, 0);
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-search-entry.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-search-entry.ui");
 
   bindings = gtk_binding_set_by_class (klass);
   gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "unfocus", 0);
@@ -288,4 +284,11 @@ ide_search_entry_init (IdeSearchEntry *self)
                     "changed",
                     G_CALLBACK (ide_search_entry_changed),
                     NULL);
+
+  dzl_suggestion_entry_set_position_func (DZL_SUGGESTION_ENTRY (self),
+                                          search_popover_position_func,
+                                          NULL,
+                                          NULL);
+
+  ide_search_entry_init_shortcuts (self);
 }
diff --git a/src/libide/search/ide-search-entry.h b/src/libide/gui/ide-search-entry.h
similarity index 89%
rename from src/libide/search/ide-search-entry.h
rename to src/libide/gui/ide-search-entry.h
index 70d4d3aac..f1bd206ea 100644
--- a/src/libide/search/ide-search-entry.h
+++ b/src/libide/gui/ide-search-entry.h
@@ -20,9 +20,11 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <dazzle.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/search/ide-search-entry.ui b/src/libide/gui/ide-search-entry.ui
similarity index 84%
rename from src/libide/search/ide-search-entry.ui
rename to src/libide/gui/ide-search-entry.ui
index c5650d24e..f5e623ca1 100644
--- a/src/libide/search/ide-search-entry.ui
+++ b/src/libide/gui/ide-search-entry.ui
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <template class="IdeSearchEntry" parent="DzlSuggestionEntry">
-    <property name="primary-icon-name">edit-find-symbolic</property>
     <child internal-child="popover">
       <object class="DzlSuggestionPopover">
         <property name="title-ellipsize">middle</property>
diff --git a/src/libide/session/ide-session-addin.c b/src/libide/gui/ide-session-addin.c
similarity index 88%
rename from src/libide/session/ide-session-addin.c
rename to src/libide/gui/ide-session-addin.c
index da00f7824..576e91023 100644
--- a/src/libide/session/ide-session-addin.c
+++ b/src/libide/gui/ide-session-addin.c
@@ -28,6 +28,7 @@ G_DEFINE_INTERFACE (IdeSessionAddin, ide_session_addin, IDE_TYPE_OBJECT)
 
 static void
 ide_session_addin_real_save_async (IdeSessionAddin     *self,
+                                   IdeWorkbench         *workbench,
                                    GCancellable        *cancellable,
                                    GAsyncReadyCallback  callback,
                                    gpointer             user_data)
@@ -49,6 +50,7 @@ ide_session_addin_real_save_finish (IdeSessionAddin  *self,
 
 static void
 ide_session_addin_real_restore_async (IdeSessionAddin     *self,
+                                      IdeWorkbench         *workbench,
                                       GVariant            *state,
                                       GCancellable        *cancellable,
                                       GAsyncReadyCallback  callback,
@@ -81,6 +83,7 @@ ide_session_addin_default_init (IdeSessionAddinInterface *iface)
 /**
  * ide_session_addin_save_async:
  * @self: a #IdeSessionAddin
+ * @workbench: an #IdeWorkbench
  * @cancellable: (nullable): A #GCancellable or %NULL
  * @callback: callback to execute upon completion
  * @user_data: closure data for @callback
@@ -90,18 +93,20 @@ ide_session_addin_default_init (IdeSessionAddinInterface *iface)
  * The resulting state will be provided when restoring the addin
  * at a future time.
  *
- * Since: 3.32
+ * Since: 3.30
  */
 void
 ide_session_addin_save_async (IdeSessionAddin     *self,
+                              IdeWorkbench        *workbench,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
 {
   g_return_if_fail (IDE_IS_SESSION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  IDE_SESSION_ADDIN_GET_IFACE (self)->save_async (self, cancellable, callback, user_data);
+  IDE_SESSION_ADDIN_GET_IFACE (self)->save_async (self, workbench, cancellable, callback, user_data);
 }
 
 /**
@@ -114,7 +119,7 @@ ide_session_addin_save_async (IdeSessionAddin     *self,
  *
  * Returns: (transfer full) (nullable): a #GVariant or %NULL.
  *
- * Since: 3.32
+ * Since: 3.30
  */
 GVariant *
 ide_session_addin_save_finish (IdeSessionAddin  *self,
@@ -130,6 +135,7 @@ ide_session_addin_save_finish (IdeSessionAddin  *self,
 /**
  * ide_session_addin_restore_async:
  * @self: a #IdeSessionAddin
+ * @workbench: an #IdeWorkbench
  * @state: a #GVariant of previous state
  * @cancellable: (nullable): A #GCancellable or %NULL
  * @callback: callback to execute upon completion
@@ -137,19 +143,21 @@ ide_session_addin_save_finish (IdeSessionAddin  *self,
  *
  * Asynchronous request to restore session state by the addin.
  *
- * Since: 3.32
+ * Since: 3.30
  */
 void
 ide_session_addin_restore_async (IdeSessionAddin     *self,
+                                 IdeWorkbench        *workbench,
                                  GVariant            *state,
                                  GCancellable        *cancellable,
                                  GAsyncReadyCallback  callback,
                                  gpointer             user_data)
 {
   g_return_if_fail (IDE_IS_SESSION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  IDE_SESSION_ADDIN_GET_IFACE (self)->restore_async (self, state, cancellable, callback, user_data);
+  IDE_SESSION_ADDIN_GET_IFACE (self)->restore_async (self, workbench, state, cancellable, callback, 
user_data);
 }
 
 gboolean
diff --git a/src/libide/session/ide-session-addin.h b/src/libide/gui/ide-session-addin.h
similarity index 87%
rename from src/libide/session/ide-session-addin.h
rename to src/libide/gui/ide-session-addin.h
index 687944391..14425f5e3 100644
--- a/src/libide/session/ide-session-addin.h
+++ b/src/libide/gui/ide-session-addin.h
@@ -20,8 +20,13 @@
 
 #pragma once
 
-#include "ide-object.h"
-#include "ide-version-macros.h"
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-workbench.h"
 
 G_BEGIN_DECLS
 
@@ -35,6 +40,7 @@ struct _IdeSessionAddinInterface
   GTypeInterface parent;
 
   void      (*save_async)     (IdeSessionAddin      *self,
+                               IdeWorkbench         *workbench,
                                GCancellable         *cancellable,
                                GAsyncReadyCallback   callback,
                                gpointer              user_data);
@@ -42,6 +48,7 @@ struct _IdeSessionAddinInterface
                                GAsyncResult         *result,
                                GError              **error);
   void      (*restore_async)  (IdeSessionAddin      *self,
+                               IdeWorkbench         *workbench,
                                GVariant             *state,
                                GCancellable         *cancellable,
                                GAsyncReadyCallback   callback,
@@ -53,6 +60,7 @@ struct _IdeSessionAddinInterface
 
 IDE_AVAILABLE_IN_3_32
 void      ide_session_addin_save_async     (IdeSessionAddin      *self,
+                                            IdeWorkbench         *workbench,
                                             GCancellable         *cancellable,
                                             GAsyncReadyCallback   callback,
                                             gpointer              user_data);
@@ -62,6 +70,7 @@ GVariant *ide_session_addin_save_finish    (IdeSessionAddin      *self,
                                             GError              **error);
 IDE_AVAILABLE_IN_3_32
 void      ide_session_addin_restore_async  (IdeSessionAddin      *self,
+                                            IdeWorkbench         *workbench,
                                             GVariant             *state,
                                             GCancellable         *cancellable,
                                             GAsyncReadyCallback   callback,
diff --git a/src/libide/gui/ide-session-private.h b/src/libide/gui/ide-session-private.h
new file mode 100644
index 000000000..7a7f343dc
--- /dev/null
+++ b/src/libide/gui/ide-session-private.h
@@ -0,0 +1,51 @@
+/* ide-session-private.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SESSION (ide_session_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeSession, ide_session, IDE, SESSION, IdeObject)
+
+IdeSession *ide_session_new            (void);
+void        ide_session_restore_async  (IdeSession           *self,
+                                        IdeWorkbench         *workbench,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+gboolean    ide_session_restore_finish (IdeSession           *self,
+                                        GAsyncResult         *result,
+                                        GError              **error);
+void        ide_session_save_async     (IdeSession           *self,
+                                        IdeWorkbench         *workbench,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+gboolean    ide_session_save_finish    (IdeSession           *self,
+                                        GAsyncResult         *result,
+                                        GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/session/ide-session.c b/src/libide/gui/ide-session.c
similarity index 84%
rename from src/libide/session/ide-session.c
rename to src/libide/gui/ide-session.c
index df18ff00f..29d5fe81d 100644
--- a/src/libide/session/ide-session.c
+++ b/src/libide/gui/ide-session.c
@@ -23,19 +23,16 @@
 #include "config.h"
 
 #include <libpeas/peas.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-#include "ide-global.h"
-#include "ide-session.h"
-
-#include "session/ide-session-addin.h"
-#include "threading/ide-task.h"
+#include "ide-session-addin.h"
+#include "ide-session-private.h"
 
 struct _IdeSession
 {
-  IdeObject         parent_instance;
-  PeasExtensionSet *addins;
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *addins;
 };
 
 typedef struct
@@ -48,9 +45,10 @@ typedef struct
 
 typedef struct
 {
-  GPtrArray *addins;
-  GVariant  *state;
-  gint       active;
+  IdeWorkbench *workbench;
+  GPtrArray    *addins;
+  GVariant     *state;
+  gint          active;
 } Restore;
 
 G_DEFINE_TYPE (IdeSession, ide_session, IDE_TYPE_OBJECT)
@@ -62,6 +60,7 @@ restore_free (Restore *r)
 
   g_clear_pointer (&r->addins, g_ptr_array_unref);
   g_clear_pointer (&r->state, g_variant_unref);
+  g_clear_object (&r->workbench);
   g_slice_free (Restore, r);
 }
 
@@ -79,14 +78,15 @@ save_free (Save *s)
 }
 
 static void
-collect_addins_cb (PeasExtensionSet *set,
-                   PeasPluginInfo   *plugin_info,
-                   PeasExtension    *exten,
-                   gpointer          user_data)
+collect_addins_cb (IdeExtensionSetAdapter *set,
+                   PeasPluginInfo         *plugin_info,
+                   PeasExtension          *exten,
+                   gpointer                user_data)
 {
   GPtrArray *ar = user_data;
 
-  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
   g_assert (plugin_info != NULL);
   g_assert (IDE_IS_SESSION_ADDIN (exten));
   g_assert (ar != NULL);
@@ -95,42 +95,48 @@ collect_addins_cb (PeasExtensionSet *set,
 }
 
 static void
-ide_session_dispose (GObject *object)
+ide_session_destroy (IdeObject *object)
 {
   IdeSession *self = (IdeSession *)object;
 
   IDE_ENTRY;
 
-  g_clear_object (&self->addins);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (self));
+
+  ide_clear_and_destroy_object (&self->addins);
 
-  G_OBJECT_CLASS (ide_session_parent_class)->dispose (object);
+  IDE_OBJECT_CLASS (ide_session_parent_class)->destroy (object);
 
   IDE_EXIT;
 }
 
 static void
-ide_session_constructed (GObject *object)
+ide_session_parent_set (IdeObject *object,
+                        IdeObject *parent)
 {
   IdeSession *self = (IdeSession *)object;
-  IdeContext *context;
 
-  G_OBJECT_CLASS (ide_session_parent_class)->constructed (object);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
+  if (parent == NULL)
+    return;
 
-  self->addins = peas_extension_set_new (peas_engine_get_default (),
-                                         IDE_TYPE_SESSION_ADDIN,
-                                         "context", context,
-                                         NULL);
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_SESSION_ADDIN,
+                                                NULL, NULL);
 }
 
 static void
 ide_session_class_init (IdeSessionClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->constructed = ide_session_constructed;
-  object_class->dispose = ide_session_dispose;
+  i_object_class->destroy = ide_session_destroy;
+  i_object_class->parent_set = ide_session_parent_set;
 }
 
 static void
@@ -240,6 +246,7 @@ ide_session_restore_load_cb (GObject      *object,
                                       NULL);
 
       ide_session_addin_restore_async (addin,
+                                       r->workbench,
                                        state,
                                        cancellable,
                                        ide_session_restore_addin_restore_cb,
@@ -252,6 +259,7 @@ ide_session_restore_load_cb (GObject      *object,
 /**
  * ide_session_restore_async:
  * @self: an #IdeSession
+ * @workbench: an #IdeWorkbench
  * @cancellable: (nullable): a #GCancellbale or %NULL
  * @callback: the callback to execute upon completion
  * @user_data: user data for callback
@@ -260,10 +268,11 @@ ide_session_restore_load_cb (GObject      *object,
  * the point it was last saved (typically upon shutdown). This includes
  * open documents and editor splits to the degree possible.
  *
- * Since: 3.32
+ * Since: 3.30
  */
 void
 ide_session_restore_async (IdeSession          *self,
+                           IdeWorkbench        *workbench,
                            GCancellable        *cancellable,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
@@ -276,14 +285,16 @@ ide_session_restore_async (IdeSession          *self,
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_SESSION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_session_restore_async);
 
   r = g_slice_new0 (Restore);
+  r->workbench = g_object_ref (workbench);
   r->addins = g_ptr_array_new_with_free_func (g_object_unref);
-  peas_extension_set_foreach (self->addins, collect_addins_cb, r->addins);
+  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, r->addins);
   r->active = r->addins->len;
   ide_task_set_task_data (task, r, restore_free);
 
@@ -421,6 +432,7 @@ ide_session_save_addin_save_cb (GObject      *object,
 /**
  * ide_session_save_async:
  * @self: an #IdeSession
+ * @workbench: an #IdeWorkbench
  * @cancellable: (nullable): a #GCancellable, or %NULL
  * @callback: a callback to execute upon completion
  * @user_data: user data for @callback
@@ -429,10 +441,11 @@ ide_session_save_addin_save_cb (GObject      *object,
  * so that the project may be restored to the current layout when the project
  * is re-opened at a later time.
  *
- * Since: 3.32
+ * Since: 3.30
  */
 void
 ide_session_save_async (IdeSession          *self,
+                        IdeWorkbench        *workbench,
                         GCancellable        *cancellable,
                         GAsyncReadyCallback  callback,
                         gpointer             user_data)
@@ -443,6 +456,7 @@ ide_session_save_async (IdeSession          *self,
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_SESSION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
@@ -450,7 +464,7 @@ ide_session_save_async (IdeSession          *self,
 
   s = g_slice_new0 (Save);
   s->addins = g_ptr_array_new_with_free_func (g_object_unref);
-  peas_extension_set_foreach (self->addins, collect_addins_cb, s->addins);
+  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, s->addins);
   s->active = s->addins->len;
   g_variant_dict_init (&s->dict, NULL);
   s->dict_needs_clear = TRUE;
@@ -467,6 +481,7 @@ ide_session_save_async (IdeSession          *self,
       IdeSessionAddin *addin = g_ptr_array_index (s->addins, i);
 
       ide_session_addin_save_async (addin,
+                                    workbench,
                                     cancellable,
                                     ide_session_save_addin_save_cb,
                                     g_object_ref (task));
@@ -495,3 +510,9 @@ ide_session_save_finish (IdeSession    *self,
 
   IDE_RETURN (ret);
 }
+
+IdeSession *
+ide_session_new (void)
+{
+  return g_object_new (IDE_TYPE_SESSION, NULL);
+}
diff --git a/src/libide/layout/ide-shortcut-label.h b/src/libide/gui/ide-shortcut-label-private.h
similarity index 98%
rename from src/libide/layout/ide-shortcut-label.h
rename to src/libide/gui/ide-shortcut-label-private.h
index 8789afd0d..5d27f230f 100644
--- a/src/libide/layout/ide-shortcut-label.h
+++ b/src/libide/gui/ide-shortcut-label-private.h
@@ -1,4 +1,4 @@
-/* ide-shortcut-label.h
+/* ide-shortcut-label-private.h
  *
  * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  *
diff --git a/src/libide/layout/ide-shortcut-label.c b/src/libide/gui/ide-shortcut-label.c
similarity index 99%
rename from src/libide/layout/ide-shortcut-label.c
rename to src/libide/gui/ide-shortcut-label.c
index 38d2b1ca5..d561f5d9c 100644
--- a/src/libide/layout/ide-shortcut-label.c
+++ b/src/libide/gui/ide-shortcut-label.c
@@ -24,7 +24,7 @@
 
 #include <dazzle.h>
 
-#include "layout/ide-shortcut-label.h"
+#include "ide-shortcut-label-private.h"
 
 struct _IdeShortcutLabel
 {
diff --git a/src/libide/gui/ide-shortcuts-window-private.h b/src/libide/gui/ide-shortcuts-window-private.h
new file mode 100644
index 000000000..5ce88ce59
--- /dev/null
+++ b/src/libide/gui/ide-shortcuts-window-private.h
@@ -0,0 +1,31 @@
+/* ide-shortcuts-window.h
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_WINDOW (ide_shortcuts_window_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutsWindow, ide_shortcuts_window, IDE, SHORTCUTS_WINDOW, GtkShortcutsWindow)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-shortcuts-window.c b/src/libide/gui/ide-shortcuts-window.c
new file mode 100644
index 000000000..5f7dc3494
--- /dev/null
+++ b/src/libide/gui/ide-shortcuts-window.c
@@ -0,0 +1,48 @@
+/* ide-shortcuts-window.c
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-shortcuts-window"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcuts-window-private.h"
+
+struct _IdeShortcutsWindow
+{
+  GtkShortcutsWindow parent_instance;
+};
+
+G_DEFINE_TYPE (IdeShortcutsWindow, ide_shortcuts_window, GTK_TYPE_SHORTCUTS_WINDOW)
+
+static void
+ide_shortcuts_window_class_init (IdeShortcutsWindowClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-shortcuts-window.ui");
+}
+
+static void
+ide_shortcuts_window_init (IdeShortcutsWindow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libide/gui/ide-shortcuts-window.ui b/src/libide/gui/ide-shortcuts-window.ui
new file mode 100644
index 000000000..8c3191238
--- /dev/null
+++ b/src/libide/gui/ide-shortcuts-window.ui
@@ -0,0 +1,547 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.19 -->
+  <template class="IdeShortcutsWindow">
+    <property name="modal">true</property>
+    <child>
+      <object class="GtkShortcutsSection">
+        <property name="visible">true</property>
+        <property name="section-name">editor</property>
+        <property name="title" translatable="yes" context="shortcut window">Editor Shortcuts</property>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">General</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Global Search</property>
+                <property name="accelerator">&lt;ctrl&gt;period</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Preferences</property>
+                <property name="accelerator">&lt;ctrl&gt;comma</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Command Bar</property>
+                <property name="accelerator">&lt;ctrl&gt;Return</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Terminal</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;t</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Terminal in Build 
Runtime</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;&lt;shift&gt;t</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Keyboard 
Shortcuts</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;question</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Toggle Focus 
Mode</property>
+                <property name="accelerator">F11</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Panels</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Toggle left 
panel</property>
+                <property name="accelerator">F9</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Toggle bottom 
panel</property>
+                <property name="accelerator">&lt;ctrl&gt;F9</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Files</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;n</property>
+                <property name="title" translatable="yes" context="shortcut window">Create new 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;o</property>
+                <property name="title" translatable="yes" context="shortcut window">Open a 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;s</property>
+                <property name="title" translatable="yes" context="shortcut window">Save the 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;s</property>
+                <property name="title" translatable="yes" context="shortcut window">Save all 
documents</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;w</property>
+                <property name="title" translatable="yes" context="shortcut window">Close the 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;primary&gt;&lt;shift&gt;w</property>
+                <property name="title" translatable="yes" context="shortcut window">Close all 
documents</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;Page_Down</property>
+                <property name="title" translatable="yes" context="shortcut window">Switch to the next 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;Page_Up</property>
+                <property name="title" translatable="yes" context="shortcut window">Switch to the previous 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;k</property>
+                <property name="title" translatable="yes" context="shortcut window">Show list of open 
documents</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Find and replace</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;f</property>
+                <property name="title" translatable="yes" context="shortcut window">Find</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;h</property>
+                <property name="title" translatable="yes" context="shortcut window">Find and 
replace</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;g</property>
+                <property name="title" translatable="yes" context="shortcut window">Find the next 
match</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;g</property>
+                <property name="title" translatable="yes" context="shortcut window">Find the previous 
match</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;k</property>
+                <property name="title" translatable="yes" context="shortcut window">Clear 
highlight</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Copy and Paste</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;c</property>
+                <property name="title" translatable="yes" context="shortcut window">Copy selected text to 
clipboard</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;x</property>
+                <property name="title" translatable="yes" context="shortcut window">Cut selected text to 
clipboard</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;v</property>
+                <property name="title" translatable="yes" context="shortcut window">Paste text from 
clipboard</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;d</property>
+                <property name="title" translatable="yes" context="shortcut window">Duplicate current line 
or selection</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Undo and Redo</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;z</property>
+                <property name="title" translatable="yes" context="shortcut window">Undo previous 
command</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;z</property>
+                <property name="title" translatable="yes" context="shortcut window">Redo previous 
command</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Editing</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;a</property>
+                <property name="title" translatable="yes" context="shortcut window">Increment number at 
cursor</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;x</property>
+                <property name="title" translatable="yes" context="shortcut window">Decrement number at 
cursor</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;j</property>
+                <property name="title" translatable="yes" context="shortcut window">Join selected 
lines</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;space</property>
+                <property name="title" translatable="yes" context="shortcut window">Show completion 
window</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">Insert</property>
+                <property name="title" translatable="yes" context="shortcut window">Toggle 
overwrite</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;i</property>
+                <property name="title" translatable="yes" context="shortcut window">Reindent line</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;d</property>
+                <property name="title" translatable="yes" context="shortcut window">Delete line</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;r</property>
+                <property name="title" translatable="yes" context="shortcut window">Rename symbol</property>
+                <property name="subtitle" translatable="yes" context="shortcut window">Requires semantic 
language support</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Navigation</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;n</property>
+                <property name="title" translatable="yes" context="shortcut window">Move to next error in 
file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;p</property>
+                <property name="title" translatable="yes" context="shortcut window">Move to previous error 
in file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;shift&gt;&lt;alt&gt;Left</property>
+                <property name="title" translatable="yes" context="shortcut window">Move to previous edit 
location</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;shift&gt;&lt;alt&gt;Right</property>
+                <property name="title" translatable="yes" context="shortcut window">Move to next edit 
location</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;period</property>
+                <property name="title" translatable="yes" context="shortcut window">Jump to definition of 
symbol</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;&lt;shift&gt;Up</property>
+                <property name="title" translatable="yes" context="shortcut window">Move viewport up within 
the file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;&lt;shift&gt;Down</property>
+                <property name="title" translatable="yes" context="shortcut window">Move viewport down 
within the file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;&lt;shift&gt;End</property>
+                <property name="title" translatable="yes" context="shortcut window">Move viewport to end of 
file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;alt&gt;&lt;shift&gt;Home</property>
+                <property name="title" translatable="yes" context="shortcut window">Move viewport to 
beginning of file</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;percent</property>
+                <property name="title" translatable="yes" context="shortcut window">Move to matching 
bracket</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Selections</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;a</property>
+                <property name="title" translatable="yes" context="shortcut window">Select all</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;backslash</property>
+                <property name="title" translatable="yes" context="shortcut window">Unselect all</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+            <object class="GtkShortcutsGroup">
+              <property name="visible">true</property>
+              <property name="title" translatable="yes" context="shortcut window">Build and Run</property>
+              <child>
+                <object class="GtkShortcutsShortcut">
+                  <property name="visible">true</property>
+                  <property name="accelerator">&lt;ctrl&gt;F7</property>
+                   <property name="title" translatable="yes" context="shortcut window">Build</property>
+                </object>
+              </child>
+              <child>
+                <object class="GtkShortcutsShortcut">
+                  <property name="visible">true</property>
+                  <property name="accelerator">&lt;ctrl&gt;F5</property>
+                  <property name="title" translatable="yes" context="shortcut window">Run</property>
+                </object>
+              </child>
+              <child>
+                <object class="GtkShortcutsShortcut">
+                  <property name="visible">true</property>
+                  <property name="accelerator">&lt;ctrl&gt;F8</property>
+                  <property name="title" translatable="yes" context="shortcut window">Profile</property>
+                </object>
+              </child>
+            </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Touchpad gestures</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="shortcut-type">gesture-two-finger-swipe-right</property>
+                <property name="title" translatable="yes" context="shortcut window">Switch to the next 
document</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="shortcut-type">gesture-two-finger-swipe-left</property>
+                <property name="title" translatable="yes" context="shortcut window">Switch to the previous 
document</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkShortcutsSection">
+        <property name="visible">true</property>
+        <property name="section-name">terminal</property>
+        <property name="title" translatable="yes" context="shortcut window">Terminal Shortcuts</property>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">General</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Global Search</property>
+                <property name="accelerator">&lt;ctrl&gt;period</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Preferences</property>
+                <property name="accelerator">&lt;ctrl&gt;comma</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Command Bar</property>
+                <property name="accelerator">&lt;ctrl&gt;Return</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Terminal</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;t</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Terminal in Build 
Runtime</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;alt&gt;&lt;shift&gt;t</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="title" translatable="yes" context="shortcut window">Keyboard 
Shortcuts</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;question</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Copy and Paste</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;c</property>
+                <property name="title" translatable="yes" context="shortcut window">Copy selected text to 
clipboard</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;v</property>
+                <property name="title" translatable="yes" context="shortcut window">Paste text from 
clipboard</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Search</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;f</property>
+                <property name="title" translatable="yes" context="shortcut window">Find text within 
terminal</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-surface.c b/src/libide/gui/ide-surface.c
new file mode 100644
index 000000000..43224679c
--- /dev/null
+++ b/src/libide/gui/ide-surface.c
@@ -0,0 +1,259 @@
+/* ide-surface.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-surface"
+
+#include "config.h"
+
+#include "ide-gui-private.h"
+#include "ide-surface.h"
+
+typedef struct
+{
+  gchar *icon_name;
+  gchar *title;
+} IdeSurfacePrivate;
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_TITLE,
+  N_PROPS
+};
+
+static void dock_item_iface_init (DzlDockItemInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeSurface, ide_surface, DZL_TYPE_DOCK_BIN,
+                         G_ADD_PRIVATE (IdeSurface)
+                         G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dock_item_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_surface_finalize (GObject *object)
+{
+  IdeSurface *self = (IdeSurface *)object;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->title, g_free);
+
+  G_OBJECT_CLASS (ide_surface_parent_class)->finalize (object);
+}
+
+static void
+ide_surface_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeSurface *self = IDE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      g_value_set_string (value, dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (self)));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, dzl_dock_item_get_title (DZL_DOCK_ITEM (self)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_surface_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeSurface *self = IDE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      ide_surface_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_surface_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_surface_class_init (IdeSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_surface_finalize;
+  object_class->get_property = ide_surface_get_property;
+  object_class->set_property = ide_surface_set_property;
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon name for the surface",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the surface, if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_surface_init (IdeSurface *self)
+{
+}
+
+/**
+ * ide_surface_new:
+ *
+ * Creates a new #IdeSurface.
+ *
+ * Surfaces contain the main window contents that are placed inside of an
+ * #IdeWorkspace (window). You may have multiple surfaces in a workspace,
+ * and the user can switch between them.
+ *
+ * Returns: (transfer full): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_SURFACE, NULL);
+}
+
+void
+ide_surface_set_icon_name (IdeSurface  *self,
+                           const gchar *icon_name)
+{
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (!ide_str_equal0 (priv->icon_name, icon_name))
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+void
+ide_surface_set_title (IdeSurface  *self,
+                       const gchar *title)
+{
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (!ide_str_equal0 (priv->title, title))
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+/**
+ * ide_surface_foreach_page:
+ * @self: a #IdeSurface
+ * @callback: (scope call): callback to execute for each page
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for every page found within the surface @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_surface_foreach_page (IdeSurface  *self,
+                          GtkCallback  callback,
+                          gpointer     user_data)
+{
+  g_return_if_fail (IDE_IS_SURFACE (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_SURFACE_GET_CLASS (self)->foreach_page)
+    IDE_SURFACE_GET_CLASS (self)->foreach_page (self, callback, user_data);
+}
+
+static gchar *
+ide_surface_real_get_icon_name (DzlDockItem *item)
+{
+  IdeSurface *self = (IdeSurface *)item;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SURFACE (self), NULL);
+
+  return g_strdup (priv->icon_name);
+}
+
+static gchar *
+ide_surface_real_get_title (DzlDockItem *item)
+{
+  IdeSurface *self = (IdeSurface *)item;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SURFACE (self), NULL);
+
+  return g_strdup (priv->title);
+}
+
+static void
+dock_item_iface_init (DzlDockItemInterface *iface)
+{
+  iface->get_icon_name = ide_surface_real_get_icon_name;
+  iface->get_title = ide_surface_real_get_title;
+}
+
+gboolean
+ide_surface_agree_to_shutdown (IdeSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_SURFACE (self), FALSE);
+
+  if (IDE_SURFACE_GET_CLASS (self)->agree_to_shutdown)
+    return IDE_SURFACE_GET_CLASS (self)->agree_to_shutdown (self);
+
+  return TRUE;
+}
+
+void
+_ide_surface_set_fullscreen (IdeSurface *self,
+                             gboolean    fullscreen)
+{
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (IDE_SURFACE_GET_CLASS (self)->set_fullscreen)
+    IDE_SURFACE_GET_CLASS (self)->set_fullscreen (self, fullscreen);
+}
diff --git a/src/libide/gui/ide-surface.h b/src/libide/gui/ide-surface.h
new file mode 100644
index 000000000..2be97c69f
--- /dev/null
+++ b/src/libide/gui/ide-surface.h
@@ -0,0 +1,67 @@
+/* ide-surface.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SURFACE (ide_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSurface, ide_surface, IDE, SURFACE, DzlDockBin)
+
+struct _IdeSurfaceClass
+{
+  DzlDockBinClass parent_class;
+
+  void     (*foreach_page)        (IdeSurface  *self,
+                                   GtkCallback  callback,
+                                   gpointer     user_data);
+  gboolean (*agree_to_shutdown)   (IdeSurface  *self);
+  void     (*set_fullscreen)      (IdeSurface  *self,
+                                   gboolean     fullscreen);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_surface_new               (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_set_icon_name     (IdeSurface  *self,
+                                          const gchar *icon_name);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_set_title         (IdeSurface  *self,
+                                          const gchar *title);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_foreach_page      (IdeSurface  *self,
+                                          GtkCallback  callback,
+                                          gpointer     user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_surface_agree_to_shutdown (IdeSurface  *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-surfaces-button.c b/src/libide/gui/ide-surfaces-button.c
new file mode 100644
index 000000000..447ebf475
--- /dev/null
+++ b/src/libide/gui/ide-surfaces-button.c
@@ -0,0 +1,107 @@
+/* ide-surfaces-button.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-surfaces-button"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-surfaces-button.h"
+
+struct _IdeSurfacesButton
+{
+  DzlMenuButton parent_instance;
+};
+
+G_DEFINE_TYPE (IdeSurfacesButton, ide_surfaces_button, DZL_TYPE_MENU_BUTTON)
+
+static void
+ide_surfaces_button_items_changed_cb (IdeSurfacesButton *self,
+                                      guint              position,
+                                      guint              added,
+                                      guint              removed,
+                                      GMenuModel        *model)
+{
+  gboolean visible = FALSE;
+  guint n_items;
+  guint count = 0;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACES_BUTTON (self));
+  g_assert (G_IS_MENU_MODEL (model));
+
+  /* We either have multiple sections, or a single section with
+   * possibly multiple children. Any of these means visible.
+   */
+
+  n_items = g_menu_model_get_n_items (model);
+  visible = n_items > 1;
+
+  for (guint i = 0; !visible && i < n_items; i++)
+    {
+      g_autoptr(GMenuLinkIter) iter = g_menu_model_iterate_item_links (model, i);
+
+      while (g_menu_link_iter_next (iter))
+        {
+          g_autoptr(GMenuModel) child = g_menu_link_iter_get_value (iter);
+          count += g_menu_model_get_n_items (child);
+        }
+
+      visible = count > 1;
+    }
+
+  gtk_widget_set_visible (GTK_WIDGET (self), visible);
+}
+
+static void
+ide_surfaces_button_notify_model (IdeSurfacesButton *self,
+                                  GParamSpec        *pspec,
+                                  gpointer           user_data)
+{
+  GMenuModel *model;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACES_BUTTON (self));
+
+  if ((model = dzl_menu_button_get_model (DZL_MENU_BUTTON (self))))
+    {
+      g_signal_connect_object (model,
+                               "items-changed",
+                               G_CALLBACK (ide_surfaces_button_items_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      ide_surfaces_button_items_changed_cb (self, 0, 0, 0, model);
+    }
+}
+
+static void
+ide_surfaces_button_class_init (IdeSurfacesButtonClass *klass)
+{
+}
+
+static void
+ide_surfaces_button_init (IdeSurfacesButton *self)
+{
+  g_signal_connect (self,
+                    "notify::model",
+                    G_CALLBACK (ide_surfaces_button_notify_model),
+                    NULL);
+}
diff --git a/src/libide/layout/ide-layout.h b/src/libide/gui/ide-surfaces-button.h
similarity index 66%
rename from src/libide/layout/ide-layout.h
rename to src/libide/gui/ide-surfaces-button.h
index b5d27a51c..d9efe9808 100644
--- a/src/libide/layout/ide-layout.h
+++ b/src/libide/gui/ide-surfaces-button.h
@@ -1,6 +1,6 @@
-/* ide-layout.h
+/* ide-surfaces-button.h
  *
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018 Christian Hergert <unknown domain org>
  *
  * 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
@@ -20,23 +20,18 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
+#include <dazzle.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT (ide_layout_get_type())
+#define IDE_TYPE_SURFACES_BUTTON (ide_surfaces_button_get_type())
 
 IDE_AVAILABLE_IN_3_32
-G_DECLARE_DERIVABLE_TYPE (IdeLayout, ide_layout, IDE, LAYOUT, DzlDockBin)
-
-struct _IdeLayoutClass
-{
-  DzlDockBinClass parent_class;
-
-  /*< private >*/
-  gpointer _reserved[8];
-};
+G_DECLARE_FINAL_TYPE (IdeSurfacesButton, ide_surfaces_button, IDE, SURFACES_BUTTON, DzlMenuButton)
 
 G_END_DECLS
diff --git a/src/libide/search/ide-tagged-entry.c b/src/libide/gui/ide-tagged-entry.c
similarity index 99%
rename from src/libide/search/ide-tagged-entry.c
rename to src/libide/gui/ide-tagged-entry.c
index fac6a3b01..e719bb0bf 100644
--- a/src/libide/search/ide-tagged-entry.c
+++ b/src/libide/gui/ide-tagged-entry.c
@@ -18,6 +18,8 @@
  *
  * Author: Cosimo Cecchi <cosimoc redhat com>
  *
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #include "config.h"
diff --git a/src/libide/search/ide-tagged-entry.h b/src/libide/gui/ide-tagged-entry.h
similarity index 97%
rename from src/libide/search/ide-tagged-entry.h
rename to src/libide/gui/ide-tagged-entry.h
index a49e6eebc..261985d2d 100644
--- a/src/libide/search/ide-tagged-entry.h
+++ b/src/libide/gui/ide-tagged-entry.h
@@ -3,29 +3,30 @@
  * Copyright 2013 Ignacio Casal Quinteiro
  *
  * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published by 
+ * it under the terms of the GNU Lesser General Public License as published by
  * the Free Software Foundation; either version 2 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 Lesser General Public 
+ * 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 Lesser General Public License 
+ * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * Author: Cosimo Cecchi <cosimoc redhat com>
  *
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #ifndef __IDE_TAGGED_ENTRY_H__
 #define __IDE_TAGGED_ENTRY_H__
 
 #include <gtk/gtk.h>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/gui/ide-transient-sidebar.c b/src/libide/gui/ide-transient-sidebar.c
new file mode 100644
index 000000000..2fa15b032
--- /dev/null
+++ b/src/libide/gui/ide-transient-sidebar.c
@@ -0,0 +1,355 @@
+/* ide-transient-sidebar.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-transient-sidebar"
+
+#include "config.h"
+
+#include "ide-frame.h"
+#include "ide-grid.h"
+#include "ide-transient-sidebar.h"
+
+typedef struct
+{
+  DzlSignalGroup *toplevel_signals;
+  GWeakRef        page_ref;
+  gint            hold_count;
+} IdeTransientSidebarPrivate;
+
+static void ide_transient_sidebar_page_destroyed (IdeTransientSidebar *self,
+                                                  IdePage             *page);
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransientSidebar, ide_transient_sidebar, IDE_TYPE_PANEL)
+
+static gboolean
+has_page_related_focus (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  g_autoptr(IdePage) page = NULL;
+  GtkWidget *focus_page;
+  GtkWidget *toplevel;
+  GtkWidget *focus;
+  GtkWidget *grid;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+
+  /* If there is no page, then nothing more to do */
+  page = g_weak_ref_get (&priv->page_ref);
+  if (page == NULL)
+    return FALSE;
+
+  /* We need the toplevel to get the current focus */
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+  if (!GTK_IS_WINDOW (toplevel))
+    return FALSE;
+
+  /* Synthesize succes when there is no focus, this can happen inbetween
+   * various state transitions.
+   */
+  focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+  if (focus == NULL)
+    return TRUE;
+
+  /* If focus is inside this widget, then we don't want to hide */
+  if (gtk_widget_is_ancestor (focus, GTK_WIDGET (self)))
+    return TRUE;
+
+  /* If focus is in the page, then we definitely don't want to hide */
+  if (gtk_widget_is_ancestor (focus, GTK_WIDGET (page)))
+    return TRUE;
+
+  /* If the focus has entered another page, then we can release. */
+  focus_page = gtk_widget_get_ancestor (focus, IDE_TYPE_PAGE);
+  if (focus_page && focus_page != GTK_WIDGET (page))
+    return FALSE;
+
+  /* If we found ourselves a grid, and it has no pages in it, we shall
+   * expect that there are no more pages to apply.
+   */
+  grid = gtk_widget_get_ancestor (focus, IDE_TYPE_GRID);
+  if (grid != NULL &&
+      ide_grid_count_pages (IDE_GRID (grid)) == 0)
+    return FALSE;
+
+  /* Focus hasn't landed anywhere that indicates to us that the
+   * page definitely isn't visible anymore, so we can just keep
+   * the panel visible for now.
+   */
+
+  return TRUE;
+}
+
+static void
+set_visible (IdeTransientSidebar *self,
+             gboolean             visible)
+{
+  const gchar *prop_name;
+  GtkPositionType pos;
+  GtkWidget *bin;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+
+  if (!(bin = gtk_widget_get_ancestor (GTK_WIDGET (self), DZL_TYPE_DOCK_BIN)))
+    {
+      g_warning ("Failed to locate DzlDockBin for transition");
+      return;
+    }
+
+  gtk_container_child_get (GTK_CONTAINER (bin), GTK_WIDGET (self),
+                           "position", &pos,
+                           NULL);
+
+  switch (pos)
+    {
+    case GTK_POS_TOP:
+      prop_name = "top-visible";
+      break;
+
+    case GTK_POS_BOTTOM:
+      prop_name = "bottom-visible";
+      break;
+
+    case GTK_POS_LEFT:
+      prop_name = "left-visible";
+      break;
+
+    case GTK_POS_RIGHT:
+      prop_name = "right-visible";
+      break;
+
+    default:
+      g_return_if_reached ();
+    }
+
+  g_object_set (bin, prop_name, visible, NULL);
+}
+
+static void
+ide_transient_sidebar_after_set_focus (IdeTransientSidebar *self,
+                                       GtkWidget           *focus,
+                                       GtkWindow           *toplevel)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (!toplevel || GTK_IS_WINDOW (toplevel));
+  g_assert (priv->hold_count >= 0);
+
+  if (priv->hold_count > 0)
+    return;
+
+  /*
+   * If we are currently visible, then check to see if the focus has gone
+   * somewhere outside the panel or the page. If so, we need to dismiss
+   * the panel.
+   *
+   * We try to be tolerant of sibling focus on such things like the stack
+   * header.
+   */
+  if (gtk_widget_get_visible (GTK_WIDGET (self)))
+    {
+      if (!has_page_related_focus (self))
+        {
+          g_autoptr(GtkWidget) old_page = g_weak_ref_get (&priv->page_ref);
+
+          if (old_page != NULL)
+            g_signal_handlers_disconnect_by_func (old_page,
+                                                  G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                                  self);
+
+          set_visible (self, FALSE);
+          g_weak_ref_set (&priv->page_ref, NULL);
+        }
+    }
+}
+
+static void
+ide_transient_sidebar_page_destroyed (IdeTransientSidebar *self,
+                                      IdePage             *page)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (IDE_IS_PAGE (page));
+
+  g_signal_handlers_disconnect_by_func (page,
+                                        G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                        self);
+
+  g_weak_ref_set (&priv->page_ref, NULL);
+
+  ide_transient_sidebar_after_set_focus (self, NULL, NULL);
+}
+
+static void
+ide_transient_sidebar_hierarchy_changed (GtkWidget *widget,
+                                         GtkWidget *old_toplevel)
+{
+  IdeTransientSidebar *self = (IdeTransientSidebar *)widget;
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  toplevel = gtk_widget_get_toplevel (widget);
+  if (!GTK_IS_WINDOW (toplevel))
+    toplevel = NULL;
+
+  dzl_signal_group_set_target (priv->toplevel_signals, toplevel);
+}
+
+static void
+ide_transient_sidebar_finalize (GObject *object)
+{
+  IdeTransientSidebar *self = (IdeTransientSidebar *)object;
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_clear_object (&priv->toplevel_signals);
+  g_weak_ref_clear (&priv->page_ref);
+
+  G_OBJECT_CLASS (ide_transient_sidebar_parent_class)->finalize (object);
+}
+
+static void
+ide_transient_sidebar_class_init (IdeTransientSidebarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_transient_sidebar_finalize;
+
+  widget_class->hierarchy_changed = ide_transient_sidebar_hierarchy_changed;
+}
+
+static void
+ide_transient_sidebar_init (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  GtkWidget *paned;
+  GtkWidget *stack;
+
+  g_weak_ref_init (&priv->page_ref, NULL);
+
+  priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
+
+  dzl_signal_group_connect_data (priv->toplevel_signals,
+                                 "set-focus",
+                                 G_CALLBACK (ide_transient_sidebar_after_set_focus),
+                                 self, NULL,
+                                 G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+
+  if (NULL != (paned = gtk_bin_get_child (GTK_BIN (self))) &&
+      DZL_IS_MULTI_PANED (paned) &&
+      NULL != (stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (paned), 0)) &&
+      DZL_IS_DOCK_STACK (stack))
+    {
+      GtkWidget *tab_strip;
+
+      /* We want to hide the tab strip in the stack for the transient bar */
+      tab_strip = dzl_gtk_widget_find_child_typed (stack, DZL_TYPE_TAB_STRIP);
+      if (tab_strip != NULL)
+        gtk_widget_hide (tab_strip);
+    }
+}
+
+/**
+ * ide_transient_sidebar_set_page:
+ * @self: a #IdeTransientSidebar
+ * @page: (nullable): An #IdePage or %NULL
+ *
+ * Sets the page for which the panel is transient for. When focus leaves the
+ * sidebar or the page, the panel will be dismissed.
+ *
+ * Since: 3.32
+ */
+void
+ide_transient_sidebar_set_page (IdeTransientSidebar *self,
+                                IdePage             *page)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  g_autoptr(GtkWidget) old_page = NULL;
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
+
+  old_page = g_weak_ref_get (&priv->page_ref);
+  if (old_page != NULL)
+    g_signal_handlers_disconnect_by_func (old_page,
+                                          G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                          self);
+
+  if (page != NULL)
+    g_signal_connect_object (page,
+                             "destroy",
+                             G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  g_weak_ref_set (&priv->page_ref, page);
+}
+
+void
+ide_transient_sidebar_set_panel (IdeTransientSidebar *self,
+                                 GtkWidget           *panel)
+{
+  GtkWidget *stack;
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (panel));
+
+  stack = gtk_widget_get_parent (GTK_WIDGET (panel));
+
+  if (GTK_IS_STACK (stack))
+    gtk_stack_set_visible_child (GTK_STACK (stack), panel);
+  else
+    g_warning ("Failed to locate stack containing panel");
+}
+
+void
+ide_transient_sidebar_lock (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (priv->hold_count >= 0);
+
+  priv->hold_count++;
+
+  if (!dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (self)))
+    set_visible (self, TRUE);
+}
+
+void
+ide_transient_sidebar_unlock (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (priv->hold_count > 0);
+
+  priv->hold_count--;
+
+  if (priv->hold_count == 0)
+    {
+      if (dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (self)))
+        set_visible (self, FALSE);
+    }
+}
diff --git a/src/libide/gui/ide-transient-sidebar.h b/src/libide/gui/ide-transient-sidebar.h
new file mode 100644
index 000000000..0e27c0525
--- /dev/null
+++ b/src/libide/gui/ide-transient-sidebar.h
@@ -0,0 +1,58 @@
+/* ide-transient-sidebar.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-panel.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSIENT_SIDEBAR (ide_transient_sidebar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransientSidebar, ide_transient_sidebar, IDE, TRANSIENT_SIDEBAR, IdePanel)
+
+struct _IdeTransientSidebarClass
+{
+  IdePanelClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_set_panel (IdeTransientSidebar *self,
+                                      GtkWidget           *panel);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_set_page  (IdeTransientSidebar *self,
+                                      IdePage             *page);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_lock      (IdeTransientSidebar *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_unlock    (IdeTransientSidebar *self);
+
+G_END_DECLS
diff --git a/src/libide/util/ide-window-settings.h b/src/libide/gui/ide-window-settings-private.h
similarity index 93%
rename from src/libide/util/ide-window-settings.h
rename to src/libide/gui/ide-window-settings-private.h
index f4ef33115..e8fecd2b4 100644
--- a/src/libide/util/ide-window-settings.h
+++ b/src/libide/gui/ide-window-settings-private.h
@@ -24,6 +24,6 @@
 
 G_BEGIN_DECLS
 
-void ide_window_settings_register (GtkWindow *window);
+void _ide_window_settings_register (GtkWindow *window);
 
 G_END_DECLS
diff --git a/src/libide/util/ide-window-settings.c b/src/libide/gui/ide-window-settings.c
similarity index 96%
rename from src/libide/util/ide-window-settings.c
rename to src/libide/gui/ide-window-settings.c
index 63a336f39..dc2701c48 100644
--- a/src/libide/util/ide-window-settings.c
+++ b/src/libide/gui/ide-window-settings.c
@@ -18,7 +18,11 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "util/ide-window-settings.h"
+#define G_LOG_DOMAIN "ide-window-settings"
+
+#include "config.h"
+
+#include "ide-window-settings-private.h"
 
 #define GB_WINDOW_MIN_WIDTH  1280
 #define GB_WINDOW_MIN_HEIGHT 720
@@ -132,7 +136,7 @@ ide_window_settings__window_destroy (GtkWindow *window)
 }
 
 void
-ide_window_settings_register (GtkWindow *window)
+_ide_window_settings_register (GtkWindow *window)
 {
   if (settings == NULL)
     {
diff --git a/src/libide/gui/ide-workbench-addin.c b/src/libide/gui/ide-workbench-addin.c
new file mode 100644
index 000000000..3876fe118
--- /dev/null
+++ b/src/libide/gui/ide-workbench-addin.c
@@ -0,0 +1,402 @@
+/* ide-workbench-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-workbench-addin"
+
+#include "config.h"
+
+#include "ide-workbench-addin.h"
+
+G_DEFINE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, G_TYPE_OBJECT)
+
+static void ide_workbench_addin_real_open_at_async (IdeWorkbenchAddin   *self,
+                                                    GFile               *file,
+                                                    const gchar         *hint,
+                                                    gint                 at_line,
+                                                    gint                 at_line_offset,
+                                                    IdeBufferOpenFlags   flags,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+static void ide_workbench_addin_real_open_async    (IdeWorkbenchAddin   *self,
+                                                    GFile               *file,
+                                                    const gchar         *hint,
+                                                    IdeBufferOpenFlags   flags,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+
+static void
+ide_workbench_addin_real_load_project_async (IdeWorkbenchAddin   *self,
+                                             IdeProjectInfo      *project_info,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  ide_task_report_new_error (self, callback, user_data,
+                             ide_workbench_addin_real_load_project_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Loading projects is not supported");
+}
+
+static gboolean
+ide_workbench_addin_real_load_project_finish (IdeWorkbenchAddin  *self,
+                                              GAsyncResult       *result,
+                                              GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_real_unload_project_async (IdeWorkbenchAddin   *self,
+                                               IdeProjectInfo      *project_info,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+  ide_task_report_new_error (self, callback, user_data,
+                             ide_workbench_addin_real_unload_project_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Unloading projects is not supported");
+}
+
+static gboolean
+ide_workbench_addin_real_unload_project_finish (IdeWorkbenchAddin  *self,
+                                                GAsyncResult       *result,
+                                                GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_real_open_async (IdeWorkbenchAddin   *self,
+                                     GFile               *file,
+                                     const gchar         *hint,
+                                     IdeBufferOpenFlags   flags,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  IdeWorkbenchAddinInterface *iface;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  iface = IDE_WORKBENCH_ADDIN_GET_IFACE (self);
+
+  if (iface->open_at_async == (gpointer)ide_workbench_addin_real_open_at_async)
+    {
+      ide_task_report_new_error (self, callback, user_data,
+                                 ide_workbench_addin_real_open_async,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Opening files is not supported");
+      return;
+    }
+
+  iface->open_at_async (self, file, hint, -1, -1, flags, cancellable, callback, user_data);
+}
+
+static void
+ide_workbench_addin_real_open_at_async (IdeWorkbenchAddin   *self,
+                                        GFile               *file,
+                                        const gchar         *hint,
+                                        gint                 at_line,
+                                        gint                 at_line_offset,
+                                        IdeBufferOpenFlags   flags,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeWorkbenchAddinInterface *iface;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  iface = IDE_WORKBENCH_ADDIN_GET_IFACE (self);
+
+  if (iface->open_async == (gpointer)ide_workbench_addin_real_open_async)
+    {
+      ide_task_report_new_error (self, callback, user_data,
+                                 ide_workbench_addin_real_open_at_async,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Opening files is not supported");
+      return;
+    }
+
+  iface->open_async (self, file, hint, flags, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_workbench_addin_real_open_finish (IdeWorkbenchAddin  *self,
+                                      GAsyncResult       *result,
+                                      GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_default_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load_project_async = ide_workbench_addin_real_load_project_async;
+  iface->load_project_finish = ide_workbench_addin_real_load_project_finish;
+  iface->unload_project_async = ide_workbench_addin_real_unload_project_async;
+  iface->unload_project_finish = ide_workbench_addin_real_unload_project_finish;
+  iface->open_async = ide_workbench_addin_real_open_async;
+  iface->open_at_async = ide_workbench_addin_real_open_at_async;
+  iface->open_finish = ide_workbench_addin_real_open_finish;
+}
+
+void
+ide_workbench_addin_load (IdeWorkbenchAddin *self,
+                          IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load (self, workbench);
+}
+
+void
+ide_workbench_addin_unload (IdeWorkbenchAddin *self,
+                            IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload (self, workbench);
+}
+
+void
+ide_workbench_addin_load_project_async (IdeWorkbenchAddin   *self,
+                                        IdeProjectInfo      *project_info,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load_project_async (self,
+                                                            project_info,
+                                                            cancellable,
+                                                            callback,
+                                                            user_data);
+}
+
+gboolean
+ide_workbench_addin_load_project_finish (IdeWorkbenchAddin  *self,
+                                         GAsyncResult       *result,
+                                         GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load_project_finish (self, result, error);
+}
+
+void
+ide_workbench_addin_unload_project_async (IdeWorkbenchAddin   *self,
+                                          IdeProjectInfo      *project_info,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload_project_async (self,
+                                                              project_info,
+                                                              cancellable,
+                                                              callback,
+                                                              user_data);
+}
+
+gboolean
+ide_workbench_addin_unload_project_finish (IdeWorkbenchAddin  *self,
+                                           GAsyncResult       *result,
+                                           GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload_project_finish (self, result, error);
+}
+
+void
+ide_workbench_addin_workspace_added (IdeWorkbenchAddin *self,
+                                     IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_added)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_added (self, workspace);
+}
+
+void
+ide_workbench_addin_workspace_removed (IdeWorkbenchAddin *self,
+                                       IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_removed)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_removed (self, workspace);
+}
+
+gboolean
+ide_workbench_addin_can_open (IdeWorkbenchAddin *self,
+                              GFile             *file,
+                              const gchar       *content_type,
+                              gint              *priority)
+{
+  gint real_priority;
+
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  if (priority == NULL)
+    priority = &real_priority;
+  else
+    *priority = 0;
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->can_open)
+    return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->can_open (self, file, content_type, priority);
+
+  return FALSE;
+}
+
+void
+ide_workbench_addin_open_async (IdeWorkbenchAddin   *self,
+                                GFile               *file,
+                                const gchar         *content_type,
+                                IdeBufferOpenFlags   flags,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_async (self,
+                                                    file,
+                                                    content_type,
+                                                    flags,
+                                                    cancellable,
+                                                    callback,
+                                                    user_data);
+}
+
+void
+ide_workbench_addin_open_at_async (IdeWorkbenchAddin   *self,
+                                   GFile               *file,
+                                   const gchar         *content_type,
+                                   gint                 at_line,
+                                   gint                 at_line_offset,
+                                   IdeBufferOpenFlags   flags,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_at_async (self,
+                                                       file,
+                                                       content_type,
+                                                       at_line,
+                                                       at_line_offset,
+                                                       flags,
+                                                       cancellable,
+                                                       callback,
+                                                       user_data);
+}
+
+gboolean
+ide_workbench_addin_open_finish (IdeWorkbenchAddin  *self,
+                                 GAsyncResult       *result,
+                                 GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_finish (self, result, error);
+}
+
+/**
+ * ide_workbench_addin_vcs_changed:
+ * @self: a #IdeWorkbenchAddin
+ * @vcs: (nullable): an #IdeVcs
+ *
+ * This function notifies an #IdeWorkbenchAddin that the version control
+ * system has changed. This happens when ide_workbench_set_vcs() is called
+ * or after an addin is loaded.
+ *
+ * This is helpful for plugins that want to react to VCS changes such as
+ * changing branches, or tracking commits.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_addin_vcs_changed (IdeWorkbenchAddin *self,
+                                 IdeVcs            *vcs)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_VCS (vcs));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->vcs_changed)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->vcs_changed (self, vcs);
+}
+
+/**
+ * ide_workbench_addin_project_loaded:
+ * @self: an #IdeWorkbenchAddin
+ * @project_info: an #IdeProjectInfo
+ *
+ * This function is called after the project has been loaded.
+ *
+ * It is useful for situations where you do not need to influence the
+ * project loading, but do need to perform operations after it has
+ * completed.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_addin_project_loaded (IdeWorkbenchAddin *self,
+                                    IdeProjectInfo    *project_info)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->project_loaded)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->project_loaded (self, project_info);
+}
diff --git a/src/libide/gui/ide-workbench-addin.h b/src/libide/gui/ide-workbench-addin.h
new file mode 100644
index 000000000..ec7f3db3f
--- /dev/null
+++ b/src/libide/gui/ide-workbench-addin.h
@@ -0,0 +1,159 @@
+/* ide-workbench-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-workbench.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH_ADDIN (ide_workbench_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, IDE, WORKBENCH_ADDIN, GObject)
+
+struct _IdeWorkbenchAddinInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)                  (IdeWorkbenchAddin     *self,
+                                     IdeWorkbench          *workbench);
+  void     (*unload)                (IdeWorkbenchAddin     *self,
+                                     IdeWorkbench          *workbench);
+  void     (*load_project_async)    (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*load_project_finish)   (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*unload_project_async)  (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*unload_project_finish) (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*project_loaded)        (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info);
+  void     (*workspace_added)       (IdeWorkbenchAddin     *self,
+                                     IdeWorkspace          *workspace);
+  void     (*workspace_removed)     (IdeWorkbenchAddin     *self,
+                                     IdeWorkspace          *workspace);
+  gboolean (*can_open)              (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     gint                  *priority);
+  void     (*open_async)            (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     IdeBufferOpenFlags     flags,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  void     (*open_at_async)         (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     gint                   at_line,
+                                     gint                   at_line_offset,
+                                     IdeBufferOpenFlags     flags,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*open_finish)           (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*vcs_changed)           (IdeWorkbenchAddin     *self,
+                                     IdeVcs                *vcs);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_load                  (IdeWorkbenchAddin    *self,
+                                                    IdeWorkbench         *workbench);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_unload                (IdeWorkbenchAddin    *self,
+                                                    IdeWorkbench         *workbench);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_load_project_async    (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_load_project_finish   (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_unload_project_async  (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_unload_project_finish (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_project_loaded        (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_workspace_added       (IdeWorkbenchAddin    *self,
+                                                    IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_workspace_removed     (IdeWorkbenchAddin    *self,
+                                                    IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_can_open              (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    gint                 *priority);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_open_async            (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    IdeBufferOpenFlags    flags,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_open_at_async         (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    gint                  at_line,
+                                                    gint                  at_line_offset,
+                                                    IdeBufferOpenFlags    flags,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_open_finish           (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_vcs_changed           (IdeWorkbenchAddin    *self,
+                                                    IdeVcs               *vcs);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbenchAddin *ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
+                                                            const gchar  *module_name);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workbench.c b/src/libide/gui/ide-workbench.c
new file mode 100644
index 000000000..ddd963959
--- /dev/null
+++ b/src/libide/gui/ide-workbench.c
@@ -0,0 +1,2299 @@
+/* ide-workbench.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-workbench"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-debugger.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-context-private.h"
+#include "ide-foundry-init.h"
+#include "ide-thread-private.h"
+
+#include "ide-application.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-primary-workspace.h"
+#include "ide-session-private.h"
+#include "ide-workbench.h"
+#include "ide-workbench-addin.h"
+#include "ide-workspace.h"
+
+/**
+ * SECTION:ide-workbench
+ * @title: IdeWorkbench
+ * @short_description: window group for all windows within a project
+ *
+ * The #IdeWorkbench is a #GtkWindowGroup containing the #IdeContext (root
+ * data-structure for a project) and all of the windows associated with the
+ * project.
+ *
+ * Usually, windows within the #IdeWorkbench are an #IdeWorkspace. They can
+ * react to changes in the #IdeContext or its descendants to represent the
+ * project and it's state.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeWorkbench
+{
+  GtkWindowGroup    parent_instance;
+
+  /* MRU of workspaces, link embedded in workspace */
+  GQueue            mru_queue;
+
+  /* Owned references */
+  PeasExtensionSet *addins;
+  GCancellable     *cancellable;
+  IdeContext       *context;
+  IdeBuildSystem   *build_system;
+  IdeProjectInfo   *project_info;
+  IdeVcs           *vcs;
+  IdeVcsMonitor    *vcs_monitor;
+  IdeSearchEngine  *search_engine;
+  IdeSession       *session;
+
+  /* Various flags */
+  guint             unloaded : 1;
+};
+
+typedef struct
+{
+  GPtrArray          *addins;
+  IdeWorkbenchAddin  *preferred;
+  GFile              *file;
+  gchar              *hint;
+  gchar              *content_type;
+  IdeBufferOpenFlags  flags;
+  gint                at_line;
+  gint                at_line_offset;
+} Open;
+
+typedef struct
+{
+  IdeProjectInfo *project_info;
+  GPtrArray      *addins;
+  GType           workspace_type;
+  gint64          present_time;
+} LoadProject;
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_VCS,
+  N_PROPS
+};
+
+static void ide_workbench_action_close       (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_open        (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_dump_tasks  (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_object_tree (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_inspector   (IdeWorkbench *self,
+                                              GVariant     *param);
+
+
+DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
+  { "close", ide_workbench_action_close },
+  { "open", ide_workbench_action_open },
+  { "-inspector", ide_workbench_action_inspector },
+  { "-object-tree", ide_workbench_action_object_tree },
+  { "-dump-tasks", ide_workbench_action_dump_tasks },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                                ide_workbench_init_action_group))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+load_project_free (LoadProject *lp)
+{
+  g_clear_object (&lp->project_info);
+  g_clear_pointer (&lp->addins, g_ptr_array_unref);
+  g_slice_free (LoadProject, lp);
+}
+
+static void
+open_free (Open *o)
+{
+  g_clear_pointer (&o->addins, g_ptr_array_unref);
+  g_clear_object (&o->preferred);
+  g_clear_object (&o->file);
+  g_clear_pointer (&o->hint, g_free);
+  g_clear_pointer (&o->content_type, g_free);
+  g_slice_free (Open, o);
+}
+
+static gboolean
+ignore_error (GError *error)
+{
+  return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+         g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED);
+}
+
+static void
+ide_workbench_set_context (IdeWorkbench *self,
+                           IdeContext   *context)
+{
+  g_autoptr(IdeContext) new_context = NULL;
+  g_autoptr(IdeBufferManager) bufmgr = NULL;
+  IdeBuildSystem *build_system;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!context || IDE_IS_CONTEXT (context));
+
+  if (context == NULL)
+    context = new_context = ide_context_new ();
+
+  g_set_object (&self->context, context);
+
+  /* Make sure we have access to buffer manager early */
+  bufmgr = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUFFER_MANAGER);
+
+  /* And use a fallback build system if one is not already available */
+  if ((build_system = ide_context_peek_child_typed (context, IDE_TYPE_BUILD_SYSTEM)))
+    self->build_system = g_object_ref (build_system);
+  else
+    self->build_system = ide_object_ensure_child_typed (IDE_OBJECT (context), 
IDE_TYPE_FALLBACK_BUILD_SYSTEM);
+
+  /* Setup session monitor for future use */
+  self->session = ide_session_new ();
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (self->session));
+}
+
+static void
+ide_workbench_addin_added_workspace_cb (IdeWorkspace      *workspace,
+                                        IdeWorkbenchAddin *addin)
+{
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+
+  ide_workbench_addin_workspace_added (addin, workspace);
+}
+
+static void
+ide_workbench_addin_removed_workspace_cb (IdeWorkspace      *workspace,
+                                          IdeWorkbenchAddin *addin)
+{
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+
+  ide_workbench_addin_workspace_removed (addin, workspace);
+}
+
+static void
+ide_workbench_addin_added_cb (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ide_workbench_addin_load (addin, self);
+
+  /* Notify of the VCS system up-front */
+  if (self->vcs != NULL)
+    ide_workbench_addin_vcs_changed (addin, self->vcs);
+
+  /*
+   * If we already loaded a project, then give the plugin a
+   * chance to handle that, even if it is delayed a bit.
+   */
+
+  if (self->project_info != NULL)
+    ide_workbench_addin_load_project_async (addin, self->project_info, NULL, NULL, NULL);
+
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)ide_workbench_addin_added_workspace_cb,
+                                   addin);
+}
+
+static void
+ide_workbench_addin_removed_cb (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  /* Notify of workspace removals so addins don't need to manually
+   * track them for cleanup.
+   */
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)ide_workbench_addin_removed_workspace_cb,
+                                   addin);
+
+  ide_workbench_addin_unload (addin, self);
+}
+
+static void
+ide_workbench_notify_context_title (IdeWorkbench *self,
+                                    GParamSpec   *pspec,
+                                    IdeContext   *context)
+{
+  g_autofree gchar *formatted = NULL;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  title = ide_context_dup_title (context);
+  formatted = g_strdup_printf (_("Builder — %s"), title);
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)gtk_window_set_title,
+                                   formatted);
+}
+
+static void
+ide_workbench_notify_context_workdir (IdeWorkbench *self,
+                                      GParamSpec   *pspec,
+                                      IdeContext   *context)
+{
+  g_autoptr(GFile) workdir = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  workdir = ide_context_ref_workdir (context);
+  ide_vcs_monitor_set_root (self->vcs_monitor, workdir);
+}
+
+static void
+ide_workbench_constructed (GObject *object)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  if (self->context == NULL)
+    self->context = ide_context_new ();
+
+  g_signal_connect_object (self->context,
+                           "notify::title",
+                           G_CALLBACK (ide_workbench_notify_context_title),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->context,
+                           "notify::workdir",
+                           G_CALLBACK (ide_workbench_notify_context_workdir),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object);
+
+  self->vcs_monitor = g_object_new (IDE_TYPE_VCS_MONITOR, NULL);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (self->vcs_monitor));
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_WORKBENCH_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_workbench_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_workbench_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_addin_added_cb,
+                              self);
+}
+
+static void
+ide_workbench_finalize (GObject *object)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_object (&self->build_system);
+  g_clear_object (&self->vcs);
+  g_clear_object (&self->search_engine);
+  g_clear_object (&self->session);
+  g_clear_object (&self->project_info);
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->context);
+
+  G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object);
+}
+
+static void
+ide_workbench_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_workbench_get_context (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_workbench_set_context (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_class_init (IdeWorkbenchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_workbench_constructed;
+  object_class->finalize = ide_workbench_finalize;
+  object_class->get_property = ide_workbench_get_property;
+  object_class->set_property = ide_workbench_set_property;
+
+  /**
+   * IdeWorkbench:context:
+   *
+   * The "context" property is the #IdeContext for the project.
+   *
+   * The #IdeContext is the root #IdeObject used in the tree of
+   * objects representing the project and the workings of the IDE.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The IdeContext for the workbench",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeWorkbench:vcs:
+   *
+   * The "vcs" property contains an #IdeVcs that represents the version control
+   * system that is currently loaded for the project.
+   *
+   * The #IdeVcs is registered by an #IdeWorkbenchAddin when loading a project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VCS] =
+    g_param_spec_object ("vcs",
+                         "Vcs",
+                         "The version control system, if any",
+                         IDE_TYPE_VCS,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_workbench_init (IdeWorkbench *self)
+{
+}
+
+static void
+collect_addins_cb (PeasExtensionSet *set,
+                   PeasPluginInfo   *plugin_info,
+                   PeasExtension    *exten,
+                   gpointer          user_data)
+{
+  GPtrArray *ar = user_data;
+  g_ptr_array_add (ar, g_object_ref (exten));
+}
+
+static GPtrArray *
+ide_workbench_collect_addins (IdeWorkbench *self)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins, collect_addins_cb, ar);
+  return g_steal_pointer (&ar);
+}
+
+static IdeWorkbenchAddin *
+ide_workbench_find_addin (IdeWorkbench *self,
+                          const gchar  *hint)
+{
+  PeasEngine *engine;
+  PeasPluginInfo *plugin_info;
+  PeasExtension *exten = NULL;
+
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (hint != NULL, NULL);
+
+  engine = peas_engine_get_default ();
+
+  if ((plugin_info = peas_engine_get_plugin_info (engine, hint)))
+    exten = peas_extension_set_get_extension (self->addins, plugin_info);
+
+  return exten ? g_object_ref (IDE_WORKBENCH_ADDIN (exten)) : NULL;
+}
+
+/**
+ * ide_workbench_new:
+ *
+ * Creates a new #IdeWorkbench.
+ *
+ * This does not create any windows, you'll need to request that a workspace
+ * be created based on the kind of workspace you want to display to the user.
+ *
+ * Returns: an #IdeWorkbench
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_new (void)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  return g_object_new (IDE_TYPE_WORKBENCH, NULL);
+}
+
+/**
+ * ide_workbench_new_for_context:
+ *
+ * Creates a new #IdeWorkbench using @context for the #IdeWorkbench:context.
+ *
+ * Returns: (transfer full): an #IdeWorkbench
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_new_for_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return g_object_new (IDE_TYPE_CONTEXT,
+                       "visible", TRUE,
+                       NULL);
+}
+
+/**
+ * ide_workbench_get_context:
+ * @self: an #IdeWorkbench
+ *
+ * Gets the #IdeContext for the workbench.
+ *
+ * Returns: (transfer none): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_workbench_get_context (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->context;
+}
+
+/**
+ * ide_workbench_from_widget:
+ * @widget: a #GtkWidget
+ *
+ * Finds the #IdeWorkbench associated with a widget.
+ *
+ * Returns: (nullable) (transfer none): an #IdeWorkbench or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_from_widget (GtkWidget *widget)
+{
+  GtkWindowGroup *group;
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  /*
+   * The workbench is a window group, and the workspaces belong to us. So we
+   * just need to get the toplevel window group property, and cast.
+   */
+
+  if ((toplevel = gtk_widget_get_toplevel (widget)) &&
+      GTK_IS_WINDOW (toplevel) &&
+      (group = gtk_window_get_group (GTK_WINDOW (toplevel))) &&
+      IDE_IS_WORKBENCH (group))
+    return IDE_WORKBENCH (group);
+
+  return NULL;
+}
+
+/**
+ * ide_workbench_foreach_workspace:
+ * @self: an #IdeWorkbench
+ * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace
+ * @user_data: user data for @callback
+ *
+ * Iterates the available workspaces in the workbench. Workspaces are iterated
+ * in most-recently-used order.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_foreach_workspace (IdeWorkbench *self,
+                                 GtkCallback   callback,
+                                 gpointer      user_data)
+{
+  GList *copy;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (callback != NULL);
+
+  /* Copy for re-entrancy safety */
+  copy = g_list_copy (self->mru_queue.head);
+
+  for (const GList *iter = copy; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
+      g_assert (IDE_IS_WORKSPACE (workspace));
+      callback (iter->data, user_data);
+    }
+
+  g_list_free (copy);
+}
+
+/**
+ * ide_workbench_foreach_page:
+ * @self: a #IdeWorkbench
+ * @callback: (scope call): a callback to execute for each page
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for every page loaded in the workbench, by iterating
+ * workspaces in order of most-recently-used.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_foreach_page (IdeWorkbench *self,
+                            GtkCallback   callback,
+                            gpointer      user_data)
+{
+  GList *copy;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (callback != NULL);
+
+  /* Make a copy to be safe against auto-cleanup removals */
+  copy = g_list_copy (self->mru_queue.head);
+  for (const GList *iter = copy; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
+      g_assert (IDE_IS_WORKSPACE (workspace));
+      ide_workspace_foreach_page (workspace, callback, user_data);
+    }
+  g_list_free (copy);
+}
+
+static void
+ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self,
+                                               GParamSpec   *pspec,
+                                               IdeWorkspace *workspace)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
+
+  if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
+    {
+      GList *mru_link = _ide_workspace_get_mru_link (workspace);
+
+      g_queue_unlink (&self->mru_queue, mru_link);
+
+      g_assert (mru_link->prev == NULL);
+      g_assert (mru_link->next == NULL);
+      g_assert (mru_link->data == (gpointer)workspace);
+
+      g_queue_push_head_link (&self->mru_queue, mru_link);
+    }
+}
+
+static void
+insert_action_groups_foreach_cb (IdeWorkspace *workspace,
+                                 gpointer      user_data)
+{
+  IdeWorkbench *self = user_data;
+  struct {
+    const gchar *name;
+    GType        child_type;
+  } groups[] = {
+    { "config-manager", IDE_TYPE_CONFIGURATION_MANAGER },
+    { "build-manager", IDE_TYPE_BUILD_MANAGER },
+    { "device-manager", IDE_TYPE_DEVICE_MANAGER },
+    { "run-manager", IDE_TYPE_RUN_MANAGER },
+    { "test-manager", IDE_TYPE_TEST_MANAGER },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  for (guint i = 0; i < G_N_ELEMENTS (groups); i++)
+    {
+      IdeObject *child;
+
+      if ((child = ide_context_peek_child_typed (self->context, groups[i].child_type)))
+        gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                        groups[i].name,
+                                        G_ACTION_GROUP (child));
+    }
+}
+
+/**
+ * ide_workbench_add_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Adds @workspace to @workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_add_workspace (IdeWorkbench *self,
+                             IdeWorkspace *workspace)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  g_autofree gchar *title = NULL;
+  g_autofree gchar *formatted = NULL;
+  GList *mru_link;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  /* Now add the window to the workspace (which takes no reference, as the
+   * window will take a reference back to us.
+   */
+  if (gtk_window_get_group (GTK_WINDOW (workspace)) != GTK_WINDOW_GROUP (self))
+    gtk_window_group_add_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
+
+  g_assert (gtk_window_has_group (GTK_WINDOW (workspace)));
+  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
+
+  /* Now place the workspace into our MRU tracking */
+  mru_link = _ide_workspace_get_mru_link (workspace);
+
+  if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
+    g_queue_push_head_link (&self->mru_queue, mru_link);
+  else
+    g_queue_push_tail_link (&self->mru_queue, mru_link);
+
+  /* Update the context for the workspace, even if we're not loaded,
+   * this IdeContext will be updated later.
+   */
+  _ide_workspace_set_context (workspace, self->context);
+
+  /* This causes the workspace to get an additional reference to the group
+   * (which already happens from GtkWindow:group), but IdeWorkspace will
+   * remove itself in IdeWorkspace.destroy.
+   */
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                  "workbench",
+                                  G_ACTION_GROUP (self));
+
+  /* Give the workspace access to all the action groups of the context that
+   * might be useful for them to access (debug-manager, run-manager, etc).
+   */
+  if (self->project_info != NULL)
+    insert_action_groups_foreach_cb (workspace, self);
+
+  /* Track toplevel focus changes to maintain a most-recently-used queue. */
+  g_signal_connect_object (workspace,
+                           "notify::has-toplevel-focus",
+                           G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Notify all the addins about the new workspace. */
+  if ((addins = ide_workbench_collect_addins (self)))
+    {
+      for (guint i = 0; i < addins->len; i++)
+        {
+          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+          ide_workbench_addin_workspace_added (addin, workspace);
+        }
+    }
+
+  title = ide_context_dup_title (self->context);
+  formatted = g_strdup_printf (_("Builder — %s"), title);
+  gtk_window_set_title (GTK_WINDOW (workspace), formatted);
+}
+
+/**
+ * ide_workbench_remove_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Removes @workspace from @workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_remove_workspace (IdeWorkbench *self,
+                                IdeWorkspace *workspace)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  GList *list;
+  GList *mru_link;
+  guint count = 0;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  /* Stop tracking MRU changes */
+  mru_link = _ide_workspace_get_mru_link (workspace);
+  g_queue_unlink (&self->mru_queue, mru_link);
+  g_signal_handlers_disconnect_by_func (workspace,
+                                        G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
+                                        self);
+
+  /* Notify all the addins about losing the workspace. */
+  if ((addins = ide_workbench_collect_addins (self)))
+    {
+      for (guint i = 0; i < addins->len; i++)
+        {
+          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+          ide_workbench_addin_workspace_removed (addin, workspace);
+        }
+    }
+
+  /* Clear our action group (which drops an additional back-reference) */
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", NULL);
+
+  /* Only cleanup the group if it hasn't already been removed */
+  if (gtk_window_has_group (GTK_WINDOW (workspace)))
+    gtk_window_group_remove_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
+
+  /*
+   * If this is our last workspace being closed, then we want to
+   * try to cleanup the workbench and shut things down.
+   */
+
+  list = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self));
+  for (const GList *iter = list; iter; iter = iter->next)
+    {
+      GtkWindow *window = iter->data;
+
+      if (IDE_IS_WORKSPACE (window) && workspace != IDE_WORKSPACE (window))
+        count++;
+    }
+  g_list_free (list);
+
+  /*
+   * If there are no more workspaces left, then we will want to also
+   * unload the workbench opportunistically, so that the application
+   * can exit cleanly.
+   */
+  if (count == 0 && self->unloaded == FALSE)
+    ide_workbench_unload_async (self, NULL, NULL, NULL);
+}
+
+/**
+ * ide_workbench_focus_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Requests that @workspace be raised in the windows of @self, and
+ * displayed to the user.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_focus_workspace (IdeWorkbench *self,
+                               IdeWorkspace *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  ide_gtk_window_present (GTK_WINDOW (workspace));
+}
+
+static void
+ide_workbench_project_loaded_foreach_cb (PeasExtensionSet *set,
+                                         PeasPluginInfo   *plugin_info,
+                                         PeasExtension    *exten,
+                                         gpointer          user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+  IdeWorkbench *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_PROJECT_INFO (self->project_info));
+
+  ide_workbench_addin_project_loaded (addin, self->project_info);
+}
+
+static void
+ide_workbench_session_restore_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSession *session = (IdeSession *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_session_restore_finish (session, result, &error))
+    g_warning ("%s", error->message);
+}
+
+static void
+ide_workbench_load_project_completed (IdeWorkbench *self,
+                                      IdeTask      *task)
+{
+  LoadProject *lp;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_TASK (task));
+
+  lp = ide_task_get_task_data (task);
+
+  g_assert (lp != NULL);
+  g_assert (lp->addins != NULL);
+  g_assert (lp->addins->len == 0);
+
+  if (lp->workspace_type != G_TYPE_INVALID)
+    {
+      IdeWorkspace *workspace;
+
+      workspace = g_object_new (lp->workspace_type,
+                                "application", IDE_APPLICATION_DEFAULT,
+                                NULL);
+      ide_workbench_add_workspace (self, IDE_WORKSPACE (workspace));
+      gtk_window_present_with_time (GTK_WINDOW (workspace), lp->present_time);
+    }
+
+  /* Give workspaces access to the various GActionGroups */
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)insert_action_groups_foreach_cb,
+                                   self);
+
+  /* Notify addins that projects have loaded */
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_project_loaded_foreach_cb,
+                              self);
+
+  /* And now restore the user session, but don't block our task for
+   * it since the greeter is waiting on us.
+   */
+  ide_session_restore_async (self->session,
+                             self,
+                             ide_task_get_cancellable (task),
+                             ide_workbench_session_restore_cb,
+                             NULL);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_workbench_load_project_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  LoadProject *lp;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  lp = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (lp != NULL);
+  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
+  g_assert (lp->addins != NULL);
+  g_assert (lp->addins->len > 0);
+
+  if (!ide_workbench_addin_load_project_finish (addin, result, &error))
+    {
+      if (!ignore_error (error))
+        g_warning ("%s addin failed to load project: %s",
+                   G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  g_ptr_array_remove (lp->addins, addin);
+
+  if (lp->addins->len == 0)
+    ide_workbench_load_project_completed (self, task);
+}
+
+static void
+ide_workbench_init_foundry_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GCancellable *cancellable;
+  LoadProject *lp;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!_ide_foundry_init_finish (result, &error))
+    g_critical ("Failed to initialize foundry: %s", error->message);
+
+  cancellable = ide_task_get_cancellable (task);
+  self = ide_task_get_source_object (task);
+  lp = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (lp != NULL);
+  g_assert (lp->addins != NULL);
+  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
+
+  /* Now, we need to notify all of the workbench addins that we're
+   * opening the project. Once they have all completed, we'll create the
+   * new workspace window and attach it. That saves us the work of
+   * rendering various frames of the during the intensive load process.
+   */
+
+
+  for (guint i = 0; i < lp->addins->len; i++)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (lp->addins, i);
+
+      ide_workbench_addin_load_project_async (addin,
+                                              lp->project_info,
+                                              cancellable,
+                                              ide_workbench_load_project_cb,
+                                              g_object_ref (task));
+    }
+
+  if (lp->addins->len == 0)
+    ide_workbench_load_project_completed (self, task);
+}
+
+/**
+ * ide_workbench_load_project_async:
+ * @self: a #IdeWorkbench
+ * @project_info: an #IdeProjectInfo describing the project to open
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Requests that a project be opened in the workbench.
+ *
+ * @project_info should contain enough information to discover and load the
+ * project. Depending on the various fields of the #IdeProjectInfo,
+ * different plugins may become active as part of loading the project.
+ *
+ * Note that this may only be called once for an #IdeWorkbench. If you need
+ * to open a second project, you need to create and register a second
+ * workbench first, and then open using that secondary workbench.
+ *
+ * @callback should call ide_workbench_load_project_finish() to obtain the
+ * result of the open request.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_load_project_async (IdeWorkbench        *self,
+                                  IdeProjectInfo      *project_info,
+                                  GType                workspace_type,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autofree gchar *name = NULL;
+  const gchar *project_id;
+  LoadProject *lp;
+  GFile *directory;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (workspace_type != IDE_TYPE_WORKSPACE);
+  g_return_if_fail (workspace_type == G_TYPE_INVALID ||
+                    g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (self->unloaded == FALSE);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_load_project_async);
+
+  if (self->project_info != NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Cannot load project, a project is already loaded");
+      IDE_EXIT;
+    }
+
+  _ide_context_set_has_project (self->context);
+
+  g_set_object (&self->project_info, project_info);
+
+  /* Update context project-id based on project-info */
+  if ((project_id = ide_project_info_get_id (project_info)))
+    {
+      g_autofree gchar *generated = ide_create_project_id (project_id);
+      ide_context_set_project_id (self->context, generated);
+    }
+
+  /*
+   * Track the directory root based on project info. If we didn't get a
+   * directory set, then take the parent of the project file.
+   */
+
+  if ((directory = ide_project_info_get_directory (project_info)))
+    {
+      ide_context_set_workdir (self->context, directory);
+    }
+  else
+    {
+      GFile *file = ide_project_info_get_file (project_info);
+
+      if (g_file_query_file_type (file, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY)
+        {
+          ide_context_set_workdir (self->context, file);
+          directory = file;
+        }
+      else
+        {
+          ide_context_set_workdir (self->context, (parent = g_file_get_parent (file)));
+          directory = parent;
+        }
+
+      ide_project_info_set_directory (project_info, directory);
+    }
+
+  g_assert (G_IS_FILE (directory));
+
+  name = g_file_get_basename (directory);
+  ide_context_set_title (self->context, name);
+
+  /* If there has not been a project name set, make the default matching
+   * the directory name. A plugin may update the name with more information
+   * based on .doap files, etc.
+   */
+  if (!ide_project_info_get_name (project_info))
+    ide_project_info_set_name (project_info, name);
+
+  /* Setup some information we're going to need later on when loading the
+   * individual workbench addins (and then creating the workspace).
+   */
+  lp = g_slice_new0 (LoadProject);
+  lp->project_info = g_object_ref (project_info);
+  /* HACK: Workaround for lack of last event time */
+  lp->present_time = g_get_monotonic_time () / 1000L;
+  lp->addins = ide_workbench_collect_addins (self);
+  lp->workspace_type = workspace_type;
+  ide_task_set_task_data (task, lp, load_project_free);
+
+  /*
+   * Before we load any addins, we want to register the Foundry subsystems
+   * such as the device manager, diagnostics engine, configurations, etc.
+   * This makes sure that we have some basics setup before addins load.
+   */
+  _ide_foundry_init_async (self->context,
+                           cancellable,
+                           ide_workbench_init_foundry_cb,
+                           g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_workbench_load_project_finish:
+ * @self: a #IdeWorkbench
+ *
+ * Completes an asynchronous request to open a project using
+ * ide_workbench_load_project_async().
+ *
+ * Returns: %TRUE if the project was successfully opened; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_load_project_finish (IdeWorkbench  *self,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+print_object_tree (IdeObject *object,
+                   gpointer   depthptr)
+{
+  gint depth = GPOINTER_TO_INT (depthptr);
+  g_autofree gchar *space = g_strnfill (depth * 2, ' ');
+  g_autofree gchar *info = ide_object_repr (object);
+
+  g_print ("%s%s\n", space, info);
+  ide_object_foreach (object,
+                      (GFunc)print_object_tree,
+                      GINT_TO_POINTER (depth + 1));
+}
+
+static void
+ide_workbench_action_object_tree (IdeWorkbench *self,
+                                  GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  print_object_tree (IDE_OBJECT (self->context), NULL);
+}
+
+static void
+ide_workbench_action_dump_tasks (IdeWorkbench *self,
+                                 GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  _ide_dump_tasks ();
+}
+
+static void
+ide_workbench_action_inspector (IdeWorkbench *self,
+                                GVariant     *param)
+{
+  gtk_window_set_interactive_debugging (TRUE);
+}
+
+static void
+ide_workbench_action_close (IdeWorkbench *self,
+                            GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (param == NULL);
+
+  if (self->unloaded == FALSE)
+    ide_workbench_unload_async (self, NULL, NULL, NULL);
+}
+
+static void
+ide_workbench_action_open (IdeWorkbench *self,
+                           GVariant     *param)
+{
+  GtkFileChooserNative *chooser;
+  IdeWorkspace *workspace;
+  gint ret;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (param == NULL);
+
+  workspace = ide_workbench_get_current_workspace (self);
+
+  chooser = gtk_file_chooser_native_new (_("Open File…"),
+                                         GTK_WINDOW (workspace),
+                                         GTK_FILE_CHOOSER_ACTION_OPEN,
+                                         _("Open"),
+                                         _("Cancel"));
+  gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), FALSE);
+  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE);
+  gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE);
+
+  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser));
+
+  if (ret == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
+
+      for (const GSList *iter = files; iter; iter = iter->next)
+        {
+          GFile *file = iter->data;
+
+          g_assert (G_IS_FILE (file));
+
+          ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL);
+        }
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
+}
+
+/**
+ * ide_workbench_get_search_engine:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the search engine for the workbench, if any.
+ *
+ * Returns: (transfer none): an #IdeSearchEngine
+ *
+ * Since: 3.32
+ */
+IdeSearchEngine *
+ide_workbench_get_search_engine (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (self->context != NULL, NULL);
+
+  if (self->search_engine == NULL)
+      self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
+                                                           IDE_TYPE_SEARCH_ENGINE);
+
+  return self->search_engine;
+}
+
+/**
+ * ide_workbench_get_project_info:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeProjectInfo for the workbench, if a project has been or is
+ * currently, loading.
+ *
+ * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL
+ *
+ * Since: 3.32
+ */
+IdeProjectInfo *
+ide_workbench_get_project_info (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->project_info;
+}
+
+static void
+ide_workbench_unload_project_completed (IdeWorkbench *self,
+                                        IdeTask      *task)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_TASK (task));
+
+  g_clear_object (&self->addins);
+  ide_workbench_foreach_workspace (self, (GtkCallback)gtk_widget_destroy, NULL);
+
+  if (self->context != NULL)
+    {
+      ide_object_destroy (IDE_OBJECT (self->context));
+      g_clear_object (&self->context);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_workbench_unload_project_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GPtrArray *addins;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  addins = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (addins != NULL);
+  g_assert (addins->len > 0);
+
+  if (!ide_workbench_addin_unload_project_finish (addin, result, &error))
+    {
+      if (!ignore_error (error))
+        g_warning ("%s failed to unload project: %s",
+                   G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  g_ptr_array_remove (addins, addin);
+
+  if (addins->len == 0)
+    ide_workbench_unload_project_completed (self, task);
+}
+
+static void
+ide_workbench_session_save_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeSession *session = (IdeSession *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GPtrArray *addins;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  /* Not much we can display to the user, as we're tearing widgets down */
+  if (!ide_session_save_finish (session, result, &error))
+    g_warning ("%s", error->message);
+
+  /* Now we can request that each of our addins unload the project. */
+
+  self = ide_task_get_source_object (task);
+  addins = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (addins != NULL);
+
+  if (addins->len == 0)
+    {
+      ide_workbench_unload_project_completed (self, task);
+      return;
+    }
+
+  for (guint i = 0; i < addins->len; i++)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+
+      ide_workbench_addin_unload_project_async (addin,
+                                                self->project_info,
+                                                ide_task_get_cancellable (task),
+                                                ide_workbench_unload_project_cb,
+                                                g_object_ref (task));
+    }
+}
+
+/**
+ * ide_workbench_unload_async:
+ * @self: an #IdeWorkbench
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously unloads the workbench.
+ *
+ * All #IdeWorkspace windows will be closed after calling this
+ * function.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_unload_async (IdeWorkbench        *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) addins = NULL;
+  GApplication *app;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_unload_async);
+
+  if (self->unloaded)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  self->unloaded = TRUE;
+
+  /* Keep the GApplication alive for the lifetime of the task */
+  app = g_application_get_default ();
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (g_application_release),
+                           app,
+                           G_CONNECT_SWAPPED);
+  g_application_hold (app);
+
+  /*
+   * Remove our workbench from the application, so that no new
+   * open-file requests can keep us alive while we're shutting
+   * down.
+   */
+
+  ide_application_remove_workbench (IDE_APPLICATION (app), self);
+
+  /* If we haven't loaded a project, then there is nothing to
+   * do right now, just let ide_workbench_addin_unload() be called
+   * when the workbench disposes.
+   */
+  if (self->project_info == NULL)
+    {
+      ide_workbench_unload_project_completed (self, task);
+      return;
+    }
+
+  addins = ide_workbench_collect_addins (self);
+  ide_task_set_task_data (task, g_ptr_array_ref (addins), g_ptr_array_unref);
+
+  /* First unload the session while we are stable */
+  ide_session_save_async (self->session,
+                          self,
+                          cancellable,
+                          ide_workbench_session_save_cb,
+                          g_steal_pointer (&task));
+
+}
+
+/**
+ * ide_workbench_unload_finish:
+ * @self: an #IdeWorkbench
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+
+ * Completes a request to unload the workbench.
+ *
+ * Returns: %TRUE if the workbench was unloaded successfully,
+ *   otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_unload_finish (IdeWorkbench *self,
+                             GAsyncResult *result,
+                             GError **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_open_all_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  gint *n_active;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_workbench_open_finish (self, result, &error))
+    g_message ("Failed to open file: %s", error->message);
+
+  n_active = ide_task_get_task_data (task);
+  g_assert (n_active != NULL);
+
+  (*n_active)--;
+
+  if (*n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_workbench_open_all_async:
+ * @self: an #IdeWorkbench
+ * @files: (array length=n_files): an array of #GFile
+ * @n_files: number of #GFiles contained in @files
+ * @hint: (nullable): an optional hint about what addin to use
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the workbench open all of the #GFile denoted by @files.
+ *
+ * If @hint is provided, that will be used to determine what workbench
+ * addin to use when opening the file. The @hint name should match the
+ * module name of the plugin.
+ *
+ * Call ide_workbench_open_finish() from @callback to complete this
+ * operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_all_async (IdeWorkbench         *self,
+                              GFile               **files,
+                              guint                 n_files,
+                              const gchar          *hint,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) ar = NULL;
+  gint *n_active;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_open_all_async);
+
+  if (n_files == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ar = g_ptr_array_new_full (n_files, g_object_unref);
+  for (guint i = 0; i < n_files; i++)
+    g_ptr_array_add (ar, g_object_ref (files[i]));
+
+  n_active = g_new0 (gint, 1);
+  *n_active = ar->len;
+  ide_task_set_task_data (task, n_active, g_free);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      GFile *file = g_ptr_array_index (ar, i);
+
+      ide_workbench_open_async (self,
+                                file,
+                                hint,
+                                IDE_BUFFER_OPEN_FLAGS_NONE,
+                                cancellable,
+                                ide_workbench_open_all_cb,
+                                g_object_ref (task));
+    }
+}
+
+/**
+ * ide_workbench_open_async:
+ * @self: an #IdeWorkbench
+ * @file: a #GFile
+ * @hint: (nullable): an optional hint about what addin to use
+ * @flags: optional flags when opening the file
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the workbench open @file.
+ *
+ * If @hint is provided, that will be used to determine what workbench
+ * addin to use when opening the file. The @hint name should match the
+ * module name of the plugin.
+ *
+ * @flags may be ignored by some backends.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_async (IdeWorkbench        *self,
+                          GFile               *file,
+                          const gchar         *hint,
+                          IdeBufferOpenFlags   flags,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_workbench_open_at_async (self,
+                               file,
+                               hint,
+                               -1,
+                               -1,
+                               flags,
+                               cancellable,
+                               callback,
+                               user_data);
+}
+
+static void
+ide_workbench_open_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  IdeWorkbenchAddin *next;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  Open *o;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  o = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (o != NULL);
+  g_assert (o->addins != NULL);
+  g_assert (o->addins->len > 0);
+
+  if (ide_workbench_addin_open_finish (addin, result, &error))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  g_debug ("%s did not open the file, trying next.",
+           G_OBJECT_TYPE_NAME (addin));
+
+  g_ptr_array_remove (o->addins, addin);
+
+  /*
+   * We failed to open the file, try the next addin that is
+   * left which said it supported the content-type.
+   */
+
+  if (o->addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Failed to locate addin supporting file");
+      return;
+    }
+
+  next = g_ptr_array_index (o->addins, 0);
+
+  ide_workbench_addin_open_at_async (next,
+                                     o->file,
+                                     o->content_type,
+                                     o->at_line,
+                                     o->at_line_offset,
+                                     o->flags,
+                                     cancellable,
+                                     ide_workbench_open_cb,
+                                     g_steal_pointer (&task));
+}
+
+static gint
+sort_by_priority (gconstpointer a,
+                  gconstpointer b,
+                  gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin_a = *(IdeWorkbenchAddin **)a;
+  IdeWorkbenchAddin *addin_b = *(IdeWorkbenchAddin **)b;
+  Open *o = user_data;
+  gint prio_a = 0;
+  gint prio_b = 0;
+
+  if (!ide_workbench_addin_can_open (addin_a, o->file, o->content_type, &prio_a))
+    return 1;
+
+  if (!ide_workbench_addin_can_open (addin_b, o->file, o->content_type, &prio_b))
+    return -1;
+
+  return prio_a - prio_b;
+}
+
+static void
+ide_workbench_open_query_info_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GFileInfo) info = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbenchAddin *first;
+  GCancellable *cancellable;
+  Open *o;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  o = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (o != NULL);
+  g_assert (o->addins != NULL);
+  g_assert (o->addins->len > 0);
+
+  if ((info = g_file_query_info_finish (file, result, &error)))
+    o->content_type = g_strdup (g_file_info_get_content_type (info));
+
+  /* Remove unsupported addins while iterating backwards so that
+   * we can preserve the ordering of the array as we go.
+   */
+  for (guint i = o->addins->len; i > 0; i--)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (o->addins, i - 1);
+      gint prio = G_MAXINT;
+
+      if (!ide_workbench_addin_can_open (addin, o->file, o->content_type, &prio))
+        {
+          g_ptr_array_remove_index_fast (o->addins, i - 1);
+          if (o->preferred == addin)
+            g_clear_object (&o->preferred);
+        }
+    }
+
+  if (o->addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "No addins can open the file");
+      return;
+    }
+
+  /*
+   * Now sort the addins by priority, so that we can attempt to load them
+   * in the preferred ordering.
+   */
+  g_ptr_array_sort_with_data (o->addins, sort_by_priority, o);
+
+  /*
+   * Ensure that we place the preferred at the head of the array, so
+   * that it gets preference over default priorities.
+   */
+  if (o->preferred != NULL)
+    {
+      g_ptr_array_insert (o->addins, 0, g_object_ref (o->preferred));
+
+      for (guint i = 1; i < o->addins->len; i++)
+        {
+          if (g_ptr_array_index (o->addins, i) == (gpointer)o->preferred)
+            {
+              g_ptr_array_remove_index (o->addins, i);
+              break;
+            }
+        }
+    }
+
+  /* Now start requesting that addins attempt to load the file. */
+
+  first = g_ptr_array_index (o->addins, 0);
+
+  ide_workbench_addin_open_at_async (first,
+                                     o->file,
+                                     o->content_type,
+                                     o->at_line,
+                                     o->at_line_offset,
+                                     o->flags,
+                                     cancellable,
+                                     ide_workbench_open_cb,
+                                     g_steal_pointer (&task));
+}
+
+/**
+ * ide_workbench_open_at_async:
+ * @self: an #IdeWorkbench
+ * @file: a #GFile
+ * @hint: (nullable): an optional hint about what addin to use
+ * @at_line: the line number to open at, or -1 to ignore
+ * @at_line_offset: the line offset to open at, or -1 to ignore
+ * @flags: optional #IdeBufferOpenFlags
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Like ide_workbench_open_async(), this allows opening a file
+ * within the workbench. However, it also allows specifying a
+ * line and column offset within the file to focus. Usually, this
+ * only makes sense for files that can be opened in an editor.
+ *
+ * @at_line and @at_line_offset may be < 0 to ignore the parameters.
+ *
+ * @flags may be ignored by some backends
+ *
+ * Use ide_workbench_open_finish() to receive teh result of this
+ * asynchronous operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_at_async (IdeWorkbench        *self,
+                             GFile               *file,
+                             const gchar         *hint,
+                             gint                 at_line,
+                             gint                 at_line_offset,
+                             IdeBufferOpenFlags   flags,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) addins = NULL;
+  Open *o;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (self->unloaded == FALSE);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Canonicalize parameters */
+  if (at_line < 0)
+    at_line = -1;
+  if (at_line_offset < 0)
+    at_line_offset = -1;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_open_at_async);
+
+  /*
+   * Make sure we might have an addin to load after discovering
+   * the files content-type.
+   */
+  if (!(addins = ide_workbench_collect_addins (self)) || addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "No addins could open the file");
+      return;
+    }
+
+  o = g_slice_new0 (Open);
+  o->addins = g_ptr_array_ref (addins);
+  if (hint != NULL)
+    o->preferred = ide_workbench_find_addin (self, hint);
+  o->file = g_object_ref (file);
+  o->hint = g_strdup (hint);
+  o->flags = flags;
+  o->at_line = at_line;
+  o->at_line_offset = at_line_offset;
+  ide_task_set_task_data (task, o, open_free);
+
+  g_file_query_info_async (file,
+                           G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           cancellable,
+                           ide_workbench_open_query_info_cb,
+                           g_steal_pointer (&task));
+}
+
+/**
+ * ide_workbench_open_finish:
+ * @self: an #IdeWorkbench
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to open a file using either
+ * ide_workbench_open_async() or ide_workbench_open_at_async().
+ *
+ * Returns: %TRUE if the file was successfully opened; otherwise
+ *   %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_open_finish (IdeWorkbench  *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_workbench_get_current_workspace:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the most recently focused workspace, which may be used to
+ * deliver events such as opening new pages.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkspace *
+ide_workbench_get_current_workspace (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  if (self->mru_queue.length > 0)
+    return IDE_WORKSPACE (self->mru_queue.head->data);
+
+  return NULL;
+}
+
+/**
+ * ide_workbench_activate:
+ * @self: a #IdeWorkbench
+ *
+ * This function will attempt to raise the most recently focused workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_activate (IdeWorkbench *self)
+{
+  IdeWorkspace *workspace;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+
+  if ((workspace = ide_workbench_get_current_workspace (self)))
+    ide_workbench_focus_workspace (self, workspace);
+}
+
+static void
+ide_workbench_propagate_vcs_cb (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+  IdeVcs *vcs = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (!vcs || IDE_IS_VCS (vcs));
+
+  ide_workbench_addin_vcs_changed (addin, vcs);
+}
+
+/**
+ * ide_workbench_get_vcs:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeVcs that has been loaded for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeVcs or %NULL
+ *
+ * Since: 3.32
+ */
+IdeVcs *
+ide_workbench_get_vcs (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->vcs;
+}
+
+/**
+ * ide_workbench_get_vcs_monitor:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeVcsMonitor for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeVcsMonitor or %NULL
+ *
+ * Since: 3.32
+ */
+IdeVcsMonitor *
+ide_workbench_get_vcs_monitor (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->vcs_monitor;
+}
+
+static void
+remove_non_matching_vcs_cb (IdeObject *child,
+                            IdeVcs    *vcs)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (IDE_IS_VCS (vcs));
+
+  if (IDE_IS_VCS (child) && IDE_VCS (child) != vcs)
+    ide_object_destroy (child);
+}
+
+/**
+ * ide_workbench_set_vcs:
+ * @self: a #IdeWorkbench
+ * @vcs: (nullable): an #IdeVcs
+ *
+ * Sets the #IdeVcs for the workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_set_vcs (IdeWorkbench *self,
+                       IdeVcs       *vcs)
+{
+  g_autoptr(IdeVcs) local_vcs = NULL;
+  g_autoptr(GFile) local_workdir = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!vcs || IDE_IS_VCS (vcs));
+
+  if (vcs == self->vcs)
+    return;
+
+  if (vcs == NULL)
+    {
+      local_workdir = ide_context_ref_workdir (self->context);
+      vcs = local_vcs = IDE_VCS (ide_directory_vcs_new (local_workdir));
+    }
+
+  g_set_object (&self->vcs, vcs);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (vcs));
+  ide_object_foreach (IDE_OBJECT (self->context),
+                      (GFunc)remove_non_matching_vcs_cb,
+                      vcs);
+
+  if ((workdir = ide_vcs_get_workdir (vcs)))
+    ide_context_set_workdir (self->context, workdir);
+
+  ide_vcs_monitor_set_vcs (self->vcs_monitor, self->vcs);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_propagate_vcs_cb,
+                              self->vcs);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS]);
+}
+
+/**
+ * ide_workbench_get_build_system:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeBuildSystem for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeBuildSystem or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBuildSystem *
+ide_workbench_get_build_system (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->build_system;
+}
+
+static void
+remove_non_matching_build_systems_cb (IdeObject      *child,
+                                      IdeBuildSystem *build_system)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (IDE_IS_BUILD_SYSTEM (build_system));
+
+  if (IDE_IS_BUILD_SYSTEM (child) && IDE_BUILD_SYSTEM (child) != build_system)
+    ide_object_destroy (child);
+}
+
+/**
+ * ide_workbench_set_build_system:
+ * @self: a #IdeWorkbench
+ * @build_system: (nullable): an #IdeBuildSystem or %NULL
+ *
+ * Sets the #IdeBuildSystem for the workbench.
+ *
+ * If @build_system is %NULL, then a fallback build system will be used
+ * instead. It does not provide building capabilities, but allows for some
+ * components that require a build system to continue functioning.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_set_build_system (IdeWorkbench   *self,
+                                IdeBuildSystem *build_system)
+{
+  g_autoptr(IdeBuildSystem) local_build_system = NULL;
+  IdeBuildManager *build_manager;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!build_system || IDE_IS_BUILD_SYSTEM (build_system));
+
+  if (build_system == self->build_system)
+    return;
+
+  /* We want there to always be a build system available so that various
+   * plugins don't need lots of extra code to handle the %NULL case. So
+   * if @build_system is %NULL, then we'll create a fallback build system
+   * and assign that instead.
+   */
+
+  if (build_system == NULL)
+    build_system = local_build_system = ide_fallback_build_system_new ();
+
+  /* We want to add our new build system before removing the old build
+   * system to ensure there is always an #IdeBuildSystem child of the
+   * IdeContext.
+   */
+  g_set_object (&self->build_system, build_system);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (build_system));
+
+  /* Now remove any previous build-system from the context */
+  ide_object_foreach (IDE_OBJECT (self->context),
+                      (GFunc)remove_non_matching_build_systems_cb,
+                      build_system);
+
+  /* Ask the build-manager to setup a new pipeline */
+  if ((build_manager = ide_context_peek_child_typed (self->context, IDE_TYPE_BUILD_MANAGER)))
+    ide_build_manager_invalidate (build_manager);
+}
+
+/**
+ * ide_workbench_get_workspace_by_type:
+ * @self: a #IdeWorkbench
+ * @type: a #GType of a subclass of #IdeWorkspace
+ *
+ * Gets the most-recently-used workspace that matches @type.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkspace *
+ide_workbench_get_workspace_by_type (IdeWorkbench *self,
+                                     GType         type)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_WORKSPACE), NULL);
+
+  for (const GList *iter = self->mru_queue.head; iter; iter = iter->next)
+    {
+      if (G_TYPE_CHECK_INSTANCE_TYPE (iter->data, type))
+        return IDE_WORKSPACE (iter->data);
+    }
+
+  return NULL;
+}
+
+gboolean
+_ide_workbench_is_last_workspace (IdeWorkbench *self,
+                                  IdeWorkspace *workspace)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+
+  return self->mru_queue.length == 1 &&
+         g_queue_peek_head (&self->mru_queue) == (gpointer)workspace;
+}
+
+/**
+ * ide_workbench_has_project:
+ * @self: a #IdeWorkbench
+ *
+ * Returns %TRUE if a project is loaded (or currently loading) in the
+ * workbench.
+ *
+ * Returns: %TRUE if the workbench has a project
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_has_project (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+
+  return self->project_info != NULL;
+}
+
+/**
+ * ide_workbench_addin_find_by_module_name:
+ * @workbench: an #IdeWorkbench
+ * @module_name: the name of the addin module
+ *
+ * Finds the addin (if any) matching the plugin's @module_name.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkbenchAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkbenchAddin *
+ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
+                                         const gchar  *module_name)
+{
+  PeasPluginInfo *plugin_info;
+  PeasExtension *ret = NULL;
+  PeasEngine *engine;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (workbench), NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  if (workbench->addins == NULL)
+    return NULL;
+
+  engine = peas_engine_get_default ();
+
+  if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
+    ret = peas_extension_set_get_extension (workbench->addins, plugin_info);
+
+  return IDE_WORKBENCH_ADDIN (ret);
+}
diff --git a/src/libide/gui/ide-workbench.h b/src/libide/gui/ide-workbench.h
new file mode 100644
index 000000000..b3a0ae7cd
--- /dev/null
+++ b/src/libide/gui/ide-workbench.h
@@ -0,0 +1,144 @@
+/* ide-workbench.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-foundry.h>
+#include <libide-projects.h>
+#include <libide-search.h>
+#include <libide-vcs.h>
+
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH (ide_workbench_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeWorkbench, ide_workbench, IDE, WORKBENCH, GtkWindowGroup)
+
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_new                   (void);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_new_for_context       (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_activate              (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeProjectInfo  *ide_workbench_get_project_info      (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_has_project           (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext      *ide_workbench_get_context           (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkspace    *ide_workbench_get_current_workspace (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkspace    *ide_workbench_get_workspace_by_type (IdeWorkbench         *self,
+                                                      GType                 type);
+IDE_AVAILABLE_IN_3_32
+IdeSearchEngine *ide_workbench_get_search_engine     (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_from_widget           (GtkWidget            *widget);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_add_workspace         (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_remove_workspace      (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_focus_workspace       (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_foreach_workspace     (IdeWorkbench         *self,
+                                                      GtkCallback           callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_foreach_page          (IdeWorkbench         *self,
+                                                      GtkCallback           callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_load_project_async    (IdeWorkbench         *self,
+                                                      IdeProjectInfo       *project_info,
+                                                      GType                 workspace_type,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_load_project_finish   (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_unload_async          (IdeWorkbench         *self,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_unload_finish         (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_async            (IdeWorkbench         *self,
+                                                      GFile                *file,
+                                                      const gchar          *hint,
+                                                      IdeBufferOpenFlags    flags,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_at_async         (IdeWorkbench         *self,
+                                                      GFile                *file,
+                                                      const gchar          *hint,
+                                                      gint                  at_line,
+                                                      gint                  at_line_offset,
+                                                      IdeBufferOpenFlags    flags,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_all_async        (IdeWorkbench         *self,
+                                                      GFile               **files,
+                                                      guint                 n_files,
+                                                      const gchar          *hint,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_open_finish           (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+IdeVcs          *ide_workbench_get_vcs               (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_set_vcs               (IdeWorkbench         *self,
+                                                      IdeVcs               *vcs);
+IDE_AVAILABLE_IN_3_32
+IdeVcsMonitor   *ide_workbench_get_vcs_monitor       (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildSystem  *ide_workbench_get_build_system      (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_set_build_system      (IdeWorkbench         *self,
+                                                      IdeBuildSystem       *build_system);
+
+
+G_END_DECLS
diff --git a/src/libide/workers/ide-worker-manager.c b/src/libide/gui/ide-worker-manager.c
similarity index 98%
rename from src/libide/workers/ide-worker-manager.c
rename to src/libide/gui/ide-worker-manager.c
index 85e6c4fb2..aef90a2dc 100644
--- a/src/libide/workers/ide-worker-manager.c
+++ b/src/libide/gui/ide-worker-manager.c
@@ -26,15 +26,13 @@
 #include <gio/gio.h>
 #include <gio/gunixsocketaddress.h>
 #include <glib/gi18n.h>
+#include <libide-threading.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <unistd.h>
 
-#include "ide-debug.h"
-
-#include "workers/ide-worker-process.h"
-#include "workers/ide-worker-manager.h"
-#include "threading/ide-task.h"
+#include "ide-worker-process.h"
+#include "ide-worker-manager.h"
 
 struct _IdeWorkerManager
 {
diff --git a/src/libide/workers/ide-worker-manager.h b/src/libide/gui/ide-worker-manager.h
similarity index 100%
rename from src/libide/workers/ide-worker-manager.h
rename to src/libide/gui/ide-worker-manager.h
diff --git a/src/libide/workers/ide-worker-process.c b/src/libide/gui/ide-worker-process.c
similarity index 98%
rename from src/libide/workers/ide-worker-process.c
rename to src/libide/gui/ide-worker-process.c
index 1a6111dcc..01f56efdf 100644
--- a/src/libide/workers/ide-worker-process.c
+++ b/src/libide/gui/ide-worker-process.c
@@ -24,13 +24,10 @@
 
 #include <dazzle.h>
 #include <libpeas/peas.h>
+#include <libide-threading.h>
 
-#include "ide-debug.h"
-
-#include "logging/ide-log.h"
-#include "workers/ide-worker-process.h"
-#include "workers/ide-worker.h"
-#include "threading/ide-task.h"
+#include "ide-worker-process.h"
+#include "ide-worker.h"
 
 struct _IdeWorkerProcess
 {
diff --git a/src/libide/workers/ide-worker-process.h b/src/libide/gui/ide-worker-process.h
similarity index 100%
rename from src/libide/workers/ide-worker-process.h
rename to src/libide/gui/ide-worker-process.h
diff --git a/src/libide/workers/ide-worker.c b/src/libide/gui/ide-worker.c
similarity index 97%
rename from src/libide/workers/ide-worker.c
rename to src/libide/gui/ide-worker.c
index 3fd51fa65..a01b64787 100644
--- a/src/libide/workers/ide-worker.c
+++ b/src/libide/gui/ide-worker.c
@@ -22,7 +22,9 @@
 
 #include "config.h"
 
-#include "workers/ide-worker.h"
+#include <libide-core.h>
+
+#include "ide-worker.h"
 
 G_DEFINE_INTERFACE (IdeWorker, ide_worker, G_TYPE_OBJECT)
 
diff --git a/src/libide/workers/ide-worker.h b/src/libide/gui/ide-worker.h
similarity index 96%
rename from src/libide/workers/ide-worker.h
rename to src/libide/gui/ide-worker.h
index be8002baf..0de20edd9 100644
--- a/src/libide/workers/ide-worker.h
+++ b/src/libide/gui/ide-worker.h
@@ -20,9 +20,7 @@
 
 #pragma once
 
-#include <gio/gio.h>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/gui/ide-workspace-actions.c b/src/libide/gui/ide-workspace-actions.c
new file mode 100644
index 000000000..1257cd6ad
--- /dev/null
+++ b/src/libide/gui/ide-workspace-actions.c
@@ -0,0 +1,92 @@
+/* ide-workspace-actions.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-workspace-actions"
+
+#include "config.h"
+
+#include "ide-gui-private.h"
+
+static void
+ide_workspace_actions_close (GSimpleAction *action,
+                             GVariant      *param,
+                             gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  gtk_window_close (GTK_WINDOW (self));
+}
+
+static void
+ide_workspace_actions_show_menu (GSimpleAction *action,
+                                 GVariant      *param,
+                                 gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+  GtkWidget *titlebar;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  titlebar = gtk_window_get_titlebar (GTK_WINDOW (self));
+  if (GTK_IS_STACK (titlebar))
+    titlebar = gtk_stack_get_visible_child (GTK_STACK (titlebar));
+
+  if (IDE_IS_HEADER_BAR (titlebar))
+    _ide_header_bar_show_menu (IDE_HEADER_BAR (titlebar));
+}
+
+static void
+ide_workspace_actions_surface (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+  const gchar *surface;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  surface = g_variant_get_string (param, NULL);
+
+  ide_workspace_set_visible_surface_name (self, surface);
+}
+
+static const GActionEntry actions[] = {
+  { "show-menu", ide_workspace_actions_show_menu },
+  { "surface", ide_workspace_actions_surface, "s" },
+  { "close", ide_workspace_actions_close },
+};
+
+void
+_ide_workspace_init_actions (IdeWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
diff --git a/src/libide/gui/ide-workspace-addin.c b/src/libide/gui/ide-workspace-addin.c
new file mode 100644
index 000000000..c44fee4ca
--- /dev/null
+++ b/src/libide/gui/ide-workspace-addin.c
@@ -0,0 +1,118 @@
+/* ide-workspace-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-workspace-addin"
+
+#include "config.h"
+
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+/**
+ * SECTION:ide-workspace-addin
+ * @title: IdeWorkspaceAddin
+ * @short_description: Extend the #IdeWorkspace windows
+ *
+ * The #IdeWorkspaceAddin is created with each #IdeWorkspace, allowing
+ * plugins a chance to modify each window that is created.
+ *
+ * If you set `X-Workspace-Kind=primary` in your `.plugin` file, your
+ * addin will only be loaded in the primary workspace. You may specify
+ * multiple workspace kinds such as `primary` or `secondary` separated
+ * by a comma such as `primary,secondary;`.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeWorkspaceAddin, ide_workspace_addin, G_TYPE_OBJECT)
+
+static void
+ide_workspace_addin_default_init (IdeWorkspaceAddinInterface *iface)
+{
+}
+
+/**
+ * ide_workspace_addin_load:
+ * @self: a #IdeWorkspaceAddin
+ *
+ * Lods the #IdeWorkspaceAddin.
+ *
+ * This is a good place to modify the workspace from your addin.
+ * Remember to unmodify the workspace in ide_workspace_addin_unload().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_load (IdeWorkspaceAddin *self,
+                          IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->load)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->load (self, workspace);
+}
+
+/**
+ * ide_workspace_addin_unload:
+ * @self: a #IdeWorkspaceAddin
+ *
+ * Unloads the #IdeWorkspaceAddin.
+ *
+ * This is a good place to unmodify the workspace from anything you
+ * did in ide_workspace_addin_load().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_unload (IdeWorkspaceAddin *self,
+                            IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->unload)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->unload (self, workspace);
+}
+
+/**
+ * ide_workspace_addin_surface_set:
+ * @self: an #IdeWorkspaceAddin
+ * @surface: (nullable): an #IdeSurface or %NULL
+ *
+ * This function is called to notify the addin of the current surface.
+ * It may be set to %NULL before unloading the addin to allow addins
+ * to do surface change state handling and cleanup in one function.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_surface_set (IdeWorkspaceAddin *self,
+                                 IdeSurface        *surface)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (!surface || IDE_IS_SURFACE (surface));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->surface_set)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->surface_set (self, surface);
+}
diff --git a/src/libide/gui/ide-workspace-addin.h b/src/libide/gui/ide-workspace-addin.h
new file mode 100644
index 000000000..8bf602c88
--- /dev/null
+++ b/src/libide/gui/ide-workspace-addin.h
@@ -0,0 +1,54 @@
+/* ide-workspace-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKSPACE_ADDIN (ide_workspace_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeWorkspaceAddin, ide_workspace_addin, IDE, WORKSPACE_ADDIN, GObject)
+
+struct _IdeWorkspaceAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)        (IdeWorkspaceAddin *self,
+                       IdeWorkspace      *workspace);
+  void (*unload)      (IdeWorkspaceAddin *self,
+                       IdeWorkspace      *workspace);
+  void (*surface_set) (IdeWorkspaceAddin *self,
+                       IdeSurface        *surface);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_load        (IdeWorkspaceAddin *self,
+                                      IdeWorkspace      *workspace);
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_unload      (IdeWorkspaceAddin *self,
+                                      IdeWorkspace      *workspace);
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_surface_set (IdeWorkspaceAddin *self,
+                                      IdeSurface        *surface);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace.c b/src/libide/gui/ide-workspace.c
new file mode 100644
index 000000000..7db4e9a03
--- /dev/null
+++ b/src/libide/gui/ide-workspace.c
@@ -0,0 +1,971 @@
+/* ide-workspace.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-workspace"
+
+#include "config.h"
+
+#include <libide-plugins.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+#define MUX_ACTIONS_KEY "IDE_WORKSPACE_MUX_ACTIONS"
+
+typedef struct
+{
+  /* Used as a link in IdeWorkbench's GQueue to track the most-recently-used
+   * workspaces based on recent focus.
+   */
+  GList mru_link;
+
+  /* This cancellable auto-cancels when the window is destroyed using
+   * ::delete-event() so that async operations can be made to auto-cancel.
+   */
+  GCancellable *cancellable;
+
+  /* The context for our workbench. It may not have a project loaded until
+   * the ide_workbench_load_project_async() workflow has been called, but it
+   * is usable without a project (albeit restricted).
+   */
+  IdeContext *context;
+
+  /* Our addins for the workspace window, that are limited by the "kind" of
+   * workspace that is loaded. Plugin files can specify X-Workspace-Kind to
+   * limit the plugin to specific type(s) of workspace.
+   */
+  IdeExtensionSetAdapter *addins;
+
+  /* We use an overlay as our top-most child so that plugins can potentially
+   * render any widget a layer above the UI.
+   */
+  GtkOverlay *overlay;
+
+  /* All workspaces are comprised of a series of "surfaces". However there may
+   * only ever be a single surface in a workspace (such as the editor workspace
+   * which is dedicated for editing).
+   */
+  GtkStack *surfaces;
+
+  /* The event box ensures that we can have events that will be used by the
+   * fullscreen overlay so that it gets delivery of crossing events.
+   */
+  GtkEventBox *event_box;
+
+  /* A MRU that is updated as pages are focused. It allows us to move through
+   * the pages in the order they've been most-recently focused.
+   */
+  GQueue page_mru;
+} IdeWorkspacePrivate;
+
+typedef struct
+{
+  GtkCallback callback;
+  gpointer    user_data;
+} ForeachPage;
+
+enum {
+  SURFACE_SET,
+  N_SIGNALS
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_VISIBLE_SURFACE,
+  N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeWorkspace, ide_workspace, DZL_TYPE_APPLICATION_WINDOW,
+                                  G_ADD_PRIVATE (IdeWorkspace)
+                                  G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_workspace_addin_added_cb (IdeExtensionSetAdapter *set,
+                              PeasPluginInfo         *plugin_info,
+                              PeasExtension          *exten,
+                              gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeWorkspace *self = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  g_debug ("Loading workspace addin from module %s",
+           peas_plugin_info_get_module_name (plugin_info));
+
+  ide_workspace_addin_load (addin, self);
+}
+
+static void
+ide_workspace_addin_removed_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeWorkspace *self = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  g_debug ("Unloading workspace addin from module %s",
+           peas_plugin_info_get_module_name (plugin_info));
+
+  ide_workspace_addin_surface_set (addin, NULL);
+  ide_workspace_addin_unload (addin, self);
+}
+
+static void
+ide_workspace_real_context_set (IdeWorkspace *self,
+                                IdeContext   *context)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  priv->addins = ide_extension_set_adapter_new (NULL,
+                                                NULL,
+                                                IDE_TYPE_WORKSPACE_ADDIN,
+                                                "Workspace-Kind",
+                                                IDE_WORKSPACE_GET_CLASS (self)->kind);
+
+  g_signal_connect (priv->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_workspace_addin_added_cb),
+                    self);
+
+  g_signal_connect (priv->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_workspace_addin_removed_cb),
+                    self);
+
+  ide_extension_set_adapter_foreach (priv->addins,
+                                     ide_workspace_addin_added_cb,
+                                     self);
+}
+
+static void
+ide_workspace_addin_surface_set_cb (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeSurface *surface = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  ide_workspace_addin_surface_set (addin, surface);
+}
+
+static void
+ide_workspace_real_surface_set (IdeWorkspace *self,
+                                IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (priv->addins != NULL)
+    ide_extension_set_adapter_foreach (priv->addins,
+                                       ide_workspace_addin_surface_set_cb,
+                                       surface);
+}
+
+/**
+ * ide_workspace_foreach_surface:
+ * @self: a #IdeWorkspace
+ * @callback: (scope call): a #GtkCallback to execute for every surface
+ * @user_data: user data for @callback
+ *
+ * Calls callback for every #IdeSurface based #GtkWidget that is registered
+ * in the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_foreach_surface (IdeWorkspace *self,
+                               GtkCallback   callback,
+                               gpointer      user_data)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces), callback, user_data);
+}
+
+static void
+ide_workspace_agree_to_shutdown_cb (GtkWidget *widget,
+                                    gpointer   user_data)
+{
+  gboolean *blocked = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACE (widget));
+  g_assert (blocked != NULL);
+
+  *blocked |= !ide_surface_agree_to_shutdown (IDE_SURFACE (widget));
+}
+
+static gboolean
+ide_workspace_agree_to_shutdown (IdeWorkspace *self)
+{
+  gboolean blocked = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  ide_workspace_foreach_surface (self,
+                                 ide_workspace_agree_to_shutdown_cb,
+                                 &blocked);
+
+  return !blocked;
+}
+
+static gboolean
+ide_workspace_delete_event (GtkWidget   *widget,
+                            GdkEventAny *any)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  IdeWorkbench *workbench;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (any != NULL);
+
+  /* TODO:
+   *
+   * If there are any active transfers, we want to ask the user if they
+   * are sure they want to exit and risk losing them. We can allow them
+   * to be completed in the background.
+   *
+   * Note that we only want to do this on the final workspace window.
+   */
+
+  if (!ide_workspace_agree_to_shutdown (self))
+    return GDK_EVENT_STOP;
+
+  g_cancellable_cancel (priv->cancellable);
+
+  workbench = ide_widget_get_workbench (widget);
+
+  if (ide_workbench_has_project (workbench) &&
+      _ide_workbench_is_last_workspace (workbench, self))
+    {
+      gtk_widget_hide (GTK_WIDGET (self));
+      ide_workbench_unload_async (workbench, NULL, NULL, NULL);
+      return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_workspace_notify_surface_cb (IdeWorkspace *self,
+                                 GParamSpec   *pspec,
+                                 GtkStack     *surfaces)
+{
+  GtkWidget *visible_child;
+  IdeHeaderBar *header_bar;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (GTK_IS_STACK (surfaces));
+
+  visible_child = gtk_stack_get_visible_child (surfaces);
+  if (!IDE_IS_SURFACE (visible_child))
+    visible_child = NULL;
+
+  if (visible_child != NULL)
+    gtk_widget_grab_focus (visible_child);
+
+  if ((header_bar = ide_workspace_get_header_bar (self)))
+    {
+      if (visible_child != NULL)
+        dzl_gtk_widget_mux_action_groups (GTK_WIDGET (header_bar), visible_child, MUX_ACTIONS_KEY);
+      else
+        dzl_gtk_widget_mux_action_groups (GTK_WIDGET (header_bar), NULL, MUX_ACTIONS_KEY);
+    }
+
+  g_signal_emit (self, signals [SURFACE_SET], 0, visible_child);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_SURFACE]);
+}
+
+static void
+ide_workspace_destroy (GtkWidget *widget)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWindowGroup *group;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  ide_clear_and_destroy_object (&priv->addins);
+
+  group = gtk_window_get_group (GTK_WINDOW (self));
+  if (IDE_IS_WORKBENCH (group))
+    ide_workbench_remove_workspace (IDE_WORKBENCH (group), self);
+
+  GTK_WIDGET_CLASS (ide_workspace_parent_class)->destroy (widget);
+}
+
+/**
+ * ide_workspace_class_set_kind:
+ * @klass: a #IdeWorkspaceClass
+ *
+ * Sets the shorthand name for the kind of workspace. This is used to limit
+ * what #IdeWorkspaceAddin may load within the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_class_set_kind (IdeWorkspaceClass *klass,
+                              const gchar       *kind)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE_CLASS (klass));
+
+  klass->kind = g_intern_string (kind);
+}
+
+
+static void
+ide_workspace_foreach_page_cb (GtkWidget *widget,
+                               gpointer   user_data)
+{
+  ForeachPage *state = user_data;
+
+  if (IDE_IS_SURFACE (widget))
+    ide_surface_foreach_page (IDE_SURFACE (widget), state->callback, state->user_data);
+}
+
+static void
+ide_workspace_real_foreach_page (IdeWorkspace *self,
+                                 GtkCallback   callback,
+                                 gpointer      user_data)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  ForeachPage state = { callback, user_data };
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
+                         ide_workspace_foreach_page_cb,
+                         &state);
+}
+
+static void
+ide_workspace_set_surface_fullscreen_cb (GtkWidget *widget,
+                                         gpointer   user_data)
+{
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_SURFACE (widget))
+    _ide_surface_set_fullscreen (IDE_SURFACE (widget), !!user_data);
+}
+
+static void
+ide_workspace_real_set_fullscreen (DzlApplicationWindow *window,
+                                   gboolean              fullscreen)
+{
+  IdeWorkspace *self = (IdeWorkspace *)window;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  DZL_APPLICATION_WINDOW_CLASS (ide_workspace_parent_class)->set_fullscreen (window, fullscreen);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
+                         ide_workspace_set_surface_fullscreen_cb,
+                         GUINT_TO_POINTER (fullscreen));
+}
+
+static void
+ide_workspace_grab_focus (GtkWidget *widget)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeSurface *surface;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  if ((surface = ide_workspace_get_visible_surface (self)))
+    gtk_widget_grab_focus (GTK_WIDGET (surface));
+}
+
+static void
+ide_workspace_finalize (GObject *object)
+{
+  IdeWorkspace *self = (IdeWorkspace *)object;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_clear_object (&priv->context);
+  g_clear_object (&priv->cancellable);
+
+  G_OBJECT_CLASS (ide_workspace_parent_class)->finalize (object);
+}
+
+static void
+ide_workspace_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeWorkspace *self = IDE_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_workspace_get_context (self));
+      break;
+
+    case PROP_VISIBLE_SURFACE:
+      g_value_set_object (value, ide_workspace_get_visible_surface (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workspace_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeWorkspace *self = IDE_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_VISIBLE_SURFACE:
+      ide_workspace_set_visible_surface (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workspace_class_init (IdeWorkspaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlApplicationWindowClass *window_class = DZL_APPLICATION_WINDOW_CLASS (klass);
+
+  object_class->finalize = ide_workspace_finalize;
+  object_class->get_property = ide_workspace_get_property;
+  object_class->set_property = ide_workspace_set_property;
+
+  widget_class->destroy = ide_workspace_destroy;
+  widget_class->delete_event = ide_workspace_delete_event;
+  widget_class->grab_focus = ide_workspace_grab_focus;
+
+  window_class->set_fullscreen = ide_workspace_real_set_fullscreen;
+
+  klass->foreach_page = ide_workspace_real_foreach_page;
+  klass->context_set = ide_workspace_real_context_set;
+  klass->surface_set = ide_workspace_real_surface_set;
+
+  /**
+   * IdeWorkspace:context:
+   *
+   * The "context" property is the #IdeContext for the workspace. This is set
+   * when the workspace joins a workbench.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The IdeContext for the workspace, inherited from workbench",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeWorkspace:visible-surface:
+   *
+   * The "visible-surface" property contains the currently foremost surface
+   * in the workspaces stack of surfaces. Usually, this is the editor surface,
+   * but may be other surfaces such as build preferences, profiler, etc.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VISIBLE_SURFACE] =
+    g_param_spec_object ("visible-surface",
+                         "Visible Surface",
+                         "The currently visible surface",
+                         IDE_TYPE_SURFACE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeWorkspace::surface-set:
+   * @self: an #IdeWorkspace
+   * @surface: (nullable): an #IdeSurface
+   *
+   * The "surface-set" signal is emitted when the current surface changes
+   * within the workspace.
+   *
+   * Since: 3.32
+   */
+  signals [SURFACE_SET] =
+    g_signal_new ("surface-set",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeWorkspaceClass, surface_set),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_SURFACE);
+  g_signal_set_va_marshaller (signals [SURFACE_SET],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-workspace.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, event_box);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, overlay);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, surfaces);
+}
+
+static void
+ide_workspace_init (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autofree gchar *app_id = NULL;
+
+  priv->mru_link.data = self;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (priv->surfaces,
+                           "notify::visible-child",
+                           G_CALLBACK (ide_workspace_notify_surface_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Add org-gnome-Builder style CSS identifier */
+  app_id = g_strdelimit (g_strdup (ide_get_application_id ()), ".", '-');
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), app_id);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "workspace");
+
+  /* Add events for motion controller of fullscreen titlebar */
+  gtk_widget_add_events (GTK_WIDGET (priv->event_box),
+                         (GDK_POINTER_MOTION_MASK |
+                          GDK_ENTER_NOTIFY_MASK |
+                          GDK_LEAVE_NOTIFY_MASK));
+
+  /* Initialize GActions for workspace */
+  _ide_workspace_init_actions (self);
+}
+
+GList *
+_ide_workspace_get_mru_link (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  return &priv->mru_link;
+}
+
+/**
+ * ide_workspace_get_context:
+ *
+ * Gets the #IdeContext for the #IdeWorkspace, which is set when the
+ * workspace joins an #IdeWorkbench.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_workspace_get_context (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  return priv->context;
+}
+
+void
+_ide_workspace_set_context (IdeWorkspace *self,
+                            IdeContext   *context)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+  g_return_if_fail (priv->context == NULL);
+
+  if (g_set_object (&priv->context, context))
+    {
+      if (IDE_WORKSPACE_GET_CLASS (self)->context_set)
+        IDE_WORKSPACE_GET_CLASS (self)->context_set (self, context);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+    }
+}
+
+/**
+ * ide_workspace_get_cancellable:
+ * @self: a #IdeWorkspace
+ *
+ * Gets a cancellable for a window. This is useful when you want operations
+ * to be cancelled if a window is closed.
+ *
+ * Returns: (transfer none): a #GCancellable
+ *
+ * Since: 3.32
+ */
+GCancellable *
+ide_workspace_get_cancellable (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if (priv->cancellable == NULL)
+    priv->cancellable = g_cancellable_new ();
+
+  return priv->cancellable;
+}
+
+/**
+ * ide_workspace_foreach_page:
+ * @self: a #IdeWorkspace
+ * @callback: (scope call): a callback to execute for each view
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for each #IdePage found within the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_foreach_page (IdeWorkspace *self,
+                            GtkCallback   callback,
+                            gpointer      user_data)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->foreach_page)
+    IDE_WORKSPACE_GET_CLASS (self)->foreach_page (self, callback, user_data);
+}
+
+/**
+ * ide_workspace_get_header_bar:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the headerbar for the workspace, if it is an #IdeHeaderBar.
+ * Also works around Gtk giving back a GtkStack for the header bar.
+ *
+ * Returns: (nullable) (transfer none): an #IdeHeaderBar or %NULL
+ *
+ * Since: 3.32
+ */
+IdeHeaderBar *
+ide_workspace_get_header_bar (IdeWorkspace *self)
+{
+  GtkWidget *titlebar;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if ((titlebar = gtk_window_get_titlebar (GTK_WINDOW (self))))
+    {
+      if (GTK_IS_STACK (titlebar))
+        titlebar = gtk_stack_get_visible_child (GTK_STACK (titlebar));
+
+      if (IDE_IS_HEADER_BAR (titlebar))
+        return IDE_HEADER_BAR (titlebar);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_workspace_add_surface:
+ * @self: a #IdeWorkspace
+ *
+ * Adds a new #IdeSurface to the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_add_surface (IdeWorkspace *self,
+                           IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autofree gchar *title = NULL;
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_SURFACE (surface));
+
+  if (DZL_IS_DOCK_ITEM (surface))
+    title = dzl_dock_item_get_title (DZL_DOCK_ITEM (surface));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->surfaces), GTK_WIDGET (surface),
+                                     "name", gtk_widget_get_name (GTK_WIDGET (surface)),
+                                     "title", title,
+                                     NULL);
+}
+
+/**
+ * ide_workspace_set_visible_surface_name:
+ * @self: a #IdeWorkspace
+ * @visible_surface_name: the name of the #IdeSurface
+ *
+ * Sets the visible surface based on the name of the surface.  The name of the
+ * surface comes from gtk_widget_get_name(), which should be set when creating
+ * the surface using gtk_widget_set_name().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_set_visible_surface_name (IdeWorkspace *self,
+                                        const gchar  *visible_surface_name)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (visible_surface_name != NULL);
+
+  gtk_stack_set_visible_child_name (priv->surfaces, visible_surface_name);
+}
+
+/**
+ * ide_workspace_get_visible_surface:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the currently visible #IdeSurface, or %NULL
+ *
+ * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+IdeSurface *
+ide_workspace_get_visible_surface (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  child = gtk_stack_get_visible_child (priv->surfaces);
+  if (!IDE_IS_SURFACE (child))
+    child = NULL;
+
+  return IDE_SURFACE (child);
+}
+
+/**
+ * ide_workspace_set_visible_surface:
+ * @self: a #IdeWorkspace
+ * @surface: an #IdeSurface
+ *
+ * Sets the #IdeWorkspace:visible-surface property which is the currently
+ * visible #IdeSurface in the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_set_visible_surface (IdeWorkspace *self,
+                                   IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_SURFACE (surface));
+
+  gtk_stack_set_visible_child (priv->surfaces, GTK_WIDGET (surface));
+}
+
+/**
+ * ide_workspace_get_surface_by_name:
+ * @self: a #IdeWorkspace
+ * @name: the name of the surface
+ *
+ * Locates an #IdeSurface that has been added to the workspace by the name
+ * that was registered for the widget using gtk_widget_set_name().
+ *
+ * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+IdeSurface *
+ide_workspace_get_surface_by_name (IdeWorkspace *self,
+                                   const gchar  *name)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  child = gtk_stack_get_child_by_name (priv->surfaces, name);
+
+  return IDE_IS_SURFACE (child) ? IDE_SURFACE (child) : NULL;
+}
+
+static GObject *
+ide_workspace_get_internal_child (GtkBuildable *buildable,
+                                  GtkBuilder   *builder,
+                                  const gchar  *child_name)
+{
+  IdeWorkspace *self = (IdeWorkspace *)buildable;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (GTK_IS_BUILDABLE (buildable));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (child_name != NULL);
+
+  if (ide_str_equal0 (child_name, "surfaces"))
+    return G_OBJECT (priv->surfaces);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = ide_workspace_get_internal_child;
+}
+
+/**
+ * ide_workspace_get_overlay:
+ * @self: a #IdeWorkspace
+ *
+ * Gets a #GtkOverlay that contains all of the primary contents of the window
+ * (everything except the headerbar). This can be used by plugins to draw
+ * above the workspace contents.
+ *
+ * Returns: (transfer none): a #GtkOverlay
+ *
+ * Since: 3.32
+ */
+GtkOverlay *
+ide_workspace_get_overlay (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  return priv->overlay;
+}
+
+/**
+ * ide_workspace_get_most_recent_page:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the most recently focused #IdePage.
+ *
+ * Returns: (transfer none) (nullable): an #IdePage or %NULL
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_workspace_get_most_recent_page (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if (priv->page_mru.head != NULL)
+    return IDE_PAGE (priv->page_mru.head->data);
+
+  return NULL;
+}
+
+void
+_ide_workspace_add_page_mru (IdeWorkspace *self,
+                             GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (mru_link->prev == NULL);
+  g_return_if_fail (mru_link->next == NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  g_debug ("Adding %s to page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_push_head_link (&priv->page_mru, mru_link);
+}
+
+void
+_ide_workspace_remove_page_mru (IdeWorkspace *self,
+                                GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  g_debug ("Removing %s from page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_unlink (&priv->page_mru, mru_link);
+}
+
+void
+_ide_workspace_move_front_page_mru (IdeWorkspace *self,
+                                    GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  if (mru_link == priv->page_mru.head)
+    return;
+
+  g_debug ("Moving %s to front of page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_unlink (&priv->page_mru, mru_link);
+  g_queue_push_head_link (&priv->page_mru, mru_link);
+}
diff --git a/src/libide/gui/ide-workspace.h b/src/libide/gui/ide-workspace.h
new file mode 100644
index 000000000..9e899c9d0
--- /dev/null
+++ b/src/libide/gui/ide-workspace.h
@@ -0,0 +1,96 @@
+/* ide-workspace.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-projects.h>
+
+#include "ide-header-bar.h"
+#include "ide-page.h"
+#include "ide-surface.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKSPACE (ide_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeWorkspace, ide_workspace, IDE, WORKSPACE, DzlApplicationWindow)
+
+struct _IdeWorkspaceClass
+{
+  DzlApplicationWindowClass parent_class;
+
+  const gchar *kind;
+
+  void (*context_set)  (IdeWorkspace *self,
+                        IdeContext   *context);
+  void (*foreach_page) (IdeWorkspace *self,
+                        GtkCallback   callback,
+                        gpointer      user_data);
+  void (*surface_set)  (IdeWorkspace *self,
+                        IdeSurface   *surface);
+
+  /*< private >*/
+  gpointer _reserved[32];
+};
+
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_class_set_kind           (IdeWorkspaceClass *klass,
+                                                      const gchar       *kind);
+IDE_AVAILABLE_IN_3_32
+IdeHeaderBar *ide_workspace_get_header_bar           (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext   *ide_workspace_get_context              (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+GCancellable *ide_workspace_get_cancellable          (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_foreach_page             (IdeWorkspace      *self,
+                                                      GtkCallback        callback,
+                                                      gpointer           user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_foreach_surface          (IdeWorkspace      *self,
+                                                      GtkCallback        callback,
+                                                      gpointer           user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_add_surface              (IdeWorkspace      *self,
+                                                      IdeSurface        *surface);
+IDE_AVAILABLE_IN_3_32
+IdeSurface   *ide_workspace_get_surface_by_name      (IdeWorkspace      *self,
+                                                      const gchar       *name);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_set_visible_surface_name (IdeWorkspace      *self,
+                                                      const gchar       *visible_surface_name);
+IDE_AVAILABLE_IN_3_32
+IdeSurface   *ide_workspace_get_visible_surface      (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_set_visible_surface      (IdeWorkspace      *self,
+                                                      IdeSurface        *surface);
+IDE_AVAILABLE_IN_3_32
+GtkOverlay   *ide_workspace_get_overlay              (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+IdePage      *ide_workspace_get_most_recent_page     (IdeWorkspace      *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace.ui b/src/libide/gui/ide-workspace.ui
new file mode 100644
index 000000000..6af729f22
--- /dev/null
+++ b/src/libide/gui/ide-workspace.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeWorkspace" parent="DzlApplicationWindow">
+    <child>
+      <object class="GtkEventBox" id="event_box">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkStack" id="surfaces">
+                <property name="homogeneous">false</property>
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/src/libide/gui/libide-gui.gresource.xml b/src/libide/gui/libide-gui.gresource.xml
new file mode 100644
index 000000000..cec829c7c
--- /dev/null
+++ b/src/libide/gui/libide-gui.gresource.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-gui">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+  <gresource prefix="/org/gnome/libide-gui/ui">
+    <file preprocess="xml-stripblanks">ide-environment-editor-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-frame-header.ui</file>
+    <file preprocess="xml-stripblanks">ide-frame.ui</file>
+    <file preprocess="xml-stripblanks">ide-header-bar.ui</file>
+    <file preprocess="xml-stripblanks">ide-notification-list-box-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-notification-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-notifications-button.ui</file>
+    <file preprocess="xml-stripblanks">ide-omni-bar.ui</file>
+    <file preprocess="xml-stripblanks">ide-panel.ui</file>
+    <file preprocess="xml-stripblanks">ide-preferences-language-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-preferences-window.ui</file>
+    <file preprocess="xml-stripblanks">ide-primary-workspace.ui</file>
+    <file preprocess="xml-stripblanks">ide-run-button.ui</file>
+    <file preprocess="xml-stripblanks">ide-search-entry.ui</file>
+    <file preprocess="xml-stripblanks">ide-shortcuts-window.ui</file>
+    <file preprocess="xml-stripblanks">ide-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/gui/libide-gui.h b/src/libide/gui/libide-gui.h
new file mode 100644
index 000000000..258cd487e
--- /dev/null
+++ b/src/libide/gui/libide-gui.h
@@ -0,0 +1,70 @@
+/* ide-gui.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-io.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#define IDE_GUI_INSIDE
+
+#include "ide-application.h"
+#include "ide-application-addin.h"
+#include "ide-cell-renderer-fancy.h"
+#include "ide-command.h"
+#include "ide-command-provider.h"
+#include "ide-config-view-addin.h"
+#include "ide-environment-editor.h"
+#include "ide-frame.h"
+#include "ide-frame-addin.h"
+#include "ide-frame-header.h"
+#include "ide-header-bar.h"
+#include "ide-fancy-tree-view.h"
+#include "ide-grid.h"
+#include "ide-grid-column.h"
+#include "ide-gui-global.h"
+#include "ide-header-bar.h"
+#include "ide-marked-view.h"
+#include "ide-notifications-button.h"
+#include "ide-omni-bar-addin.h"
+#include "ide-omni-bar.h"
+#include "ide-page.h"
+#include "ide-pane.h"
+#include "ide-panel.h"
+#include "ide-preferences-addin.h"
+#include "ide-preferences-surface.h"
+#include "ide-preferences-window.h"
+#include "ide-primary-workspace.h"
+#include "ide-search-entry.h"
+#include "ide-session-addin.h"
+#include "ide-surface.h"
+#include "ide-surfaces-button.h"
+#include "ide-tagged-entry.h"
+#include "ide-transfer-button.h"
+#include "ide-transient-sidebar.h"
+#include "ide-workbench.h"
+#include "ide-workbench-addin.h"
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+#undef IDE_GUI_INSIDE
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
new file mode 100644
index 000000000..3c494df9a
--- /dev/null
+++ b/src/libide/gui/meson.build
@@ -0,0 +1,212 @@
+libide_gui_header_subdir = join_paths(libide_header_subdir, 'gui')
+libide_include_directories += include_directories('.')
+
+libide_gui_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_gui_public_headers = [
+  'ide-application.h',
+  'ide-application-addin.h',
+  'ide-cell-renderer-fancy.h',
+  'ide-command.h',
+  'ide-command-provider.h',
+  'ide-config-view-addin.h',
+  'ide-environment-editor.h',
+  'ide-fancy-tree-view.h',
+  'ide-frame-addin.h',
+  'ide-frame-header.h',
+  'ide-frame.h',
+  'ide-grid-column.h',
+  'ide-grid.h',
+  'ide-gui-global.h',
+  'ide-header-bar.h',
+  'ide-marked-view.h',
+  'ide-notifications-button.h',
+  'ide-omni-bar-addin.h',
+  'ide-omni-bar.h',
+  'ide-page.h',
+  'ide-pane.h',
+  'ide-panel.h',
+  'ide-preferences-addin.h',
+  'ide-preferences-surface.h',
+  'ide-preferences-window.h',
+  'ide-primary-workspace.h',
+  'ide-search-entry.h',
+  'ide-session-addin.h',
+  'ide-surface.h',
+  'ide-surfaces-button.h',
+  'ide-tagged-entry.h',
+  'ide-transfer-button.h',
+  'ide-transient-sidebar.h',
+  'ide-worker.h',
+  'ide-workbench.h',
+  'ide-workbench-addin.h',
+  'ide-workspace.h',
+  'ide-workspace-addin.h',
+  'libide-gui.h',
+]
+
+install_headers(libide_gui_public_headers, subdir: libide_gui_header_subdir)
+
+#
+# Sources
+#
+
+libide_gui_private_headers = [
+  'gs-markdown-private.h',
+  'ide-application-private.h',
+  'ide-environment-editor-row.h',
+  'ide-frame-wrapper.h',
+  'ide-gui-private.h',
+  'ide-keybindings.h',
+  'ide-notification-list-box-row-private.h',
+  'ide-notifications-button-popover-private.h',
+  'ide-notification-stack-private.h',
+  'ide-notification-view-private.h',
+  'ide-preferences-builtin-private.h',
+  'ide-preferences-language-row-private.h',
+  'ide-run-button.h',
+  'ide-session-private.h',
+  'ide-window-settings-private.h',
+  'ide-shortcut-label-private.h',
+  'ide-shortcuts-window-private.h',
+  'ide-worker-manager.h',
+  'ide-worker-process.h',
+]
+
+libide_gui_private_sources = [
+  'gs-markdown.c',
+  'ide-application-actions.c',
+  'ide-application-color.c',
+  'ide-application-shortcuts.c',
+  'ide-application-plugins.c',
+  'ide-environment-editor-row.c',
+  'ide-frame-actions.c',
+  'ide-frame-shortcuts.c',
+  'ide-frame-wrapper.c',
+  'ide-grid-actions.c',
+  'ide-grid-column-actions.c',
+  'ide-header-bar-shortcuts.c',
+  'ide-keybindings.c',
+  'ide-notification-list-box-row.c',
+  'ide-notification-stack.c',
+  'ide-notification-view.c',
+  'ide-notifications-button-popover.c',
+  'ide-preferences-builtin.c',
+  'ide-preferences-language-row.c',
+  'ide-primary-workspace-actions.c',
+  'ide-run-button.c',
+  'ide-session.c',
+  'ide-shortcuts-window.c',
+  'ide-window-settings.c',
+  'ide-worker-manager.c',
+  'ide-worker-process.c',
+  'ide-workspace-actions.c',
+]
+
+libide_gui_public_sources = [
+  'ide-application.c',
+  'ide-application-addin.c',
+  'ide-application-command-line.c',
+  'ide-application-open.c',
+  'ide-cell-renderer-fancy.c',
+  'ide-command.c',
+  'ide-command-provider.c',
+  'ide-config-view-addin.c',
+  'ide-environment-editor.c',
+  'ide-fancy-tree-view.c',
+  'ide-frame-addin.c',
+  'ide-frame-header.c',
+  'ide-frame.c',
+  'ide-grid-column.c',
+  'ide-grid.c',
+  'ide-gui-global.c',
+  'ide-header-bar.c',
+  'ide-marked-view.c',
+  'ide-notifications-button.c',
+  'ide-omni-bar-addin.c',
+  'ide-omni-bar.c',
+  'ide-page.c',
+  'ide-pane.c',
+  'ide-panel.c',
+  'ide-primary-workspace.c',
+  'ide-preferences-addin.c',
+  'ide-preferences-surface.c',
+  'ide-preferences-window.c',
+  'ide-search-entry.c',
+  'ide-session-addin.c',
+  'ide-shortcut-label.c',
+  'ide-surface.c',
+  'ide-surfaces-button.c',
+  'ide-tagged-entry.c',
+  'ide-transient-sidebar.c',
+  'ide-transfer-button.c',
+  'ide-workbench.c',
+  'ide-workbench-addin.c',
+  'ide-workspace.c',
+  'ide-workspace-addin.c',
+  'ide-worker.c',
+]
+
+libide_gui_sources = libide_gui_public_sources + libide_gui_private_sources
+
+#
+# Generated Resource Files
+#
+
+libide_gui_resources = gnome.compile_resources(
+  'ide-gui-resources',
+  'libide-gui.gresource.xml',
+  c_name: 'ide_gui',
+)
+libide_gui_generated_headers += [libide_gui_resources[1]]
+libide_gui_sources += libide_gui_resources[0]
+
+
+#
+# Dependencies
+#
+
+libide_gui_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libgtksource_dep,
+  libdazzle_dep,
+  libpeas_dep,
+  libwebkit_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_foundry_dep,
+  libide_debugger_dep,
+  libide_plugins_dep,
+  libide_projects_dep,
+  libide_search_dep,
+  libide_themes_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_gui = static_library('ide-gui-' + libide_api_version, libide_gui_sources,
+   dependencies: libide_gui_deps,
+         c_args: libide_args + release_args + ['-DIDE_GUI_COMPILATION'],
+)
+
+libide_gui_dep = declare_dependency(
+              sources: libide_gui_private_headers + libide_gui_generated_headers,
+         dependencies: libide_gui_deps,
+           link_whole: libide_gui,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_gui_public_sources)
+gnome_builder_public_headers += files(libide_gui_public_headers)
+gnome_builder_private_sources += files(libide_gui_private_sources)
+gnome_builder_private_headers += files(libide_gui_private_headers)
+gnome_builder_include_subdirs += libide_gui_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-gui.h', '-DIDE_GUI_COMPILATION']
diff --git a/src/libide/search/ide-search-engine.c b/src/libide/search/ide-search-engine.c
index 4320391ee..85e82d3f5 100644
--- a/src/libide/search/ide-search-engine.c
+++ b/src/libide/search/ide-search-engine.c
@@ -23,12 +23,12 @@
 #include "config.h"
 
 #include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-threading.h>
 
-#include "search/ide-search-engine.h"
-#include "search/ide-search-provider.h"
-#include "search/ide-search-result.h"
-#include "threading/ide-task.h"
-#include "util/ide-glib.h"
+#include "ide-search-engine.h"
+#include "ide-search-provider.h"
+#include "ide-search-result.h"
 
 #define DEFAULT_MAX_RESULTS 50
 
@@ -82,31 +82,65 @@ request_destroy (Request *r)
 }
 
 static void
-ide_search_engine_constructed (GObject *object)
+on_extension_added_cb (PeasExtensionSet *set,
+                       PeasPluginInfo   *plugin_info,
+                       PeasExtension    *exten,
+                       gpointer          user_data)
+{
+  ide_object_append (IDE_OBJECT (user_data), IDE_OBJECT (exten));
+}
+
+static void
+on_extension_removed_cb (PeasExtensionSet *set,
+                         PeasPluginInfo   *plugin_info,
+                         PeasExtension    *exten,
+                         gpointer          user_data)
+{
+  ide_object_remove (IDE_OBJECT (user_data), IDE_OBJECT (exten));
+}
+
+static void
+ide_search_engine_parent_set (IdeObject *object,
+                              IdeObject *parent)
 {
   IdeSearchEngine *self = (IdeSearchEngine *)object;
-  IdeContext *context;
 
   g_assert (IDE_IS_SEARCH_ENGINE (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  G_OBJECT_CLASS (ide_search_engine_parent_class)->constructed (object);
-
-  context = ide_object_get_context (IDE_OBJECT (self));
+  if (parent == NULL)
+    {
+      g_clear_object (&self->extensions);
+      return;
+    }
 
   self->extensions = peas_extension_set_new (peas_engine_get_default (),
                                              IDE_TYPE_SEARCH_PROVIDER,
-                                             "context", context,
                                              NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (on_extension_added_cb),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (on_extension_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->extensions,
+                              on_extension_added_cb,
+                              self);
 }
 
 static void
-ide_search_engine_dispose (GObject *object)
+ide_search_engine_destroy (IdeObject *object)
 {
   IdeSearchEngine *self = (IdeSearchEngine *)object;
 
   g_clear_object (&self->extensions);
 
-  G_OBJECT_CLASS (ide_search_engine_parent_class)->dispose (object);
+  IDE_OBJECT_CLASS (ide_search_engine_parent_class)->destroy (object);
 }
 
 static void
@@ -131,11 +165,13 @@ ide_search_engine_get_property (GObject    *object,
 static void
 ide_search_engine_class_init (IdeSearchEngineClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);
+
+  g_object_class->get_property = ide_search_engine_get_property;
 
-  object_class->constructed = ide_search_engine_constructed;
-  object_class->dispose = ide_search_engine_dispose;
-  object_class->get_property = ide_search_engine_get_property;
+  object_class->destroy = ide_search_engine_destroy;
+  object_class->parent_set = ide_search_engine_parent_set;
 
   properties [PROP_BUSY] =
     g_param_spec_boolean ("busy",
@@ -144,7 +180,7 @@ ide_search_engine_class_init (IdeSearchEngineClass *klass)
                           FALSE,
                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  g_object_class_install_properties (object_class, N_PROPS, properties);
+  g_object_class_install_properties (g_object_class, N_PROPS, properties);
 }
 
 static void



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