[gnome-builder] libide-gui: port workbench/workspace to GTK 4



commit bc3e6bb23735dfcfd5ca4936fd8d788982983e4a
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 21:54:36 2022 -0700

    libide-gui: port workbench/workspace to GTK 4
    
    This uses libpanel for a lot of the workspace internals that used to be
    in tree. Additionally, availability macros have been cleaned up, libdazzle
    usage removed, pane and page cleanup, notebook tab support, and a lot more
    here.

 src/libide/gui/gtk/menus.ui                    |   51 +-
 src/libide/gui/ide-frame.c                     | 1341 +++------------------
 src/libide/gui/ide-frame.h                     |   66 +-
 src/libide/gui/ide-frame.ui                    |  153 ++-
 src/libide/gui/ide-grid-actions.c              |   73 --
 src/libide/gui/ide-grid.c                      | 1513 ++----------------------
 src/libide/gui/ide-grid.h                      |   62 +-
 src/libide/gui/ide-page-private.h              |   29 +
 src/libide/gui/ide-page.c                      |  682 ++++-------
 src/libide/gui/ide-page.h                      |  134 +--
 src/libide/gui/ide-page.ui                     |   26 +
 src/libide/gui/ide-pane.c                      |  261 +++-
 src/libide/gui/ide-pane.h                      |   32 +-
 src/libide/gui/ide-panel-position.c            |  174 +++
 src/libide/gui/ide-panel-position.h            |   74 ++
 src/libide/gui/ide-primary-workspace-actions.c |    6 +-
 src/libide/gui/ide-primary-workspace-private.h |   29 +
 src/libide/gui/ide-primary-workspace.c         |  220 +++-
 src/libide/gui/ide-primary-workspace.h         |    7 +-
 src/libide/gui/ide-primary-workspace.ui        |  120 +-
 src/libide/gui/ide-workbench-addin.c           |  114 +-
 src/libide/gui/ide-workbench-addin.h           |   60 +-
 src/libide/gui/ide-workbench-private.h         |   31 +
 src/libide/gui/ide-workbench.c                 |  457 ++++---
 src/libide/gui/ide-workbench.h                 |   75 +-
 src/libide/gui/ide-workspace-addin.c           |   52 +-
 src/libide/gui/ide-workspace-addin.h           |   29 +-
 src/libide/gui/ide-workspace-private.h         |   57 +
 src/libide/gui/ide-workspace.c                 | 1378 ++++++++++++---------
 src/libide/gui/ide-workspace.h                 |  136 ++-
 src/libide/gui/libide-gui.gresource.xml        |    3 +
 src/libide/gui/meson.build                     |   13 +
 src/libide/gui/style.css                       |   98 ++
 33 files changed, 3113 insertions(+), 4443 deletions(-)
---
diff --git a/src/libide/gui/gtk/menus.ui b/src/libide/gui/gtk/menus.ui
index 91b72fddb..a5b20291d 100644
--- a/src/libide/gui/gtk/menus.ui
+++ b/src/libide/gui/gtk/menus.ui
@@ -1,15 +1,13 @@
 <?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>
-    <section id="ide-primary-workspace-surfaces-menu-utils-section"/>
-  </menu>
   <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-theme-section">
+      <item>
+        <attribute name="custom">theme_selector</attribute>
+      </item>
+    </section>
     <section id="ide-primary-workspace-menu-projects-section"/>
     <section id="ide-primary-workspace-menu-placeholder1"/>
-    <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>
@@ -17,6 +15,7 @@
         <attribute name="action">workbench.close</attribute>
       </item>
     </section>
+    <section id="ide-primary-workspace-menu-placeholder2"/>
     <section id="ide-primary-workspace-menu-placeholder3"/>
     <section id="ide-primary-workspace-menu-app-section">
       <item>
@@ -28,7 +27,7 @@
       <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="action">win.show-help-overlay</attribute>
         <attribute name="accel">&lt;primary&gt;question</attribute>
       </item>
       <item>
@@ -51,37 +50,11 @@
       </item>
     </section>
   </menu>
-  <menu id="ide-primary-workspace-new-menu">
-    <section id="new-document-section">
-    </section>
-    <section id="open-document-section">
-    </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">builder-run-start-symbolic</attribute>
-        <attribute name="accel">&lt;Control&gt;F5</attribute>
-      </item>
-    </section>
-  </menu>
-  <menu id="project-tree-menu">
-    <section id="project-tree-menu-placeholder0"/>
-    <section id="project-tree-menu-new-section"/>
-    <section id="project-tree-menu-placeholder1"/>
-    <section id="project-tree-menu-open-section"/>
-    <section id="project-tree-menu-buildui"/>
-    <section id="project-tree-menu-vcs"/>
-    <section id="project-tree-menu-placeholder2"/>
-    <section id="project-tree-menu-destructive-section"/>
-    <section id="project-tree-menu-placeholder3"/>
-    <section id="project-tree-menu-display-options-parent-section"/>
-    <section id="project-tree-menu-placeholder4"/>
+  <menu id="new-document-menu">
+    <section id="new-document-section"/>
+    <section id="open-document-section"/>
+    <section id="new-browser-section"/>
+    <section id="new-terminal-section"/>
   </menu>
 </interface>
 
diff --git a/src/libide/gui/ide-frame.c b/src/libide/gui/ide-frame.c
index 793269914..9ac1f93e4 100644
--- a/src/libide/gui/ide-frame.c
+++ b/src/libide/gui/ide-frame.c
@@ -1,6 +1,6 @@
 /* ide-frame.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2021 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
@@ -22,20 +22,15 @@
 
 #include "config.h"
 
-#include <dazzle.h>
+#include <adwaita.h>
 #include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
 #include <libide-core.h>
 #include <libide-threading.h>
-#include <libpeas/peas.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-frame
@@ -48,140 +43,25 @@
  *
  * 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, #IdeFrame implements
- * the #GListModel interface for each of the #IdePage.
- *
- * Since: 3.32
  */
 
-typedef struct
-{
-  DzlBindingGroup      *bindings;
-  DzlSignalGroup       *signals;
-  GPtrArray            *pages;
-  GPtrArray            *in_transition;
-  PeasExtensionSet     *addins;
-
-  /*
-   * Our gestures are used to do interactive moves when the user
-   * does a three finger swipe. We create the dummy gesture to
-   * ensure things work, because it for some reason does not without
-   * the dummy gesture set.
-   *
-   * https://bugzilla.gnome.org/show_bug.cgi?id=788914
-   */
-  GtkGesture           *dummy;
-  GtkGesture           *pan;
-  DzlBoxTheatric       *pan_theatric;
-  IdePage              *pan_page;
-
-  /* Template references */
-  GtkBox               *empty_placeholder;
-  DzlEmptyState        *failed_state;
-  IdeFrameHeader       *header;
-  GtkStack             *stack;
-  GtkStack             *top_stack;
-  GtkEventBox          *event_box;
-} IdeFramePrivate;
-
-typedef struct
+struct _IdeFrame
 {
-  IdeFrame *source;
-  IdeFrame *dest;
-  IdePage  *page;
-  DzlBoxTheatric *theatric;
-} AnimationState;
+  PanelFrame        parent_instance;
+  PeasExtensionSet *addins;
+  guint             use_tabbar : 1;
+};
+
+G_DEFINE_TYPE (IdeFrame, ide_frame, PANEL_TYPE_FRAME)
 
 enum {
   PROP_0,
-  PROP_HAS_VIEW,
-  PROP_VISIBLE_CHILD,
+  PROP_USE_TABBAR,
   N_PROPS
 };
 
-enum {
-  CHANGE_CURRENT_PAGE,
-  N_SIGNALS
-};
-
-static void buildable_iface_init     (GtkBuildableIface   *iface);
-static void list_model_iface_init    (GListModelInterface *iface);
-static void animation_state_complete (gpointer             data);
-
-G_DEFINE_TYPE_WITH_CODE (IdeFrame, ide_frame, GTK_TYPE_BOX,
-                         G_ADD_PRIVATE (IdeFrame)
-                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
-                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
-
-static GParamSpec *properties [N_PROPS];
-static guint signals [N_SIGNALS];
-
-static inline gboolean
-is_uninitialized (GtkAllocation *alloc)
-{
-  return (alloc->x == -1 && alloc->y == -1 &&
-          alloc->width == 1 && alloc->height == 1);
-}
-
-static void
-ide_frame_set_cursor (IdeFrame    *self,
-                      const gchar *name)
-{
-  GdkWindow *window;
-  GdkDisplay *display;
-  GdkCursor *cursor;
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (name != NULL);
-
-  window = gtk_widget_get_window (GTK_WIDGET (self));
-  display = gtk_widget_get_display (GTK_WIDGET (self));
-  cursor = gdk_cursor_new_from_name (display, name);
-
-  gdk_window_set_cursor (window, cursor);
-
-  g_clear_object (&cursor);
-}
-
-static void
-ide_frame_page_failed (IdeFrame   *self,
-                       GParamSpec *pspec,
-                       IdePage    *page)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (IDE_IS_PAGE (page));
-
-  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_frame_bindings_notify_source (IdeFrame        *self,
-                                  GParamSpec      *pspec,
-                                  DzlBindingGroup *bindings)
-{
-  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_FRAME (self));
-
-  source = dzl_binding_group_get_source (bindings);
-
-  if (source == 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 GSettings *editor_settings;
+static GParamSpec *properties[N_PROPS];
 
 static void
 ide_frame_notify_addin_of_page (PeasExtensionSet *set,
@@ -202,215 +82,18 @@ ide_frame_notify_addin_of_page (PeasExtensionSet *set,
 
 static void
 ide_frame_notify_visible_child (IdeFrame   *self,
-                                GParamSpec *pspec,
-                                GtkStack   *stack)
+                                GParamSpec *pspec)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  GtkWidget *visible_child;
+  PanelWidget *visible_child;
 
   g_assert (IDE_IS_FRAME (self));
-  g_assert (GTK_IS_STACK (stack));
-
-  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
-    return;
-
-  if ((visible_child = gtk_stack_get_visible_child (priv->stack)))
-    {
-      if (gtk_widget_in_destruction (visible_child))
-        visible_child = NULL;
-    }
-
-  /*
-   * Mux/Proxy actions to our level so that they also be activated
-   * from the header bar without any weirdness by the View.
-   */
-  dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child,
-                                    "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 page */
-  if (visible_child != NULL &&
-      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));
-  else
-    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_placeholder));
-
-  /* Allow the header to update settings */
-  _ide_frame_header_update (priv->header, IDE_PAGE (visible_child));
 
-  /* Ensure action state is up to date */
-  _ide_frame_update_actions (self);
+  visible_child = panel_frame_get_visible_child (PANEL_FRAME (self));
 
-  if (priv->addins != NULL)
-    peas_extension_set_foreach (priv->addins,
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->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]);
-}
-
-static void
-collect_widgets (GtkWidget *widget,
-                 gpointer   user_data)
-{
-  g_ptr_array_add (user_data, widget);
-}
-
-static void
-ide_frame_change_current_page (IdeFrame *self,
-                               gint      direction)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  g_autoptr(GPtrArray) ar = NULL;
-  GtkWidget *visible_child;
-  gint position = 0;
-
-  g_assert (IDE_IS_FRAME (self));
-
-  if (direction < -1)
-    direction = -1;
-  else if (direction > 1)
-    direction = 1;
-
-  visible_child = gtk_stack_get_visible_child (priv->stack);
-
-  if (visible_child == NULL)
-    return;
-
-  gtk_container_child_get (GTK_CONTAINER (priv->stack), visible_child,
-                           "position", &position,
-                           NULL);
-
-  ar = g_ptr_array_new ();
-  gtk_container_foreach (GTK_CONTAINER (priv->stack), collect_widgets, ar);
-  if (ar->len == 0)
-    g_return_if_reached ();
-
-  position = (position + (int)ar->len - direction) % (int)ar->len;
-
-  visible_child = g_ptr_array_index (ar, position);
-  gtk_stack_set_visible_child (priv->stack, visible_child);
-}
-
-static void
-ide_frame_add (GtkContainer *container,
-               GtkWidget    *widget)
-{
-  IdeFrame *self = (IdeFrame *)container;
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_FRAME (self));
-  g_return_if_fail (GTK_IS_WIDGET (widget));
-
-  if (IDE_IS_PAGE (widget))
-    gtk_container_add (GTK_CONTAINER (priv->stack), widget);
-  else
-    GTK_CONTAINER_CLASS (ide_frame_parent_class)->add (container, widget);
-
-  gtk_widget_queue_resize (GTK_WIDGET (self));
-}
-
-static void
-ide_frame_page_added (IdeFrame *self,
-                      IdePage  *page)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  guint position;
-
-  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_frame_header_popdown (priv->header);
-
-  /* Notify GListModel consumers of the new page and it's position within
-   * our stack of page widgets.
-   */
-  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 page is displayed and focus the widget so the
-   * user can immediately start typing.
-   */
-  ide_frame_set_visible_child (self, page);
-  gtk_widget_grab_focus (GTK_WIDGET (page));
-}
-
-static void
-ide_frame_page_removed (IdeFrame *self,
-                        IdePage  *page)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (IDE_IS_PAGE (page));
-
-  if (priv->pages != NULL)
-    {
-      guint position = 0;
-
-      /* 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->pages->len == 1)
-        _ide_frame_header_hide (priv->header);
-
-      /*
-       * 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, page, NULL, &position))
-        {
-          for (guint i = 0; i < priv->pages->len; i++)
-            {
-              if ((gpointer)page == g_ptr_array_index (priv->pages, i))
-                {
-                  g_ptr_array_remove_index (priv->pages, i);
-                  g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
-                }
-            }
-        }
-    }
-}
-
-static void
-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_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_frame_real_agree_to_close_async);
-  ide_task_set_priority (task, G_PRIORITY_LOW);
-  ide_task_return_boolean (task, TRUE);
-}
-
-static gboolean
-ide_frame_real_agree_to_close_finish (IdeFrame      *self,
-                                      GAsyncResult  *result,
-                                      GError       **error)
-{
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (IDE_IS_TASK (result));
-
-  return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
 
 static void
@@ -430,7 +113,7 @@ ide_frame_addin_added (PeasExtensionSet *set,
 
   ide_frame_addin_load (addin, self);
 
-  visible_child = ide_frame_get_visible_child (self);
+  visible_child = IDE_PAGE (panel_frame_get_visible_child (PANEL_FRAME (self)));
 
   if (visible_child != NULL)
     ide_frame_addin_set_page (addin, visible_child);
@@ -454,351 +137,79 @@ ide_frame_addin_removed (PeasExtensionSet *set,
   ide_frame_addin_unload (addin, self);
 }
 
-static gboolean
-ide_frame_pan_begin (IdeFrame         *self,
-                     GdkEventSequence *sequence,
-                     GtkGesturePan    *gesture)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  GtkAllocation alloc;
-  cairo_surface_t *surface = NULL;
-  GdkDevice *device;
-  IdePage *page;
-  GdkWindow *window;
-  GtkWidget *grid;
-  cairo_t *cr;
-  gdouble x, y;
-  gboolean enable_animations;
-  GdkModifierType state = 0;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (GTK_IS_GESTURE_PAN (gesture));
-  g_assert (priv->pan_theatric == NULL);
-
-  if (!(page = ide_frame_get_visible_child (self)) ||
-      !(device = gtk_gesture_get_device (GTK_GESTURE (gesture))) ||
-      !(window = gtk_widget_get_window (GTK_WIDGET (page))))
-    goto failure;
-
-  gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
-  gdk_device_get_state (device, window, NULL, &state);
-
-  g_object_get (gtk_settings_get_default (),
-                "gtk-enable-animations", &enable_animations,
-                NULL);
-
-  if (sequence != NULL ||
-      !enable_animations ||
-      (state & GDK_SHIFT_MASK) == 0 ||
-      is_uninitialized (&alloc) ||
-      NULL == (surface = gdk_window_create_similar_surface (window,
-                                                            CAIRO_CONTENT_COLOR,
-                                                            alloc.width,
-                                                            alloc.height)))
-    goto failure;
-
-  gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
-
-  cr = cairo_create (surface);
-  gtk_widget_draw (GTK_WIDGET (page), cr);
-  cairo_destroy (cr);
-
-  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_page = g_object_ref (page);
-  priv->pan_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
-                                     "surface", surface,
-                                     "target", grid,
-                                     "x", alloc.x + (gint)x,
-                                     "y", alloc.y,
-                                     "width", alloc.width,
-                                     "height", alloc.height,
-                                     NULL);
-
-  g_clear_pointer (&surface, cairo_surface_destroy);
-
-  /* Hide the page while we begin the possible transition to another
-   * layout stack.
-   */
-  gtk_widget_hide (GTK_WIDGET (priv->pan_page));
-
-  /*
-   * 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_frame_set_cursor (self, "none");
-
-  IDE_RETURN (TRUE);
-
-failure:
-  if (sequence != NULL)
-    gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
-
-  IDE_RETURN (FALSE);
-}
-
 static void
-ide_frame_pan_update (IdeFrame         *self,
-                      GdkEventSequence *sequence,
-                      GtkGestureSwipe  *gesture)
+ide_frame_reload_addins (IdeFrame *self)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  GtkAllocation alloc;
-  GtkWidget *grid;
-  gdouble x, y;
-
   IDE_ENTRY;
 
   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));
-
-  if (priv->pan_theatric == NULL)
-    {
-      if (sequence != NULL)
-        gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
-      IDE_EXIT;
-    }
-
-  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_GRID);
-  gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
-                                    &alloc.x, &alloc.y);
-
-  g_object_set (priv->pan_theatric,
-                "x", alloc.x + (gint)x,
-                NULL);
-
-  IDE_EXIT;
-}
-
-static void
-ide_frame_pan_end (IdeFrame         *self,
-                   GdkEventSequence *sequence,
-                   GtkGesturePan    *gesture)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-  IdeFramePrivate *dest_priv;
-  IdeFrame *dest;
-  GtkAllocation alloc;
-  GtkWidget *grid;
-  GtkWidget *column;
-  gdouble x, y;
-  gint direction;
-  gint index = 0;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (GTK_IS_GESTURE_PAN (gesture));
-
-  if (priv->pan_theatric == NULL || priv->pan_page == NULL)
-    IDE_GOTO (cleanup);
-
-  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
-
-  gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
-
-  if (x > DISTANCE_THRESHOLD (&alloc))
-    direction = 1;
-  else if (x < -DISTANCE_THRESHOLD (&alloc))
-    direction = -1;
-  else
-    direction = 0;
-
-  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
-  g_assert (grid != NULL);
-  g_assert (IDE_IS_GRID (grid));
-
-  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
-  g_assert (column != NULL);
-  g_assert (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);
-  dest_priv = ide_frame_get_instance_private (dest);
-  g_assert (dest != NULL);
-  g_assert (IDE_IS_FRAME (dest));
-
-  gtk_widget_get_allocation (GTK_WIDGET (dest), &alloc);
-
-  if (!is_uninitialized (&alloc))
-    {
-      AnimationState *state;
-
-      state = g_slice_new0 (AnimationState);
-      state->source = g_object_ref (self);
-      state->dest = g_object_ref (dest);
-      state->page = g_object_ref (priv->pan_page);
-      state->theatric = g_object_ref (priv->pan_theatric);
-
-      gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->top_stack), grid, 0, 0,
-                                        &alloc.x, &alloc.y);
-
-      /*
-       * Use EASE_OUT_CUBIC, because user initiated the beginning of the
-       * acceleration curve just by swiping. No need to duplicate.
-       */
-      dzl_object_animate_full (state->theatric,
-                               DZL_ANIMATION_EASE_OUT_CUBIC,
-                               TRANSITION_DURATION,
-                               gtk_widget_get_frame_clock (GTK_WIDGET (self)),
-                               animation_state_complete,
-                               state,
-                               "x", alloc.x,
-                               "width", alloc.width,
-                               NULL);
-
-      if (dest != self)
-        {
-          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",
-                     dest != self ? "another" : "same");
-    }
-  else
-    {
-      g_autoptr(IdePage) page = g_object_ref (priv->pan_page);
-
-      IDE_TRACE_MSG ("Moving page to a previously non-existant column");
-
-      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_page);
-
-  gtk_widget_queue_draw (gtk_widget_get_toplevel (GTK_WIDGET (self)));
-
-  ide_frame_set_cursor (self, "arrow");
-
-  IDE_EXIT;
-}
-
-static void
-ide_frame_constructed (GObject *object)
-{
-  IdeFrame *self = (IdeFrame *)object;
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_assert (IDE_IS_FRAME (self));
-
-  G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object);
-
-  priv->addins = peas_extension_set_new (peas_engine_get_default (),
+  g_clear_object (&self->addins);
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
                                          IDE_TYPE_FRAME_ADDIN,
                                          NULL);
-
-  g_signal_connect (priv->addins,
+  g_signal_connect (self->addins,
                     "extension-added",
                     G_CALLBACK (ide_frame_addin_added),
                     self);
-
-  g_signal_connect (priv->addins,
+  g_signal_connect (self->addins,
                     "extension-removed",
                     G_CALLBACK (ide_frame_addin_removed),
                     self);
+  peas_extension_set_foreach (self->addins, ide_frame_addin_added, self);
 
-  peas_extension_set_foreach (priv->addins,
-                              ide_frame_addin_added,
-                              self);
-
-  gtk_widget_add_events (GTK_WIDGET (priv->event_box), GDK_TOUCH_MASK);
-  priv->pan = g_object_new (GTK_TYPE_GESTURE_PAN,
-                            "widget", priv->event_box,
-                            "orientation", GTK_ORIENTATION_HORIZONTAL,
-                            "n-points", 3,
-                            NULL);
-  g_signal_connect_swapped (priv->pan,
-                            "begin",
-                            G_CALLBACK (ide_frame_pan_begin),
-                            self);
-  g_signal_connect_swapped (priv->pan,
-                            "update",
-                            G_CALLBACK (ide_frame_pan_update),
-                            self);
-  g_signal_connect_swapped (priv->pan,
-                            "end",
-                            G_CALLBACK (ide_frame_pan_end),
-                            self);
-  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->pan),
-                                              GTK_PHASE_BUBBLE);
-
-  /*
-   * FIXME: Our priv->pan gesture does not activate unless we add another
-   *        dummy gesture. I currently have no idea why that is.
-   *
-   *        https://bugzilla.gnome.org/show_bug.cgi?id=788914
-   */
-  priv->dummy = gtk_gesture_rotate_new (GTK_WIDGET (priv->event_box));
-  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->dummy),
-                                              GTK_PHASE_BUBBLE);
+  IDE_EXIT;
 }
 
 static void
-ide_frame_grab_focus (GtkWidget *widget)
+status_page_pressed_cb (IdeFrame        *self,
+                        double           x,
+                        double           y,
+                        int              n_press,
+                        GtkGestureClick *click)
 {
-  IdeFrame *self = (IdeFrame *)widget;
-  IdePage *child;
+  GtkRoot *root;
 
   g_assert (IDE_IS_FRAME (self));
+  g_assert (GTK_IS_GESTURE_CLICK (click));
 
-  child = ide_frame_get_visible_child (self);
-
-  if (child != NULL)
-    gtk_widget_grab_focus (GTK_WIDGET (child));
-  else
-    GTK_WIDGET_CLASS (ide_frame_parent_class)->grab_focus (widget);
+  root = gtk_widget_get_root (GTK_WIDGET (self));
+  gtk_root_set_focus (root, NULL);
 }
 
 static void
-ide_frame_destroy (GtkWidget *widget)
+ide_frame_constructed (GObject *object)
 {
-  IdeFrame *self = (IdeFrame *)widget;
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdeFrame *self = (IdeFrame *)object;
+  PanelFrameHeader *header;
 
   g_assert (IDE_IS_FRAME (self));
 
-  g_clear_object (&priv->addins);
+  G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object);
 
-  g_clear_pointer (&priv->in_transition, g_ptr_array_unref);
+  self->use_tabbar = g_settings_get_boolean (editor_settings, "use-tabbar");
+  if (self->use_tabbar)
+    header = PANEL_FRAME_HEADER (panel_frame_tab_bar_new ());
+  else
+    header = PANEL_FRAME_HEADER (panel_frame_header_bar_new ());
+  panel_frame_set_header (PANEL_FRAME (self), header);
+  g_settings_bind (editor_settings, "use-tabbar",
+                   self, "use-tabbar",
+                   G_SETTINGS_BIND_GET);
 
-  if (priv->pages != NULL)
-    {
-      g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->pages->len, 0);
-      g_clear_pointer (&priv->pages, g_ptr_array_unref);
-    }
+  ide_frame_reload_addins (self);
+}
 
-  if (priv->bindings != NULL)
-    {
-      dzl_binding_group_set_source (priv->bindings, NULL);
-      g_clear_object (&priv->bindings);
-    }
+static void
+ide_frame_dispose (GObject *object)
+{
+  IdeFrame *self = (IdeFrame *)object;
 
-  if (priv->signals != NULL)
-    {
-      dzl_signal_group_set_target (priv->signals, NULL);
-      g_clear_object (&priv->signals);
-    }
+  g_assert (IDE_IS_FRAME (self));
 
-  g_clear_object (&priv->pan);
+  g_clear_object (&self->addins);
 
-  GTK_WIDGET_CLASS (ide_frame_parent_class)->destroy (widget);
+  G_OBJECT_CLASS (ide_frame_parent_class)->dispose (object);
 }
 
 static void
@@ -811,12 +222,8 @@ ide_frame_get_property (GObject    *object,
 
   switch (prop_id)
     {
-    case PROP_HAS_VIEW:
-      g_value_set_boolean (value, ide_frame_get_has_page (self));
-      break;
-
-    case PROP_VISIBLE_CHILD:
-      g_value_set_object (value, ide_frame_get_visible_child (self));
+    case PROP_USE_TABBAR:
+      g_value_set_boolean (value, ide_frame_get_use_tabbar (self));
       break;
 
     default:
@@ -834,8 +241,8 @@ ide_frame_set_property (GObject      *object,
 
   switch (prop_id)
     {
-    case PROP_VISIBLE_CHILD:
-      ide_frame_set_visible_child (self, g_value_get_object (value));
+    case PROP_USE_TABBAR:
+      ide_frame_set_use_tabbar (self, g_value_get_boolean (value));
       break;
 
     default:
@@ -848,121 +255,37 @@ 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_frame_constructed;
+  object_class->dispose = ide_frame_dispose;
   object_class->get_property = ide_frame_get_property;
   object_class->set_property = ide_frame_set_property;
 
-  widget_class->destroy = ide_frame_destroy;
-  widget_class->grab_focus = ide_frame_grab_focus;
-
-  container_class->add = ide_frame_add;
-
-  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-page", NULL, NULL,
+  properties [PROP_USE_TABBAR] =
+    g_param_spec_boolean ("use-tabbar",
+                          "Use Tabbar",
+                          "If tabs should be used",
                           FALSE,
-                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_VISIBLE_CHILD] =
-    g_param_spec_object ("visible-child",
-                         "Visible Child",
-                         "The current page to be displayed",
-                         IDE_TYPE_PAGE,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  signals [CHANGE_CURRENT_PAGE] =
-    g_signal_new_class_handler ("change-current-page",
-                                G_TYPE_FROM_CLASS (klass),
-                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
-                                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, "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_placeholder);
-  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);
+  gtk_widget_class_bind_template_callback (widget_class, status_page_pressed_cb);
 }
 
 static void
 ide_frame_init (IdeFrame *self)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  if (editor_settings == NULL)
+    editor_settings = g_settings_new ("org.gnome.builder.editor");
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  _ide_frame_init_actions (self);
-  _ide_frame_init_shortcuts (self);
-
-  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_PAGE);
-
-  dzl_signal_group_connect_swapped (priv->signals,
-                                    "notify::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_frame_bindings_notify_source),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  dzl_binding_group_bind (priv->bindings, "title",
-                          priv->header, "title",
-                          G_BINDING_SYNC_CREATE);
-
-  dzl_binding_group_bind (priv->bindings, "modified",
-                          priv->header, "modified",
-                          G_BINDING_SYNC_CREATE);
-
-  dzl_binding_group_bind (priv->bindings, "primary-color-bg",
-                          priv->header, "background-rgba",
-                          G_BINDING_SYNC_CREATE);
-
-  dzl_binding_group_bind (priv->bindings, "primary-color-fg",
-                          priv->header, "foreground-rgba",
-                          G_BINDING_SYNC_CREATE);
-
-  g_signal_connect_object (priv->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_frame_page_added),
-                           self,
-                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
-
-  g_signal_connect_object (priv->stack,
-                           "remove",
-                           G_CALLBACK (ide_frame_page_removed),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  _ide_frame_header_set_pages (priv->header, G_LIST_MODEL (self));
-  _ide_frame_header_update (priv->header, NULL);
+  g_signal_connect (self,
+                    "notify::visible-child",
+                    G_CALLBACK (ide_frame_notify_visible_child),
+                    NULL);
 }
 
 GtkWidget *
@@ -972,496 +295,122 @@ ide_frame_new (void)
 }
 
 /**
- * ide_frame_set_visible_child:
- * @self: a #IdeFrame
+ * ide_frame_addin_find_by_module_name:
+ * @frame: An #IdeFrame
+ * @module_name: the module name which provides the addin
  *
- * Sets the current page for the stack.
+ * 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
+ * #IdeFrameAddinInterface, then %NULL is returned.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL
  */
-void
-ide_frame_set_visible_child (IdeFrame *self,
-                             IdePage  *page)
+IdeFrameAddin *
+ide_frame_addin_find_by_module_name (IdeFrame    *frame,
+                                     const gchar *module_name)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  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);
+  PeasExtension *ret = NULL;
+  PeasPluginInfo *plugin_info;
 
-  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (page));
-}
+  g_return_val_if_fail (IDE_IS_FRAME (frame), NULL);
+  g_return_val_if_fail (frame->addins != NULL, NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
 
-/**
- * ide_frame_get_visible_child:
- * @self: a #IdeFrame
- *
- * Gets the visible #IdePage if there is one; otherwise %NULL.
- *
- * Returns: (nullable) (transfer none): An #IdePage or %NULL
- *
- * Since: 3.32
- */
-IdePage *
-ide_frame_get_visible_child (IdeFrame *self)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
 
-  g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
+  if (plugin_info != NULL)
+    ret = peas_extension_set_get_extension (frame->addins, plugin_info);
+  else
+    g_warning ("No addin could be found matching module \"%s\"", module_name);
 
-  return IDE_PAGE (gtk_stack_get_visible_child (priv->stack));
+  return ret ? IDE_FRAME_ADDIN (ret) : NULL;
 }
 
 /**
- * ide_frame_get_titlebar:
+ * ide_frame_get_position:
  * @self: a #IdeFrame
  *
- * Gets the #IdeFrameHeader header that is at the top of the stack.
- *
- * Returns: (transfer none) (type IdeFrameHeader): The layout stack header.
+ * Gets the position in the grid of a frame.
  *
- * Since: 3.32
+ * Returns: (transfer full): a new #IdePanelPosition
  */
-GtkWidget *
-ide_frame_get_titlebar (IdeFrame *self)
+IdePanelPosition *
+ide_frame_get_position (IdeFrame *self)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdePanelPosition *ret;
+  PanelGrid *grid;
+  guint n_columns;
 
   g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
 
-  return GTK_WIDGET (priv->header);
-}
-
-/**
- * ide_frame_get_has_page:
- * @self: an #IdeFrame
- *
- * Gets the "has-page" property.
- *
- * This property is a convenience to allow widgets to easily bind
- * properties based on whether or not a page is visible in the stack.
- *
- * Returns: %TRUE if the stack has a page
- *
- * Since: 3.32
- */
-gboolean
-ide_frame_get_has_page (IdeFrame *self)
-{
-  IdePage *visible_child;
-
-  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
-
-  visible_child = ide_frame_get_visible_child (self);
-
-  return visible_child != NULL;
-}
-
-static void
-ide_frame_close_page_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      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_PAGE (page));
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_FRAME (self));
-
-  if (!ide_page_agree_to_close_finish (page, result, &error))
-    {
-      g_message ("%s does not agree to close: %s",
-                 G_OBJECT_TYPE_NAME (page),
-                 error ? error->message : "No reason");
-      return;
-    }
-
-  /* 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 (page));
-  if (GTK_IS_WINDOW (toplevel) &&
-      NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) &&
-      (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 (page));
-
-  /* We don't want to leave the widget focus in an indeterminate
-   * state so we immediately focus the next child in the stack.
-   * But only do so if we had focus previously.
-   */
-  if (had_focus)
-    {
-      IdePage *visible_child = ide_frame_get_visible_child (self);
-
-      if (visible_child != NULL)
-        gtk_widget_grab_focus (GTK_WIDGET (visible_child));
-    }
-}
-
-void
-_ide_frame_request_close (IdeFrame *self,
-                          IdePage  *page)
-{
-  g_return_if_fail (IDE_IS_FRAME (self));
-  g_return_if_fail (IDE_IS_PAGE (page));
-
-  ide_page_agree_to_close_async (page,
-                                        NULL,
-                                        ide_frame_close_page_cb,
-                                        g_object_ref (self));
-}
-
-static GType
-ide_frame_get_item_type (GListModel *model)
-{
-  return IDE_TYPE_PAGE;
-}
-
-static guint
-ide_frame_get_n_items (GListModel *model)
-{
-  IdeFrame *self = (IdeFrame *)model;
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_assert (IDE_IS_FRAME (self));
-
-  return priv->pages ? priv->pages->len : 0;
-}
-
-static gpointer
-ide_frame_get_item (GListModel *model,
-                    guint       position)
-{
-  IdeFrame *self = (IdeFrame *)model;
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
-
-  g_assert (IDE_IS_FRAME (self));
-  g_assert (position < priv->pages->len);
+  /* Frames are always in the center grid */
+  ret = ide_panel_position_new ();
+  ide_panel_position_set_edge (ret, PANEL_DOCK_POSITION_CENTER);
 
-  return g_object_ref (g_ptr_array_index (priv->pages, position));
-}
+  /* Implausible but handle it anyway */
+  grid = PANEL_GRID (gtk_widget_get_ancestor (GTK_WIDGET (self), PANEL_TYPE_GRID));
+  if (grid == NULL)
+    return ret;
 
-static void
-list_model_iface_init (GListModelInterface *iface)
-{
-  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;
-}
+  n_columns = panel_grid_get_n_columns (grid);
 
-void
-ide_frame_agree_to_close_async (IdeFrame            *self,
-                                GCancellable        *cancellable,
-                                GAsyncReadyCallback  callback,
-                                gpointer             user_data)
-{
-  g_return_if_fail (IDE_IS_FRAME (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  IDE_FRAME_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
-}
-
-gboolean
-ide_frame_agree_to_close_finish (IdeFrame      *self,
-                                 GAsyncResult  *result,
-                                 GError       **error)
-{
-  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
-  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
-
-  return IDE_FRAME_GET_CLASS (self)->agree_to_close_finish (self, result, error);
-}
-
-static void
-animation_state_complete (gpointer data)
-{
-  IdeFramePrivate *priv;
-  AnimationState *state = data;
-
-  g_assert (state != NULL);
-  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)
+  for (guint c = 0; c < n_columns; c++)
     {
-      gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->page));
+      PanelGridColumn *grid_column = panel_grid_get_column (grid, c);
+      guint n_rows = panel_grid_column_get_n_rows (grid_column);
 
-      /* Now remove it from our temporary transition. Be careful in case we were
-       * destroyed in the mean time.
-       */
-      priv = ide_frame_get_instance_private (state->source);
-
-      if (priv->in_transition != NULL)
+      for (guint r = 0; r < n_rows; r++)
         {
-          guint position = 0;
+          PanelFrame *frame = panel_grid_column_get_row (grid_column, r);
 
-          if (g_ptr_array_find_with_equal_func (priv->pages, state->page, NULL, &position))
+          if (frame == PANEL_FRAME (self))
             {
-              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);
+              ide_panel_position_set_column (ret, c);
+              ide_panel_position_set_row (ret, r);
+              return ret;
             }
         }
     }
 
-  /*
-   * We might need to reshow the widget in cases where we are in a
-   * 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->page));
-  ide_frame_set_visible_child (state->dest, state->page);
-
-  g_clear_object (&state->source);
-  g_clear_object (&state->dest);
-  g_clear_object (&state->page);
-  g_clear_object (&state->theatric);
-  g_slice_free (AnimationState, state);
-}
+  g_critical ("Failed to locate frame within grid");
 
-void
-_ide_frame_transfer (IdeFrame *self,
-                     IdeFrame *dest,
-                     IdePage  *page)
-{
-  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_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
-   * begin a transition to the new colors. We also want to do this upfront so
-   * that we can reduce the amount of style invalidation caused during the
-   * transitions.
-   */
-
-  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 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 (page)))
-    {
-      GtkAllocation alloc, dest_alloc;
-      cairo_surface_t *surface = NULL;
-      GdkWindow *window;
-      GtkWidget *grid;
-      gboolean enable_animations;
-
-      grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
-
-      gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
-      gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc);
-
-      g_object_get (gtk_settings_get_default (),
-                    "gtk-enable-animations", &enable_animations,
-                    NULL);
-
-      if (enable_animations &&
-          grid != NULL &&
-          !is_uninitialized (&alloc) &&
-          !is_uninitialized (&dest_alloc) &&
-          dest_alloc.width > 0 && dest_alloc.height > 0 &&
-          NULL != (window = gtk_widget_get_window (GTK_WIDGET (page))) &&
-          NULL != (surface = gdk_window_create_similar_surface (window,
-                                                                CAIRO_CONTENT_COLOR,
-                                                                alloc.width,
-                                                                alloc.height)))
-        {
-          DzlBoxTheatric *theatric = NULL;
-          AnimationState *state;
-          cairo_t *cr;
-
-          cr = cairo_create (surface);
-          gtk_widget_draw (GTK_WIDGET (page), cr);
-          cairo_destroy (cr);
-
-          gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0,
-                                            &alloc.x, &alloc.y);
-          gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->stack), grid, 0, 0,
-                                            &dest_alloc.x, &dest_alloc.y);
-
-          theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
-                                   "surface", surface,
-                                   "height", alloc.height,
-                                   "target", grid,
-                                   "width", alloc.width,
-                                   "x", alloc.x,
-                                   "y", alloc.y,
-                                   NULL);
-
-          state = g_slice_new0 (AnimationState);
-          state->source = g_object_ref (self);
-          state->dest = g_object_ref (dest);
-          state->page = g_object_ref (page);
-          state->theatric = theatric;
-
-          dzl_object_animate_full (theatric,
-                                   DZL_ANIMATION_EASE_IN_OUT_CUBIC,
-                                   TRANSITION_DURATION,
-                                   gtk_widget_get_frame_clock (GTK_WIDGET (self)),
-                                   animation_state_complete,
-                                   state,
-                                   "x", dest_alloc.x,
-                                   "width", dest_alloc.width,
-                                   "y", dest_alloc.y,
-                                   "height", dest_alloc.height,
-                                   NULL);
-
-          /*
-           * 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 (page));
-          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
-
-          cairo_surface_destroy (surface);
-
-          return;
-        }
-    }
-
-  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);
+  return ret;
 }
 
-/**
- * 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 page found in @self.
- *
- * Since: 3.32
- */
-void
-ide_frame_foreach_page (IdeFrame    *self,
-                        GtkCallback  callback,
-                        gpointer     user_data)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (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_frame_addin_find_by_module_name:
- * @frame: An #IdeFrame
- * @module_name: the module name which provides the addin
- *
- * 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
- * #IdeFrameAddinInterface, then %NULL is returned.
- *
- * Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL
- *
- * Since: 3.32
- */
-IdeFrameAddin *
-ide_frame_addin_find_by_module_name (IdeFrame    *frame,
-                                     const gchar *module_name)
+gboolean
+ide_frame_get_use_tabbar (IdeFrame *self)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (frame);
-  PeasExtension *ret = NULL;
-  PeasPluginInfo *plugin_info;
-
-  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);
-
-  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
-
-  if (plugin_info != NULL)
-    ret = peas_extension_set_get_extension (priv->addins, plugin_info);
-  else
-    g_warning ("No addin could be found matching module \"%s\"", module_name);
+  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
 
-  return ret ? IDE_FRAME_ADDIN (ret) : NULL;
+  return self->use_tabbar;
 }
 
 void
-ide_frame_add_with_depth (IdeFrame  *self,
-                          GtkWidget *widget,
-                          guint      position)
+ide_frame_set_use_tabbar (IdeFrame *self,
+                          gboolean  use_tabbar)
 {
-  IdeFramePrivate *priv = ide_frame_get_instance_private (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,
-                                     "position", position,
-                                     NULL);
-}
 
-static void
-ide_frame_add_child (GtkBuildable *buildable,
-                     GtkBuilder   *builder,
-                     GObject      *object,
-                     const gchar  *type)
-{
-  IdeFrame *self = (IdeFrame *)buildable;
-  GtkBuildableIface *parent = g_type_interface_peek_parent (GTK_BUILDABLE_GET_IFACE (buildable));
+  use_tabbar = !!use_tabbar;
 
-  if (g_strcmp0 (type, "placeholder") == 0 && GTK_IS_WIDGET (object))
-    ide_frame_set_placeholder (self, GTK_WIDGET (object));
-  else
-    parent->add_child (buildable, builder, object, type);
-}
+  if (use_tabbar != self->use_tabbar)
+    {
+      PanelFrameHeader *header;
 
-static void
-buildable_iface_init (GtkBuildableIface *iface)
-{
-  iface->add_child = ide_frame_add_child;
-}
+      self->use_tabbar = use_tabbar;
 
-void
-ide_frame_set_placeholder (IdeFrame  *self,
-                           GtkWidget *placeholder)
-{
-  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+      if (self->use_tabbar)
+        header = PANEL_FRAME_HEADER (panel_frame_tab_bar_new ());
+      else
+        header = PANEL_FRAME_HEADER (panel_frame_header_bar_new ());
 
-  g_return_if_fail (IDE_IS_FRAME (self));
-  g_return_if_fail (!placeholder || GTK_IS_WIDGET (placeholder));
+      panel_frame_set_header (PANEL_FRAME (self), header);
 
-  gtk_container_foreach (GTK_CONTAINER (priv->empty_placeholder),
-                         (GtkCallback) gtk_widget_destroy,
-                         NULL);
+      ide_frame_reload_addins (self);
 
-  if (placeholder != NULL)
-    gtk_container_add (GTK_CONTAINER (priv->empty_placeholder), placeholder);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_TABBAR]);
+    }
 }
diff --git a/src/libide/gui/ide-frame.h b/src/libide/gui/ide-frame.h
index 8b4078e5a..de758fcad 100644
--- a/src/libide/gui/ide-frame.h
+++ b/src/libide/gui/ide-frame.h
@@ -24,64 +24,26 @@
 # error "Only <libide-gui.h> can be included directly."
 #endif
 
-#include <gtk/gtk.h>
-#include <libide-core.h>
+#include <libpanel.h>
 
 #include "ide-page.h"
+#include "ide-panel-position.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);
-IDE_AVAILABLE_IN_3_34
-void       ide_frame_set_placeholder       (IdeFrame             *self,
-                                            GtkWidget            *placeholder);
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeFrame, ide_frame, IDE, FRAME, PanelFrame)
+
+IDE_AVAILABLE_IN_ALL
+GtkWidget        *ide_frame_new            (void);
+IDE_AVAILABLE_IN_ALL
+IdePanelPosition *ide_frame_get_position   (IdeFrame *self);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_frame_get_use_tabbar (IdeFrame *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_frame_set_use_tabbar (IdeFrame *self,
+                                            gboolean  use_tabbar);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-frame.ui b/src/libide/gui/ide-frame.ui
index 793826110..8c9e363e2 100644
--- a/src/libide/gui/ide-frame.ui
+++ b/src/libide/gui/ide-frame.ui
@@ -1,55 +1,128 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <template class="IdeFrame" parent="GtkBox">
-    <property name="orientation">vertical</property>
-    <child>
-      <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>
-      </object>
-    </child>
-    <child>
-      <object class="GtkEventBox" id="event_box">
-        <property name="visible">true</property>
-        <child>
-          <object class="GtkStack" id="top_stack">
-            <property name="expand">true</property>
-            <property name="homogeneous">false</property>
-            <property name="interpolate-size">false</property>
-            <property name="visible">true</property>
-            <child>
-              <object class="GtkBox" id="empty_placeholder">
-                <property name="expand">true</property>
-                <property name="visible">true</property>
-                <child>
-                  <object class="IdeFrameEmptyState">
-                    <property name="expand">true</property>
-                    <property name="visible">true</property>
-                  </object>
-                </child>
+  <template class="IdeFrame" parent="PanelFrame">
+    <property name="placeholder">
+      <object class="AdwStatusPage" id="status">
+        <property name="title" translatable="yes">Open a File or Terminal</property>
+        <property name="description" translatable="yes">Use the page switcher above or use one of the 
following:</property>
+        <property name="child">
+          <object class="GtkGrid">
+            <property name="halign">center</property>
+            <property name="column-spacing">48</property>
+            <property name="row-spacing">6</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Search</property>
+                <layout>
+                  <property name="row">0</property>
+                  <property name="column">0</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Ctrl+Enter</property>
+                <layout>
+                  <property name="row">0</property>
+                  <property name="column">1</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Project Sidebar</property>
+                <layout>
+                  <property name="row">1</property>
+                  <property name="column">0</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">F9</property>
+                <layout>
+                  <property name="row">1</property>
+                  <property name="column">1</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Open File</property>
+                <layout>
+                  <property name="row">2</property>
+                  <property name="column">0</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Ctrl+O</property>
+                <layout>
+                  <property name="row">2</property>
+                  <property name="column">1</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">New Terminal</property>
+                <layout>
+                  <property name="row">3</property>
+                  <property name="column">0</property>
+                </layout>
               </object>
             </child>
             <child>
-              <object class="DzlEmptyState" id="failed_state">
-                <property name="icon-name">computer-fail-symbolic</property>
-                <property name="pixel-size">160</property>
-                <property name="title" translatable="yes">Uh oh, something went wrong</property>
-                <property name="subtitle" translatable="yes">There was a failure while trying to perform the 
operation.</property>
-                <property name="visible">true</property>
+              <object class="GtkLabel">
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Ctrl+Shift+T</property>
+                <layout>
+                  <property name="row">3</property>
+                  <property name="column">1</property>
+                </layout>
               </object>
             </child>
             <child>
-              <object class="IdeFrameWrapper" id="stack">
-                <property name="expand">true</property>
-                <property name="homogeneous">false</property>
-                <property name="interpolate-size">false</property>
-                <property name="visible">true</property>
+              <object class="GtkBox">
+                <property name="spacing">6</property>
+                <property name="homogeneous">true</property>
+                <layout>
+                  <property name="row">4</property>
+                  <property name="column">0</property>
+                  <property name="column-span">2</property>
+                </layout>
+                <child>
+                  <object class="GtkButton">
+                    <property name="label" translatable="yes">Open File…</property>
+                    <property name="action-name">workbench.open</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton">
+                    <property name="label" translatable="yes">New Terminal</property>
+                    <property name="action-name">terminal.terminal-on-host</property>
+                    <property name="action-target">''</property>
+                  </object>
+                </child>
               </object>
             </child>
           </object>
+        </property>
+        <child>
+          <object class="GtkGestureClick">
+            <property name="propagation-phase">bubble</property>
+            <signal name="pressed" handler="status_page_pressed_cb" swapped="true" object="IdeFrame"/>
+          </object>
         </child>
       </object>
-    </child>
+    </property>
   </template>
 </interface>
diff --git a/src/libide/gui/ide-grid.c b/src/libide/gui/ide-grid.c
index e31b1c744..990dc1613 100644
--- a/src/libide/gui/ide-grid.c
+++ b/src/libide/gui/ide-grid.c
@@ -1,6 +1,6 @@
 /* ide-grid.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 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,1516 +18,197 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-
 #define G_LOG_DOMAIN "ide-grid"
 
 #include "config.h"
 
-#include <string.h>
-
+#include "ide-frame.h"
 #include "ide-grid.h"
-#include "ide-gui-private.h"
-
-/**
- * SECTION:ide-grid
- * @title: IdeGrid
- * @short_description: A grid for #IdePage
- *
- * The #IdeGrid provides a grid of pages that the user may
- * manipulate.
- *
- * 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.
- *
- * #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
- */
-
-typedef struct
-{
-  /* Owned references */
-  DzlSignalGroup *toplevel_signals;
-  GQueue          focus_column;
-  GArray         *stack_info;
-
-  /*
-   * This owned reference is our box highlight theatric that we
-   * animate while doing a DnD drop interaction.
-   */
-  DzlBoxTheatric *drag_theatric;
-  DzlAnimation   *drag_anim;
-
-  /*
-   * This unowned reference is simply used to compare to a new focus
-   * page to see if we have changed our current page. It is not to
-   * be used directly, only for pointer comparison.
-   */
-  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;
-} IdeGridPrivate;
-
-typedef struct
-{
-  IdeGridColumn *column;
-  IdeFrame      *stack;
-  GdkRectangle         area;
-  gint                 drop;
-  gint                 x;
-  gint                 y;
-} DropLocate;
 
-typedef struct
+struct _IdeGrid
 {
-  IdeFrame *stack;
-  guint           len;
-} StackInfo;
-
-enum {
-  PROP_0,
-  PROP_CURRENT_COLUMN,
-  PROP_CURRENT_STACK,
-  PROP_CURRENT_PAGE,
-  N_PROPS
-};
-
-enum {
-  CREATE_FRAME,
-  CREATE_VIEW,
-  N_SIGNALS
-};
-
-enum {
-  DROP_ONTO,
-  DROP_ABOVE,
-  DROP_BELOW,
-  DROP_LEFT_OF,
-  DROP_RIGHT_OF,
+  PanelGrid parent_instance;
 };
 
-static void list_model_iface_init (GListModelInterface *iface);
-
-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_grid_cull (IdeGrid *self)
-{
-  guint n_columns;
-
-  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--)
-    {
-      IdeGridColumn *column;
-      guint n_stacks;
-
-      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)
-        return;
-
-      for (guint j = n_stacks; j > 0; j--)
-        {
-          IdeFrame *stack;
-          guint n_items;
-
-          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)
-            gtk_widget_destroy (GTK_WIDGET (stack));
-        }
-
-      if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) == 0)
-        gtk_widget_destroy (GTK_WIDGET (column));
-    }
-}
-
-static gboolean
-ide_grid_do_cull (gpointer data)
-{
-  IdeGrid *self = data;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  g_assert (IDE_IS_GRID (self));
-
-  priv->cull_source = 0;
-
-  ide_grid_cull (self);
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-ide_grid_queue_cull (IdeGrid *self)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (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_grid_do_cull,
-                                                 g_object_ref (self),
-                                                 g_object_unref);
-}
-
-static void
-ide_grid_update_actions (IdeGrid *self)
-{
-  guint n_children;
-
-  g_assert (IDE_IS_GRID (self));
-
-  n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
-
-  for (guint i = 0; i < n_children; i++)
-    {
-      GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
-
-      g_assert (IDE_IS_GRID_COLUMN (column));
-
-      _ide_grid_column_update_actions (IDE_GRID_COLUMN (column));
-    }
-}
-
-static IdeFrame *
-ide_grid_real_create_frame (IdeGrid *self)
-{
-  return g_object_new (IDE_TYPE_FRAME,
-                       "expand", TRUE,
-                       "visible", TRUE,
-                       NULL);
-}
-
-static GtkWidget *
-ide_grid_create_frame (IdeGrid *self)
-{
-  IdeFrame *ret = NULL;
-
-  g_assert (IDE_IS_GRID (self));
-
-  g_signal_emit (self, signals [CREATE_FRAME], 0, &ret);
-  g_return_val_if_fail (IDE_IS_FRAME (ret), NULL);
-  return GTK_WIDGET (ret);
-}
-
-static GtkWidget *
-ide_grid_create_column (IdeGrid *self)
-{
-  GtkWidget *stack;
-
-  g_assert (IDE_IS_GRID (self));
-
-  stack = ide_grid_create_frame (self);
-
-  if (stack != NULL)
-    {
-      GtkWidget *column = g_object_new (IDE_TYPE_GRID_COLUMN,
-                                        "visible", TRUE,
-                                        NULL);
-      gtk_container_add (GTK_CONTAINER (column), stack);
-      return column;
-    }
-
-  return NULL;
-}
-
-static void
-ide_grid_after_set_focus (IdeGrid *self,
-                                 GtkWidget     *widget,
-                                 GtkWidget     *toplevel)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (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 *page;
-
-      if (gtk_widget_is_ancestor (widget, GTK_WIDGET (self)))
-        {
-          column = gtk_widget_get_ancestor (widget, IDE_TYPE_GRID_COLUMN);
-
-          if (column != NULL)
-            ide_grid_set_current_column (self, IDE_GRID_COLUMN (column));
-        }
-
-      /*
-       * self->_last_focused_page is an unowned reference, we only
-       * use it for pointer comparison, nothing more.
-       */
-      page = gtk_widget_get_ancestor (widget, IDE_TYPE_PAGE);
-      if (page != (GtkWidget *)priv->_last_focused_page)
-        {
-          priv->_last_focused_page = (IdePage *)page;
-          ide_object_notify_in_main (self, properties [PROP_CURRENT_PAGE]);
-
-          if (page != NULL && column != NULL)
-            {
-              GtkWidget *stack;
-
-              stack = gtk_widget_get_ancestor (GTK_WIDGET (page), IDE_TYPE_FRAME);
-              if (stack != NULL)
-                ide_grid_column_set_current_stack (IDE_GRID_COLUMN (column),
-                                                          IDE_FRAME (stack));
-            }
-        }
-    }
-}
-
-static void
-ide_grid_hierarchy_changed (GtkWidget *widget,
-                                   GtkWidget *old_toplevel)
-{
-  IdeGrid *self = (IdeGrid *)widget;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  GtkWidget *toplevel;
-
-  g_assert (IDE_IS_GRID (self));
-  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
-
-  /*
-   * Setup focus tracking so that we can update our "current stack" when the
-   * user selected focus changes.
-   */
-
-  toplevel = gtk_widget_get_toplevel (widget);
-
-  if (GTK_IS_WINDOW (toplevel))
-    dzl_signal_group_set_target (priv->toplevel_signals, toplevel);
-  else
-    dzl_signal_group_set_target (priv->toplevel_signals, NULL);
-
-  /*
-   * If we've been added to a widget and still do not have a stack added, then
-   * we'll emit our ::create-frame signal to create that now. We do this here
-   * to allow the consumer to connect to ::create-frame before adding the
-   * widget to the hierarchy.
-   */
-
-  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (widget)) == 0)
-    {
-      GtkWidget *column = ide_grid_create_column (self);
-      gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (column));
-    }
-}
-
-static void
-ide_grid_add (GtkContainer *container,
-                     GtkWidget    *widget)
-{
-  IdeGrid *self = (IdeGrid *)container;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  g_assert (IDE_IS_GRID (self));
-  g_assert (GTK_IS_WIDGET (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_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 pages */
-      children = gtk_container_get_children (GTK_CONTAINER (widget));
-      for (const GList *iter = children; iter; iter = iter->next)
-        if (IDE_IS_FRAME (iter->data))
-          _ide_grid_stack_added (self, iter->data);
-      g_list_free (children);
-    }
-  else if (IDE_IS_FRAME (widget))
-    {
-      IdeGridColumn *column;
-
-      column = ide_grid_get_current_column (self);
-      gtk_container_add (GTK_CONTAINER (column), widget);
-      ide_grid_set_current_column (self, column);
-    }
-  else if (IDE_IS_PAGE (widget))
-    {
-      IdeGridColumn *column = NULL;
-      guint n_columns;
-
-      /* If we have an empty layout stack, we'll prefer to add the
-       * 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));
-
-      for (guint i = 0; i < n_columns; i++)
-        {
-          GtkWidget *ele = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
-
-          g_assert (IDE_IS_GRID_COLUMN (ele));
-
-          if (_ide_grid_column_is_empty (IDE_GRID_COLUMN (ele)))
-            {
-              column = IDE_GRID_COLUMN (ele);
-              break;
-            }
-        }
-
-      if (column == NULL)
-        column = ide_grid_get_current_column (self);
-
-      g_assert (IDE_IS_GRID_COLUMN (column));
-
-      gtk_container_add (GTK_CONTAINER (column), widget);
-    }
-  else
-    {
-      g_warning ("%s must be one of IdeFrame, IdePage, or IdeGrid",
-                 G_OBJECT_TYPE_NAME (self));
-      return;
-    }
-
-  ide_grid_update_actions (self);
-}
-
-static void
-ide_grid_remove (GtkContainer *container,
-                        GtkWidget    *widget)
-{
-  IdeGrid *self = (IdeGrid *)container;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  gboolean notify = FALSE;
-
-  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_grid_parent_class)->remove (container, widget);
-
-  ide_grid_update_actions (self);
-
-  if (notify)
-    {
-      GtkWidget *head = g_queue_peek_head (&priv->focus_column);
-
-      if (head != NULL)
-        gtk_widget_grab_focus (head);
-
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]);
-    }
-}
-
-static gboolean
-ide_grid_get_drop_area (IdeGrid        *self,
-                               gint                  x,
-                               gint                  y,
-                               GdkRectangle         *out_area,
-                               IdeGridColumn **out_column,
-                               IdeFrame      **out_stack,
-                               gint                 *out_drop)
-{
-  GtkAllocation alloc;
-  GtkWidget *column;
-  GtkWidget *stack = NULL;
-
-  g_assert (IDE_IS_GRID (self));
-  g_assert (out_area != NULL);
-  g_assert (out_column != NULL);
-  g_assert (out_stack != NULL);
-  g_assert (out_drop != NULL);
-
-  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
-
-  column = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (self), x + alloc.x, 0);
-  if (column != NULL)
-    stack = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (column), 0, y + alloc.y);
-
-  if (column != NULL && stack != NULL)
-    {
-      GtkAllocation stack_alloc;
-
-      gtk_widget_get_allocation (stack, &stack_alloc);
-
-      gtk_widget_translate_coordinates (stack,
-                                        GTK_WIDGET (self),
-                                        0, 0,
-                                        &stack_alloc.x, &stack_alloc.y);
-
-      *out_area = stack_alloc;
-      *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);
-
-      if (FALSE) {}
-      else if (x < (stack_alloc.width / 4))
-        {
-          out_area->y = 0;
-          out_area->height = alloc.height;
-          out_area->width = stack_alloc.width / 4;
-          *out_drop = DROP_LEFT_OF;
-        }
-      else if (x > (stack_alloc.width / 4 * 3))
-        {
-          out_area->y = 0;
-          out_area->height = alloc.height;
-          out_area->x = dzl_cairo_rectangle_x2 (&stack_alloc) - (stack_alloc.width / 4);
-          out_area->width = stack_alloc.width / 4;
-          *out_drop = DROP_RIGHT_OF;
-        }
-      else if (y < (stack_alloc.height / 4))
-        {
-          out_area->height = stack_alloc.height / 4;
-          *out_drop = DROP_ABOVE;
-        }
-      else if (y > (stack_alloc.height / 4 * 3))
-        {
-          out_area->y = dzl_cairo_rectangle_y2 (&stack_alloc) - (stack_alloc.height / 4);
-          out_area->height = stack_alloc.height / 4;
-          *out_drop = DROP_BELOW;
-        }
-
-      return TRUE;
-    }
-
-  return FALSE;
-}
-
-static gboolean
-ide_grid_drag_motion (GtkWidget      *widget,
-                             GdkDragContext *context,
-                             gint            x,
-                             gint            y,
-                             guint           time_)
-{
-  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_GRID (self));
-  g_assert (GDK_IS_DRAG_CONTEXT (context));
-
-  if (priv->drag_anim != NULL)
-    {
-      dzl_animation_stop (priv->drag_anim);
-      g_clear_weak_pointer (&priv->drag_anim);
-    }
-
-  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
-
-  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
-    return GDK_EVENT_PROPAGATE;
-
-  if (priv->drag_theatric == NULL)
-    {
-      priv->drag_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
-                                          "x", area.x,
-                                          "y", area.y,
-                                          "width", area.width,
-                                          "height", area.height,
-                                          "alpha", 0.3,
-                                          "background", "#729fcf",
-                                          "target", self,
-                                          NULL);
-      return GDK_EVENT_STOP;
-    }
-
-  drag_anim = dzl_object_animate (priv->drag_theatric,
-                                  DZL_ANIMATION_EASE_OUT_CUBIC,
-                                  100,
-                                  gtk_widget_get_frame_clock (GTK_WIDGET (self)),
-                                  "x", area.x,
-                                  "width", area.width,
-                                  "y", area.y,
-                                  "height", area.height,
-                                  NULL);
-  g_set_weak_pointer (&priv->drag_anim, drag_anim);
-
-  gtk_widget_queue_draw (GTK_WIDGET (self));
-
-  return GDK_EVENT_STOP;
-}
-
-static void
-ide_grid_drag_data_received (GtkWidget        *widget,
-                                    GdkDragContext   *context,
-                                    gint              x,
-                                    gint              y,
-                                    GtkSelectionData *data,
-                                    guint             info,
-                                    guint             time_)
-{
-  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_GRID (self));
-  g_assert (GDK_IS_DRAG_CONTEXT (context));
-
-  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
-    return;
-
-  g_assert (IDE_IS_GRID_COLUMN (column));
-  g_assert (IDE_IS_FRAME (stack));
-
-  if (!(uris = gtk_selection_data_get_uris (data)))
-    return;
-
-  for (guint i = 0; uris[i] != NULL; i++)
-    {
-      const gchar *uri = uris[i];
-      IdePage *page = NULL;
-      gint column_index = 0;
-      gint stack_index = 0;
-
-      g_signal_emit (self, signals [CREATE_VIEW], 0, uri, &page);
-
-      if (page == NULL)
-        {
-          g_debug ("Failed to load IdePage for \"%s\"", uri);
-          continue;
-        }
-
-      gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column),
-                               "index", &column_index,
-                               NULL);
-      gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack),
-                               "index", &stack_index,
-                               NULL);
-
-      switch (drop)
-        {
-        case DROP_ONTO:
-          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
-          break;
-
-        case DROP_ABOVE:
-          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 (page));
-          break;
-
-        case DROP_BELOW:
-          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 (page));
-          break;
-
-        case DROP_LEFT_OF:
-          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 (page));
-          break;
-
-        case DROP_RIGHT_OF:
-          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 (page));
-          break;
-
-        default:
-          g_assert_not_reached ();
-        }
-    }
-}
-
-static void
-ide_grid_drag_leave (GtkWidget      *widget,
-                            GdkDragContext *context,
-                            guint           time_)
-{
-  IdeGrid *self = (IdeGrid *)widget;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  g_assert (IDE_IS_GRID (self));
-  g_assert (GDK_IS_DRAG_CONTEXT (context));
-
-  if (priv->drag_anim != NULL)
-    {
-      dzl_animation_stop (priv->drag_anim);
-      g_clear_weak_pointer (&priv->drag_anim);
-    }
-
-  g_clear_object (&priv->drag_theatric);
-  gtk_widget_queue_draw (GTK_WIDGET (self));
-}
-
-static gboolean
-ide_grid_drag_failed (GtkWidget      *widget,
-                             GdkDragContext *context,
-                             GtkDragResult   result)
-{
-  IdeGrid *self = (IdeGrid *)widget;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  g_assert (IDE_IS_GRID (self));
-  g_assert (GDK_IS_DRAG_CONTEXT (context));
-
-  if (priv->drag_anim != NULL)
-    {
-      dzl_animation_stop (priv->drag_anim);
-      g_clear_weak_pointer (&priv->drag_anim);
-    }
-
-  g_clear_object (&priv->drag_theatric);
-  gtk_widget_queue_draw (GTK_WIDGET (self));
-
-  return GDK_EVENT_PROPAGATE;
-}
-
-static void
-ide_grid_grab_focus (GtkWidget *widget)
-{
-  IdeGrid *self = (IdeGrid *)widget;
-  IdeFrame *stack;
-
-  g_assert (IDE_IS_GRID (self));
-
-  stack = ide_grid_get_current_stack (self);
-
-  if (stack != NULL)
-    gtk_widget_grab_focus (GTK_WIDGET (stack));
-  else
-    GTK_WIDGET_CLASS (ide_grid_parent_class)->grab_focus (widget);
-}
-
-static void
-ide_grid_destroy (GtkWidget *widget)
-{
-  IdeGrid *self = (IdeGrid *)widget;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  dzl_clear_source (&priv->cull_source);
-
-  GTK_WIDGET_CLASS (ide_grid_parent_class)->destroy (widget);
-}
-
-static void
-ide_grid_finalize (GObject *object)
-{
-  IdeGrid *self = (IdeGrid *)object;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (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);
-
-  g_clear_pointer (&priv->stack_info, g_array_unref);
-  g_clear_object (&priv->toplevel_signals);
-
-  G_OBJECT_CLASS (ide_grid_parent_class)->finalize (object);
-}
-
-static void
-ide_grid_get_property (GObject    *object,
-                       guint       prop_id,
-                       GValue     *value,
-                       GParamSpec *pspec)
+typedef struct
 {
-  IdeGrid *self = IDE_GRID (object);
-
-  switch (prop_id)
-    {
-    case PROP_CURRENT_COLUMN:
-      g_value_set_object (value, ide_grid_get_current_column (self));
-      break;
-
-    case PROP_CURRENT_STACK:
-      g_value_set_object (value, ide_grid_get_current_stack (self));
-      break;
+  IdePage *page;
+  guint column;
+  guint row;
+  guint depth;
+} PageInfo;
 
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
+G_DEFINE_TYPE (IdeGrid, ide_grid, PANEL_TYPE_GRID)
 
-static void
-ide_grid_set_property (GObject      *object,
-                       guint         prop_id,
-                       const GValue *value,
-                       GParamSpec   *pspec)
+static PanelFrame *
+ide_grid_real_create_frame (PanelGrid *grid)
 {
-  IdeGrid *self = IDE_GRID (object);
-
-  switch (prop_id)
-    {
-    case PROP_CURRENT_COLUMN:
-      ide_grid_set_current_column (self, g_value_get_object (value));
-      break;
+  g_assert (PANEL_IS_GRID (grid));
 
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
+  return PANEL_FRAME (ide_frame_new ());
 }
 
 static void
 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_grid_finalize;
-  object_class->get_property = ide_grid_get_property;
-  object_class->set_property = ide_grid_set_property;
-
-  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_grid_add;
-  container_class->remove = ide_grid_remove;
-
-  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_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 IdeFrame",
-                         IDE_TYPE_FRAME,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+  PanelGridClass *grid_class = PANEL_GRID_CLASS (klass);
 
-  properties [PROP_CURRENT_PAGE] =
-    g_param_spec_object ("current-page",
-                         "Current 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, "idegrid");
-
-  /**
-   * IdeGrid::create-frame:
-   * @self: an #IdeGrid
-   *
-   * Creates a new stack to be added to the grid.
-   *
-   * Returns: (transfer full): A newly created #IdeFrame
-   *
-   * Since: 3.34
-   */
-  signals [CREATE_FRAME] =
-    g_signal_new (g_intern_static_string ("create-frame"),
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (IdeGridClass, create_frame),
-                  g_signal_accumulator_first_wins, NULL, NULL,
-                  IDE_TYPE_FRAME, 0);
-
-  /**
-   * IdeGrid::create-page:
-   * @self: an #IdeGrid
-   * @uri: the URI to open
-   *
-   * Creates a new page for @uri to be added to the grid.
-   *
-   * Returns: (transfer full): A newly created #IdePage
-   *
-   * Since: 3.32
-   */
-  signals [CREATE_VIEW] =
-    g_signal_new (g_intern_static_string ("create-page"),
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  G_STRUCT_OFFSET (IdeGridClass, create_page),
-                  g_signal_accumulator_first_wins, NULL, NULL,
-                  IDE_TYPE_PAGE,
-                  1,
-                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+  grid_class->create_frame = ide_grid_real_create_frame;
 }
 
 static void
 ide_grid_init (IdeGrid *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,
-                     G_N_ELEMENTS (target_entries),
-                     GDK_ACTION_COPY);
-
-  priv->stack_info = g_array_new (FALSE, FALSE, sizeof (StackInfo));
-
-  priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
+  PanelGridColumn *column;
+  PanelFrame *row;
 
-  dzl_signal_group_connect_object (priv->toplevel_signals,
-                                   "set-focus",
-                                   G_CALLBACK (ide_grid_after_set_focus),
-                                   self,
-                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+  column = panel_grid_get_column (PANEL_GRID (self), 0);
+  row = panel_grid_column_get_row (column, 0);
 
-  _ide_grid_init_actions (self);
+  (void)row;
 }
 
-/**
- * ide_grid_new:
- *
- * Creates a new #IdeGrid.
- *
- * Returns: (transfer full): A newly created #IdeGrid
- *
- * Since: 3.32
- */
 GtkWidget *
 ide_grid_new (void)
 {
   return g_object_new (IDE_TYPE_GRID, NULL);
 }
 
-/**
- * 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 #IdeFrame or %NULL.
- *
- * Since: 3.32
- */
-IdeFrame *
-ide_grid_get_current_stack (IdeGrid *self)
-{
-  IdeGridColumn *column;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
-
-  column = ide_grid_get_current_column (self);
-  if (column != NULL)
-    return ide_grid_column_get_current_stack (column);
-
-  return NULL;
-}
-
-/**
- * ide_grid_get_nth_column:
- * @self: a #IdeGrid
- * @nth: the index of the column, or -1
- *
- * Gets the @nth column from the grid.
- *
- * If @nth is -1, then a new column at the beginning of the
- * grid is created.
- *
- * 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 #IdeGridColumn.
- *
- * Since: 3.32
- */
-IdeGridColumn *
-ide_grid_get_nth_column (IdeGrid *self,
-                                gint           nth)
-{
-  GtkWidget *column;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
-
-  if (nth < 0)
-    {
-      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_grid_create_column (self);
-      gtk_container_add (GTK_CONTAINER (self), column);
-    }
-  else
-    {
-      column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), nth);
-    }
-
-  g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL);
-
-  return IDE_GRID_COLUMN (column);
-}
-
-/*
- * _ide_grid_get_nth_stack:
- *
- * This will get the @nth stack. If it does not yet exist,
- * it will be created.
- *
- * If nth == -1, a new stack will be created at index 0.
- *
- * If nth >= the number of stacks, a new stack will be created
- * at the end of the grid.
- *
- * Returns: (not nullable) (transfer none): An #IdeFrame.
- */
-IdeFrame *
-_ide_grid_get_nth_stack (IdeGrid *self,
-                                gint           nth)
-{
-  IdeGridColumn *column;
-  IdeFrame *stack;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
-
-  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_FRAME (stack), NULL);
-
-  return stack;
-}
-
-/**
- * _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
- * cannot be found, it will be created.
- *
- * If @nth is less-than 0, a new column will be inserted at the top.
- *
- * 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 #IdeFrame.
- *
- * Since: 3.32
- */
-IdeFrame *
-_ide_grid_get_nth_stack_for_column (IdeGrid       *self,
-                                           IdeGridColumn *column,
-                                           gint                 nth)
-{
-  GtkWidget *stack;
-
-  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_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_grid_create_frame (self);
-      gtk_container_add (GTK_CONTAINER (self), stack);
-    }
-  else
-    {
-      stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), nth);
-    }
-
-  g_assert (IDE_IS_FRAME (stack));
-
-  return IDE_FRAME (stack);
-}
-
-/**
- * ide_grid_get_current_column:
- * @self: a #IdeGrid
- *
- * Gets the most recently focused column of the grid.
- *
- * Returns: (transfer none) (not nullable): An #IdeGridColumn
- *
- * Since: 3.32
- */
-IdeGridColumn *
-ide_grid_get_current_column (IdeGrid *self)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  GtkWidget *ret = NULL;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
-
-  if (priv->focus_column.head != NULL)
-    ret = priv->focus_column.head->data;
-  else if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) > 0)
-    ret = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0);
-
-  if (ret == NULL)
-    {
-      ret = ide_grid_create_column (self);
-      gtk_container_add (GTK_CONTAINER (self), ret);
-    }
-
-  g_return_val_if_fail (IDE_IS_GRID_COLUMN (ret), NULL);
-
-  return IDE_GRID_COLUMN (ret);
-}
-
-/**
- * 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.
- *
- * @column can be %NULL out of convenience.
- *
- * Since: 3.32
- */
-void
-ide_grid_set_current_column (IdeGrid       *self,
-                                    IdeGridColumn *column)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  GList *iter;
-
-  g_return_if_fail (IDE_IS_GRID (self));
-  g_return_if_fail (!column || IDE_IS_GRID_COLUMN (column));
-
-  if (column == NULL)
-    return;
-
-  if (gtk_widget_get_parent (GTK_WIDGET (column)) != GTK_WIDGET (self))
-    {
-      g_warning ("Attempt to set current column with non-descendant");
-      return;
-    }
-
-  if (NULL != (iter = g_queue_find (&priv->focus_column, column)))
-    {
-      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_grid_update_actions (self);
-      return;
-    }
-
-  g_warning ("%s does not contain %s",
-             G_OBJECT_TYPE_NAME (self), G_OBJECT_TYPE_NAME (column));
-}
-
-/**
- * ide_grid_get_current_page:
- * @self: a #IdeGrid
- *
- * Gets the most recent page used by the user as determined by tracking
- * the window focus.
- *
- * Returns: (transfer none): An #IdePage or %NULL
- *
- * Since: 3.32
- */
-IdePage *
-ide_grid_get_current_page (IdeGrid *self)
-{
-  IdeFrame *stack;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
-
-  stack = ide_grid_get_current_stack (self);
-
-  if (stack != NULL)
-    return ide_frame_get_visible_child (stack);
-
-  return NULL;
-}
-
-static void
-collect_pages (GtkWidget *widget,
-               GPtrArray *ar)
-{
-  if (IDE_IS_PAGE (widget))
-    g_ptr_array_add (ar, widget);
-}
-
 /**
  * ide_grid_foreach_page:
  * @self: a #IdeGrid
- * @callback: (scope call) (closure user_data): A callback for each page
- * @user_data: user data for @callback
+ * @callback: (scope call): callback to execute for each page found
+ * @user_data: closure data for @callback
  *
- * This function will call @callback for every page found in @self.
- *
- * Since: 3.32
+ * Calls @callback for each #IdePage found in the grid.
  */
 void
-ide_grid_foreach_page (IdeGrid     *self,
-                       GtkCallback  callback,
-                       gpointer     user_data)
+ide_grid_foreach_page (IdeGrid         *self,
+                       IdePageCallback  callback,
+                       gpointer         user_data)
 {
-  g_autoptr(GPtrArray) pages = NULL;
+  g_autoptr(GArray) pages = NULL;
   guint n_columns;
 
   g_return_if_fail (IDE_IS_GRID (self));
   g_return_if_fail (callback != NULL);
 
-  pages = g_ptr_array_new ();
-
-  n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+  pages = g_array_new (FALSE, FALSE, sizeof (PageInfo));
+  n_columns = panel_grid_get_n_columns (PANEL_GRID (self));
 
   for (guint i = 0; i < n_columns; i++)
     {
-      GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
-      guint n_stacks;
-
-      g_assert (IDE_IS_GRID_COLUMN (column));
-
-      n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
+      PanelGridColumn *column = panel_grid_get_column (PANEL_GRID (self), i);
+      guint n_rows = panel_grid_column_get_n_rows (column);
 
-      for (guint j = 0; j < n_stacks; j++)
+      for (guint j = 0; j < n_rows; j++)
         {
-          GtkWidget *stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j);
+          PanelFrame *frame = panel_grid_column_get_row (column, j);
+          guint n_pages = panel_frame_get_n_pages (frame);
 
-          g_assert (IDE_IS_FRAME (stack));
+          for (guint k = 0; k < n_pages; k++)
+            {
+              PanelWidget *widget = panel_frame_get_page (frame, k);
 
-          ide_frame_foreach_page (IDE_FRAME (stack),
-                                  (GtkCallback) collect_pages,
-                                  pages);
+              if (IDE_IS_PAGE (widget))
+                {
+                  PageInfo info = { IDE_PAGE (widget), i, j, k };
+                  g_array_append_val (pages, info);
+                }
+            }
         }
     }
 
   for (guint i = 0; i < pages->len; i++)
-    callback (g_ptr_array_index (pages, i), user_data);
-}
-
-static GType
-ide_grid_get_item_type (GListModel *model)
-{
-  return IDE_TYPE_PAGE;
-}
-
-static guint
-ide_grid_get_n_items (GListModel *model)
-{
-  IdeGrid *self = (IdeGrid *)model;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  guint n_items = 0;
-
-  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;
-
-  return n_items;
-}
-
-static gpointer
-ide_grid_get_item (GListModel *model,
-                          guint       position)
-{
-  IdeGrid *self = (IdeGrid *)model;
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-
-  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++)
     {
-      const StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i);
-
-      if (position >= info->len)
-        {
-          position -= info->len;
-          continue;
-        }
-
-      return g_list_model_get_item (G_LIST_MODEL (info->stack), position);
+      const PageInfo *info = &g_array_index (pages, PageInfo, i);
+      callback (info->page, user_data);
     }
-
-  g_warning ("Failed to locate position %u within %s",
-             position, G_OBJECT_TYPE_NAME (self));
-
-  return NULL;
 }
 
-static void
-list_model_iface_init (GListModelInterface *iface)
+guint
+ide_grid_count_pages (IdeGrid *self)
 {
-  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;
-}
+  guint count = 0;
+  guint n_columns;
 
-static void
-ide_grid_stack_items_changed (IdeGrid  *self,
-                                     guint           position,
-                                     guint           removed,
-                                     guint           added,
-                                     IdeFrame *stack)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  guint real_position = 0;
+  g_return_val_if_fail (IDE_IS_GRID (self), 0);
 
-  g_assert (IDE_IS_GRID (self));
-  g_assert (IDE_IS_FRAME (stack));
+  n_columns = panel_grid_get_n_columns (PANEL_GRID (self));
 
-  for (guint i = 0; i < priv->stack_info->len; i++)
+  for (guint i = 0; i < n_columns; i++)
     {
-      StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i);
+      PanelGridColumn *column = panel_grid_get_column (PANEL_GRID (self), i);
+      guint n_rows = panel_grid_column_get_n_rows (column);
 
-      if (info->stack == stack)
+      for (guint j = 0; j < n_rows; j++)
         {
-          info->len -= removed;
-          info->len += added;
+          PanelFrame *frame = panel_grid_column_get_row (column, j);
+          guint n_pages = panel_frame_get_n_pages (frame);
 
-          g_list_model_items_changed (G_LIST_MODEL (self),
-                                      real_position + position,
-                                      removed,
-                                      added);
-
-          ide_object_notify_in_main (G_OBJECT (self), properties [PROP_CURRENT_PAGE]);
-
-          ide_grid_queue_cull (self);
-
-          return;
+          count += n_pages;
         }
-
-      real_position += info->len;
     }
 
-  g_warning ("Failed to locate %s within %s",
-             G_OBJECT_TYPE_NAME (stack), G_OBJECT_TYPE_NAME (self));
+  return count;
 }
 
 void
-_ide_grid_stack_added (IdeGrid  *self,
-                              IdeFrame *stack)
+ide_grid_get_page_position (IdeGrid *self,
+                            IdePage *page,
+                            guint   *out_column,
+                            guint   *out_row,
+                            guint   *out_depth)
 {
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  StackInfo info = { 0 };
-  guint n_items;
+  GtkWidget *frame;
+  GtkWidget *column;
+  GtkWidget *grid;
+  guint n_pages;
+  guint n_rows;
+  guint n_columns;
 
   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;
-  info.len = 0;
+  g_return_if_fail (IDE_IS_PAGE (self));
 
-  g_array_append_val (priv->stack_info, info);
+  *out_column = 0;
+  *out_row = 0;
+  *out_depth = 0;
 
-  g_signal_connect_object (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_grid_stack_items_changed (self, 0, 0, n_items, stack);
-}
-
-void
-_ide_grid_stack_removed (IdeGrid  *self,
-                                IdeFrame *stack)
-{
-  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
-  guint position = 0;
-
-  g_return_if_fail (IDE_IS_GRID (self));
-  g_return_if_fail (IDE_IS_FRAME (stack));
+  if (!(frame = gtk_widget_get_ancestor (GTK_WIDGET (page), PANEL_TYPE_FRAME)) ||
+      !(column = gtk_widget_get_ancestor (GTK_WIDGET (frame), PANEL_TYPE_GRID_COLUMN)) ||
+      !(grid = gtk_widget_get_ancestor (GTK_WIDGET (column), PANEL_TYPE_GRID)))
+    return;
 
-  g_signal_handlers_disconnect_by_func (stack,
-                                        G_CALLBACK (ide_grid_stack_items_changed),
-                                        self);
+  n_pages = panel_frame_get_n_pages (PANEL_FRAME (frame));
+  n_rows = panel_grid_column_get_n_rows (PANEL_GRID_COLUMN (column));
+  n_columns = panel_grid_get_n_columns (PANEL_GRID (grid));
 
-  for (guint i = 0; i < priv->stack_info->len; i++)
+  for (guint i = 0; i < n_pages; i++)
     {
-      const StackInfo info = g_array_index (priv->stack_info, StackInfo, i);
+      PanelWidget *widget = panel_frame_get_page (PANEL_FRAME (frame), i);
 
-      if (info.stack == stack)
+      if (widget == PANEL_WIDGET (page))
         {
-          g_array_remove_index (priv->stack_info, i);
-          g_list_model_items_changed (G_LIST_MODEL (self), position, info.len, 0);
+          *out_depth = i;
           break;
         }
     }
-}
-
-static void
-count_pages_cb (GtkWidget *widget,
-                gpointer   data)
-{
-  (*(guint *)data)++;
-}
-
-guint
-ide_grid_count_pages (IdeGrid *self)
-{
-  guint count = 0;
-
-  g_return_val_if_fail (IDE_IS_GRID (self), 0);
-
-  ide_grid_foreach_page (self, count_pages_cb, &count);
-
-  return count;
-}
-
-/**
- * ide_grid_focus_neighbor:
- * @self: An #IdeGrid
- * @dir: the direction for the focus change
- *
- * Attempts to focus a neighbor #IdePage in the grid based on
- * the direction requested.
- *
- * If an #IdePage was focused, it will be returned to the caller.
- *
- * Returns: (transfer none) (nullable): An #IdePage or %NULL
- *
- * Since: 3.32
- */
-IdePage *
-ide_grid_focus_neighbor (IdeGrid    *self,
-                                GtkDirectionType  dir)
-{
-  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_GRID (self), NULL);
-  g_return_val_if_fail (dir <= GTK_DIR_RIGHT, NULL);
-
-  /* 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),
-                           "index", &column_pos,
-                           NULL);
-
-  gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack),
-                           "index", &stack_pos,
-                           NULL);
-
-  switch (dir)
+  for (guint i = 0; i < n_rows; i++)
     {
-    case GTK_DIR_DOWN:
-      n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
-      if (n_children - stack_pos == 1)
-        return NULL;
-      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_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_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_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_grid_focus_neighbor (self, GTK_DIR_DOWN) &&
-          !ide_grid_focus_neighbor (self, GTK_DIR_RIGHT))
+      if (PANEL_FRAME (frame) == panel_grid_column_get_row (PANEL_GRID_COLUMN (column), i))
         {
-          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);
+          *out_row = i;
+          break;
         }
-      break;
+    }
 
-    case GTK_DIR_TAB_BACKWARD:
-      if (!ide_grid_focus_neighbor (self, GTK_DIR_UP) &&
-          !ide_grid_focus_neighbor (self, GTK_DIR_LEFT))
+  for (guint i = 0; i < n_columns; i++)
+    {
+      if (PANEL_GRID_COLUMN (column) == panel_grid_get_column (PANEL_GRID (grid), i))
         {
-          n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
-          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);
+          *out_column = i;
+          break;
         }
-      break;
-
-    default:
-      g_assert_not_reached ();
     }
-
-  if (page != NULL)
-    gtk_widget_child_focus (GTK_WIDGET (page), GTK_DIR_TAB_FORWARD);
-
-  return page;
 }
diff --git a/src/libide/gui/ide-grid.h b/src/libide/gui/ide-grid.h
index 04516e6b5..e15a86d4d 100644
--- a/src/libide/gui/ide-grid.h
+++ b/src/libide/gui/ide-grid.h
@@ -1,6 +1,6 @@
 /* ide-grid.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2022 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,54 +24,32 @@
 # error "Only <libide-gui.h> can be included directly."
 #endif
 
-#include <dazzle.h>
+#include <libpanel.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);
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdeGrid, ide_grid, IDE, GRID, PanelGrid)
+
+IDE_AVAILABLE_IN_ALL
+GtkWidget *ide_grid_new               (void);
+IDE_AVAILABLE_IN_ALL
+guint      ide_grid_count_pages       (IdeGrid         *self);
+IDE_AVAILABLE_IN_ALL
+void       ide_grid_get_page_position (IdeGrid         *self,
+                                       IdePage         *page,
+                                       guint           *column,
+                                       guint           *row,
+                                       guint           *depth);
+IDE_AVAILABLE_IN_ALL
+void        ide_grid_foreach_page     (IdeGrid         *self,
+                                       IdePageCallback  callback,
+                                       gpointer         user_data);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-page-private.h b/src/libide/gui/ide-page-private.h
new file mode 100644
index 000000000..002a692c8
--- /dev/null
+++ b/src/libide/gui/ide-page-private.h
@@ -0,0 +1,29 @@
+/* ide-page-private.h
+ *
+ * Copyright 2022 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-page.h"
+
+G_BEGIN_DECLS
+
+GList *_ide_page_get_mru_link (IdePage *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-page.c b/src/libide/gui/ide-page.c
index a52bfd89b..514374ef2 100644
--- a/src/libide/gui/ide-page.c
+++ b/src/libide/gui/ide-page.c
@@ -22,44 +22,36 @@
 
 #include "config.h"
 
-#include <libide-threading.h>
 #include <string.h>
 
+#include <libide-gtk.h>
+#include <libide-threading.h>
+
+#include "ide-application.h"
 #include "ide-gui-global.h"
-#include "ide-gui-private.h"
-#include "ide-page.h"
-#include "ide-workspace.h"
+#include "ide-page-private.h"
+#include "ide-workspace-private.h"
 
 typedef struct
 {
-  GList        mru_link;
+  GList           mru_link;
 
-  const gchar *menu_id;
-  const gchar *icon_name;
-  gchar       *title;
-  GIcon       *icon;
+  const char     *menu_id;
 
-  GdkRGBA      primary_color_bg;
-  GdkRGBA      primary_color_fg;
+  GtkBox         *content_box;
+  GtkOverlay     *overlay;
+  GtkProgressBar *progress_bar;
 
-  guint        failed : 1;
-  guint        modified : 1;
-  guint        can_split : 1;
-  guint        primary_color_bg_set : 1;
-  guint        primary_color_fg_set : 1;
+  guint           failed : 1;
+  guint           modified : 1;
+  guint           can_split : 1;
 } IdePagePrivate;
 
 enum {
   PROP_0,
   PROP_CAN_SPLIT,
   PROP_FAILED,
-  PROP_ICON,
-  PROP_ICON_NAME,
   PROP_MENU_ID,
-  PROP_MODIFIED,
-  PROP_PRIMARY_COLOR_BG,
-  PROP_PRIMARY_COLOR_FG,
-  PROP_TITLE,
   N_PROPS
 };
 
@@ -68,11 +60,24 @@ enum {
   N_SIGNALS
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (IdePage, ide_page, GTK_TYPE_BOX)
+static void buildable_iface_init (GtkBuildableIface *iface);
 
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdePage, ide_page, PANEL_TYPE_WIDGET,
+                                  G_ADD_PRIVATE (IdePage)
+                                  G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GtkBuildableIface *parent_buildable;
 static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
+GList *
+_ide_page_get_mru_link (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  g_assert (IDE_IS_PAGE (self));
+  return &priv->mru_link;
+}
+
 static void
 ide_page_real_agree_to_close_async (IdePage             *self,
                                     GCancellable        *cancellable,
@@ -102,51 +107,37 @@ ide_page_real_agree_to_close_finish (IdePage       *self,
 }
 
 static void
-find_focus_child (GtkWidget *widget,
-                  gboolean  *handled)
+ide_page_root (GtkWidget *widget)
 {
-  if (!*handled)
-    *handled = gtk_widget_child_focus (widget, GTK_DIR_TAB_FORWARD);
-}
+  IdePage *self = (IdePage *)widget;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  GtkWidget *toplevel;
 
-static void
-ide_page_grab_focus (GtkWidget *widget)
-{
-  gboolean handled = FALSE;
+  g_assert (IDE_IS_PAGE (self));
 
-  g_assert (IDE_IS_PAGE (widget));
+  GTK_WIDGET_CLASS (ide_page_parent_class)->root (widget);
 
-  /*
-   * This default grab_focus override just looks for the first child (generally
-   * something like a scrolled window) and tries to move forward on focusing
-   * the child widget. In most cases, this should work without intervention
-   * from the child subclass.
-   */
+  toplevel = GTK_WIDGET (gtk_widget_get_native (widget));
 
-  gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) find_focus_child, &handled);
+  if (IDE_IS_WORKSPACE (toplevel))
+    _ide_workspace_add_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
 }
 
 static void
-ide_page_hierarchy_changed (GtkWidget *widget,
-                            GtkWidget *previous_toplevel)
+ide_page_unroot (GtkWidget *widget)
 {
   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);
+  toplevel = GTK_WIDGET (gtk_widget_get_native (widget));
 
   if (IDE_IS_WORKSPACE (toplevel))
-    _ide_workspace_add_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
+    _ide_workspace_remove_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
+
+  GTK_WIDGET_CLASS (ide_page_parent_class)->unroot (widget);
 }
 
 /**
@@ -157,8 +148,6 @@ ide_page_hierarchy_changed (GtkWidget *widget,
  * 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)
@@ -175,12 +164,6 @@ ide_page_mark_used (IdePage *self)
 static void
 ide_page_finalize (GObject *object)
 {
-  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_page_parent_class)->finalize (object);
 }
 
@@ -202,34 +185,10 @@ ide_page_get_property (GObject    *object,
       g_value_set_boolean (value, ide_page_get_failed (self));
       break;
 
-    case PROP_ICON_NAME:
-      g_value_set_static_string (value, ide_page_get_icon_name (self));
-      break;
-
-    case PROP_ICON:
-      g_value_set_object (value, ide_page_get_icon (self));
-      break;
-
     case PROP_MENU_ID:
       g_value_set_static_string (value, ide_page_get_menu_id (self));
       break;
 
-    case PROP_MODIFIED:
-      g_value_set_boolean (value, ide_page_get_modified (self));
-      break;
-
-    case PROP_PRIMARY_COLOR_BG:
-      g_value_set_boxed (value, ide_page_get_primary_color_bg (self));
-      break;
-
-    case PROP_PRIMARY_COLOR_FG:
-      g_value_set_boxed (value, ide_page_get_primary_color_fg (self));
-      break;
-
-    case PROP_TITLE:
-      g_value_set_string (value, ide_page_get_title (self));
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -253,34 +212,10 @@ ide_page_set_property (GObject      *object,
       ide_page_set_failed (self, g_value_get_boolean (value));
       break;
 
-    case PROP_ICON_NAME:
-      ide_page_set_icon_name (self, g_value_get_string (value));
-      break;
-
-    case PROP_ICON:
-      ide_page_set_icon (self, g_value_get_object (value));
-      break;
-
     case PROP_MENU_ID:
       ide_page_set_menu_id (self, g_value_get_string (value));
       break;
 
-    case PROP_MODIFIED:
-      ide_page_set_modified (self, g_value_get_boolean (value));
-      break;
-
-    case PROP_PRIMARY_COLOR_BG:
-      ide_page_set_primary_color_bg (self, g_value_get_boxed (value));
-      break;
-
-    case PROP_PRIMARY_COLOR_FG:
-      ide_page_set_primary_color_fg (self, g_value_get_boxed (value));
-      break;
-
-    case PROP_TITLE:
-      ide_page_set_title (self, g_value_get_string (value));
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -296,8 +231,8 @@ ide_page_class_init (IdePageClass *klass)
   object_class->get_property = ide_page_get_property;
   object_class->set_property = ide_page_set_property;
 
-  widget_class->grab_focus = ide_page_grab_focus;
-  widget_class->hierarchy_changed = ide_page_hierarchy_changed;
+  widget_class->root = ide_page_root;
+  widget_class->unroot = ide_page_unroot;
 
   klass->agree_to_close_async = ide_page_real_agree_to_close_async;
   klass->agree_to_close_finish = ide_page_real_agree_to_close_finish;
@@ -316,20 +251,6 @@ ide_page_class_init (IdePageClass *klass)
                           FALSE,
                           (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
-  properties [PROP_ICON] =
-    g_param_spec_object ("icon",
-                         "Icon",
-                         "A GIcon for the view",
-                         G_TYPE_ICON,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_ICON_NAME] =
-    g_param_spec_string ("icon-name",
-                         "Icon Name",
-                         "The icon-name describing the view content",
-                         "text-x-generic-symbolic",
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
   properties [PROP_MENU_ID] =
     g_param_spec_string ("menu-id",
                          "Menu ID",
@@ -337,56 +258,6 @@ ide_page_class_init (IdePageClass *klass)
                          NULL,
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
-  properties [PROP_MODIFIED] =
-    g_param_spec_boolean ("modified",
-                          "Modified",
-                          "If the view has been modified from the saved content",
-                          FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdePage:primary-color-bg:
-   *
-   * The "primary-color-bg" property should describe the primary color
-   * of the content of the view (if any).
-   *
-   * This can be used by the layout stack to alter the color of the
-   * header to match that of the content.
-   *
-   * Since: 3.32
-   */
-  properties [PROP_PRIMARY_COLOR_BG] =
-    g_param_spec_boxed ("primary-color-bg",
-                        "Primary Color Background",
-                        "The primary foreground color of the content",
-                        GDK_TYPE_RGBA,
-                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  /**
-   * IdePage:primary-color-fg:
-   *
-   * The "primary-color-fg" property should describe the foreground
-   * to use for content above primary-color-bg.
-   *
-   * This can be used by the layout stack to alter the color of the
-   * foreground to match that of the content.
-   *
-   * Since: 3.32
-   */
-  properties [PROP_PRIMARY_COLOR_FG] =
-    g_param_spec_boxed ("primary-color-fg",
-                        "Primary Color Foreground",
-                        "The primary foreground color of the content",
-                        GDK_TYPE_RGBA,
-                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_TITLE] =
-    g_param_spec_string ("title",
-                         "Title",
-                         "The title of the document or view",
-                         NULL,
-                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   /**
@@ -401,8 +272,6 @@ ide_page_class_init (IdePageClass *klass)
    * set to %TRUE. The default is %FALSE.
    *
    * Returns: (transfer full): A newly created #IdePage
-   *
-   * Since: 3.32
    */
   signals [CREATE_SPLIT] =
     g_signal_new (g_intern_static_string ("create-split"),
@@ -411,58 +280,28 @@ ide_page_class_init (IdePageClass *klass)
                   G_STRUCT_OFFSET (IdePageClass, create_split),
                   g_signal_accumulator_first_wins, NULL,
                   NULL, IDE_TYPE_PAGE, 0);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "page");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-page.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdePage, content_box);
+  gtk_widget_class_bind_template_child_private (widget_class, IdePage, overlay);
+  gtk_widget_class_bind_template_child_private (widget_class, IdePage, progress_bar);
 }
 
 static void
 ide_page_init (IdePage *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);
+  gtk_widget_init_template (GTK_WIDGET (self));
 
-  priv->mru_link.data = self;
-  priv->icon_name = g_intern_string ("text-x-generic-symbolic");
+  panel_widget_set_kind (PANEL_WIDGET (self), PANEL_WIDGET_KIND_DOCUMENT);
 
-  /* Add an action group out of convenience to plugins that want to
-   * stash a simple action somewhere.
-   */
-  gtk_widget_insert_action_group (GTK_WIDGET (self), "view", G_ACTION_GROUP (group));
-}
-
-GtkWidget *
-ide_page_new (void)
-{
-  return g_object_new (IDE_TYPE_PAGE, NULL);
-}
-
-const gchar *
-ide_page_get_title (IdePage *self)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
-
-  return priv->title;
-}
-
-void
-ide_page_set_title (IdePage     *self,
-                    const gchar *title)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_PAGE (self));
-
-  if (g_strcmp0 (title, priv->title) != 0)
-    {
-      g_free (priv->title);
-      priv->title = g_strdup (title);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
-    }
+  priv->mru_link.data = self;
 }
 
-const gchar *
+const char *
 ide_page_get_menu_id (IdePage *self)
 {
   IdePagePrivate *priv = ide_page_get_instance_private (self);
@@ -473,8 +312,8 @@ ide_page_get_menu_id (IdePage *self)
 }
 
 void
-ide_page_set_menu_id (IdePage     *self,
-                      const gchar *menu_id)
+ide_page_set_menu_id (IdePage    *self,
+                      const char *menu_id)
 {
   IdePagePrivate *priv = ide_page_get_instance_private (self);
 
@@ -484,7 +323,13 @@ ide_page_set_menu_id (IdePage     *self,
 
   if (menu_id != priv->menu_id)
     {
+      GMenu *menu;
+
       priv->menu_id = menu_id;
+
+      menu = ide_application_get_menu_by_id (IDE_APPLICATION_DEFAULT, menu_id);
+      panel_widget_set_menu_model (PANEL_WIDGET (self), G_MENU_MODEL (menu));
+
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
     }
 }
@@ -539,99 +384,6 @@ ide_page_set_failed (IdePage  *self,
     }
 }
 
-gboolean
-ide_page_get_modified (IdePage *self)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
-
-  return priv->modified;
-}
-
-void
-ide_page_set_modified (IdePage  *self,
-                       gboolean  modified)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_PAGE (self));
-
-  modified = !!modified;
-
-  if (priv->modified != modified)
-    {
-      priv->modified = modified;
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
-    }
-}
-
-/**
- * ide_page_get_icon:
- * @self: a #IdePage
- *
- * Gets the #GIcon to represent the view.
- *
- * Returns: (transfer none) (nullable): A #GIcon or %NULL
- *
- * Since: 3.32
- */
-GIcon *
-ide_page_get_icon (IdePage *self)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
-
-  if (priv->icon == NULL)
-    {
-      if (priv->icon_name != NULL)
-        priv->icon = g_icon_new_for_string (priv->icon_name, NULL);
-    }
-
-  return priv->icon;
-}
-
-void
-ide_page_set_icon (IdePage *self,
-                   GIcon   *icon)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (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_page_get_icon_name (IdePage *self)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
-
-  return priv->icon_name;
-}
-
-void
-ide_page_set_icon_name (IdePage     *self,
-                        const gchar *icon_name)
-{
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-
-  g_return_if_fail (IDE_IS_PAGE (self));
-
-  icon_name = g_intern_string (icon_name);
-
-  if (icon_name != priv->icon_name)
-    {
-      priv->icon_name = icon_name;
-      g_clear_object (&priv->icon);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
-    }
-}
-
 gboolean
 ide_page_get_can_split (IdePage *self)
 {
@@ -669,8 +421,6 @@ ide_page_set_can_split (IdePage  *self,
  * The view should be added to an #IdeLayoutStack where appropriate.
  *
  * Returns: (nullable) (transfer full): A newly created #IdePage or %NULL.
- *
- * Since: 3.32
  */
 IdePage *
 ide_page_create_split (IdePage *self)
@@ -690,206 +440,238 @@ ide_page_create_split (IdePage *self)
 }
 
 /**
- * ide_page_get_primary_color_bg:
+ * ide_page_report_error:
  * @self: a #IdePage
+ * @format: a printf-style format string
  *
- * 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.
- *
- * Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
+ * This function reports an error to the user in the layout view.
  *
- * Since: 3.32
+ * @format should be a printf-style format string followed by the
+ * arguments for the format.
  */
-const GdkRGBA *
-ide_page_get_primary_color_bg (IdePage *self)
+void
+ide_page_report_error (IdePage    *self,
+                       const char *format,
+                       ...)
 {
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  g_autofree char *message = NULL;
+  GtkInfoBar *infobar;
+  GtkLabel *label;
+  va_list args;
 
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  va_start (args, format);
+  message = g_strdup_vprintf (format, args);
+  va_end (args);
 
-  return priv->primary_color_bg_set ?  &priv->primary_color_bg : NULL;
+  infobar = g_object_new (GTK_TYPE_INFO_BAR,
+                          "message-type", GTK_MESSAGE_WARNING,
+                          "show-close-button", TRUE,
+                          "visible", TRUE,
+                          NULL);
+  g_signal_connect (infobar,
+                    "response",
+                    G_CALLBACK (gtk_widget_unparent),
+                    NULL);
+  g_signal_connect (infobar,
+                    "close",
+                    G_CALLBACK (gtk_widget_unparent),
+                    NULL);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", message,
+                        "visible", TRUE,
+                        "wrap", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+
+  gtk_info_bar_add_child (infobar, GTK_WIDGET (label));
+  gtk_widget_insert_after (GTK_WIDGET (infobar), GTK_WIDGET (self), NULL);
 }
 
 /**
- * ide_page_set_primary_color_bg:
+ * ide_page_get_file_or_directory:
  * @self: a #IdePage
- * @primary_color_bg: (nullable): a #GdkRGBA or %NULL
  *
- * Sets the #IdePage:primary-color-bg property.
- * If @primary_color_bg is %NULL, the property is unset.
+ * Gets a #GFile representing a file or directory that best maps to this
+ * page. A terminal might use the current working directory while an editor
+ * or designer might use the backing file.
  *
- * Since: 3.32
+ * Returns: (transfer full) (nullable): a #GFile or %NULL
  */
+GFile *
+ide_page_get_file_or_directory (IdePage *self)
+{
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  if (IDE_PAGE_GET_CLASS (self)->get_file_or_directory)
+    return IDE_PAGE_GET_CLASS (self)->get_file_or_directory (self);
+
+  return NULL;
+}
+
 void
-ide_page_set_primary_color_bg (IdePage       *self,
-                               const GdkRGBA *primary_color_bg)
+ide_page_add_content_widget (IdePage   *self,
+                             GtkWidget *widget)
 {
   IdePagePrivate *priv = ide_page_get_instance_private (self);
-  gboolean old_set;
-  GdkRGBA old;
 
   g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
 
-  old_set = priv->primary_color_bg_set;
-  old = priv->primary_color_bg;
+  gtk_box_append (priv->content_box, widget);
+}
 
-  if (primary_color_bg != NULL)
-    {
-      priv->primary_color_bg = *primary_color_bg;
-      priv->primary_color_bg_set = TRUE;
-    }
-  else
+static void
+ide_page_add_child (GtkBuildable *buildable,
+                    GtkBuilder   *builder,
+                    GObject      *object,
+                    const char   *name)
+{
+  IdePage *self = (IdePage *)buildable;
+
+  g_assert (IDE_IS_PAGE (self));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (G_IS_OBJECT (object));
+
+  if (GTK_IS_WIDGET (object))
     {
-      memset (&priv->primary_color_bg, 0, sizeof priv->primary_color_bg);
-      priv->primary_color_bg_set = FALSE;
+      if (g_strcmp0 (name, "content") == 0)
+        {
+          ide_page_add_content_widget (self, GTK_WIDGET (object));
+          return;
+        }
     }
 
-  if (old_set != priv->primary_color_bg_set ||
-      !gdk_rgba_equal (&old, &priv->primary_color_bg))
-    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_BG]);
+  parent_buildable->add_child (buildable, builder, object, name);
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  parent_buildable = g_type_interface_peek_parent (iface);
+  iface->add_child = ide_page_add_child;
 }
 
 /**
- * ide_page_get_primary_color_fg:
+ * ide_page_set_progress:
  * @self: a #IdePage
+ * @notification: (nullable): an #IdeNotification or %NULL
  *
- * 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.
- *
- * Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
+ * Set interactive progress for the page.
  *
- * Since: 3.32
+ * When the operation is completed, the caller shoudl call this method
+ * again and reutrn a value of %NULL for @notification.
  */
-const GdkRGBA *
-ide_page_get_primary_color_fg (IdePage *self)
+void
+ide_page_set_progress (IdePage         *self,
+                       IdeNotification *notification)
 {
   IdePagePrivate *priv = ide_page_get_instance_private (self);
 
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+  g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_if_fail (!notification || IDE_IS_NOTIFICATION (notification));
+
+  if (notification == NULL)
+    {
+      ide_gtk_widget_hide_with_fade (GTK_WIDGET (priv->progress_bar));
+      return;
+    }
 
-  return priv->primary_color_fg_set ?  &priv->primary_color_fg : NULL;
+  gtk_progress_bar_set_fraction (priv->progress_bar, .0);
+  gtk_widget_show (GTK_WIDGET (priv->progress_bar));
+  g_object_bind_property (notification, "progress",
+                          priv->progress_bar, "fraction",
+                          G_BINDING_SYNC_CREATE);
 }
 
 /**
- * ide_page_set_primary_color_fg:
+ * ide_page_get_position:
  * @self: a #IdePage
- * @primary_color_fg: (nullable): a #GdkRGBA or %NULL
  *
- * Sets the #IdePage:primary-color-fg property.
- * If @primary_color_fg is %NULL, the property is unset.
+ * Gets the position of a page within the workspace.
  *
- * Since: 3.32
+ * Returns: (transfer full) (nullable): an #IdePanelPosition or %NULL
+ *   if the page is not rooted.
  */
-void
-ide_page_set_primary_color_fg (IdePage       *self,
-                               const GdkRGBA *primary_color_fg)
+IdePanelPosition *
+ide_page_get_position (IdePage *self)
 {
-  IdePagePrivate *priv = ide_page_get_instance_private (self);
-  gboolean old_set;
-  GdkRGBA old;
+  IdePanelPosition *position;
+  GtkWidget *frame;
+  guint n_pages;
 
-  g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
 
-  old_set = priv->primary_color_fg_set;
-  old = priv->primary_color_fg;
+  if (!(frame = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME)))
+    return NULL;
 
-  if (primary_color_fg != NULL)
-    {
-      priv->primary_color_fg = *primary_color_fg;
-      priv->primary_color_fg_set = TRUE;
-    }
-  else
+  if (!(position = ide_frame_get_position (IDE_FRAME (frame))))
+    return NULL;
+
+  n_pages = panel_frame_get_n_pages (PANEL_FRAME (frame));
+
+  for (guint i = 0; i < n_pages; i++)
     {
-      memset (&priv->primary_color_fg, 0, sizeof priv->primary_color_fg);
-      priv->primary_color_fg_set = FALSE;
+      if (panel_frame_get_page (PANEL_FRAME (frame), i) == PANEL_WIDGET (self))
+        {
+          ide_panel_position_set_depth (position, i);
+          return position;
+        }
     }
 
-  if (old_set != priv->primary_color_fg_set ||
-      !gdk_rgba_equal (&old, &priv->primary_color_fg))
-    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_FG]);
+  g_critical ("Failed to find page within frame");
+
+  return position;
 }
 
-/**
- * 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.
- *
- * @format should be a printf-style format string followed by the
- * arguments for the format.
- *
- * Since: 3.32
- */
 void
-ide_page_report_error (IdePage     *self,
-                       const gchar *format,
-                       ...)
+ide_page_destroy (IdePage *self)
 {
-  g_autofree gchar *message = NULL;
-  GtkInfoBar *infobar;
-  GtkWidget *content_area;
-  GtkLabel *label;
-  va_list args;
+  GtkWidget *frame;
 
   g_return_if_fail (IDE_IS_PAGE (self));
 
-  va_start (args, format);
-  message = g_strdup_vprintf (format, args);
-  va_end (args);
+  if ((frame = gtk_widget_get_ancestor (GTK_WIDGET (self), PANEL_TYPE_FRAME)))
+    panel_frame_remove (PANEL_FRAME (frame), PANEL_WIDGET (self));
+}
 
-  infobar = g_object_new (GTK_TYPE_INFO_BAR,
-                          "message-type", GTK_MESSAGE_WARNING,
-                          "show-close-button", TRUE,
-                          "visible", TRUE,
-                          NULL);
-  g_signal_connect (infobar,
-                    "response",
-                    G_CALLBACK (gtk_widget_destroy),
-                    NULL);
-  g_signal_connect (infobar,
-                    "close",
-                    G_CALLBACK (gtk_widget_destroy),
-                    NULL);
+void
+ide_page_observe (IdePage  *self,
+                  IdePage **location)
+{
+  g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_if_fail (location != NULL);
 
-  label = g_object_new (GTK_TYPE_LABEL,
-                        "label", message,
-                        "visible", TRUE,
-                        "wrap", TRUE,
-                        "xalign", 0.0f,
-                        NULL);
+  *location = self;
+  g_signal_connect_swapped (self,
+                            "destroy",
+                            G_CALLBACK (g_nullify_pointer),
+                            location);
+}
 
-  content_area = gtk_info_bar_get_content_area (infobar);
-  gtk_container_add (GTK_CONTAINER (content_area), GTK_WIDGET (label));
+void
+ide_page_unobserve (IdePage  *self,
+                    IdePage **location)
+{
+  g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_if_fail (location != NULL);
 
-  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (infobar),
-                                     "position", 0,
-                                     NULL);
+  g_signal_handlers_disconnect_by_func (self,
+                                        G_CALLBACK (g_nullify_pointer),
+                                        location);
+  *location = NULL;
 }
 
-/**
- * ide_page_get_file_or_directory:
- * @self: a #IdePage
- *
- * Gets a #GFile representing a file or directory that best maps to this
- * page. A terminal might use the current working directory while an editor
- * or designer might use the backing file.
- *
- * Returns: (transfer full) (nullable): a #GFile or %NULL
- *
- * Since: 3.40
- */
-GFile *
-ide_page_get_file_or_directory (IdePage *self)
+void
+ide_clear_page (IdePage **location)
 {
-  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+  IdePage *self = *location;
 
-  if (IDE_PAGE_GET_CLASS (self)->get_file_or_directory)
-    return IDE_PAGE_GET_CLASS (self)->get_file_or_directory (self);
+  if (self == NULL)
+    return;
 
-  return NULL;
+  ide_page_unobserve (self, location);
+  ide_page_destroy (self);
 }
diff --git a/src/libide/gui/ide-page.h b/src/libide/gui/ide-page.h
index 90a29cf7f..c0bbbafae 100644
--- a/src/libide/gui/ide-page.h
+++ b/src/libide/gui/ide-page.h
@@ -24,19 +24,25 @@
 # error "Only <libide-gui.h> can be included directly."
 #endif
 
-#include <gtk/gtk.h>
+#include <libpanel.h>
+
 #include <libide-core.h>
 
+#include "ide-panel-position.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)
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdePage, ide_page, IDE, PAGE, PanelWidget)
+
+typedef void (*IdePageCallback) (IdePage  *page,
+                                 gpointer  user_data);
 
 struct _IdePageClass
 {
-  GtkBoxClass parent_class;
+  PanelWidgetClass parent_class;
 
   void           (*agree_to_close_async)  (IdePage              *self,
                                            GCancellable         *cancellable,
@@ -49,74 +55,60 @@ struct _IdePageClass
   GFile         *(*get_file_or_directory) (IdePage              *self);
 
   /*< private >*/
-  gpointer _reserved[16];
+  gpointer _reserved[8];
 };
 
-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);
-IDE_AVAILABLE_IN_3_40
-GFile         *ide_page_get_file_or_directory (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_page_get_can_split         (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_set_can_split         (IdePage              *self,
+                                                  gboolean              can_split);
+IDE_AVAILABLE_IN_ALL
+IdePage          *ide_page_create_split          (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_page_get_failed            (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_set_failed            (IdePage              *self,
+                                                  gboolean              failed);
+IDE_AVAILABLE_IN_ALL
+const gchar      *ide_page_get_menu_id           (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_set_menu_id           (IdePage              *self,
+                                                  const gchar          *menu_id);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_agree_to_close_async  (IdePage              *self,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_page_agree_to_close_finish (IdePage              *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_mark_used             (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_report_error          (IdePage              *self,
+                                                  const gchar          *format,
+                                                  ...) G_GNUC_PRINTF (2, 3);
+IDE_AVAILABLE_IN_ALL
+GFile            *ide_page_get_file_or_directory (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_set_progress          (IdePage              *self,
+                                                  IdeNotification      *notification);
+IDE_AVAILABLE_IN_ALL
+IdePanelPosition *ide_page_get_position          (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_observe               (IdePage              *self,
+                                                  IdePage             **location);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_unobserve             (IdePage              *self,
+                                                  IdePage             **location);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_destroy               (IdePage              *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_clear_page                 (IdePage             **location);
+IDE_AVAILABLE_IN_ALL
+void              ide_page_add_content_widget    (IdePage              *self,
+                                                  GtkWidget            *widget);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-page.ui b/src/libide/gui/ide-page.ui
new file mode 100644
index 000000000..7e000d28e
--- /dev/null
+++ b/src/libide/gui/ide-page.ui
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdePage" parent="PanelWidget">
+    <property name="hexpand">true</property>
+    <property name="vexpand">true</property>
+    <property name="icon-name">text-x-generic-symbolic</property>
+    <child>
+      <object class="GtkOverlay" id="overlay">
+        <child>
+          <object class="GtkBox" id="content_box">
+            <property name="orientation">vertical</property>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="valign">start</property>
+            <property name="hexpand">true</property>
+            <style>
+              <class name="osd"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-pane.c b/src/libide/gui/ide-pane.c
index bb1c7ec0a..073a66b78 100644
--- a/src/libide/gui/ide-pane.c
+++ b/src/libide/gui/ide-pane.c
@@ -22,18 +22,154 @@
 
 #include "config.h"
 
+#include <libide-tree.h>
+
 #include "ide-pane.h"
 
-G_DEFINE_TYPE (IdePane, ide_pane, DZL_TYPE_DOCK_WIDGET)
+typedef struct
+{
+  GList *popovers;
+} IdePanePrivate;
+
+static void popover_positioner_iface_init (IdePopoverPositionerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdePane, ide_pane, PANEL_TYPE_WIDGET,
+                         G_ADD_PRIVATE (IdePane)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_POPOVER_POSITIONER, popover_positioner_iface_init))
+
+static gboolean
+release_popover_in_idle (gpointer data)
+{
+  struct {
+    IdePane *self;
+    GtkPopover *popover;
+  } *pair = data;
+
+  gtk_widget_unparent (GTK_WIDGET (pair->popover));
+  g_clear_object (&pair->self);
+  g_clear_object (&pair->popover);
+  g_slice_free1 (sizeof *pair, pair);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_pane_popover_closed_cb (IdePane    *self,
+                            GtkPopover *popover)
+{
+  IdePanePrivate *priv = ide_pane_get_instance_private (self);
+  struct {
+    IdePane *self;
+    GtkPopover *popover;
+  } *pair;
+
+  g_assert (IDE_IS_PANE (self));
+  g_assert (GTK_IS_POPOVER (popover));
+
+  g_signal_handlers_disconnect_by_func (popover,
+                                        G_CALLBACK (ide_pane_popover_closed_cb),
+                                        self);
+  priv->popovers = g_list_remove (priv->popovers, popover);
+
+  /* Perform the unparent from the idle as the popover menu will not be
+   * activating the action until after the popover is closed. That way
+   * we don't lose our action muxer before the action is fired.
+   */
+  pair = g_slice_alloc0 (sizeof *pair);
+  pair->self = g_object_ref (self);
+  pair->popover = g_object_ref (popover);
+  g_idle_add_full (G_PRIORITY_HIGH,
+                   release_popover_in_idle,
+                   pair,
+                   NULL);
+
+}
+
+static void
+ide_pane_popover_positioner_present (IdePopoverPositioner *positioner,
+                                     GtkPopover           *popover,
+                                     GtkWidget            *relative_to,
+                                     const GdkRectangle   *pointing_to)
+{
+  IdePane *self = (IdePane *)positioner;
+  IdePanePrivate *priv = ide_pane_get_instance_private (self);
+  g_autoptr(IdePanelPosition) position = NULL;
+  PanelDockPosition edge = 0;
+  GdkRectangle translated;
+  double x, y;
+
+  g_assert (IDE_IS_PANE (self));
+  g_assert (GTK_IS_POPOVER (popover));
+  g_assert (GTK_IS_WIDGET (relative_to));
+  g_assert (pointing_to != NULL);
+
+  if ((position = ide_pane_get_position (self)) &&
+      ide_panel_position_get_edge (position, &edge))
+    {
+      if (edge == PANEL_DOCK_POSITION_START)
+        gtk_popover_set_position (popover, GTK_POS_RIGHT);
+      else
+        gtk_popover_set_position (popover, GTK_POS_LEFT);
+    }
+
+  gtk_widget_translate_coordinates (GTK_WIDGET (relative_to),
+                                    GTK_WIDGET (self),
+                                    pointing_to->x, pointing_to->y,
+                                    &x, &y);
+  translated = (GdkRectangle) { x, y, pointing_to->width, pointing_to->height };
+  gtk_popover_set_pointing_to (popover, &translated);
+
+  priv->popovers = g_list_append (priv->popovers, popover);
+  gtk_widget_set_parent (GTK_WIDGET (popover), GTK_WIDGET (self));
+  g_signal_connect_object (popover,
+                           "closed",
+                           G_CALLBACK (ide_pane_popover_closed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_popover_popup (popover);
+}
+
+static void
+popover_positioner_iface_init (IdePopoverPositionerInterface *iface)
+{
+  iface->present = ide_pane_popover_positioner_present;
+}
+
+static void
+ide_pane_size_allocate (GtkWidget *widget,
+                        int        width,
+                        int        height,
+                        int        baseline)
+{
+  IdePane *self = (IdePane *)widget;
+  IdePanePrivate *priv = ide_pane_get_instance_private (self);
+
+  g_assert (IDE_IS_PANE (self));
+
+  GTK_WIDGET_CLASS (ide_pane_parent_class)->size_allocate (widget, width, height, baseline);
+
+  for (const GList *iter = priv->popovers; iter != NULL; iter = iter->next)
+    {
+      GtkPopover *popover = iter->data;
+
+      g_assert (GTK_IS_POPOVER (popover));
+
+      gtk_popover_present (popover);
+    }
+}
 
 static void
 ide_pane_class_init (IdePaneClass *klass)
 {
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->size_allocate = ide_pane_size_allocate;
 }
 
 static void
 ide_pane_init (IdePane *self)
 {
+  panel_widget_set_kind (PANEL_WIDGET (self), PANEL_WIDGET_KIND_UTILITY);
 }
 
 /**
@@ -44,11 +180,130 @@ ide_pane_init (IdePane *self)
  * 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);
 }
+
+void
+ide_pane_destroy (IdePane *self)
+{
+  GtkWidget *frame;
+
+  g_return_if_fail (IDE_IS_PANE (self));
+
+  if ((frame = gtk_widget_get_ancestor (GTK_WIDGET (self), PANEL_TYPE_FRAME)))
+    panel_frame_remove (PANEL_FRAME (frame), PANEL_WIDGET (self));
+}
+
+void
+ide_pane_observe (IdePane  *self,
+                  IdePane **location)
+{
+  g_return_if_fail (IDE_IS_PANE (self));
+  g_return_if_fail (location != NULL);
+
+  *location = self;
+  g_signal_connect_swapped (self,
+                            "destroy",
+                            G_CALLBACK (g_nullify_pointer),
+                            location);
+}
+
+void
+ide_pane_unobserve (IdePane  *self,
+                    IdePane **location)
+{
+  g_return_if_fail (IDE_IS_PANE (self));
+  g_return_if_fail (location != NULL);
+
+  g_signal_handlers_disconnect_by_func (self,
+                                        G_CALLBACK (g_nullify_pointer),
+                                        location);
+  *location = NULL;
+}
+
+void
+ide_clear_pane (IdePane **location)
+{
+  IdePane *self = *location;
+
+  if (self == NULL)
+    return;
+
+  ide_pane_unobserve (self, location);
+  ide_pane_destroy (self);
+}
+
+/**
+ * ide_pane_get_position:
+ * @self: a #IdePane
+ *
+ * Gets the position of the panel or %NULL
+ *
+ * Returns: (transfer full) (nullable): an #IdePanelPosition or %NULL
+ */
+IdePanelPosition *
+ide_pane_get_position (IdePane *self)
+{
+  static GType dock_child_type = G_TYPE_INVALID;
+  PanelDockPosition position;
+  IdePanelPosition *ret;
+  GtkWidget *frame;
+  GtkWidget *paned;
+  GtkWidget *edge;
+  guint n_pages;
+  int depth = 0;
+  int row_or_column = 0;
+
+  g_return_val_if_fail (IDE_IS_PANE (self), NULL);
+
+  if (dock_child_type == G_TYPE_INVALID)
+    {
+      if (!(dock_child_type = g_type_from_name ("PanelDockChild")))
+        g_return_val_if_reached (NULL);
+    }
+
+  if (!(frame = gtk_widget_get_ancestor (GTK_WIDGET (self), PANEL_TYPE_FRAME)))
+    g_return_val_if_reached (NULL);
+
+  n_pages = panel_frame_get_n_pages (PANEL_FRAME (frame));
+
+  for (guint i = 0; i < n_pages; i++)
+    {
+      if (panel_frame_get_page (PANEL_FRAME (frame), i) == PANEL_WIDGET (self))
+        {
+          depth = i;
+          break;
+        }
+    }
+
+  if (!(paned = gtk_widget_get_ancestor (frame, PANEL_TYPE_PANED)))
+    g_return_val_if_reached (NULL);
+
+  for (GtkWidget *child = gtk_widget_get_first_child (paned);
+       child != NULL && !gtk_widget_is_ancestor (frame, child);
+       child = gtk_widget_get_next_sibling (child))
+    row_or_column++;
+
+  if (!(edge = gtk_widget_get_ancestor (paned, dock_child_type)))
+    g_return_val_if_reached (NULL);
+
+  g_object_get (edge,
+                "position", &position,
+                NULL);
+
+  ret = ide_panel_position_new ();
+  ide_panel_position_set_edge (ret, position);
+  ide_panel_position_set_depth (ret, depth);
+
+  if (position == PANEL_DOCK_POSITION_START ||
+      position == PANEL_DOCK_POSITION_END)
+    ide_panel_position_set_row (ret, row_or_column);
+  else
+    ide_panel_position_set_column (ret, row_or_column);
+
+  return ret;
+}
diff --git a/src/libide/gui/ide-pane.h b/src/libide/gui/ide-pane.h
index 0eed763d5..0aea03cb6 100644
--- a/src/libide/gui/ide-pane.h
+++ b/src/libide/gui/ide-pane.h
@@ -1,6 +1,6 @@
 /* ide-pane.h
  *
- * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2022 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,25 +24,37 @@
 # error "Only <libide-gui.h> can be included directly."
 #endif
 
-#include <dazzle.h>
+#include <libpanel.h>
+
 #include <libide-core.h>
 
+#include "ide-panel-position.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)
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdePane, ide_pane, IDE, PANE, PanelWidget)
 
 struct _IdePaneClass
 {
-  DzlDockWidgetClass parent_class;
-
-  /*< private >*/
-  gpointer _reserved[16];
+  PanelWidgetClass parent_class;
 };
 
-IDE_AVAILABLE_IN_3_32
-GtkWidget *ide_pane_new (void);
+IDE_AVAILABLE_IN_ALL
+GtkWidget *ide_pane_new       (void);
+IDE_AVAILABLE_IN_ALL
+void       ide_pane_destroy   (IdePane *self);
+IDE_AVAILABLE_IN_ALL
+void       ide_pane_observe   (IdePane  *self,
+                               IdePane **location);
+IDE_AVAILABLE_IN_ALL
+void       ide_pane_unobserve (IdePane  *self,
+                               IdePane **location);
+IDE_AVAILABLE_IN_ALL
+void       ide_clear_pane     (IdePane **location);
+IDE_AVAILABLE_IN_ALL
+IdePanelPosition *ide_pane_get_position (IdePane *self);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-panel-position.c b/src/libide/gui/ide-panel-position.c
new file mode 100644
index 000000000..2d8acdd20
--- /dev/null
+++ b/src/libide/gui/ide-panel-position.c
@@ -0,0 +1,174 @@
+/* ide-panel-position.c
+ *
+ * Copyright 2022 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-panel-position"
+
+#include "config.h"
+
+#include "ide-panel-position.h"
+
+struct _IdePanelPosition
+{
+  guint column : 8;
+  guint row : 8;
+  guint depth : 9;
+  PanelDockPosition edge : 3;
+  guint column_set : 1;
+  guint row_set : 1;
+  guint depth_set : 1;
+  guint edge_set : 1;
+};
+
+G_DEFINE_BOXED_TYPE (IdePanelPosition, ide_panel_position, ide_panel_position_ref, ide_panel_position_unref)
+
+IdePanelPosition *
+ide_panel_position_new (void)
+{
+  return g_rc_box_alloc0 (sizeof (IdePanelPosition));
+}
+
+IdePanelPosition *
+ide_panel_position_ref (IdePanelPosition *self)
+{
+  return g_rc_box_acquire (self);
+}
+
+void
+ide_panel_position_unref (IdePanelPosition *self)
+{
+  g_rc_box_release (self);
+}
+
+gboolean
+ide_panel_position_get_edge (IdePanelPosition  *self,
+                             PanelDockPosition *edge)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (edge != NULL)
+    *edge = self->edge;
+
+  return self->edge_set;
+}
+
+void
+ide_panel_position_set_edge (IdePanelPosition  *self,
+                             PanelDockPosition  edge)
+{
+  g_return_if_fail (self != NULL);
+
+  self->edge = edge;
+  self->edge_set = TRUE;
+}
+
+/**
+ * ide_panel_position_get_column:
+ * @self: a #IdePanelPosition
+ * @column: (out): a location for a column
+ *
+ * Returns: %TRUE if the column is set
+ */
+gboolean
+ide_panel_position_get_column (IdePanelPosition *self,
+                               guint            *column)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (column != NULL)
+    *column = self->column;
+
+  return self->column_set;
+}
+
+void
+ide_panel_position_set_column (IdePanelPosition *self,
+                               guint             column)
+{
+  g_return_if_fail (self != NULL);
+
+  self->column = column;
+  self->column_set = TRUE;
+}
+
+/**
+ * ide_panel_position_get_row:
+ * @self: a #IdePanelPosition
+ * @row: (out): a location for the row
+ *
+ * Returns: %TRUE if the row is set
+ */
+gboolean
+ide_panel_position_get_row (IdePanelPosition *self,
+                            guint            *row)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (row != NULL)
+    *row = self->row;
+
+  return self->row_set;
+}
+
+void
+ide_panel_position_set_row (IdePanelPosition *self,
+                            guint             row)
+{
+  g_return_if_fail (self != NULL);
+
+  self->row = row;
+  self->row_set = TRUE;
+}
+
+/**
+ * ide_panel_position_get_depth:
+ * @self: a #IdePanelPosition
+ * @depth: (out): a location for the depth
+ *
+ * Returns: %TRUE if the depth is set
+ */
+gboolean
+ide_panel_position_get_depth (IdePanelPosition *self,
+                              guint            *depth)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (depth != NULL)
+    *depth = self->depth;
+
+  return self->depth_set;
+}
+
+void
+ide_panel_position_set_depth (IdePanelPosition *self,
+                              guint             depth)
+{
+  g_return_if_fail (self != NULL);
+
+  self->depth = depth;
+  self->depth_set = TRUE;
+}
+
+gboolean
+ide_panel_position_is_indeterminate (IdePanelPosition *self)
+{
+  g_return_val_if_fail (self != NULL, TRUE);
+
+  return !self->column_set || !self->row_set || !self->edge_set;
+}
diff --git a/src/libide/gui/ide-panel-position.h b/src/libide/gui/ide-panel-position.h
new file mode 100644
index 000000000..127459a9a
--- /dev/null
+++ b/src/libide/gui/ide-panel-position.h
@@ -0,0 +1,74 @@
+/* ide-panel-position.h
+ *
+ * Copyright 2022 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 <libpanel.h>
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PANEL_POSITION (ide_panel_position_get_type())
+
+typedef struct _IdePanelPosition IdePanelPosition;
+
+IDE_AVAILABLE_IN_ALL
+GType             ide_panel_position_get_type         (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_ALL
+IdePanelPosition *ide_panel_position_new              (void);
+IDE_AVAILABLE_IN_ALL
+IdePanelPosition *ide_panel_position_ref              (IdePanelPosition  *self);
+IDE_AVAILABLE_IN_ALL
+void              ide_panel_position_unref            (IdePanelPosition  *self);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_panel_position_get_edge         (IdePanelPosition  *self,
+                                                       PanelDockPosition *edge);
+IDE_AVAILABLE_IN_ALL
+void              ide_panel_position_set_edge         (IdePanelPosition  *self,
+                                                       PanelDockPosition  edge);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_panel_position_get_row          (IdePanelPosition  *self,
+                                                       guint             *row);
+IDE_AVAILABLE_IN_ALL
+void              ide_panel_position_set_row          (IdePanelPosition  *self,
+                                                       guint              row);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_panel_position_get_column       (IdePanelPosition  *self,
+                                                       guint             *column);
+IDE_AVAILABLE_IN_ALL
+void              ide_panel_position_set_column       (IdePanelPosition  *self,
+                                                       guint              column);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_panel_position_get_depth        (IdePanelPosition  *self,
+                                                       guint             *depth);
+IDE_AVAILABLE_IN_ALL
+void              ide_panel_position_set_depth        (IdePanelPosition  *self,
+                                                       guint              depth);
+IDE_AVAILABLE_IN_ALL
+gboolean          ide_panel_position_is_indeterminate (IdePanelPosition  *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdePanelPosition, ide_panel_position_unref)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-primary-workspace-actions.c b/src/libide/gui/ide-primary-workspace-actions.c
index 669b12225..e40563a99 100644
--- a/src/libide/gui/ide-primary-workspace-actions.c
+++ b/src/libide/gui/ide-primary-workspace-actions.c
@@ -23,12 +23,12 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
-#include <libide-foundry.h>
 #include <libpeas/peas.h>
 
+#include <libide-foundry.h>
+
 #include "ide-gui-global.h"
-#include "ide-gui-private.h"
-#include "ide-primary-workspace.h"
+#include "ide-primary-workspace-private.h"
 
 typedef struct
 {
diff --git a/src/libide/gui/ide-primary-workspace-private.h b/src/libide/gui/ide-primary-workspace-private.h
new file mode 100644
index 000000000..38d134025
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace-private.h
@@ -0,0 +1,29 @@
+/* ide-primary-workspace-private.h
+ *
+ * Copyright 2017-2022 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-primary-workspace.h"
+
+G_BEGIN_DECLS
+
+void _ide_primary_workspace_init_actions (IdePrimaryWorkspace *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-primary-workspace.c b/src/libide/gui/ide-primary-workspace.c
index 1a7ca43c8..4735b19af 100644
--- a/src/libide/gui/ide-primary-workspace.c
+++ b/src/libide/gui/ide-primary-workspace.c
@@ -22,14 +22,15 @@
 
 #include "config.h"
 
+#include "ide-frame.h"
+#include "ide-grid.h"
 #include "ide-gui-global.h"
-#include "ide-gui-private.h"
 #include "ide-header-bar.h"
+#include "ide-notifications-button.h"
 #include "ide-omni-bar.h"
-#include "ide-primary-workspace.h"
+#include "ide-primary-workspace-private.h"
 #include "ide-run-button.h"
-#include "ide-surface.h"
-#include "ide-window-settings-private.h"
+#include "ide-workspace-private.h"
 
 /**
  * SECTION:ide-primary-workspace
@@ -42,10 +43,6 @@
  *
  * 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
@@ -54,10 +51,17 @@ struct _IdePrimaryWorkspace
 
   /* Template widgets */
   IdeHeaderBar       *header_bar;
-  DzlMenuButton      *surface_menu_button;
   IdeRunButton       *run_button;
   GtkLabel           *project_title;
-  DzlShortcutTooltip *search_tooltip;
+  GtkMenuButton      *add_button;
+  PanelDock          *dock;
+  PanelPaned         *edge_start;
+  PanelPaned         *edge_end;
+  PanelPaned         *edge_bottom;
+  IdeGrid            *grid;
+  GtkOverlay         *overlay;
+  IdeOmniBar         *omni_bar;
+  IdeJoinedMenu      *build_menu;
 };
 
 G_DEFINE_FINAL_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE_TYPE_WORKSPACE)
@@ -67,8 +71,10 @@ ide_primary_workspace_context_set (IdeWorkspace *workspace,
                                    IdeContext   *context)
 {
   IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+  IdeConfigManager *config_manager;
   IdeProjectInfo *project_info;
   IdeWorkbench *workbench;
+  GMenuModel *config_menu;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
@@ -80,58 +86,216 @@ ide_primary_workspace_context_set (IdeWorkspace *workspace,
   project_info = ide_workbench_get_project_info (workbench);
 
   if (project_info)
-    g_object_bind_property (project_info, "name", self->project_title, "label",
+    g_object_bind_property (project_info, "name",
+                            self->project_title, "label",
                             G_BINDING_SYNC_CREATE);
+
+  config_manager = ide_config_manager_from_context (context);
+  config_menu = ide_config_manager_get_menu (config_manager);
+  ide_joined_menu_prepend_menu (self->build_menu, G_MENU_MODEL (config_menu));
+}
+
+static void
+ide_primary_workspace_add_page (IdeWorkspace     *workspace,
+                                IdePage          *page,
+                                IdePanelPosition *position)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  _ide_workspace_add_widget (workspace,
+                             PANEL_WIDGET (page),
+                             position,
+                             self->edge_start,
+                             self->edge_end,
+                             self->edge_bottom,
+                             self->grid);
+}
+
+static void
+ide_primary_workspace_add_pane (IdeWorkspace     *workspace,
+                                IdePane          *pane,
+                                IdePanelPosition *position)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  _ide_workspace_add_widget (workspace,
+                             PANEL_WIDGET (pane),
+                             position,
+                             self->edge_start,
+                             self->edge_end,
+                             self->edge_bottom,
+                             self->grid);
 }
 
 static void
-ide_primary_workspace_surface_set (IdeWorkspace *workspace,
-                                   IdeSurface   *surface)
+ide_primary_workspace_add_grid_column (IdeWorkspace *workspace,
+                                       guint         position)
+{
+  panel_grid_insert_column (PANEL_GRID (IDE_PRIMARY_WORKSPACE (workspace)->grid), position);
+}
+
+static void
+ide_primary_workspace_add_overlay (IdeWorkspace *workspace,
+                                   GtkWidget    *overlay)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  gtk_overlay_add_overlay (self->overlay, overlay);
+}
+
+static void
+ide_primary_workspace_remove_overlay (IdeWorkspace *workspace,
+                                      GtkWidget    *overlay)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  gtk_overlay_remove_overlay (self->overlay, overlay);
+}
+
+static IdeFrame *
+ide_primary_workspace_get_most_recent_frame (IdeWorkspace *workspace)
 {
   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;
+  return IDE_FRAME (panel_grid_get_most_recent_frame (PANEL_GRID (self->grid)));
+}
+
+static PanelFrame *
+ide_primary_workspace_get_frame_at_position (IdeWorkspace     *workspace,
+                                             IdePanelPosition *position)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+  g_assert (position != 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);
-    }
+  return _ide_workspace_find_frame (workspace,
+                                    position,
+                                    self->edge_start,
+                                    self->edge_end,
+                                    self->edge_bottom,
+                                    self->grid);
+}
 
-  IDE_WORKSPACE_CLASS (ide_primary_workspace_parent_class)->surface_set (workspace, surface);
+static gboolean
+ide_primary_workspace_can_search (IdeWorkspace *workspace)
+{
+  return TRUE;
+}
+
+static IdeHeaderBar *
+ide_primary_workspace_get_header_bar (IdeWorkspace *workspace)
+{
+  return IDE_PRIMARY_WORKSPACE (workspace)->header_bar;
+}
+
+static void
+ide_primary_workspace_foreach_page (IdeWorkspace    *workspace,
+                                    IdePageCallback  callback,
+                                    gpointer         user_data)
+{
+  ide_grid_foreach_page (IDE_PRIMARY_WORKSPACE (workspace)->grid, callback, user_data);
+}
+
+static void
+ide_primary_workspace_dispose (GObject *object)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)object;
+
+  /* Ensure that the grid is removed first so that it will cleanup
+   * addins/pages/etc before we ever get to removing the workspace
+   * addins as part of the parent class.
+   */
+  panel_dock_remove (self->dock, GTK_WIDGET (self->grid));
+  self->grid = NULL;
+
+  G_OBJECT_CLASS (ide_primary_workspace_parent_class)->dispose (object);
 }
 
 static void
 ide_primary_workspace_class_init (IdePrimaryWorkspaceClass *klass)
 {
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
   IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
 
-  ide_workspace_class_set_kind (workspace_class, "primary");
+  object_class->dispose = ide_primary_workspace_dispose;
 
-  workspace_class->surface_set = ide_primary_workspace_surface_set;
+  workspace_class->add_grid_column = ide_primary_workspace_add_grid_column;
+  workspace_class->add_overlay = ide_primary_workspace_add_overlay;
+  workspace_class->add_page = ide_primary_workspace_add_page;
+  workspace_class->add_pane = ide_primary_workspace_add_pane;
+  workspace_class->can_search = ide_primary_workspace_can_search;
   workspace_class->context_set = ide_primary_workspace_context_set;
+  workspace_class->foreach_page = ide_primary_workspace_foreach_page;
+  workspace_class->get_frame_at_position = ide_primary_workspace_get_frame_at_position;
+  workspace_class->get_header_bar = ide_primary_workspace_get_header_bar;
+  workspace_class->get_most_recent_frame = ide_primary_workspace_get_most_recent_frame;
+  workspace_class->remove_overlay = ide_primary_workspace_remove_overlay;
+
+  ide_workspace_class_set_kind (workspace_class, "primary");
 
   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, add_button);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, build_menu);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, dock);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, edge_bottom);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, edge_end);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, edge_start);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, grid);
   gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, omni_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, overlay);
   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_tooltip);
-  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, surface_menu_button);
 
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Return, GDK_CONTROL_MASK, 
"workbench.global-search", NULL);
+
+  g_type_ensure (IDE_TYPE_GRID);
+  g_type_ensure (IDE_TYPE_NOTIFICATIONS_BUTTON);
+  g_type_ensure (IDE_TYPE_OMNI_BAR);
   g_type_ensure (IDE_TYPE_RUN_BUTTON);
 }
 
 static void
 ide_primary_workspace_init (IdePrimaryWorkspace *self)
 {
+  GMenu *build_menu;
+  GMenu *menu;
+
   gtk_widget_init_template (GTK_WIDGET (self));
 
+  menu = ide_application_get_menu_by_id (IDE_APPLICATION_DEFAULT, "new-document-menu");
+  gtk_menu_button_set_menu_model (self->add_button, G_MENU_MODEL (menu));
+
+  build_menu = ide_application_get_menu_by_id (IDE_APPLICATION_DEFAULT, "build-menu");
+  ide_joined_menu_append_menu (self->build_menu, G_MENU_MODEL (build_menu));
+
   _ide_primary_workspace_init_actions (self);
-  _ide_window_settings_register (GTK_WINDOW (self));
+}
+
+/**
+ * ide_primary_workspace_get_omni_bar:
+ * @self: an #IdePrimaryWorkspace
+ *
+ * Retrieves the #IdeOmniBar of @self.
+ *
+ * Returns: (transfer none): an #IdeOmniBar
+ */
+IdeOmniBar *
+ide_primary_workspace_get_omni_bar (IdePrimaryWorkspace *self)
+{
+  g_return_val_if_fail (IDE_IS_PRIMARY_WORKSPACE (self), NULL);
+
+  return self->omni_bar;
 }
diff --git a/src/libide/gui/ide-primary-workspace.h b/src/libide/gui/ide-primary-workspace.h
index a20da72eb..b326ad52e 100644
--- a/src/libide/gui/ide-primary-workspace.h
+++ b/src/libide/gui/ide-primary-workspace.h
@@ -25,14 +25,17 @@
 #endif
 
 #include "ide-application.h"
-#include "ide-surface.h"
+#include "ide-omni-bar.h"
 #include "ide-workspace.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_PRIMARY_WORKSPACE (ide_primary_workspace_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE, PRIMARY_WORKSPACE, IdeWorkspace)
 
+IDE_AVAILABLE_IN_ALL
+IdeOmniBar *ide_primary_workspace_get_omni_bar (IdePrimaryWorkspace *self);
+
 G_END_DECLS
diff --git a/src/libide/gui/ide-primary-workspace.ui b/src/libide/gui/ide-primary-workspace.ui
index d9dfddef4..7cc961235 100644
--- a/src/libide/gui/ide-primary-workspace.ui
+++ b/src/libide/gui/ide-primary-workspace.ui
@@ -4,72 +4,106 @@
     <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">false</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">Switch surface</property>
+          <object class="GtkMenuButton" id="add_button">
+            <property name="icon-name">list-add-symbolic</property>
+            <property name="always-show-arrow">true</property>
+          </object>
+        </child>
+        <child type="left">
+          <object class="PanelDockSwitcher">
+            <property name="dock">dock</property>
+            <property name="position">start</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="ellipsize">end</property>
-                <property name="visible">true</property>
-                <property name="xalign">0.0</property>
+          <object class="AdwClamp">
+            <property name="orientation">horizontal</property>
+            <property name="maximum-size">500</property>
+            <child>
+              <object class="IdeOmniBar" id="omni_bar">
+                <property name="icon-name">builder-build-symbolic</property>
+                <property name="action-name">build-manager.build</property>
+                <property name="menu-model">build_menu</property>
+                <child type="placeholder">
+                  <object class="GtkLabel" id="project_title">
+                    <property name="ellipsize">end</property>
+                    <property name="xalign">0.0</property>
+                    <property name="width-chars">5</property>
+                  </object>
+                </child>
               </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="IdeSearchButton" id="search_button">
-            <property name="visible">true</property>
-            <child internal-child="entry">
-              <object class="DzlSuggestionEntry">
-                <property name="max-width-chars">20</property>
+          <object class="IdeNotificationsButton" id="notifications_button"/>
+        </child>
+        <child type="right">
+          <object class="PanelDockSwitcher">
+            <property name="dock">dock</property>
+            <property name="position">end</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="GtkButton" id="search_button">
+            <property name="action-name">workbench.global-search</property>
+            <property name="icon-name">edit-find-symbolic</property>
+            <property name="margin-end">3</property>
+            <property name="margin-start">3</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="PanelDock" id="dock">
+        <property name="start-width">300</property>
+        <property name="end-width">300</property>
+        <property name="reveal-start">true</property>
+        <property name="vexpand">true</property>
+        <child type="center">
+          <object class="GtkOverlay" id="overlay">
+            <child>
+              <object class="IdeGrid" id="grid">
               </object>
             </child>
           </object>
         </child>
-        <child type="right">
-          <object class="GtkRevealer">
-            <property name="reveal-child">false</property>
-            <property name="transition-type">slide-left</property>
-            <property name="visible">true</property>
+        <child type="start">
+          <object class="PanelPaned" id="edge_start">
+            <property name="orientation">vertical</property>
+          </object>
+        </child>
+        <child type="end">
+          <object class="PanelPaned" id="edge_end">
+            <property name="orientation">vertical</property>
             <child>
-              <object class="IdeNotificationsButton" id="notifications_button">
-                <property name="show-theatric">false</property>
-                <property name="visible">true</property>
+              <object class="PanelFrame">
               </object>
             </child>
           </object>
         </child>
+        <child type="bottom">
+          <object class="PanelPaned" id="edge_bottom">
+            <property name="orientation">horizontal</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child internal-child="statusbar">
+      <object class="PanelStatusbar">
+        <child type="suffix">
+          <object class="PanelDockSwitcher">
+            <property name="dock">dock</property>
+            <property name="position">bottom</property>
+          </object>
+        </child>
       </object>
     </child>
   </template>
-  <object class="DzlShortcutTooltip" id="search_tooltip">
-    <property name="title" translatable="yes">Search your project</property>
-    <property name="command-id">org.gnome.builder.workspace.global-search</property>
-    <property name="widget">search_button</property>
-  </object>
+  <object class="IdeJoinedMenu" id="build_menu"/>
 </interface>
diff --git a/src/libide/gui/ide-workbench-addin.c b/src/libide/gui/ide-workbench-addin.c
index 3876fe118..8b0ba2ce9 100644
--- a/src/libide/gui/ide-workbench-addin.c
+++ b/src/libide/gui/ide-workbench-addin.c
@@ -26,23 +26,6 @@
 
 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,
@@ -91,62 +74,28 @@ static void
 ide_workbench_addin_real_open_async (IdeWorkbenchAddin   *self,
                                      GFile               *file,
                                      const gchar         *hint,
+                                     int                  at_line,
+                                     int                  at_line_offset,
                                      IdeBufferOpenFlags   flags,
+                                     IdePanelPosition    *position,
                                      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);
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_async (self,
+                                                    file,
+                                                    hint,
+                                                    at_line,
+                                                    at_line_offset,
+                                                    flags,
+                                                    position,
+                                                    cancellable,
+                                                    callback,
+                                                    user_data);
 }
 
 static gboolean
@@ -165,7 +114,6 @@ ide_workbench_addin_default_init (IdeWorkbenchAddinInterface *iface)
   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;
 }
 
@@ -297,7 +245,10 @@ void
 ide_workbench_addin_open_async (IdeWorkbenchAddin   *self,
                                 GFile               *file,
                                 const gchar         *content_type,
+                                int                  at_line,
+                                int                  at_line_offset,
                                 IdeBufferOpenFlags   flags,
+                                IdePanelPosition    *position,
                                 GCancellable        *cancellable,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
@@ -309,38 +260,15 @@ ide_workbench_addin_open_async (IdeWorkbenchAddin   *self,
   IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_async (self,
                                                     file,
                                                     content_type,
+                                                    at_line,
+                                                    at_line_offset,
                                                     flags,
+                                                    position,
                                                     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,
@@ -363,8 +291,6 @@ ide_workbench_addin_open_finish (IdeWorkbenchAddin  *self,
  *
  * 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,
@@ -387,8 +313,6 @@ ide_workbench_addin_vcs_changed (IdeWorkbenchAddin *self,
  * 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,
diff --git a/src/libide/gui/ide-workbench-addin.h b/src/libide/gui/ide-workbench-addin.h
index 7f333824a..971380058 100644
--- a/src/libide/gui/ide-workbench-addin.h
+++ b/src/libide/gui/ide-workbench-addin.h
@@ -20,6 +20,11 @@
 
 #pragma once
 
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include "ide-panel-position.h"
 #include "ide-workbench.h"
 #include "ide-workspace.h"
 
@@ -27,7 +32,7 @@ G_BEGIN_DECLS
 
 #define IDE_TYPE_WORKBENCH_ADDIN (ide_workbench_addin_get_type ())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, IDE, WORKBENCH_ADDIN, GObject)
 
 struct _IdeWorkbenchAddinInterface
@@ -67,16 +72,10 @@ struct _IdeWorkbenchAddinInterface
   void     (*open_async)            (IdeWorkbenchAddin     *self,
                                      GFile                 *file,
                                      const gchar           *content_type,
+                                     int                    at_line,
+                                     int                    at_line_offset,
                                      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,
+                                     IdePanelPosition      *position,
                                      GCancellable          *cancellable,
                                      GAsyncReadyCallback    callback,
                                      gpointer               user_data);
@@ -87,72 +86,65 @@ struct _IdeWorkbenchAddinInterface
                                      IdeVcs                *vcs);
 };
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_load                  (IdeWorkbenchAddin    *self,
                                                               IdeWorkbench         *workbench);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_unload                (IdeWorkbenchAddin    *self,
                                                               IdeWorkbench         *workbench);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_load_project_async    (IdeWorkbenchAddin    *self,
                                                               IdeProjectInfo       *project_info,
                                                               GCancellable         *cancellable,
                                                               GAsyncReadyCallback   callback,
                                                               gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean           ide_workbench_addin_load_project_finish   (IdeWorkbenchAddin    *self,
                                                               GAsyncResult         *result,
                                                               GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_unload_project_async  (IdeWorkbenchAddin    *self,
                                                               IdeProjectInfo       *project_info,
                                                               GCancellable         *cancellable,
                                                               GAsyncReadyCallback   callback,
                                                               gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean           ide_workbench_addin_unload_project_finish (IdeWorkbenchAddin    *self,
                                                               GAsyncResult         *result,
                                                               GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_project_loaded        (IdeWorkbenchAddin    *self,
                                                               IdeProjectInfo       *project_info);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_workspace_added       (IdeWorkbenchAddin    *self,
                                                               IdeWorkspace         *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_workspace_removed     (IdeWorkbenchAddin    *self,
                                                               IdeWorkspace         *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean           ide_workbench_addin_can_open              (IdeWorkbenchAddin    *self,
                                                               GFile                *file,
                                                               const gchar          *content_type,
                                                               gint                 *priority);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_open_async            (IdeWorkbenchAddin    *self,
                                                               GFile                *file,
                                                               const gchar          *content_type,
+                                                              int                   at_line,
+                                                              int                   at_line_offset,
                                                               IdeBufferOpenFlags    flags,
+                                                              IdePanelPosition     *position,
                                                               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
+IDE_AVAILABLE_IN_ALL
 gboolean           ide_workbench_addin_open_finish           (IdeWorkbenchAddin    *self,
                                                               GAsyncResult         *result,
                                                               GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workbench_addin_vcs_changed           (IdeWorkbenchAddin    *self,
                                                               IdeVcs               *vcs);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkbenchAddin *ide_workbench_addin_find_by_module_name   (IdeWorkbench         *workbench,
                                                               const gchar          *module_name);
 
diff --git a/src/libide/gui/ide-workbench-private.h b/src/libide/gui/ide-workbench-private.h
new file mode 100644
index 000000000..68bc88638
--- /dev/null
+++ b/src/libide/gui/ide-workbench-private.h
@@ -0,0 +1,31 @@
+/* ide-workbench-private.h
+ *
+ * Copyright 2022 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
+
+gboolean _ide_workbench_is_last_workspace (IdeWorkbench *self,
+                                           IdeWorkspace *workspace);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workbench.c b/src/libide/gui/ide-workbench.c
index 8da7c6e1b..0369761b9 100644
--- a/src/libide/gui/ide-workbench.c
+++ b/src/libide/gui/ide-workbench.c
@@ -23,9 +23,11 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
 #include <libide-debugger.h>
+#include <libide-gtk.h>
 #include <libide-threading.h>
-#include <libpeas/peas.h>
 
 #include "ide-build-private.h"
 #include "ide-context-private.h"
@@ -34,13 +36,13 @@
 #include "ide-transfer-manager-private.h"
 
 #include "ide-application.h"
-#include "ide-command-manager.h"
 #include "ide-gui-global.h"
-#include "ide-gui-private.h"
+#include "ide-preferences-window.h"
 #include "ide-primary-workspace.h"
-#include "ide-workbench.h"
+#include "ide-shortcut-manager-private.h"
 #include "ide-workbench-addin.h"
-#include "ide-workspace.h"
+#include "ide-workbench-private.h"
+#include "ide-workspace-private.h"
 
 /**
  * SECTION:ide-workbench
@@ -54,8 +56,6 @@
  * 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
@@ -66,17 +66,17 @@ struct _IdeWorkbench
   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;
+  PeasExtensionSet    *addins;
+  GCancellable        *cancellable;
+  IdeContext          *context;
+  IdeBuildSystem      *build_system;
+  IdeProjectInfo      *project_info;
+  IdeVcs              *vcs;
+  IdeVcsMonitor       *vcs_monitor;
+  IdeSearchEngine     *search_engine;
 
   /* Various flags */
-  guint             unloaded : 1;
+  guint                unloaded : 1;
 };
 
 typedef struct
@@ -86,6 +86,7 @@ typedef struct
   GFile              *file;
   gchar              *hint;
   gchar              *content_type;
+  IdePanelPosition   *position;
   IdeBufferOpenFlags  flags;
   gint                at_line;
   gint                at_line_offset;
@@ -112,32 +113,40 @@ enum {
   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);
-static void ide_workbench_action_reload_all  (IdeWorkbench *self,
-                                              GVariant     *param);
-
-
-DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
+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_open_uri      (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);
+static void ide_workbench_action_reload_all    (IdeWorkbench *self,
+                                                GVariant     *param);
+static void ide_workbench_action_global_search (IdeWorkbench *self,
+                                                GVariant     *param);
+static void ide_workbench_action_configure     (IdeWorkbench *self,
+                                                GVariant     *param);
+
+IDE_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
   { "close", ide_workbench_action_close },
   { "open", ide_workbench_action_open },
+  { "open-uri", ide_workbench_action_open_uri, "s" },
   { "reload-files", ide_workbench_action_reload_all },
+  { "global-search", ide_workbench_action_global_search },
+  { "configure", ide_workbench_action_configure },
+  { "configure-page", ide_workbench_action_configure, "s" },
   { "-inspector", ide_workbench_action_inspector },
   { "-object-tree", ide_workbench_action_object_tree },
   { "-dump-tasks", ide_workbench_action_dump_tasks },
 })
 
 G_DEFINE_FINAL_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP,
-                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
-                                                ide_workbench_init_action_group))
+                               G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_workbench_init_action_group))
 
 static GParamSpec *properties [N_PROPS];
 
@@ -182,8 +191,6 @@ ignore_error (GError *error)
  * Helper to get the #IdeWorkbench for a given context.
  *
  * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL
- *
- * Since: 3.40
  */
 IdeWorkbench *
 ide_workbench_from_context (IdeContext *context)
@@ -271,7 +278,7 @@ ide_workbench_addin_added_cb (PeasExtensionSet *set,
     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,
+                                   (IdeWorkspaceCallback)ide_workbench_addin_added_workspace_cb,
                                    addin);
 }
 
@@ -293,7 +300,7 @@ ide_workbench_addin_removed_cb (PeasExtensionSet *set,
    * track them for cleanup.
    */
   ide_workbench_foreach_workspace (self,
-                                   (GtkCallback)ide_workbench_addin_removed_workspace_cb,
+                                   (IdeWorkspaceCallback)ide_workbench_addin_removed_workspace_cb,
                                    addin);
 
   ide_workbench_addin_unload (addin, self);
@@ -314,7 +321,7 @@ ide_workbench_notify_context_title (IdeWorkbench *self,
   title = ide_context_dup_title (context);
   formatted = g_strdup_printf (_("Builder — %s"), title);
   ide_workbench_foreach_workspace (self,
-                                   (GtkCallback)gtk_window_set_title,
+                                   (IdeWorkspaceCallback)gtk_window_set_title,
                                    formatted);
 }
 
@@ -378,9 +385,6 @@ ide_workbench_constructed (GObject *object)
   peas_extension_set_foreach (self->addins,
                               ide_workbench_addin_added_cb,
                               self);
-
-  /* Load command providers (which may register shortcuts) */
-  (void)ide_command_manager_from_context (self->context);
 }
 
 static void
@@ -419,6 +423,10 @@ ide_workbench_get_property (GObject    *object,
       g_value_set_object (value, ide_workbench_get_context (self));
       break;
 
+    case PROP_VCS:
+      g_value_set_object (value, ide_workbench_get_vcs (self));
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -462,8 +470,6 @@ ide_workbench_class_init (IdeWorkbenchClass *klass)
    *
    * 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",
@@ -479,8 +485,6 @@ ide_workbench_class_init (IdeWorkbenchClass *klass)
    * 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",
@@ -495,6 +499,7 @@ ide_workbench_class_init (IdeWorkbenchClass *klass)
 static void
 ide_workbench_init (IdeWorkbench *self)
 {
+  ide_workbench_set_action_enabled (self, "configure", FALSE);
 }
 
 static void
@@ -548,8 +553,6 @@ ide_workbench_find_addin (IdeWorkbench *self,
  * 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)
@@ -565,8 +568,6 @@ ide_workbench_new (void)
  * 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)
@@ -585,8 +586,6 @@ ide_workbench_new_for_context (IdeContext *context)
  * Gets the #IdeContext for the workbench.
  *
  * Returns: (transfer none): an #IdeContext
- *
- * Since: 3.32
  */
 IdeContext *
 ide_workbench_get_context (IdeWorkbench *self)
@@ -604,8 +603,6 @@ ide_workbench_get_context (IdeWorkbench *self)
  * 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)
@@ -621,7 +618,7 @@ ide_workbench_from_widget (GtkWidget *widget)
    * just need to get the toplevel window group property, and cast.
    */
 
-  if ((toplevel = gtk_widget_get_toplevel (widget)) &&
+  if ((toplevel = GTK_WIDGET (gtk_widget_get_native (widget))) &&
       GTK_IS_WINDOW (toplevel) &&
       (group = gtk_window_get_group (GTK_WINDOW (toplevel))) &&
       IDE_IS_WORKBENCH (group))
@@ -633,18 +630,16 @@ ide_workbench_from_widget (GtkWidget *widget)
 /**
  * ide_workbench_foreach_workspace:
  * @self: an #IdeWorkbench
- * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace
+ * @callback: (scope call): a #IdeWorkspaceCallback 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)
+ide_workbench_foreach_workspace (IdeWorkbench         *self,
+                                 IdeWorkspaceCallback  callback,
+                                 gpointer              user_data)
 {
   GList *copy;
 
@@ -659,12 +654,32 @@ ide_workbench_foreach_workspace (IdeWorkbench *self,
     {
       IdeWorkspace *workspace = iter->data;
       g_assert (IDE_IS_WORKSPACE (workspace));
-      callback (GTK_WIDGET (workspace), user_data);
+      callback (workspace, user_data);
     }
 
   g_list_free (copy);
 }
 
+typedef struct
+{
+  IdePageCallback callback;
+  gpointer user_data;
+} ForeachPage;
+
+static void
+ide_workbench_foreach_page_cb (IdeWorkspace *workspace,
+                               gpointer      user_data)
+{
+  ForeachPage *state = user_data;
+
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (state != NULL);
+  g_assert (state->callback != NULL);
+
+  ide_workspace_foreach_page (workspace, state->callback, state->user_data);
+}
+
+
 /**
  * ide_workbench_foreach_page:
  * @self: a #IdeWorkbench
@@ -673,40 +688,30 @@ ide_workbench_foreach_workspace (IdeWorkbench *self,
  *
  * 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)
+ide_workbench_foreach_page (IdeWorkbench    *self,
+                            IdePageCallback  callback,
+                            gpointer         user_data)
 {
-  GList *copy;
+  ForeachPage state = {callback, user_data};
 
   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);
+  ide_workbench_foreach_workspace (self, ide_workbench_foreach_page_cb, &state);
 }
 
 static void
-ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self,
-                                               GParamSpec   *pspec,
-                                               IdeWorkspace *workspace)
+ide_workbench_workspace_is_active_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)))
+  if (gtk_window_is_active (GTK_WINDOW (workspace)))
     {
       GList *mru_link = _ide_workspace_get_mru_link (workspace);
 
@@ -731,6 +736,7 @@ insert_action_groups_foreach_cb (IdeWorkspace *workspace,
   } groups[] = {
     { "config-manager", IDE_TYPE_CONFIG_MANAGER },
     { "build-manager", IDE_TYPE_BUILD_MANAGER },
+    { "debug-manager", IDE_TYPE_DEBUG_MANAGER },
     { "device-manager", IDE_TYPE_DEVICE_MANAGER },
     { "run-manager", IDE_TYPE_RUN_MANAGER },
     { "test-manager", IDE_TYPE_TEST_MANAGER },
@@ -757,15 +763,13 @@ insert_action_groups_foreach_cb (IdeWorkspace *workspace,
  * @workspace: an #IdeWorkspace
  *
  * Adds @workspace to @workbench.
- *
- * Since: 3.32
  */
 void
 ide_workbench_add_workspace (IdeWorkbench *self,
                              IdeWorkspace *workspace)
 {
   g_autoptr(GPtrArray) addins = NULL;
-  IdeCommandManager *command_manager;
+  IdeShortcutManager *shortcuts;
   GList *mru_link;
 
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
@@ -808,10 +812,14 @@ ide_workbench_add_workspace (IdeWorkbench *self,
   if (self->project_info != NULL)
     insert_action_groups_foreach_cb (workspace, self);
 
+  /* Setup capture shortcut controller for workspace */
+  shortcuts = ide_shortcut_manager_from_context (self->context);
+  _ide_workspace_set_shortcut_model (workspace, G_LIST_MODEL (shortcuts));
+
   /* 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),
+                           "notify::is-active",
+                           G_CALLBACK (ide_workbench_workspace_is_active_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
@@ -839,10 +847,6 @@ ide_workbench_add_workspace (IdeWorkbench *self,
       formatted = g_strdup_printf (_("Builder — %s"), title);
       gtk_window_set_title (GTK_WINDOW (workspace), formatted);
     }
-
-  /* Load shortcuts for commands */
-  command_manager = ide_command_manager_from_context (self->context);
-  _ide_command_manager_init_shortcuts (command_manager, workspace);
 }
 
 /**
@@ -851,15 +855,12 @@ ide_workbench_add_workspace (IdeWorkbench *self,
  * @workspace: an #IdeWorkspace
  *
  * Removes @workspace from @workbench.
- *
- * Since: 3.32
  */
 void
 ide_workbench_remove_workspace (IdeWorkbench *self,
                                 IdeWorkspace *workspace)
 {
   g_autoptr(GPtrArray) addins = NULL;
-  IdeCommandManager *command_manager;
   GList *list;
   GList *mru_link;
   guint count = 0;
@@ -872,13 +873,9 @@ ide_workbench_remove_workspace (IdeWorkbench *self,
   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),
+                                        G_CALLBACK (ide_workbench_workspace_is_active_cb),
                                         self);
 
-  /* Remove any shortcuts that were registered by command providers */
-  command_manager = ide_command_manager_from_context (self->context);
-  _ide_command_manager_unload_shortcuts (command_manager, workspace);
-
   /* Notify all the addins about losing the workspace. */
   if ((addins = ide_workbench_collect_addins (self)))
     {
@@ -927,8 +924,6 @@ ide_workbench_remove_workspace (IdeWorkbench *self,
  *
  * 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,
@@ -1004,7 +999,7 @@ ide_workbench_load_project_completed (IdeWorkbench *self,
 
   /* Give workspaces access to the various GActionGroups */
   ide_workbench_foreach_workspace (self,
-                                   (GtkCallback)insert_action_groups_foreach_cb,
+                                   (IdeWorkspaceCallback)insert_action_groups_foreach_cb,
                                    self);
 
   /* Notify addins that projects have loaded */
@@ -1018,6 +1013,9 @@ ide_workbench_load_project_completed (IdeWorkbench *self,
   build_manager = ide_build_manager_from_context (self->context);
   _ide_build_manager_start (build_manager);
 
+  /* Enable actions that are available to projects */
+  ide_workbench_set_action_enabled (self, "configure", TRUE);
+
   ide_task_return_boolean (task, TRUE);
 }
 
@@ -1127,8 +1125,6 @@ ide_workbench_init_foundry_cb (GObject      *object,
  *
  * @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,
@@ -1283,8 +1279,6 @@ ide_workbench_load_project_async (IdeWorkbench        *self,
  *
  * 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,
@@ -1390,13 +1384,38 @@ ide_workbench_action_reload_all (IdeWorkbench *self,
   ide_buffer_manager_reload_all_async (bufmgr, NULL, NULL, NULL);
 }
 
+static void
+ide_workbench_action_open_response_cb (IdeWorkbench         *self,
+                                       int                   response,
+                                       GtkFileChooserNative *chooser)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (GTK_IS_FILE_CHOOSER_NATIVE (chooser));
+
+  if (response == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GListModel) model = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
+      guint n_items = g_list_model_get_n_items (model);
+
+      for (guint i = 0; i < n_items; i++)
+        {
+          g_autoptr(GFile) file = g_list_model_get_item (model, i);
+
+          g_assert (G_IS_FILE (file));
+
+          ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL, NULL);
+        }
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
+}
+
 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);
@@ -1409,26 +1428,55 @@ ide_workbench_action_open (IdeWorkbench *self,
                                          _("_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));
+  g_signal_connect_object (chooser,
+                           "response",
+                           G_CALLBACK (ide_workbench_action_open_response_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
 
-  if (ret == GTK_RESPONSE_ACCEPT)
-    {
-      g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
+  gtk_native_dialog_show (GTK_NATIVE_DIALOG (chooser));
+}
 
-      for (const GSList *iter = files; iter; iter = iter->next)
-        {
-          GFile *file = iter->data;
+static void
+ide_workbench_action_open_uri (IdeWorkbench *self,
+                               GVariant     *param)
+{
+  g_autoptr(GFile) file = NULL;
 
-          g_assert (G_IS_FILE (file));
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  file = g_file_new_for_uri (g_variant_get_string (param, NULL));
+  ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_workbench_action_global_search (IdeWorkbench *self,
+                                    GVariant     *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (param == NULL);
+
+  for (const GList *iter = self->mru_queue.head; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
 
-          ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL);
+      if (_ide_workspace_can_search (workspace))
+        {
+          _ide_workspace_begin_global_search (workspace);
+          IDE_EXIT;
         }
     }
 
-  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
+  IDE_EXIT;
 }
 
 /**
@@ -1438,8 +1486,6 @@ ide_workbench_action_open (IdeWorkbench *self,
  * 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)
@@ -1462,8 +1508,6 @@ ide_workbench_get_search_engine (IdeWorkbench *self)
  * currently, loading.
  *
  * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL
- *
- * Since: 3.32
  */
 IdeProjectInfo *
 ide_workbench_get_project_info (IdeWorkbench *self)
@@ -1482,6 +1526,8 @@ ide_workbench_unload_foundry_cb (GObject      *object,
   g_autoptr(GError) error = NULL;
   IdeWorkbench *self;
 
+  IDE_ENTRY;
+
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
@@ -1497,22 +1543,38 @@ ide_workbench_unload_foundry_cb (GObject      *object,
       ide_object_destroy (IDE_OBJECT (self->context));
       g_clear_object (&self->context);
     }
+
+  IDE_EXIT;
 }
 
 static void
 ide_workbench_unload_project_completed (IdeWorkbench *self,
                                         IdeTask      *task)
 {
+  GList *copy;
+
+  IDE_ENTRY;
+
   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);
+
+  copy = g_list_copy_deep (self->mru_queue.head, (GCopyFunc)g_object_ref, NULL);
+  for (const GList *iter = copy; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
+      g_assert (IDE_IS_WORKSPACE (workspace));
+      gtk_window_destroy (GTK_WINDOW (workspace));
+    }
+  g_list_free_full (copy, g_object_unref);
 
   _ide_foundry_unload_async (self->context,
                              ide_task_get_cancellable (task),
                              ide_workbench_unload_foundry_cb,
                              g_object_ref (task));
+
+  IDE_EXIT;
 }
 static void
 ide_workbench_unload_project_cb (GObject      *object,
@@ -1525,6 +1587,8 @@ ide_workbench_unload_project_cb (GObject      *object,
   IdeWorkbench *self;
   GPtrArray *addins;
 
+  IDE_ENTRY;
+
   g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
@@ -1547,6 +1611,8 @@ ide_workbench_unload_project_cb (GObject      *object,
 
   if (addins->len == 0)
     ide_workbench_unload_project_completed (self, task);
+
+  IDE_EXIT;
 }
 
 /**
@@ -1560,8 +1626,6 @@ ide_workbench_unload_project_cb (GObject      *object,
  *
  * All #IdeWorkspace windows will be closed after calling this
  * function.
- *
- * Since: 3.32
  */
 void
 ide_workbench_unload_async (IdeWorkbench        *self,
@@ -1573,6 +1637,8 @@ ide_workbench_unload_async (IdeWorkbench        *self,
   g_autoptr(GPtrArray) addins = NULL;
   GApplication *app;
 
+  IDE_ENTRY;
+
   g_return_if_fail (IDE_IS_WORKBENCH (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
@@ -1582,7 +1648,7 @@ ide_workbench_unload_async (IdeWorkbench        *self,
   if (self->unloaded)
     {
       ide_task_return_boolean (task, TRUE);
-      return;
+      IDE_EXIT;
     }
 
   self->unloaded = TRUE;
@@ -1611,7 +1677,7 @@ ide_workbench_unload_async (IdeWorkbench        *self,
   if (self->project_info == NULL)
     {
       ide_workbench_unload_project_completed (self, g_steal_pointer (&task));
-      return;
+      IDE_EXIT;
     }
 
   addins = ide_workbench_collect_addins (self);
@@ -1620,7 +1686,7 @@ ide_workbench_unload_async (IdeWorkbench        *self,
   if (addins->len == 0)
     {
       ide_workbench_unload_project_completed (self, task);
-      return;
+      IDE_EXIT;
     }
 
   for (guint i = 0; i < addins->len; i++)
@@ -1638,6 +1704,8 @@ ide_workbench_unload_async (IdeWorkbench        *self,
    * task isn't freed while it hasn't yet finished running asynchronously.
    */
   task = NULL;
+
+  IDE_EXIT;
 }
 
 /**
@@ -1650,8 +1718,6 @@ ide_workbench_unload_async (IdeWorkbench        *self,
  *
  * Returns: %TRUE if the workbench was unloaded successfully,
  *   otherwise %FALSE and @error is set.
- *
- * Since: 3.32
  */
 gboolean
 ide_workbench_unload_finish (IdeWorkbench *self,
@@ -1708,8 +1774,6 @@ ide_workbench_open_all_cb (GObject      *object,
  *
  * Call ide_workbench_open_finish() from @callback to complete this
  * operation.
- *
- * Since: 3.32
  */
 void
 ide_workbench_open_all_async (IdeWorkbench         *self,
@@ -1752,6 +1816,7 @@ ide_workbench_open_all_async (IdeWorkbench         *self,
                                 file,
                                 hint,
                                 IDE_BUFFER_OPEN_FLAGS_NONE,
+                                NULL,
                                 cancellable,
                                 ide_workbench_open_all_cb,
                                 g_object_ref (task));
@@ -1764,6 +1829,7 @@ ide_workbench_open_all_async (IdeWorkbench         *self,
  * @file: a #GFile
  * @hint: (nullable): an optional hint about what addin to use
  * @flags: optional flags when opening the file
+ * @position: (nullable): a position to open the page
  * @cancellable: (nullable): a #GCancellable
  * @callback: a #GAsyncReadyCallback to execute upon completion
  * @user_data: closure data for @callback
@@ -1775,14 +1841,13 @@ ide_workbench_open_all_async (IdeWorkbench         *self,
  * 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,
+                          IdePanelPosition    *position,
                           GCancellable        *cancellable,
                           GAsyncReadyCallback  callback,
                           gpointer             user_data)
@@ -1797,6 +1862,7 @@ ide_workbench_open_async (IdeWorkbench        *self,
                                -1,
                                -1,
                                flags,
+                               position,
                                cancellable,
                                callback,
                                user_data);
@@ -1853,15 +1919,16 @@ ide_workbench_open_cb (GObject      *object,
 
   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));
+  ide_workbench_addin_open_async (next,
+                                  o->file,
+                                  o->content_type,
+                                  o->at_line,
+                                  o->at_line_offset,
+                                  o->flags,
+                                  o->position,
+                                  cancellable,
+                                  ide_workbench_open_cb,
+                                  g_steal_pointer (&task));
 }
 
 static gint
@@ -1970,15 +2037,16 @@ ide_workbench_open_query_info_cb (GObject      *object,
 
   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_addin_open_async (first,
+                                  o->file,
+                                  o->content_type,
+                                  o->at_line,
+                                  o->at_line_offset,
+                                  o->flags,
+                                  o->position,
+                                  cancellable,
+                                  ide_workbench_open_cb,
+                                  g_steal_pointer (&task));
 }
 
 /**
@@ -2004,8 +2072,6 @@ ide_workbench_open_query_info_cb (GObject      *object,
  *
  * Use ide_workbench_open_finish() to receive teh result of this
  * asynchronous operation.
- *
- * Since: 3.32
  */
 void
 ide_workbench_open_at_async (IdeWorkbench        *self,
@@ -2014,10 +2080,12 @@ ide_workbench_open_at_async (IdeWorkbench        *self,
                              gint                 at_line,
                              gint                 at_line_offset,
                              IdeBufferOpenFlags   flags,
+                             IdePanelPosition    *position,
                              GCancellable        *cancellable,
                              GAsyncReadyCallback  callback,
                              gpointer             user_data)
 {
+  g_autoptr(IdePanelPosition) local_position = NULL;
   g_autoptr(IdeTask) task = NULL;
   g_autoptr(GPtrArray) addins = NULL;
   IdeWorkbench *other;
@@ -2028,6 +2096,9 @@ ide_workbench_open_at_async (IdeWorkbench        *self,
   g_return_if_fail (self->unloaded == FALSE);
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
+  if (position == NULL)
+    position = local_position = ide_panel_position_new ();
+
   /* Possibly re-route opening the file to another workbench if we
    * discover the file is a better fit over there.
    */
@@ -2041,6 +2112,7 @@ ide_workbench_open_at_async (IdeWorkbench        *self,
                                    at_line,
                                    at_line_offset,
                                    flags,
+                                   NULL,
                                    cancellable,
                                    callback,
                                    user_data);
@@ -2078,6 +2150,7 @@ ide_workbench_open_at_async (IdeWorkbench        *self,
   o->flags = flags;
   o->at_line = at_line;
   o->at_line_offset = at_line_offset;
+  o->position = ide_panel_position_ref (position);
   ide_task_set_task_data (task, o, open_free);
 
   g_file_query_info_async (file,
@@ -2100,8 +2173,6 @@ ide_workbench_open_at_async (IdeWorkbench        *self,
  *
  * Returns: %TRUE if the file was successfully opened; otherwise
  *   %FALSE and @error is set.
- *
- * Since: 3.32
  */
 gboolean
 ide_workbench_open_finish (IdeWorkbench  *self,
@@ -2122,8 +2193,6 @@ ide_workbench_open_finish (IdeWorkbench  *self,
  * 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)
@@ -2141,8 +2210,6 @@ ide_workbench_get_current_workspace (IdeWorkbench *self)
  * @self: a #IdeWorkbench
  *
  * This function will attempt to raise the most recently focused workspace.
- *
- * Since: 3.32
  */
 void
 ide_workbench_activate (IdeWorkbench *self)
@@ -2179,8 +2246,6 @@ ide_workbench_propagate_vcs_cb (PeasExtensionSet *set,
  * 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)
@@ -2197,8 +2262,6 @@ ide_workbench_get_vcs (IdeWorkbench *self)
  * 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)
@@ -2247,8 +2310,6 @@ ide_workbench_vcs_notify_branch_name_cb (IdeWorkbench *self,
  * @vcs: (nullable): an #IdeVcs
  *
  * Sets the #IdeVcs for the workbench.
- *
- * Since: 3.32
  */
 void
 ide_workbench_set_vcs (IdeWorkbench *self,
@@ -2302,8 +2363,6 @@ ide_workbench_set_vcs (IdeWorkbench *self,
  * 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)
@@ -2336,8 +2395,6 @@ remove_non_matching_build_systems_cb (IdeObject      *child,
  * 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,
@@ -2386,8 +2443,6 @@ ide_workbench_set_build_system (IdeWorkbench   *self,
  * 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,
@@ -2425,8 +2480,6 @@ _ide_workbench_is_last_workspace (IdeWorkbench *self,
  * workbench.
  *
  * Returns: %TRUE if the workbench has a project
- *
- * Since: 3.32
  */
 gboolean
 ide_workbench_has_project (IdeWorkbench *self)
@@ -2445,8 +2498,6 @@ ide_workbench_has_project (IdeWorkbench *self)
  * 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,
@@ -2533,8 +2584,6 @@ ide_workbench_resolve_file_worker (IdeTask      *task,
  *
  * If no file was discovered, some attempt will be made to locate a file
  * that matches appropriately.
- *
- * Since: 3.32
  */
 void
 ide_workbench_resolve_file_async (IdeWorkbench        *self,
@@ -2589,8 +2638,6 @@ ide_workbench_resolve_file_async (IdeWorkbench        *self,
  * Completes an asynchronous request to ide_workbench_resolve_file_async().
  *
  * Returns: (transfer full): a #GFile, or %NULL and @error is set
- *
- * Since: 3.32
  */
 GFile *
 ide_workbench_resolve_file_finish (IdeWorkbench  *self,
@@ -2609,3 +2656,57 @@ ide_workbench_resolve_file_finish (IdeWorkbench  *self,
 
   IDE_RETURN (g_steal_pointer (&ret));
 }
+
+static void
+ide_workbench_action_configure (IdeWorkbench *self,
+                                GVariant     *param)
+{
+  const char *page = NULL;
+  GtkWindow *window;
+  GList *windows;
+  gboolean found = FALSE;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  if (param && g_variant_is_of_type (param, G_VARIANT_TYPE_STRING))
+    page = g_variant_get_string (param, NULL);
+
+  windows = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self));
+
+  for (const GList *iter = windows; iter; iter = iter->next)
+    {
+      window = iter->data;
+
+      if (IDE_IS_PREFERENCES_WINDOW (window) &&
+          ide_preferences_window_get_mode (IDE_PREFERENCES_WINDOW (window)) == IDE_PREFERENCES_MODE_PROJECT)
+        {
+          gtk_window_present (window);
+          found = TRUE;
+          break;
+        }
+    }
+
+  g_list_free (windows);
+
+  if (!found)
+    {
+      g_autofree char *title = NULL;
+      g_autofree char *window_title = NULL;
+
+      title = ide_context_dup_title (self->context);
+      window_title = g_strdup_printf (_("Builder — %s"), title);
+
+      window = g_object_new (IDE_TYPE_PREFERENCES_WINDOW,
+                             "mode", IDE_PREFERENCES_MODE_PROJECT,
+                             "context", self->context,
+                             "default-width", 1050,
+                             "default-height", 700,
+                             "title", window_title,
+                             NULL);
+      gtk_window_group_add_window (GTK_WINDOW_GROUP (self), window);
+      gtk_window_present (window);
+    }
+
+  if (page != NULL)
+    ide_preferences_window_set_page (IDE_PREFERENCES_WINDOW (window), page);
+}
diff --git a/src/libide/gui/ide-workbench.h b/src/libide/gui/ide-workbench.h
index 3014a8ab0..b534049b8 100644
--- a/src/libide/gui/ide-workbench.h
+++ b/src/libide/gui/ide-workbench.h
@@ -36,88 +36,90 @@ G_BEGIN_DECLS
 
 #define IDE_TYPE_WORKBENCH (ide_workbench_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_FINAL_TYPE (IdeWorkbench, ide_workbench, IDE, WORKBENCH, GtkWindowGroup)
 
-IDE_AVAILABLE_IN_3_40
+IDE_AVAILABLE_IN_ALL
 IdeWorkbench    *ide_workbench_from_context          (IdeContext           *context);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkbench    *ide_workbench_new                   (void);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkbench    *ide_workbench_new_for_context       (IdeContext           *context);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_activate              (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeProjectInfo  *ide_workbench_get_project_info      (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_workbench_has_project           (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeContext      *ide_workbench_get_context           (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkspace    *ide_workbench_get_current_workspace (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkspace    *ide_workbench_get_workspace_by_type (IdeWorkbench         *self,
                                                       GType                 type);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeSearchEngine *ide_workbench_get_search_engine     (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeWorkbench    *ide_workbench_from_widget           (GtkWidget            *widget);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_add_workspace         (IdeWorkbench         *self,
                                                       IdeWorkspace         *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_remove_workspace      (IdeWorkbench         *self,
                                                       IdeWorkspace         *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_focus_workspace       (IdeWorkbench         *self,
                                                       IdeWorkspace         *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_foreach_workspace     (IdeWorkbench         *self,
-                                                      GtkCallback           callback,
+                                                      IdeWorkspaceCallback  callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_foreach_page          (IdeWorkbench         *self,
-                                                      GtkCallback           callback,
+                                                      IdePageCallback       callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 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
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_workbench_load_project_finish   (IdeWorkbench         *self,
                                                       GAsyncResult         *result,
                                                       GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_unload_async          (IdeWorkbench         *self,
                                                       GCancellable         *cancellable,
                                                       GAsyncReadyCallback   callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_workbench_unload_finish         (IdeWorkbench         *self,
                                                       GAsyncResult         *result,
                                                       GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_open_async            (IdeWorkbench         *self,
                                                       GFile                *file,
                                                       const gchar          *hint,
                                                       IdeBufferOpenFlags    flags,
+                                                      IdePanelPosition     *position,
                                                       GCancellable         *cancellable,
                                                       GAsyncReadyCallback   callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_open_at_async         (IdeWorkbench         *self,
                                                       GFile                *file,
                                                       const gchar          *hint,
                                                       gint                  at_line,
                                                       gint                  at_line_offset,
                                                       IdeBufferOpenFlags    flags,
+                                                      IdePanelPosition     *position,
                                                       GCancellable         *cancellable,
                                                       GAsyncReadyCallback   callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_open_all_async        (IdeWorkbench         *self,
                                                       GFile               **files,
                                                       guint                 n_files,
@@ -125,32 +127,37 @@ void             ide_workbench_open_all_async        (IdeWorkbench         *self
                                                       GCancellable         *cancellable,
                                                       GAsyncReadyCallback   callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 gboolean         ide_workbench_open_finish           (IdeWorkbench         *self,
                                                       GAsyncResult         *result,
                                                       GError              **error);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeVcs          *ide_workbench_get_vcs               (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_set_vcs               (IdeWorkbench         *self,
                                                       IdeVcs               *vcs);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeVcsMonitor   *ide_workbench_get_vcs_monitor       (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 IdeBuildSystem  *ide_workbench_get_build_system      (IdeWorkbench         *self);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_set_build_system      (IdeWorkbench         *self,
                                                       IdeBuildSystem       *build_system);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void             ide_workbench_resolve_file_async    (IdeWorkbench         *self,
                                                       const gchar          *filename,
                                                       GCancellable         *cancellable,
                                                       GAsyncReadyCallback   callback,
                                                       gpointer              user_data);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 GFile           *ide_workbench_resolve_file_finish   (IdeWorkbench         *self,
                                                       GAsyncResult         *result,
                                                       GError              **error);
 
+static inline IdeWorkbench *
+ide_workspace_get_workbench (IdeWorkspace *workspace)
+{
+  return IDE_WORKBENCH (gtk_window_get_group (GTK_WINDOW (workspace)));
+}
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-workspace-addin.c b/src/libide/gui/ide-workspace-addin.c
index 3b43dffc4..f6acde158 100644
--- a/src/libide/gui/ide-workspace-addin.c
+++ b/src/libide/gui/ide-workspace-addin.c
@@ -37,8 +37,6 @@
  * 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)
@@ -56,8 +54,6 @@ ide_workspace_addin_default_init (IdeWorkspaceAddinInterface *iface)
  *
  * 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,
@@ -79,8 +75,6 @@ ide_workspace_addin_load (IdeWorkspaceAddin *self,
  *
  * 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,
@@ -95,46 +89,20 @@ ide_workspace_addin_unload (IdeWorkspaceAddin *self,
 }
 
 /**
- * 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.
+ * ide_workspace_addin_page_changed:
+ * @self: a #IdeWorkspaceAddin
+ * @page: (nullable): an #IdePage or %NULL
  *
- * Since: 3.32
+ * Called when the current page has changed based on focus within
+ * the workspace.
  */
 void
-ide_workspace_addin_surface_set (IdeWorkspaceAddin *self,
-                                 IdeSurface        *surface)
+ide_workspace_addin_page_changed (IdeWorkspaceAddin *self,
+                                  IdePage           *page)
 {
-  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);
-}
-
-/**
- * ide_workspace_addin_can_close:
- * @self: an #IdeWorkspaceAddin
- *
- * This method is called to determine if the workspace can close. If the addin
- * needs to prevent the workspace closing, then return %FALSE; otherwise %TRUE.
- *
- * Returns: %TRUE if the workspace can close; otherwise %FALSE.
- *
- * Since: 3.34
- */
-gboolean
-ide_workspace_addin_can_close (IdeWorkspaceAddin *self)
-{
-  g_return_val_if_fail (IDE_IS_WORKSPACE_ADDIN (self), TRUE);
-
-  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->can_close)
-    return IDE_WORKSPACE_ADDIN_GET_IFACE (self)->can_close (self);
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
 
-  return TRUE;
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->page_changed)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->page_changed (self, page);
 }
diff --git a/src/libide/gui/ide-workspace-addin.h b/src/libide/gui/ide-workspace-addin.h
index 49e0d70a3..4c915a841 100644
--- a/src/libide/gui/ide-workspace-addin.h
+++ b/src/libide/gui/ide-workspace-addin.h
@@ -20,33 +20,44 @@
 
 #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-page.h"
 #include "ide-workspace.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_WORKSPACE_ADDIN (ide_workspace_addin_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 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     (*load)          (IdeWorkspaceAddin *self,
+                             IdeWorkspace      *workspace);
+  void     (*unload)        (IdeWorkspaceAddin *self,
+                             IdeWorkspace      *workspace);
+  void     (*page_changed)  (IdeWorkspaceAddin *self,
+                             IdePage           *page);
 };
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workspace_addin_load                (IdeWorkspaceAddin *self,
                                                             IdeWorkspace      *workspace);
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 void               ide_workspace_addin_unload              (IdeWorkspaceAddin *self,
                                                             IdeWorkspace      *workspace);
-                                                            IdeSurface        *surface);
-IDE_AVAILABLE_IN_3_40
+IDE_AVAILABLE_IN_ALL
+void               ide_workspace_addin_page_changed        (IdeWorkspaceAddin *self,
+                                                            IdePage           *page);
+IDE_AVAILABLE_IN_ALL
 IdeWorkspaceAddin *ide_workspace_addin_find_by_module_name (IdeWorkspace      *workspace,
                                                             const gchar       *module_name);
 
diff --git a/src/libide/gui/ide-workspace-private.h b/src/libide/gui/ide-workspace-private.h
new file mode 100644
index 000000000..691d303ba
--- /dev/null
+++ b/src/libide/gui/ide-workspace-private.h
@@ -0,0 +1,57 @@
+/* ide-workspace-private.h
+ *
+ * Copyright 2022 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-grid.h"
+#include "ide-panel-position.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+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_workspace_can_search          (IdeWorkspace        *self);
+void        _ide_workspace_begin_global_search (IdeWorkspace        *self);
+void        _ide_workspace_add_widget          (IdeWorkspace        *workspace,
+                                                PanelWidget         *widget,
+                                                IdePanelPosition    *position,
+                                                PanelPaned          *dock_start,
+                                                PanelPaned          *dock_end,
+                                                PanelPaned          *dock_bottom,
+                                                IdeGrid             *grid);
+PanelFrame *_ide_workspace_find_frame          (IdeWorkspace        *workspace,
+                                                IdePanelPosition    *position,
+                                                PanelPaned          *dock_start,
+                                                PanelPaned          *dock_end,
+                                                PanelPaned          *dock_bottom,
+                                                IdeGrid             *grid);
+void        _ide_workspace_set_shortcut_model  (IdeWorkspace        *self,
+                                                GListModel          *shortcuts);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace.c b/src/libide/gui/ide-workspace.c
index 3d92bb4c8..92b180436 100644
--- a/src/libide/gui/ide-workspace.c
+++ b/src/libide/gui/ide-workspace.c
@@ -22,14 +22,20 @@
 
 #include "config.h"
 
+#include <libide-search.h>
 #include <libide-plugins.h>
 
 #include "ide-gui-global.h"
-#include "ide-gui-private.h"
-#include "ide-workspace.h"
+#include "ide-page-private.h"
+#include "ide-search-popover-private.h"
+#include "ide-shortcut-bundle-private.h"
 #include "ide-workspace-addin.h"
+#include "ide-workspace-private.h"
+#include "ide-workbench-private.h"
 
 #define MUX_ACTIONS_KEY "IDE_WORKSPACE_MUX_ACTIONS"
+#define GET_PRIORITY(w)   GPOINTER_TO_INT(g_object_get_data(G_OBJECT(w),"PRIORITY"))
+#define SET_PRIORITY(w,i) g_object_set_data(G_OBJECT(w),"PRIORITY",GINT_TO_POINTER(i))
 
 typedef struct
 {
@@ -55,58 +61,92 @@ typedef struct
    */
   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;
+  /* A statusbar, if any, that was added to the workspace */
+  PanelStatusbar *statusbar;
 
-  /* 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 global search for the workspace, if any */
+  IdeSearchPopover *search_popover;
 
-  /* 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;
-  GtkBox *vbox;
+  /* GListModel of GtkShortcut w/ capture/bubble filters */
+  GtkFilterListModel *shortcut_model_bubble;
+  GtkFilterListModel *shortcut_model_capture;
 
   /* 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;
 
-  guint in_key_press : 1;
+  /* Queued source to save window size/etc */
+  guint queued_window_save;
+
+  /* Vertical box for children */
+  GtkBox *box;
+
+  /* Weak pointer to the current page. */
+  gpointer current_page_ptr;
 } IdeWorkspacePrivate;
 
 typedef struct
 {
-  GtkCallback callback;
-  gpointer    user_data;
+  IdePageCallback 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, HDY_TYPE_APPLICATION_WINDOW,
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeWorkspace, ide_workspace, ADW_TYPE_APPLICATION_WINDOW,
                                   G_ADD_PRIVATE (IdeWorkspace)
                                   G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
 
-static GtkBuildableIface *parent_builder;
 static GParamSpec *properties [N_PROPS];
-static guint signals [N_SIGNALS];
+static GSettings *settings;
+
+static void
+ide_workspace_attach_shortcuts (IdeWorkspace *self,
+                                GtkWidget    *widget)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkEventController *controller;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (G_IS_LIST_MODEL (priv->shortcut_model_bubble));
+  g_assert (G_IS_LIST_MODEL (priv->shortcut_model_capture));
+
+  controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (priv->shortcut_model_capture));
+  gtk_event_controller_set_name (controller, "ide-shortcuts-capture");
+  gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_CAPTURE);
+  gtk_event_controller_set_propagation_limit (controller, GTK_LIMIT_NONE);
+  gtk_widget_add_controller (widget, g_steal_pointer (&controller));
+
+  controller = gtk_shortcut_controller_new_for_model (G_LIST_MODEL (priv->shortcut_model_bubble));
+  gtk_event_controller_set_name (controller, "ide-shortcuts-bubble");
+  gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_BUBBLE);
+  gtk_event_controller_set_propagation_limit (controller, GTK_LIMIT_NONE);
+  gtk_widget_add_controller (widget, g_steal_pointer (&controller));
+}
+
+static IdePage *
+ide_workspace_get_focus_page (IdeWorkspace *self)
+{
+  GtkWidget *focus;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  if ((focus = gtk_root_get_focus (GTK_ROOT (self))))
+    {
+      if (!IDE_IS_PAGE (focus))
+        focus = gtk_widget_get_ancestor (focus, IDE_TYPE_PAGE);
+    }
+
+  return IDE_PAGE (focus);
+}
 
 static void
 ide_workspace_addin_added_cb (IdeExtensionSetAdapter *set,
@@ -116,6 +156,7 @@ ide_workspace_addin_added_cb (IdeExtensionSetAdapter *set,
 {
   IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
   IdeWorkspace *self = user_data;
+  IdePage *page;
 
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
   g_assert (plugin_info != NULL);
@@ -126,6 +167,9 @@ ide_workspace_addin_added_cb (IdeExtensionSetAdapter *set,
            peas_plugin_info_get_module_name (plugin_info));
 
   ide_workspace_addin_load (addin, self);
+
+  if ((page = ide_workspace_get_focus_page (self)))
+    ide_workspace_addin_page_changed (addin, page);
 }
 
 static void
@@ -145,10 +189,27 @@ ide_workspace_addin_removed_cb (IdeExtensionSetAdapter *set,
   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_page_changed (addin, NULL);
   ide_workspace_addin_unload (addin, self);
 }
 
+static void
+ide_workspace_addin_page_changed_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdePage *page = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  ide_workspace_addin_page_changed (addin, page);
+}
+
 static void
 ide_workspace_real_context_set (IdeWorkspace *self,
                                 IdeContext   *context)
@@ -181,305 +242,288 @@ ide_workspace_real_context_set (IdeWorkspace *self,
 }
 
 static void
-ide_workspace_addin_surface_set_cb (IdeExtensionSetAdapter *set,
-                                    PeasPluginInfo         *plugin_info,
-                                    PeasExtension          *exten,
-                                    gpointer                user_data)
+ide_workspace_close_request_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
 {
-  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
-  IdeSurface *surface = user_data;
+  IdeWorkspace *self = (IdeWorkspace *)object;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
 
-  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));
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->agree_to_close_finish (self, result, &error))
+    {
+      IdeWorkbench *workbench = IDE_WORKBENCH (gtk_window_get_group (GTK_WINDOW (self)));
+
+      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;
+        }
 
-  ide_workspace_addin_surface_set (addin, surface);
+      g_cancellable_cancel (priv->cancellable);
+      ide_workbench_remove_workspace (workbench, self);
+      gtk_window_destroy (GTK_WINDOW (self));
+    }
+}
+
+static gboolean
+ide_workspace_close_request (GtkWindow *window)
+{
+  IdeWorkspace *self = (IdeWorkspace *)window;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  IDE_WORKSPACE_GET_CLASS (self)->agree_to_close_async (self,
+                                                        NULL,
+                                                        ide_workspace_close_request_cb,
+                                                        NULL);
+
+  return TRUE;
 }
 
 static void
-ide_workspace_real_surface_set (IdeWorkspace *self,
-                                IdeSurface   *surface)
+ide_workspace_notify_focus_widget (IdeWorkspace *self,
+                                   GParamSpec   *pspec,
+                                   gpointer      user_data)
 {
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  IdePage *focus;
+
+  IDE_ENTRY;
 
-  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_WORKSPACE (self));
-  g_assert (!surface || IDE_IS_SURFACE (surface));
+  g_assert (pspec != NULL);
+  g_assert (user_data == NULL);
+
+  focus = ide_workspace_get_focus_page (self);
+
+  if (priv->current_page_ptr != (gpointer)focus)
+    {
+      /* Focus changed, but old page is still valid */
+      if (focus == NULL)
+        IDE_EXIT;
+
+      /* Focus changed, and we have a new widget */
+      g_set_weak_pointer (&priv->current_page_ptr, focus);
 
-  if (priv->addins != NULL)
-    ide_extension_set_adapter_foreach (priv->addins,
-                                       ide_workspace_addin_surface_set_cb,
-                                       surface);
+      /* And move this page to the front of the MRU */
+      _ide_workspace_move_front_page_mru (self, _ide_page_get_mru_link (focus));
+
+      if (priv->addins != NULL)
+        {
+          g_object_ref (focus);
+          ide_extension_set_adapter_foreach (priv->addins,
+                                             ide_workspace_addin_page_changed_cb,
+                                             focus);
+          g_object_unref (focus);
+        }
+    }
+
+  IDE_EXIT;
 }
 
 /**
- * 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.
+ * ide_workspace_class_set_kind:
+ * @klass: a #IdeWorkspaceClass
  *
- * Since: 3.32
+ * Sets the shorthand name for the kind of workspace. This is used to limit
+ * what #IdeWorkspaceAddin may load within the workspace.
  */
 void
-ide_workspace_foreach_surface (IdeWorkspace *self,
-                               GtkCallback   callback,
-                               gpointer      user_data)
+ide_workspace_class_set_kind (IdeWorkspaceClass *klass,
+                              const gchar       *kind)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-
-  g_assert (IDE_IS_WORKSPACE (self));
-  g_assert (callback != NULL);
+  g_return_if_fail (IDE_IS_WORKSPACE_CLASS (klass));
 
-  gtk_container_foreach (GTK_CONTAINER (priv->surfaces), callback, user_data);
+  klass->kind = g_intern_string (kind);
 }
 
 static void
-ide_workspace_agree_to_shutdown_cb (GtkWidget *widget,
-                                    gpointer   user_data)
+ide_workspace_real_foreach_page (IdeWorkspace    *self,
+                                 IdePageCallback  callback,
+                                 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));
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (callback != NULL);
 }
 
 static void
-ide_workspace_addin_can_close_cb (IdeExtensionSetAdapter *adapter,
-                                  PeasPluginInfo         *plugin_info,
-                                  PeasExtension          *exten,
-                                  gpointer                user_data)
+ide_workspace_agree_to_close_async (IdeWorkspace        *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
 {
-  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
-  gboolean *blocked = user_data;
-
-  g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
-  g_assert (blocked != NULL);
-
-  *blocked |= !ide_workspace_addin_can_close (addin);
+  g_autoptr(GTask) task = g_task_new (self, cancellable, callback, user_data);
+  g_task_return_boolean (task, TRUE);
 }
 
 static gboolean
-ide_workspace_agree_to_shutdown (IdeWorkspace *self)
+ide_workspace_agree_to_close_finish (IdeWorkspace *self,
+                                     GAsyncResult *result,
+                                     GError **error)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (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);
-
-  if (!blocked)
-    ide_extension_set_adapter_foreach (priv->addins,
-                                       ide_workspace_addin_can_close_cb,
-                                       &blocked);
-
-  return !blocked;
+  return g_task_propagate_boolean (G_TASK (result), error);
 }
 
 static gboolean
-ide_workspace_delete_event (GtkWidget   *widget,
-                            GdkEventAny *any)
+ide_workspace_save_settings (gpointer data)
 {
-  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspace *self = data;
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  IdeWorkbench *workbench;
+  GdkRectangle geom = {0};
+  gboolean maximized;
 
   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;
+  priv->queued_window_save = 0;
 
-  g_assert (IDE_IS_WORKSPACE (self));
-  g_assert (GTK_IS_STACK (surfaces));
+  if (!gtk_widget_get_realized (GTK_WIDGET (self)) ||
+      !gtk_widget_get_visible (GTK_WIDGET (self)) ||
+      !IDE_WORKSPACE_GET_CLASS (self)->save_size (self, &geom.width, &geom.height))
+    return G_SOURCE_REMOVE;
 
-  visible_child = gtk_stack_get_visible_child (surfaces);
-  if (!IDE_IS_SURFACE (visible_child))
-    visible_child = NULL;
+  if (settings == NULL)
+    settings = g_settings_new ("org.gnome.builder");
 
-  if (visible_child != NULL)
-    gtk_widget_grab_focus (visible_child);
+  maximized = gtk_window_is_maximized (GTK_WINDOW (self));
 
-  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_settings_set (settings, "window-size", "(ii)", geom.width, geom.height);
+  g_settings_set_boolean (settings, "window-maximized", maximized);
 
-  g_signal_emit (self, signals [SURFACE_SET], 0, visible_child);
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_SURFACE]);
+  return G_SOURCE_REMOVE;
 }
 
 static void
-ide_workspace_destroy (GtkWidget *widget)
+ide_workspace_size_allocate (GtkWidget *widget,
+                             int        width,
+                             int        height,
+                             int        baseline)
 {
   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);
+  GTK_WIDGET_CLASS (ide_workspace_parent_class)->size_allocate (widget, width, height, baseline);
 
-  group = gtk_window_get_group (GTK_WINDOW (self));
-  if (IDE_IS_WORKBENCH (group))
-    ide_workbench_remove_workspace (IDE_WORKBENCH (group), self);
+  if (priv->search_popover != NULL)
+    ide_search_popover_present (priv->search_popover, width, height);
 
-  GTK_WIDGET_CLASS (ide_workspace_parent_class)->destroy (widget);
+  if (priv->queued_window_save == 0 &&
+      IDE_WORKSPACE_GET_CLASS (self)->save_size != NULL)
+    priv->queued_window_save = g_timeout_add_seconds (1, ide_workspace_save_settings, self);
 }
 
-/**
- * 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)
+static void
+ide_workspace_restore_size (IdeWorkspace *workspace,
+                            int           width,
+                            int           height)
 {
-  g_return_if_fail (IDE_IS_WORKSPACE_CLASS (klass));
+  g_assert (IDE_IS_WORKSPACE (workspace));
 
-  klass->kind = g_intern_string (kind);
+  gtk_window_set_default_size (GTK_WINDOW (workspace), width, height);
 }
 
-
-static void
-ide_workspace_foreach_page_cb (GtkWidget *widget,
-                               gpointer   user_data)
+static gboolean
+ide_workspace_save_size (IdeWorkspace *workspace,
+                         int          *width,
+                         int          *height)
 {
-  ForeachPage *state = user_data;
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  gtk_window_get_default_size (GTK_WINDOW (workspace), width, height);
 
-  if (IDE_IS_SURFACE (widget))
-    ide_surface_foreach_page (IDE_SURFACE (widget), state->callback, state->user_data);
+  return TRUE;
 }
 
 static void
-ide_workspace_real_foreach_page (IdeWorkspace *self,
-                                 GtkCallback   callback,
-                                 gpointer      user_data)
+ide_workspace_realize (GtkWidget *widget)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  ForeachPage state = { callback, user_data };
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  GdkRectangle geom = {0};
+  gboolean maximized = FALSE;
 
   g_assert (IDE_IS_WORKSPACE (self));
-  g_assert (callback != NULL);
 
-  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
-                         ide_workspace_foreach_page_cb,
-                         &state);
-}
+  if (settings == NULL)
+    settings = g_settings_new ("org.gnome.builder");
 
-#if 0
-static void
-ide_workspace_set_surface_fullscreen_cb (GtkWidget *widget,
-                                         gpointer   user_data)
-{
-  g_assert (GTK_IS_WIDGET (widget));
+  g_settings_get (settings, "window-size", "(ii)", &geom.width, &geom.height);
+  g_settings_get (settings, "window-maximized", "b", &maximized);
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->restore_size)
+    IDE_WORKSPACE_GET_CLASS (self)->restore_size (self, geom.width, geom.height);
 
-  if (IDE_IS_SURFACE (widget))
-    _ide_surface_set_fullscreen (IDE_SURFACE (widget), !!user_data);
+  GTK_WIDGET_CLASS (ide_workspace_parent_class)->realize (widget);
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->restore_size)
+    {
+      if (maximized)
+        gtk_window_maximize (GTK_WINDOW (self));
+    }
 }
 
-static void
-ide_workspace_real_set_fullscreen (DzlApplicationWindow *window,
-                                   gboolean              fullscreen)
+static IdeFrame *
+ide_workspace_real_get_most_recent_frame (IdeWorkspace *self)
 {
-  IdeWorkspace *self = (IdeWorkspace *)window;
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  GtkWidget *titlebar;
+  IdePage *page;
 
   g_assert (IDE_IS_WORKSPACE (self));
 
-  DZL_APPLICATION_WINDOW_CLASS (ide_workspace_parent_class)->set_fullscreen (window, fullscreen);
-
-  titlebar = dzl_application_window_get_titlebar (window);
-  gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (titlebar), !fullscreen);
+  if (!(page = ide_workspace_get_most_recent_page (self)))
+    return NULL;
 
-  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
-                         ide_workspace_set_surface_fullscreen_cb,
-                         GUINT_TO_POINTER (fullscreen));
+  return IDE_FRAME (gtk_widget_get_ancestor (GTK_WIDGET (page), IDE_TYPE_FRAME));
 }
-#endif
 
-static void
-ide_workspace_grab_focus (GtkWidget *widget)
+static gboolean
+ide_workspace_real_can_search (IdeWorkspace *self)
 {
-  IdeWorkspace *self = (IdeWorkspace *)widget;
-  IdeSurface *surface;
-
-  g_assert (IDE_IS_WORKSPACE (self));
+  return FALSE;
+}
 
-  if ((surface = ide_workspace_get_visible_surface (self)))
-    gtk_widget_grab_focus (GTK_WIDGET (surface));
+static IdeHeaderBar *
+ide_workspace_real_get_header_bar (IdeWorkspace *workspace)
+{
+  return NULL;
 }
 
-static gboolean
-ide_workspace_key_press_event (GtkWidget   *widget,
-                               GdkEventKey *event)
+static void
+ide_workspace_dispose (GObject *object)
 {
-  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspace *self = (IdeWorkspace *)object;
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  gboolean ret;
+  GtkWindowGroup *group;
 
   g_assert (IDE_IS_WORKSPACE (self));
-  g_assert (event != NULL);
 
-  /* Be re-entrant safe from the shortcut manager */
-  if (priv->in_key_press)
-    return GTK_WIDGET_CLASS (ide_workspace_parent_class)->key_press_event (widget, event);
+  g_clear_pointer ((GtkWidget **)&priv->search_popover, gtk_widget_unparent);
+
+  g_clear_weak_pointer (&priv->current_page_ptr);
+
+  /* Unload addins immediately */
+  ide_clear_and_destroy_object (&priv->addins);
+
+  /* Remove the workspace from the workbench MRU/etc */
+  group = gtk_window_get_group (GTK_WINDOW (self));
+  if (IDE_IS_WORKBENCH (group))
+    ide_workbench_remove_workspace (IDE_WORKBENCH (group), self);
 
-  priv->in_key_press = TRUE;
-  ret = dzl_shortcut_manager_handle_event (NULL, event, widget);
-  priv->in_key_press = FALSE;
+  /* Chain up to ensure the GtkWindow cleans up any widgets or other
+   * state attached to the workspace. We keep the context alive during
+   * this process.
+   */
+  G_OBJECT_CLASS (ide_workspace_parent_class)->dispose (object);
 
-  return ret;
+  /* A reference is held during this so it is safe to run code after
+   * chaining up to dispose. Force release teh context now.
+   */
+  g_clear_object (&priv->context);
 }
 
 static void
@@ -490,6 +534,7 @@ ide_workspace_finalize (GObject *object)
 
   g_clear_object (&priv->context);
   g_clear_object (&priv->cancellable);
+  g_clear_handle_id (&priv->queued_window_save, g_source_remove);
 
   G_OBJECT_CLASS (ide_workspace_parent_class)->finalize (object);
 }
@@ -508,29 +553,6 @@ ide_workspace_get_property (GObject    *object,
       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);
     }
@@ -541,30 +563,32 @@ 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);
+  GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass);
 
+  object_class->dispose = ide_workspace_dispose;
   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;
-  widget_class->key_press_event = ide_workspace_key_press_event;
+  widget_class->realize = ide_workspace_realize;
+  widget_class->size_allocate = ide_workspace_size_allocate;
 
-  //window_class->set_fullscreen = ide_workspace_real_set_fullscreen;
+  window_class->close_request = ide_workspace_close_request;
 
-  klass->foreach_page = ide_workspace_real_foreach_page;
+  klass->agree_to_close_async = ide_workspace_agree_to_close_async;
+  klass->agree_to_close_finish = ide_workspace_agree_to_close_finish;
+  klass->can_search = ide_workspace_real_can_search;
   klass->context_set = ide_workspace_real_context_set;
-  klass->surface_set = ide_workspace_real_surface_set;
+  klass->foreach_page = ide_workspace_real_foreach_page;
+  klass->get_most_recent_frame = ide_workspace_real_get_most_recent_frame;
+  klass->restore_size = ide_workspace_restore_size;
+  klass->save_size = ide_workspace_save_size;
+  klass->get_header_bar = ide_workspace_real_get_header_bar;
 
   /**
    * 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",
@@ -573,51 +597,9 @@ ide_workspace_class_init (IdeWorkspaceClass *klass)
                          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);
-  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, vbox);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_comma, GDK_CONTROL_MASK, "app.preferences", 
NULL);
 }
 
 static void
@@ -626,26 +608,35 @@ 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));
+#ifdef DEVELOPMENT_BUILD
+  gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
+#endif
 
-  g_signal_connect_object (priv->surfaces,
-                           "notify::visible-child",
-                           G_CALLBACK (ide_workspace_notify_surface_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
+  priv->mru_link.data = self;
 
   /* 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");
+  gtk_widget_add_css_class (GTK_WIDGET (self), app_id);
+  gtk_widget_add_css_class (GTK_WIDGET (self), "workspace");
+
+  /* Setup container for children widgetry */
+  priv->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            NULL);
+  adw_application_window_set_content (ADW_APPLICATION_WINDOW (self),
+                                      GTK_WIDGET (priv->box));
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->has_statusbar)
+    {
+      priv->statusbar = PANEL_STATUSBAR (panel_statusbar_new ());
+      gtk_box_append (priv->box, GTK_WIDGET (priv->statusbar));
+    }
 
-  /* 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));
+  /* Track focus change to propagate to addins */
+  g_signal_connect (self,
+                    "notify::focus-widget",
+                    G_CALLBACK (ide_workspace_notify_focus_widget),
+                    NULL);
 
   /* Initialize GActions for workspace */
   _ide_workspace_init_actions (self);
@@ -668,8 +659,6 @@ _ide_workspace_get_mru_link (IdeWorkspace *self)
  * workspace joins an #IdeWorkbench.
  *
  * Returns: (transfer none) (nullable): an #IdeContext or %NULL
- *
- * Since: 3.32
  */
 IdeContext *
 ide_workspace_get_context (IdeWorkspace *self)
@@ -707,8 +696,6 @@ _ide_workspace_set_context (IdeWorkspace *self,
  * to be cancelled if a window is closed.
  *
  * Returns: (transfer none): a #GCancellable
- *
- * Since: 3.32
  */
 GCancellable *
 ide_workspace_get_cancellable (IdeWorkspace *self)
@@ -731,13 +718,11 @@ ide_workspace_get_cancellable (IdeWorkspace *self)
  * @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)
+ide_workspace_foreach_page (IdeWorkspace    *self,
+                            IdePageCallback  callback,
+                            gpointer         user_data)
 {
   g_return_if_fail (IDE_IS_WORKSPACE (self));
   g_return_if_fail (callback != NULL);
@@ -754,338 +739,663 @@ ide_workspace_foreach_page (IdeWorkspace *self,
  * 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)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  IdeHeaderBar *ret = NULL;
-  GList *children;
-
   g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
 
-  children = gtk_container_get_children (GTK_CONTAINER (priv->vbox));
-
-  for (const GList *iter = children; iter; iter = iter->next)
-    {
-      GtkWidget *widget = iter->data;
-
-      if (GTK_IS_STACK (widget))
-        widget = gtk_stack_get_visible_child (GTK_STACK (widget));
-
-      if (IDE_IS_HEADER_BAR (widget))
-        {
-          ret = IDE_HEADER_BAR (widget);
-          break;
-        }
-    }
-
-  g_list_free (children);
-
-  return ret;
+  return IDE_WORKSPACE_GET_CLASS (self)->get_header_bar (self);
 }
 
 /**
- * ide_workspace_add_surface:
+ * ide_workspace_get_most_recent_page:
  * @self: a #IdeWorkspace
  *
- * Adds a new #IdeSurface to the workspace.
+ * Gets the most recently focused #IdePage.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): an #IdePage or %NULL
  */
-void
-ide_workspace_add_surface (IdeWorkspace *self,
-                           IdeSurface   *surface)
+IdePage *
+ide_workspace_get_most_recent_page (IdeWorkspace *self)
 {
   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));
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
 
-  if (DZL_IS_DOCK_ITEM (surface))
-    title = dzl_dock_item_get_title (DZL_DOCK_ITEM (surface));
+  if (priv->page_mru.head != NULL)
+    return IDE_PAGE (priv->page_mru.head->data);
 
-  gtk_container_add_with_properties (GTK_CONTAINER (priv->surfaces), GTK_WIDGET (surface),
-                                     "name", gtk_widget_get_name (GTK_WIDGET (surface)),
-                                     "title", title,
-                                     NULL);
+  return NULL;
 }
 
 /**
- * ide_workspace_set_visible_surface_name:
+ * ide_workspace_get_most_recent_frame:
  * @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().
+ * Gets the most recently selected frame.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): an #IdeFrame or %NULL
  */
+IdeFrame *
+ide_workspace_get_most_recent_frame (IdeWorkspace *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  return IDE_WORKSPACE_GET_CLASS (self)->get_most_recent_frame (self);
+}
+
 void
-ide_workspace_set_visible_surface_name (IdeWorkspace *self,
-                                        const gchar  *visible_surface_name)
+_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 (visible_surface_name != NULL);
-
-  gtk_stack_set_visible_child_name (priv->surfaces, visible_surface_name);
-}
+  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);
+  IdePage *mru_page;
+
+  IDE_ENTRY;
+
+  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));
+
+  mru_page = mru_link->data;
+
+  g_debug ("Removing %s from page MRU",
+           G_OBJECT_TYPE_NAME (mru_page));
+
+  g_queue_unlink (&priv->page_mru, mru_link);
+
+  if ((gpointer)mru_page == priv->current_page_ptr)
+    {
+      g_clear_weak_pointer (&priv->current_page_ptr);
+      ide_extension_set_adapter_foreach (priv->addins,
+                                         ide_workspace_addin_page_changed_cb,
+                                         NULL);
+    }
+
+  IDE_EXIT;
+}
+
+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);
+}
 
 /**
- * ide_workspace_get_visible_surface:
- * @self: a #IdeWorkspace
- *
- * Gets the currently visible #IdeSurface, or %NULL
+ * ide_workspace_addin_find_by_module_name:
+ * @workspace: an #IdeWorkspace
+ * @module_name: the name of the addin module
  *
- * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ * Finds the addin (if any) matching the plugin's @module_name.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): an #IdeWorkspaceAddin or %NULL
  */
-IdeSurface *
-ide_workspace_get_visible_surface (IdeWorkspace *self)
+IdeWorkspaceAddin *
+ide_workspace_addin_find_by_module_name (IdeWorkspace *workspace,
+                                         const gchar  *module_name)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-  GtkWidget *child;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (workspace);
+  PeasPluginInfo *plugin_info;
+  PeasExtension *ret = NULL;
+  PeasEngine *engine;
 
-  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKSPACE (workspace), NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
 
-  child = gtk_stack_get_visible_child (priv->surfaces);
-  if (!IDE_IS_SURFACE (child))
-    child = NULL;
+  if (priv->addins == NULL)
+    return NULL;
+
+  engine = peas_engine_get_default ();
 
-  return IDE_SURFACE (child);
+  if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
+    ret = ide_extension_set_adapter_get_extension (priv->addins, plugin_info);
+
+  return IDE_WORKSPACE_ADDIN (ret);
 }
 
 /**
- * ide_workspace_set_visible_surface:
+ * ide_workspace_add_page:
  * @self: a #IdeWorkspace
- * @surface: an #IdeSurface
+ * @page: an #IdePage
+ * @position: the position for the page
  *
- * Sets the #IdeWorkspace:visible-surface property which is the currently
- * visible #IdeSurface in the workspace.
+ * Adds @page to @workspace.
  *
- * Since: 3.32
+ * In future versions, @position may be updated to reflect the
+ * position in which @page was added.
  */
 void
-ide_workspace_set_visible_surface (IdeWorkspace *self,
-                                   IdeSurface   *surface)
+ide_workspace_add_page (IdeWorkspace     *self,
+                        IdePage          *page,
+                        IdePanelPosition *position)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-
   g_return_if_fail (IDE_IS_WORKSPACE (self));
-  g_return_if_fail (IDE_IS_SURFACE (surface));
+  g_return_if_fail (IDE_IS_PAGE (page));
+  g_return_if_fail (position != NULL);
 
-  gtk_stack_set_visible_child (priv->surfaces, GTK_WIDGET (surface));
+  if (IDE_WORKSPACE_GET_CLASS (self)->add_page)
+    IDE_WORKSPACE_GET_CLASS (self)->add_page (self, page, position);
+  else
+    g_critical ("%s does not support adding pages",
+                G_OBJECT_TYPE_NAME (self));
 }
 
 /**
- * ide_workspace_get_surface_by_name:
+ * ide_workspace_add_pane:
  * @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().
+ * @pane: an #IdePane
+ * @position: the position for the pane
  *
- * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ * Adds @pane to @workspace.
  *
- * Since: 3.32
+ * In future versions, @position may be updated to reflect the
+ * position in which @pane was added.
  */
-IdeSurface *
-ide_workspace_get_surface_by_name (IdeWorkspace *self,
-                                   const gchar  *name)
+void
+ide_workspace_add_pane (IdeWorkspace     *self,
+                        IdePane          *pane,
+                        IdePanelPosition *position)
 {
-  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);
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_PANE (pane));
+  g_return_if_fail (position != NULL);
 
-  return IDE_IS_SURFACE (child) ? IDE_SURFACE (child) : NULL;
+  if (IDE_WORKSPACE_GET_CLASS (self)->add_pane)
+    IDE_WORKSPACE_GET_CLASS (self)->add_pane (self, pane, position);
+  else
+    g_critical ("%s does not support adding panels",
+                G_OBJECT_TYPE_NAME (self));
 }
 
-static GObject *
-ide_workspace_get_internal_child (GtkBuildable *buildable,
-                                  GtkBuilder   *builder,
-                                  const gchar  *child_name)
+static void
+ide_workspace_add_child (GtkBuildable *buildable,
+                         GtkBuilder   *builder,
+                         GObject      *object,
+                         const char   *type)
 {
   IdeWorkspace *self = (IdeWorkspace *)buildable;
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
 
-  g_assert (GTK_IS_BUILDABLE (buildable));
+  g_assert (IDE_IS_WORKSPACE (self));
   g_assert (GTK_IS_BUILDER (builder));
-  g_assert (child_name != NULL);
+  g_assert (G_IS_OBJECT (object));
 
-  if (ide_str_equal0 (child_name, "surfaces"))
-    return G_OBJECT (priv->surfaces);
+  if (GTK_IS_WIDGET (object))
+    {
+      if (g_strcmp0 (type, "titlebar") == 0)
+        {
+          gtk_box_prepend (priv->box, GTK_WIDGET (object));
+        }
+      else
+        {
+          gtk_box_append (priv->box, GTK_WIDGET (object));
 
-  return NULL;
+          if (priv->statusbar != NULL)
+            gtk_box_reorder_child_after (priv->box, GTK_WIDGET (priv->statusbar), GTK_WIDGET (object));
+        }
+    }
 }
 
-static void
-ide_workspace_add_child (GtkBuildable *buildable,
-                         GtkBuilder   *builder,
-                         GObject      *object,
-                         const char   *type)
+static GObject *
+ide_workspace_get_internal_child (GtkBuildable *buildable,
+                                  GtkBuilder   *builder,
+                                  const char   *id)
 {
   IdeWorkspace *self = (IdeWorkspace *)buildable;
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
 
   g_assert (IDE_IS_WORKSPACE (self));
   g_assert (GTK_IS_BUILDER (builder));
+  g_assert (id != NULL);
 
-  if (g_strcmp0 (type, "titlebar") == 0 && GTK_IS_WIDGET (object))
-    gtk_box_pack_start (priv->vbox, GTK_WIDGET (object), FALSE, FALSE, 0);
-  else
-    parent_builder->add_child (buildable, builder, object, type);
+  if (g_strcmp0 (id, "statusbar") == 0)
+    {
+      if (priv->statusbar == NULL)
+        {
+          priv->statusbar = PANEL_STATUSBAR (panel_statusbar_new ());
+          gtk_box_append (priv->box, GTK_WIDGET (priv->statusbar));
+        }
+
+      return G_OBJECT (priv->statusbar);
+    }
+
+  return NULL;
 }
 
 static void
 buildable_iface_init (GtkBuildableIface *iface)
 {
-  parent_builder = g_type_interface_peek_parent (iface);
-
-  iface->get_internal_child = ide_workspace_get_internal_child;
   iface->add_child = ide_workspace_add_child;
+  iface->get_internal_child = ide_workspace_get_internal_child;
 }
 
 /**
- * ide_workspace_get_overlay:
+ * ide_workspace_get_statusbar:
  * @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
+ * Gets the statusbar if any.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): a #PanelStatusbar or %NULL
  */
-GtkOverlay *
-ide_workspace_get_overlay (IdeWorkspace *self)
+PanelStatusbar *
+ide_workspace_get_statusbar (IdeWorkspace *self)
 {
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
 
   g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
 
-  return priv->overlay;
+  return priv->statusbar;
+}
+
+static void
+add_to_frame_with_depth (PanelFrame  *frame,
+                         PanelWidget *widget,
+                         guint        depth,
+                         gboolean     depth_set)
+{
+  PanelWidget *previous_page;
+  guint n_pages;
+
+  g_assert (PANEL_IS_FRAME (frame));
+  g_assert (PANEL_IS_WIDGET (widget));
+
+  previous_page = panel_frame_get_visible_child (frame);
+
+  if (!depth_set || depth > G_MAXINT)
+    depth = G_MAXINT;
+
+  SET_PRIORITY (widget, depth);
+
+  n_pages = panel_frame_get_n_pages (frame);
+
+  for (guint i = 0; i < n_pages; i++)
+    {
+      PanelWidget *child = panel_frame_get_page (frame, i);
+
+      if ((int)depth < GET_PRIORITY (child))
+        {
+          panel_frame_add_before (frame, widget, child);
+          goto reset_page;
+        }
+    }
+
+  panel_frame_add (frame, widget);
+
+reset_page:
+  if (previous_page != NULL)
+    panel_frame_set_visible_child (frame, previous_page);
+}
+
+static gboolean
+find_open_frame (IdeGrid *grid,
+                 guint   *column,
+                 guint   *row)
+{
+  guint n_columns;
+
+  g_assert (IDE_IS_GRID (grid));
+  g_assert (column != NULL);
+  g_assert (row != NULL);
+
+  n_columns = panel_grid_get_n_columns (PANEL_GRID (grid));
+
+  for (guint c = 0; c < n_columns; c++)
+    {
+      PanelGridColumn *grid_column = panel_grid_get_column (PANEL_GRID (grid), c);
+      guint n_rows = panel_grid_column_get_n_rows (grid_column);
+
+      for (guint r = 0; r < n_rows; r++)
+        {
+          PanelFrame *frame = panel_grid_column_get_row (grid_column, r);
+
+          if (panel_frame_get_empty (frame))
+            {
+              *column = c;
+              *row = r;
+              return TRUE;
+            }
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+find_most_recent_frame (IdeWorkspace *workspace,
+                        IdeGrid      *grid,
+                        guint        *column,
+                        guint        *row)
+{
+  GtkWidget *grid_column;
+  IdeFrame *frame;
+  guint n_columns;
+
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_GRID (grid));
+  g_assert (column != NULL);
+  g_assert (row != NULL);
+
+  *column = 0;
+  *row = 0;
+
+  if (!(frame = ide_workspace_get_most_recent_frame (workspace)) ||
+      !(grid_column = gtk_widget_get_ancestor (GTK_WIDGET (frame), PANEL_TYPE_GRID_COLUMN)))
+    return;
+
+  n_columns = panel_grid_get_n_columns (PANEL_GRID (grid));
+
+  for (guint c = 0; c < n_columns; c++)
+    {
+      if (grid_column == (GtkWidget *)panel_grid_get_column (PANEL_GRID (grid), c))
+        {
+          guint n_rows = panel_grid_column_get_n_rows (PANEL_GRID_COLUMN (grid_column));
+
+          for (guint r = 0; r < n_rows; r++)
+            {
+              if ((PanelFrame *)frame == panel_grid_column_get_row (PANEL_GRID_COLUMN (grid_column), r))
+                {
+                  *column = c;
+                  *row = r;
+                  return;
+                }
+            }
+        }
+    }
+}
+
+static gboolean
+dummy_cb (gpointer data)
+{
+  return G_SOURCE_REMOVE;
+}
+
+void
+_ide_workspace_add_widget (IdeWorkspace     *self,
+                           PanelWidget      *widget,
+                           IdePanelPosition *position,
+                           PanelPaned       *dock_start,
+                           PanelPaned       *dock_end,
+                           PanelPaned       *dock_bottom,
+                           IdeGrid          *grid)
+{
+  PanelFrame *frame;
+  gboolean depth_set;
+  guint depth;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (PANEL_IS_WIDGET (widget));
+  g_return_if_fail (position != NULL);
+  g_return_if_fail (!dock_start || PANEL_IS_PANED (dock_start));
+  g_return_if_fail (!dock_end || PANEL_IS_PANED (dock_end));
+  g_return_if_fail (!dock_bottom || PANEL_IS_PANED (dock_bottom));
+  g_return_if_fail (IDE_IS_GRID (grid));
+
+  if (!(frame = _ide_workspace_find_frame (self, position, dock_start, dock_end, dock_bottom, grid)))
+    {
+      /* Extreme failure case, try to be nice and wait until
+       * end of the main loop to destroy
+       */
+      g_idle_add_full (G_PRIORITY_LOW,
+                       dummy_cb,
+                       g_object_ref_sink (widget),
+                       g_object_unref);
+      IDE_EXIT;
+    }
+
+  depth_set = ide_panel_position_get_depth (position, &depth);
+  add_to_frame_with_depth (frame, widget, depth, depth_set);
+
+  IDE_EXIT;
+}
+
+PanelFrame *
+_ide_workspace_find_frame (IdeWorkspace     *self,
+                           IdePanelPosition *position,
+                           PanelPaned       *dock_start,
+                           PanelPaned       *dock_end,
+                           PanelPaned       *dock_bottom,
+                           IdeGrid          *grid)
+{
+  PanelDockPosition edge;
+  PanelPaned *paned = NULL;
+  PanelFrame *ret;
+  GtkWidget *parent;
+  guint column = 0;
+  guint row = 0;
+  guint nth = 0;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+  g_return_val_if_fail (position != NULL, NULL);
+  g_return_val_if_fail (!dock_start || PANEL_IS_PANED (dock_start), NULL);
+  g_return_val_if_fail (!dock_end || PANEL_IS_PANED (dock_end), NULL);
+  g_return_val_if_fail (!dock_bottom || PANEL_IS_PANED (dock_bottom), NULL);
+
+  if (!ide_panel_position_get_edge (position, &edge))
+    edge = PANEL_DOCK_POSITION_CENTER;
+
+  if (edge == PANEL_DOCK_POSITION_CENTER)
+    {
+      gboolean has_column = ide_panel_position_get_column (position, &column);
+      gboolean has_row = ide_panel_position_get_row (position, &row);
+
+      /* If we are adding a page, and no row or column is set, then the next
+       * best thing to do is to try to find an open frame. If we can't do that
+       * then we'll try to find the most recent frame.
+       */
+      if (!has_column && !has_row)
+        {
+          if (!find_open_frame (grid, &column, &row))
+            find_most_recent_frame (self, grid, &column, &row);
+        }
+
+      ret = panel_grid_column_get_row (panel_grid_get_column (PANEL_GRID (grid), column), row);
+
+      IDE_RETURN (ret);
+    }
+
+  switch (edge)
+    {
+    case PANEL_DOCK_POSITION_START:
+      paned = dock_start;
+      ide_panel_position_get_row (position, &nth);
+      break;
+
+    case PANEL_DOCK_POSITION_END:
+      paned = dock_end;
+      ide_panel_position_get_row (position, &nth);
+      break;
+
+    case PANEL_DOCK_POSITION_BOTTOM:
+      paned = dock_bottom;
+      ide_panel_position_get_column (position, &nth);
+      break;
+
+    case PANEL_DOCK_POSITION_TOP:
+      g_warning ("Top panel is not supported");
+      return NULL;
+
+    case PANEL_DOCK_POSITION_CENTER:
+    default:
+      return NULL;
+    }
+
+  while (!(parent = panel_paned_get_nth_child (paned, nth)))
+    {
+      parent = panel_frame_new ();
+
+      if (edge == PANEL_DOCK_POSITION_START ||
+          edge == PANEL_DOCK_POSITION_END)
+        gtk_orientable_set_orientation (GTK_ORIENTABLE (parent), GTK_ORIENTATION_VERTICAL);
+      else
+        gtk_orientable_set_orientation (GTK_ORIENTABLE (parent), GTK_ORIENTATION_HORIZONTAL);
+
+      panel_paned_append (paned, parent);
+    }
+
+  IDE_RETURN (PANEL_FRAME (parent));
 }
 
 /**
- * ide_workspace_get_most_recent_page:
- * @self: a #IdeWorkspace
+ * ide_workspace_get_frame_at_position:
+ * @self: an #IdeWorkspace
+ * @position: an #IdePanelPosition
  *
- * Gets the most recently focused #IdePage.
- *
- * Returns: (transfer none) (nullable): an #IdePage or %NULL
+ * Attempts to locate the #PanelFrame at a given position.
  *
- * Since: 3.32
+ * Returns: (transfer none) (nullable): a #PaneFrame or %NULL
  */
-IdePage *
-ide_workspace_get_most_recent_page (IdeWorkspace *self)
+PanelFrame *
+ide_workspace_get_frame_at_position (IdeWorkspace     *self,
+                                     IdePanelPosition *position)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
-
   g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+  g_return_val_if_fail (position != NULL, NULL);
 
-  if (priv->page_mru.head != NULL)
-    return IDE_PAGE (priv->page_mru.head->data);
+  if (IDE_WORKSPACE_GET_CLASS (self)->get_frame_at_position)
+    return IDE_WORKSPACE_GET_CLASS (self)->get_frame_at_position (self, position);
 
   return NULL;
 }
 
+gboolean
+_ide_workspace_can_search (IdeWorkspace *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), FALSE);
+
+  return IDE_WORKSPACE_GET_CLASS (self)->can_search (self);
+}
+
 void
-_ide_workspace_add_page_mru (IdeWorkspace *self,
-                             GList        *mru_link)
+_ide_workspace_begin_global_search (IdeWorkspace *self)
 {
   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));
+  if (priv->search_popover == NULL)
+    {
+      IdeWorkbench *workbench = ide_workspace_get_workbench (self);
+      IdeSearchEngine *search_engine = ide_workbench_get_search_engine (workbench);
 
-  g_queue_push_head_link (&priv->page_mru, mru_link);
+      priv->search_popover = IDE_SEARCH_POPOVER (ide_search_popover_new (search_engine));
+      gtk_widget_set_parent (GTK_WIDGET (priv->search_popover), GTK_WIDGET (self));
+
+      /* Popovers don't appear (as of GTK 4.7) to capture/bubble from the GtkRoot
+       * when running controllers. So we need to manually attach them for the popovers
+       * that are important enough to care about.
+       */
+      ide_workspace_attach_shortcuts (self, GTK_WIDGET (priv->search_popover));
+    }
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (priv->search_popover)))
+    gtk_popover_popup (GTK_POPOVER (priv->search_popover));
 }
 
 void
-_ide_workspace_remove_page_mru (IdeWorkspace *self,
-                                GList        *mru_link)
+ide_workspace_add_overlay (IdeWorkspace *self,
+                           GtkWidget    *overlay)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (GTK_IS_WIDGET (overlay));
+  g_return_if_fail (gtk_widget_get_parent (overlay) == NULL);
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->add_overlay == NULL)
+    g_critical ("Attempt to add overlay of type %s to workspace of type %s which does not support overlays",
+                G_OBJECT_TYPE_NAME (overlay), G_OBJECT_TYPE_NAME (self));
+  else
+    IDE_WORKSPACE_GET_CLASS (self)->add_overlay (self, overlay);
+}
 
+void
+ide_workspace_remove_overlay (IdeWorkspace *self,
+                              GtkWidget    *overlay)
+{
   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_return_if_fail (GTK_IS_WIDGET (overlay));
 
-  g_debug ("Removing %s from page MRU",
-           G_OBJECT_TYPE_NAME (mru_link->data));
+  if (IDE_WORKSPACE_GET_CLASS (self)->remove_overlay == NULL)
+    g_critical ("Attempt to remove overlay of type %s to workspace of type %s which does not support 
overlays",
+                G_OBJECT_TYPE_NAME (overlay), G_OBJECT_TYPE_NAME (self));
+  else
+    IDE_WORKSPACE_GET_CLASS (self)->remove_overlay (self, overlay);
+}
 
-  g_queue_unlink (&priv->page_mru, mru_link);
+static gboolean
+shortcut_phase_filter (gpointer item,
+                       gpointer user_data)
+{
+  return ide_shortcut_is_phase (item, user_data);
 }
 
 void
-_ide_workspace_move_front_page_mru (IdeWorkspace *self,
-                                    GList        *mru_link)
+_ide_workspace_set_shortcut_model (IdeWorkspace *self,
+                                   GListModel   *model)
 {
   IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  static GtkCustomFilter *bubble_filter;
+  static GtkCustomFilter *capture_filter;
 
   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_return_if_fail (G_IS_LIST_MODEL (model));
 
-  if (mru_link == priv->page_mru.head)
-    return;
+  if (bubble_filter == NULL)
+    bubble_filter = gtk_custom_filter_new (shortcut_phase_filter, GINT_TO_POINTER (GTK_PHASE_BUBBLE), NULL);
 
-  g_debug ("Moving %s to front of page MRU",
-           G_OBJECT_TYPE_NAME (mru_link->data));
+  if (capture_filter == NULL)
+    capture_filter = gtk_custom_filter_new (shortcut_phase_filter, GINT_TO_POINTER (GTK_PHASE_CAPTURE), 
NULL);
 
-  g_queue_unlink (&priv->page_mru, mru_link);
-  g_queue_push_head_link (&priv->page_mru, mru_link);
+  priv->shortcut_model_capture = gtk_filter_list_model_new (g_object_ref (model),
+                                                            g_object_ref (GTK_FILTER (capture_filter)));
+  priv->shortcut_model_bubble = gtk_filter_list_model_new (g_object_ref (model),
+                                                           g_object_ref (GTK_FILTER (bubble_filter)));
+
+  ide_workspace_attach_shortcuts (self, GTK_WIDGET (self));
 }
 
-/**
- * ide_workspace_addin_find_by_module_name:
- * @workspace: an #IdeWorkspace
- * @module_name: the name of the addin module
- *
- * Finds the addin (if any) matching the plugin's @module_name.
- *
- * Returns: (transfer none) (nullable): an #IdeWorkspaceAddin or %NULL
- *
- * Since: 3.40
- */
-IdeWorkspaceAddin *
-ide_workspace_addin_find_by_module_name (IdeWorkspace *workspace,
-                                         const gchar  *module_name)
+void
+ide_workspace_add_grid_column (IdeWorkspace *self,
+                               guint         position)
 {
-  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (workspace);
-  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_WORKSPACE (workspace), NULL);
-  g_return_val_if_fail (module_name != NULL, NULL);
-
-  if (priv->addins == NULL)
-    return NULL;
-
-  engine = peas_engine_get_default ();
-
-  if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
-    ret = ide_extension_set_adapter_get_extension (priv->addins, plugin_info);
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_WORKSPACE_GET_CLASS (self)->add_grid_column);
 
-  return IDE_WORKSPACE_ADDIN (ret);
+  IDE_WORKSPACE_GET_CLASS (self)->add_grid_column (self, position);
 }
diff --git a/src/libide/gui/ide-workspace.h b/src/libide/gui/ide-workspace.h
index e9a28ac7a..8c27a2d4a 100644
--- a/src/libide/gui/ide-workspace.h
+++ b/src/libide/gui/ide-workspace.h
@@ -24,74 +24,112 @@
 # error "Only <libide-gui.h> can be included directly."
 #endif
 
-#include <dazzle.h>
-#include <handy.h>
+#include <adwaita.h>
+
 #include <libide-core.h>
 #include <libide-projects.h>
 
+#include "ide-frame.h"
 #include "ide-header-bar.h"
 #include "ide-page.h"
-#include "ide-surface.h"
+#include "ide-pane.h"
+#include "ide-panel-position.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, HdyApplicationWindow)
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (IdeWorkspace, ide_workspace, IDE, WORKSPACE, AdwApplicationWindow)
+
+typedef void (*IdeWorkspaceCallback) (IdeWorkspace *workspace,
+                                      gpointer      user_data);
 
 struct _IdeWorkspaceClass
 {
-  HdyApplicationWindowClass parent_class;
+  AdwApplicationWindowClass 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);
+  guint has_statusbar : 1;
+  guint _unused_flags : 31;
 
-  /*< private >*/
-  gpointer _reserved[32];
+  void          (*context_set)           (IdeWorkspace         *self,
+                                          IdeContext           *context);
+  void          (*foreach_page)          (IdeWorkspace         *self,
+                                          IdePageCallback       callback,
+                                          gpointer              user_data);
+  IdeHeaderBar *(*get_header_bar)        (IdeWorkspace         *self);
+  IdePage      *(*get_most_recent_page)  (IdeWorkspace         *self);
+  IdeFrame     *(*get_most_recent_frame) (IdeWorkspace         *self);
+  void          (*agree_to_close_async)  (IdeWorkspace         *self,
+                                          GCancellable         *cancellable,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data);
+  gboolean      (*agree_to_close_finish) (IdeWorkspace         *self,
+                                          GAsyncResult         *result,
+                                          GError              **error);
+  void          (*add_pane)              (IdeWorkspace         *self,
+                                          IdePane              *pane,
+                                          IdePanelPosition     *position);
+  void          (*add_page)              (IdeWorkspace         *self,
+                                          IdePage              *page,
+                                          IdePanelPosition     *position);
+  void          (*add_grid_column)       (IdeWorkspace         *self,
+                                          guint                 column);
+  void          (*add_overlay)           (IdeWorkspace         *self,
+                                          GtkWidget            *overlay);
+  void          (*remove_overlay)        (IdeWorkspace         *self,
+                                          GtkWidget            *overlay);
+  PanelFrame   *(*get_frame_at_position) (IdeWorkspace         *self,
+                                          IdePanelPosition     *position);
+  void          (*restore_size)          (IdeWorkspace         *self,
+                                          int                   width,
+                                          int                   height);
+  gboolean      (*save_size)             (IdeWorkspace         *self,
+                                          int                  *width,
+                                          int                  *height);
+  gboolean      (*can_search)            (IdeWorkspace         *self);
 };
 
-IDE_AVAILABLE_IN_3_32
-void          ide_workspace_class_set_kind           (IdeWorkspaceClass *klass,
+IDE_AVAILABLE_IN_ALL
+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);
+IDE_AVAILABLE_IN_ALL
+IdeHeaderBar   *ide_workspace_get_header_bar           (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+IdeContext     *ide_workspace_get_context              (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+GCancellable   *ide_workspace_get_cancellable          (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_foreach_page             (IdeWorkspace      *self,
+                                                        IdePageCallback    callback,
+                                                        gpointer           user_data);
+IDE_AVAILABLE_IN_ALL
+IdePage        *ide_workspace_get_most_recent_page     (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+IdeFrame       *ide_workspace_get_most_recent_frame    (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+PanelFrame     *ide_workspace_get_frame_at_position    (IdeWorkspace      *self,
+                                                        IdePanelPosition  *position);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_add_pane                 (IdeWorkspace      *self,
+                                                        IdePane           *pane,
+                                                        IdePanelPosition  *position);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_add_page                 (IdeWorkspace      *self,
+                                                        IdePage           *page,
+                                                        IdePanelPosition  *position);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_add_grid_column          (IdeWorkspace      *self,
+                                                        guint              position);
+IDE_AVAILABLE_IN_ALL
+PanelStatusbar *ide_workspace_get_statusbar            (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_add_overlay              (IdeWorkspace      *self,
+                                                        GtkWidget         *widget);
+IDE_AVAILABLE_IN_ALL
+void            ide_workspace_remove_overlay           (IdeWorkspace      *self,
+                                                        GtkWidget         *widget);
 
 G_END_DECLS
diff --git a/src/libide/gui/libide-gui.gresource.xml b/src/libide/gui/libide-gui.gresource.xml
index 2c7fcb2b9..faa3a83bf 100644
--- a/src/libide/gui/libide-gui.gresource.xml
+++ b/src/libide/gui/libide-gui.gresource.xml
@@ -10,6 +10,7 @@
     <file>images/style-preview-dark.png</file>
     <file>images/style-preview-default.png</file>
     <file>images/style-preview-light.png</file>
+    <file>style.css</file>
   </gresource>
   <gresource prefix="/org/gnome/libide-gui/ui">
     <file preprocess="xml-stripblanks">ide-environment-editor-row.ui</file>
@@ -19,9 +20,11 @@
     <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-page.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-popover.ui</file>
     <file preprocess="xml-stripblanks">ide-style-variant-preview.ui</file>
   </gresource>
 </gresources>
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
index 6364db022..ed9ee4f76 100644
--- a/src/libide/gui/meson.build
+++ b/src/libide/gui/meson.build
@@ -1,3 +1,4 @@
+libide_gui_header_dir = join_paths(libide_header_dir, 'gui')
 libide_gui_header_subdir = join_paths(libide_header_subdir, 'gui')
 libide_include_directories += include_directories('.')
 
@@ -23,9 +24,13 @@ libide_gui_public_headers = [
   'ide-omni-bar.h',
   'ide-page.h',
   'ide-pane.h',
+  'ide-panel-position.h',
   'ide-preferences-addin.h',
+  'ide-preferences-choice-row.h',
   'ide-preferences-window.h',
   'ide-primary-workspace.h',
+  'ide-run-button.h',
+  'ide-search-popover.h',
   'ide-session-addin.h',
   'ide-shortcut-provider.h',
   'ide-workbench.h',
@@ -47,8 +52,11 @@ libide_gui_private_headers = [
   'ide-notification-list-box-row-private.h',
   'ide-notification-stack-private.h',
   'ide-notification-view-private.h',
+  'ide-page-private.h',
   'ide-preferences-builtin-private.h',
+  'ide-primary-workspace-private.h',
   'ide-recoloring-private.h',
+  'ide-search-popover-private.h',
   'ide-session-private.h',
   'ide-shortcut-bundle-private.h',
   'ide-shortcut-manager-private.h',
@@ -59,6 +67,7 @@ libide_gui_private_sources = [
   'ide-application-actions.c',
   'ide-application-color.c',
   'ide-application-plugins.c',
+  'ide-application-settings.c',
   'ide-environment-editor-row.c',
   'ide-notification-list-box-row.c',
   'ide-notification-stack.c',
@@ -66,6 +75,7 @@ libide_gui_private_sources = [
   'ide-preferences-builtin.c',
   'ide-primary-workspace-actions.c',
   'ide-recoloring.c',
+  'ide-search-popover.c',
   'ide-session.c',
   'ide-shortcut-bundle.c',
   'ide-shortcut-manager.c',
@@ -91,9 +101,12 @@ libide_gui_public_sources = [
   'ide-omni-bar.c',
   'ide-page.c',
   'ide-pane.c',
+  'ide-panel-position.c',
   'ide-primary-workspace.c',
   'ide-preferences-addin.c',
+  'ide-preferences-choice-row.c',
   'ide-preferences-window.c',
+  'ide-run-button.c',
   'ide-session-addin.c',
   'ide-shortcut-provider.c',
   'ide-workbench.c',
diff --git a/src/libide/gui/style.css b/src/libide/gui/style.css
new file mode 100644
index 000000000..0105575d8
--- /dev/null
+++ b/src/libide/gui/style.css
@@ -0,0 +1,98 @@
+/* Preferences */
+window.preferences list.boxed-list.style-variant button {
+  padding: 0;
+  margin: 0;
+  border: 3px solid transparent;
+  border-radius: 9px;
+  background: transparent;
+}
+window.preferences list.boxed-list.style-variant button:checked {
+  border-color: @theme_selected_bg_color;
+}
+.checkimage radio,
+.checkimage check {
+  background: none;
+  outline: none;
+  box-shadow: none;
+  border: none;
+  color: @window_fg_color;
+  -gtk-icon-source: none;
+  -gtk-icon-size: 18px;
+}
+.checkimage radio:checked,
+.checkimage check:checked {
+  -gtk-icon-source: -gtk-icontheme('object-select-symbolic');
+}
+stylevariantpreview widget.wallpaper {
+  border-radius: 6px;
+  box-shadow: 0 0 9px 1px rgba(0,0,0,.2);
+}
+stylevariantpreview widget.window {
+  box-shadow: 0 0 9px 2px rgba(0,0,0,.25);
+  border-radius: 7px;
+  border: 1px solid alpha(white, .075);
+}
+stylevariantpreview widget.window.dark {
+  background: #242424;
+}
+stylevariantpreview widget.window.light {
+  background: #fafafa;
+}
+stylevariantpreview widget.header {
+  border-radius: 7px 7px 0 0;
+  border-bottom: 1px solid alpha(white, 0.075);
+}
+stylevariantpreview widget.header.dark {
+  background: #484848;
+}
+stylevariantpreview widget.header.light {
+  background: #cccccc;
+}
+
+window popover.global-search contents {
+  padding: 0;
+  margin: 0;
+}
+window popover.global-search .navigation-sidebar {
+  border-right: 1px solid @borders;
+}
+
+notificationstack button {
+  margin: 0;
+  padding: 0;
+  background: none;
+}
+
+scrolledwindow.shadow-when-scroll undershoot.top {
+  box-shadow: inset 0px 5px 14px -14px rgba(0,0,0,.9);
+}
+
+button.pill.small {
+  font-size: .83333em;
+  border-radius: 99px;
+  margin: 0;
+  padding: 1px 12px;
+}
+
+/* Styling for search bars */
+.searchbar {
+  background-color: @popover_bg_color;
+  background-clip: padding-box;
+  color: @popover_fg_color;
+  padding: 6px;
+  border-style: solid;
+  border-color: alpha(black, .14);
+  border-left-width: 1px;
+  border-right-width: 1px;
+  border-bottom-width: 1px;
+  border-radius: 0 0 12px 12px;
+  margin: 0 10px 20px 10px;
+  box-shadow: 0 1px 5px 1px alpha(black, .09),
+              0 2px 14px 3px alpha(black, .05);
+}
+.searchbar button.flat.circular {
+  min-width: 24px;
+  min-height: 24px;
+  margin: 0px;
+  padding: 3px;
+}


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