Browse Source

Merge pull request #1880 from afshin/restore

Main area restore feature
Steven Silvester 8 năm trước cách đây
mục cha
commit
d447376ced

+ 1 - 1
package.json

@@ -20,7 +20,7 @@
     "@phosphor/properties": "^0.1.0",
     "@phosphor/signaling": "^0.1.1",
     "@phosphor/virtualdom": "^0.1.0",
-    "@phosphor/widgets": "^0.1.4",
+    "@phosphor/widgets": "^0.1.7",
     "ansi_up": "^1.3.0",
     "codemirror": "^5.23",
     "d3-dsv": "^1.0.0",

+ 1 - 5
src/application/index.ts

@@ -5,10 +5,6 @@ import {
   Application, IPlugin
 } from '@phosphor/application';
 
-import {
-  IInstanceRestorer
-} from '../instancerestorer';
-
 import {
   ModuleLoader
 } from './loader';
@@ -83,7 +79,7 @@ class JupyterLab extends Application<ApplicationShell> {
    * #### Notes
    * This is just a reference to `shell.restored`.
    */
-  get restored(): Promise<IInstanceRestorer.ILayout> {
+  get restored(): Promise<ApplicationShell.ILayout> {
     return this.shell.restored;
   }
 

+ 217 - 90
src/application/shell.ts

@@ -14,14 +14,10 @@ import {
 } from '@phosphor/signaling';
 
 import {
-  BoxLayout, BoxPanel, DockPanel, FocusTracker, Panel, SplitPanel,
-  StackedPanel, TabBar, Title, Widget
+  BoxLayout, BoxPanel, DockLayout, DockPanel, FocusTracker,
+  Panel, SplitPanel, StackedPanel, TabBar, Title, Widget
 } from '@phosphor/widgets';
 
-import {
-  IInstanceRestorer
-} from '../instancerestorer';
-
 
 /**
  * The class name added to AppShell instances.
@@ -165,7 +161,7 @@ class ApplicationShell extends Widget {
   /**
    * Promise that resolves when state is restored, returning layout description.
    */
-  get restored(): Promise<IInstanceRestorer.ILayout> {
+  get restored(): Promise<ApplicationShell.ILayout> {
     return this._restored.promise;
   }
 
@@ -191,19 +187,26 @@ class ApplicationShell extends Widget {
   */
   activateNextTab(): void {
     let current = this._currentTabBar();
-    if (current) {
-      let ci = current.currentIndex;
-      if (ci !== -1) {
-        if (ci < current.titles.length - 1) {
-          current.currentIndex += 1;
-          current.currentTitle.owner.activate();
-        } else if (ci === current.titles.length - 1) {
-          let nextBar = this._nextTabBar();
-          if (nextBar) {
-            nextBar.currentIndex = 0;
-            nextBar.currentTitle.owner.activate();
-          }
-        }
+    if (!current) {
+      return;
+    }
+
+    let ci = current.currentIndex;
+    if (ci === -1) {
+      return;
+    }
+
+    if (ci < current.titles.length - 1) {
+      current.currentIndex += 1;
+      current.currentTitle.owner.activate();
+      return;
+    }
+
+    if (ci === current.titles.length - 1) {
+      let nextBar = this._nextTabBar();
+      if (nextBar) {
+        nextBar.currentIndex = 0;
+        nextBar.currentTitle.owner.activate();
       }
     }
   }
@@ -213,20 +216,27 @@ class ApplicationShell extends Widget {
   */
   activatePreviousTab(): void {
     let current = this._currentTabBar();
-    if (current) {
-      let ci = current.currentIndex;
-      if (ci !== -1) {
-        if (ci > 0) {
-          current.currentIndex -= 1;
-          current.currentTitle.owner.activate();
-        } else if (ci === 0) {
-          let prevBar = this._previousTabBar();
-          if (prevBar) {
-            let len = prevBar.titles.length;
-            prevBar.currentIndex = len - 1;
-            prevBar.currentTitle.owner.activate();
-          }
-        }
+    if (!current) {
+      return;
+    }
+
+    let ci = current.currentIndex;
+    if (ci === -1) {
+      return;
+    }
+
+    if (ci > 0) {
+      current.currentIndex -= 1;
+      current.currentTitle.owner.activate();
+      return;
+    }
+
+    if (ci === 0) {
+      let prevBar = this._previousTabBar();
+      if (prevBar) {
+        let len = prevBar.titles.length;
+        prevBar.currentIndex = len - 1;
+        prevBar.currentTitle.owner.activate();
       }
     }
   }
@@ -317,14 +327,14 @@ class ApplicationShell extends Widget {
    * Close all widgets in the main area.
    */
   closeAll(): void {
-    each(toArray(this._dockPanel.widgets()), widget => { widget.close(); });
+    each(this._dockPanel.widgets(), widget => { widget.close(); });
     this._save();
   }
 
   /**
    * Set the layout data store for the application shell.
    */
-  setLayoutDB(database: IInstanceRestorer.ILayoutDB): void {
+  setLayoutDB(database: ApplicationShell.ILayoutDB): void {
     if (this._database) {
       throw new Error('cannot reset layout database');
     }
@@ -334,20 +344,33 @@ class ApplicationShell extends Widget {
         return;
       }
 
-      // Rehydrate the application.
-      let { currentWidget, leftArea, rightArea } = saved;
+      const { mainArea, leftArea, rightArea } = saved;
+
+      // Rehydrate the main area.
+      if (mainArea) {
+        if (mainArea.dock) {
+          this._dockPanel.restoreLayout(mainArea.dock);
+        }
+        if (mainArea.currentWidget) {
+          this.activateById(mainArea.currentWidget.id);
+        }
+      }
+
+      // Rehydrate the left area.
       if (leftArea) {
         this._leftHandler.rehydrate(leftArea);
       }
+
+      // Rehydrate the right area.
       if (rightArea) {
         this._rightHandler.rehydrate(rightArea);
       }
-      if (currentWidget) {
-        this.activateById(currentWidget.id);
-      }
+
+      // Set restored flag, save state, and resolve the restoration promise.
       this._isRestored = true;
       return this._save().then(() => { this._restored.resolve(saved); });
     });
+
     // Catch current changed events on the side handlers.
     this._tracker.currentChanged.connect(this._save, this);
     this._leftHandler.sideBar.currentChanged.connect(this._save, this);
@@ -355,58 +378,65 @@ class ApplicationShell extends Widget {
   }
 
   /*
-   * Return the TabBar that has the currently active Widget or undefined.
+   * Return the TabBar that has the currently active Widget or null.
    */
-  private _currentTabBar(): TabBar<Widget> {
+  private _currentTabBar(): TabBar<Widget> | null {
     let current = this._tracker.currentWidget;
-    if (current) {
-      let title = current.title;
-      let tabBar = find(this._dockPanel.tabBars(), bar => {
-        return ArrayExt.firstIndexOf(bar.titles, title) > -1;
-      });
-      return tabBar;
+    if (!current) {
+      return null;
     }
-    return void 0;
+
+    let title = current.title;
+    return find(this._dockPanel.tabBars(), bar => {
+      return ArrayExt.firstIndexOf(bar.titles, title) > -1;
+    }) || null;
   }
 
   /*
-   * Return the TabBar previous to the current TabBar (see above) or undefined.
+   * Return the TabBar previous to the current TabBar (see above) or null.
    */
-  private _previousTabBar(): TabBar<Widget> {
+  private _previousTabBar(): TabBar<Widget> | null {
     let current = this._currentTabBar();
     if (current) {
-      let bars = toArray(this._dockPanel.tabBars());
-      let len = bars.length;
-      let ci = ArrayExt.firstIndexOf(bars, current);
-      let prevBar: TabBar<Widget> = null;
-      if (ci > 0) {
-        prevBar = bars[ci - 1];
-      } else if (ci === 0) {
-        prevBar = bars[len - 1];
-      }
-      return prevBar;
+      return null;
     }
-    return void 0;
+    let bars = toArray(this._dockPanel.tabBars());
+    let len = bars.length;
+    let ci = ArrayExt.firstIndexOf(bars, current);
+
+    if (ci > 0) {
+      return bars[ci - 1];
+    }
+
+    if (ci === 0) {
+      return bars[len - 1];
+    }
+
+    return null;
   }
 
   /*
-   * Return the TabBar next to the current TabBar (see above) or undefined.
+   * Return the TabBar next to the current TabBar (see above) or null.
    */
-  private _nextTabBar(): TabBar<Widget> {
+  private _nextTabBar(): TabBar<Widget> | null {
     let current = this._currentTabBar();
-    if (current) {
-      let bars = toArray(this._dockPanel.tabBars());
-      let len = bars.length;
-      let ci = ArrayExt.firstIndexOf(bars, current);
-      let nextBar: TabBar<Widget> = null;
-      if (ci < (len - 1)) {
-        nextBar = bars[ci + 1];
-      } else if (ci === len - 1) {
-        nextBar = bars[0];
-      }
-      return nextBar;
+    if (!current) {
+      return null;
     }
-    return void 0;
+
+    let bars = toArray(this._dockPanel.tabBars());
+    let len = bars.length;
+    let ci = ArrayExt.firstIndexOf(bars, current);
+
+    if (ci < (len - 1)) {
+      return bars[ci + 1];
+    }
+
+    if (ci === len - 1) {
+      return bars[0];
+    }
+
+    return null;
   }
 
 
@@ -417,9 +447,11 @@ class ApplicationShell extends Widget {
     if (!this._database || !this._isRestored) {
       return;
     }
-
-    let data: IInstanceRestorer.ILayout = {
-      currentWidget: this._tracker.currentWidget,
+    let data: ApplicationShell.ILayout = {
+      mainArea: {
+        currentWidget: this._tracker.currentWidget,
+        dock: this._dockPanel.saveLayout(),
+      },
       leftArea: this._leftHandler.dehydrate(),
       rightArea: this._rightHandler.dehydrate()
     };
@@ -456,13 +488,13 @@ class ApplicationShell extends Widget {
     this._activeChanged.emit(args);
   }
 
-  private _database: IInstanceRestorer.ILayoutDB = null;
+  private _database: ApplicationShell.ILayoutDB = null;
   private _dockPanel: DockPanel;
   private _hboxPanel: BoxPanel;
   private _hsplitPanel: SplitPanel;
   private _isRestored = false;
   private _leftHandler: Private.SideBarHandler;
-  private _restored = new PromiseDelegate<IInstanceRestorer.ILayout>();
+  private _restored = new PromiseDelegate<ApplicationShell.ILayout>();
   private _rightHandler: Private.SideBarHandler;
   private _topPanel: Panel;
   private _tracker = new FocusTracker<Widget>();
@@ -483,21 +515,116 @@ namespace ApplicationShell {
   type Area = 'main' | 'top' | 'left' | 'right';
 
   /**
-   * The options for adding a widget to a side area of the shell.
+   * The restorable description of an area within the main dock panel.
    */
   export
-  interface ISideAreaOptions {
+  type AreaConfig = DockLayout.AreaConfig;
+
+  /**
+   * An arguments object for the changed signals.
+   */
+  export
+  type IChangedArgs = FocusTracker.IChangedArgs<Widget>;
+
+  /**
+   * A description of the application's user interface layout.
+   */
+  export
+  interface ILayout {
     /**
-     * The rank order of the widget among its siblings.
+     * Indicates whether fetched session restore data was actually retrieved
+     * from the state database or whether it is a fresh blank slate.
+     *
+     * #### Notes
+     * This attribute is only relevant when the layout data is retrieved via a
+     * `fetch` call. If it is set when being passed into `save`, it will be
+     * ignored.
      */
-    rank?: number;
+    readonly fresh?: boolean;
+
+    /**
+     * The main area of the user interface.
+     */
+    readonly mainArea: IMainArea | null;
+
+    /**
+     * The left area of the user interface.
+     */
+    readonly leftArea: ISideArea | null;
+
+    /**
+     * The right area of the user interface.
+     */
+    readonly rightArea: ISideArea | null;
   }
 
   /**
-   * An arguments object for the changed signals.
+   * An application layout data store.
    */
   export
-  type IChangedArgs = FocusTracker.IChangedArgs<Widget>;
+  interface ILayoutDB {
+    /**
+     * Fetch the layout state for the application.
+     *
+     * #### Notes
+     * Fetching the layout relies on all widget restoration to be complete, so
+     * calls to `fetch` are guaranteed to return after restoration is complete.
+     */
+    fetch(): Promise<ApplicationShell.ILayout>;
+
+    /**
+     * Save the layout state for the application.
+     */
+    save(data: ApplicationShell.ILayout): Promise<void>;
+  }
+
+  /**
+   * The restorable description of the main application area.
+   */
+  export
+  interface IMainArea {
+    /**
+     * The current widget that has application focus.
+     */
+    readonly currentWidget: Widget | null;
+
+    /**
+     * The contents of the main application dock panel.
+     */
+    readonly dock: DockLayout.ILayoutConfig | null;
+  };
+
+  /**
+   * The restorable description of a sidebar in the user interface.
+   */
+  export
+  interface ISideArea {
+    /**
+     * A flag denoting whether the sidebar has been collapsed.
+     */
+    readonly collapsed: boolean;
+
+    /**
+     * The current widget that has side area focus.
+     */
+    readonly currentWidget: Widget | null;
+
+    /**
+     * The collection of widgets held by the sidebar.
+     */
+    readonly widgets: Array<Widget> | null;
+  }
+
+  /**
+   * The options for adding a widget to a side area of the shell.
+   */
+  export
+  interface ISideAreaOptions {
+    /**
+     * The rank order of the widget among its siblings.
+     */
+    rank?: number;
+  }
 }
 
 
@@ -609,7 +736,7 @@ namespace Private {
     /**
      * Dehydrate the side bar data.
      */
-    dehydrate(): IInstanceRestorer.ISideArea {
+    dehydrate(): ApplicationShell.ISideArea {
       let collapsed = this._sideBar.currentTitle === null;
       let widgets = toArray(this._stackedPanel.widgets);
       let currentWidget = widgets[this._sideBar.currentIndex];
@@ -619,7 +746,7 @@ namespace Private {
     /**
      * Rehydrate the side bar.
      */
-    rehydrate(data: IInstanceRestorer.ISideArea): void {
+    rehydrate(data: ApplicationShell.ISideArea): void {
       if (data.currentWidget) {
         this.activate(data.currentWidget.id);
       } else if (data.collapsed) {

+ 228 - 111
src/instancerestorer/instancerestorer.ts

@@ -24,7 +24,7 @@ import {
 } from '@phosphor/widgets';
 
 import {
-  InstanceTracker
+  ApplicationShell, InstanceTracker
 } from '../application';
 
 import {
@@ -72,79 +72,6 @@ interface IInstanceRestorer {
  */
 export
 namespace IInstanceRestorer {
-  /**
-   * An application layout data store.
-   */
-  export
-  interface ILayoutDB {
-    /**
-     * Fetch the layout state for the application.
-     *
-     * #### Notes
-     * Fetching the layout relies on all widget restoration to be complete, so
-     * calls to `fetch` are guaranteed to return after restoration is complete.
-     */
-    fetch(): Promise<IInstanceRestorer.ILayout>;
-
-    /**
-     * Save the layout state for the application.
-     */
-    save(data: IInstanceRestorer.ILayout): Promise<void>;
-  }
-
-  /**
-   * A description of the application's user interface layout.
-   */
-  export
-  interface ILayout {
-    /**
-     * The current widget that has application focus.
-     */
-    readonly currentWidget: Widget | null;
-
-    /**
-     * Indicates whether fetched session restore data was actually retrieved
-     * from the state database or whether it is a fresh blank slate.
-     *
-     * #### Notes
-     * This attribute is only relevant when the layout data is retrieved via a
-     * `fetch` call. If it is set when being passed into `save`, it will be
-     * ignored.
-     */
-    readonly fresh?: boolean;
-
-    /**
-     * The left area of the user interface.
-     */
-    readonly leftArea: ISideArea;
-
-    /**
-     * The right area of the user interface.
-     */
-    readonly rightArea: ISideArea;
-  }
-
-  /**
-   * The restorable description of a sidebar in the user interface.
-   */
-  export
-  interface ISideArea {
-    /**
-     * A flag denoting whether the sidebar has been collapsed.
-     */
-    readonly collapsed: boolean;
-
-    /**
-     * The current widget that has side area focus.
-     */
-    readonly currentWidget: Widget | null;
-
-    /**
-     * The collection of widgets held by the sidebar.
-     */
-    readonly widgets: Array<Widget> | null;
-  }
-
   /**
    * The state restoration configuration options.
    */
@@ -268,12 +195,9 @@ class InstanceRestorer implements IInstanceRestorer {
    * Fetching the layout relies on all widget restoration to be complete, so
    * calls to `fetch` are guaranteed to return after restoration is complete.
    */
-  fetch(): Promise<IInstanceRestorer.ILayout> {
-    const blank: IInstanceRestorer.ILayout = {
-      currentWidget: null,
-      fresh: true,
-      leftArea: { collapsed: true, currentWidget: null, widgets: null },
-      rightArea: { collapsed: true, currentWidget: null, widgets: null }
+  fetch(): Promise<ApplicationShell.ILayout> {
+    const blank: ApplicationShell.ILayout = {
+      fresh: true, mainArea: null, leftArea: null, rightArea: null
     };
     let layout = this._state.fetch(KEY);
 
@@ -282,14 +206,13 @@ class InstanceRestorer implements IInstanceRestorer {
         return blank;
       }
 
-      let { current, left, right } = data as InstanceRestorer.IDehydratedLayout;
+      const { main, left, right } = data as Private.ILayout;
 
       // If any data exists, then this is not a fresh session.
       const fresh = false;
 
-      // Rehydrate main area. Coerce type of `current` in case of bad data.
-      const currentWidget = current && this._widgets.has(`${current}`) ?
-        this._widgets.get(`${current}`) : null;
+      // Rehydrate main area.
+      const mainArea = this._rehydrateMainArea(main);
 
       // Rehydrate left area.
       const leftArea = this._rehydrateSideArea(left);
@@ -297,7 +220,7 @@ class InstanceRestorer implements IInstanceRestorer {
       // Rehydrate right area.
       const rightArea = this._rehydrateSideArea(right);
 
-      return { currentWidget, fresh, leftArea, rightArea };
+      return { fresh, mainArea, leftArea, rightArea };
     }).catch(() => blank); // Let fetch fail gracefully; return blank slate.
   }
 
@@ -341,7 +264,7 @@ class InstanceRestorer implements IInstanceRestorer {
   /**
    * Save the layout state for the application.
    */
-  save(data: IInstanceRestorer.ILayout): Promise<void> {
+  save(data: ApplicationShell.ILayout): Promise<void> {
     // If there are promises that are unresolved, bail.
     if (this._promises) {
       let warning = 'save() was called prematurely.';
@@ -349,16 +272,10 @@ class InstanceRestorer implements IInstanceRestorer {
       return Promise.reject(warning);
     }
 
-    let dehydrated: InstanceRestorer.IDehydratedLayout = {};
-    let current: string;
+    let dehydrated: Private.ILayout = {};
 
     // Dehydrate main area.
-    if (data.currentWidget) {
-      current = Private.nameProperty.get(data.currentWidget);
-      if (current) {
-        dehydrated.current = current;
-      }
-    }
+    dehydrated.main = this._dehydrateMainArea(data.mainArea);
 
     // Dehydrate left area.
     dehydrated.left = this._dehydrateSideArea(data.leftArea);
@@ -370,10 +287,28 @@ class InstanceRestorer implements IInstanceRestorer {
   }
 
   /**
-   * Dehydrate a side area into a serialized description object.
+   * Dehydrate a main area description into a serializable object.
    */
-  private _dehydrateSideArea(area: IInstanceRestorer.ISideArea): InstanceRestorer.ISideArea {
-    let dehydrated: InstanceRestorer.ISideArea = { collapsed: area.collapsed };
+  private _dehydrateMainArea(area: ApplicationShell.IMainArea): Private.IMainArea {
+    return Private.serializeMain(area);
+  }
+
+  /**
+   * Reydrate a serialized main area description object.
+   *
+   * #### Notes
+   * This function consumes data that can become corrupted, so it uses type
+   * coercion to guarantee the dehydrated object is safely processed.
+   */
+  private _rehydrateMainArea(area: Private.IMainArea): ApplicationShell.IMainArea {
+    return Private.deserializeMain(area, this._widgets);
+  }
+
+  /**
+   * Dehydrate a side area description into a serializable object.
+   */
+  private _dehydrateSideArea(area: ApplicationShell.ISideArea): Private.ISideArea {
+    let dehydrated: Private.ISideArea = { collapsed: area.collapsed };
     if (area.currentWidget) {
       let current = Private.nameProperty.get(area.currentWidget);
       if (current) {
@@ -395,7 +330,7 @@ class InstanceRestorer implements IInstanceRestorer {
    * This function consumes data that can become corrupted, so it uses type
    * coercion to guarantee the dehydrated object is safely processed.
    */
-  private _rehydrateSideArea(area: InstanceRestorer.ISideArea): IInstanceRestorer.ISideArea {
+  private _rehydrateSideArea(area: Private.ISideArea): ApplicationShell.ISideArea {
     if (!area) {
       return { collapsed: true, currentWidget: null, widgets: null };
     }
@@ -457,23 +392,27 @@ namespace InstanceRestorer {
      */
     state: IStateDB;
   }
+}
 
+/*
+ * A namespace for private data.
+ */
+namespace Private {
   /**
    * The dehydrated state of the application layout.
    *
    * #### Notes
-   * This format is JSON serializable and only used internally by the instance
-   * restorer to read and write to the state database. It is meant to be a data
-   * structure that the instance restorer can translate into an
-   * `IInstanceTracker.ILayout` data structure for consumption by the
-   * application shell.
+   * This format is JSON serializable and saved in the state database.
+   * It is meant to be a data structure can translate into an
+   * `ApplicationShell.ILayout` data structure for consumption by
+   * the application shell.
    */
   export
-  interface IDehydratedLayout extends JSONObject {
+  interface ILayout extends JSONObject {
     /**
-     * The current widget that has application focus.
+     * The main area of the user interface.
      */
-    current?: string | null;
+    main?: IMainArea | null;
 
     /**
      * The left area of the user interface.
@@ -486,6 +425,22 @@ namespace InstanceRestorer {
     right?: ISideArea | null;
   }
 
+  /**
+   * The restorable description of the main application area.
+   */
+  export
+  interface IMainArea extends JSONObject {
+    /**
+     * The current widget that has application focus.
+     */
+    current?: string | null;
+
+    /**
+     * The main application dock panel.
+     */
+    dock?: ISplitArea | ITabArea | null;
+  }
+
   /**
    * The restorable description of a sidebar in the user interface.
    */
@@ -506,12 +461,54 @@ namespace InstanceRestorer {
      */
     widgets?: Array<string> | null;
   }
-}
 
-/*
- * A namespace for private data.
- */
-namespace Private {
+  /**
+   * The restorable description of a tab area in the user interface.
+   */
+  export
+  interface ITabArea extends JSONObject {
+    /**
+     * The type indicator of the serialized tab area.
+     */
+    type: 'tab-area';
+
+    /**
+     * The widgets in the tab area.
+     */
+    widgets: Array<string> | null;
+
+    /**
+     * The index of the selected tab.
+     */
+    currentIndex: number;
+  }
+
+  /**
+   * The restorable description of a split area in the user interface.
+   */
+  export
+  interface ISplitArea extends JSONObject {
+    /**
+     * The type indicator of the serialized split area.
+     */
+    type: 'split-area';
+
+    /**
+     * The orientation of the split area.
+     */
+    orientation: 'horizontal' | 'vertical';
+
+    /**
+     * The children in the split area.
+     */
+    children: Array<ITabArea | ISplitArea> | null;
+
+    /**
+     * The sizes of the children.
+     */
+    sizes: Array<number>;
+  }
+
   /**
    * An attached property for a widget's ID in the state database.
    */
@@ -520,4 +517,124 @@ namespace Private {
     name: 'name',
     create: owner => ''
   });
+
+  /**
+   * Serialize individual areas within the main area.
+   */
+  function serializeArea(area: ApplicationShell.AreaConfig): ITabArea | ISplitArea | null {
+    if (!area || !area.type) {
+      return null;
+    }
+
+    if (area.type === 'tab-area') {
+      return {
+        type: 'tab-area',
+        currentIndex: area.currentIndex,
+        widgets: area.widgets
+          .map(widget => nameProperty.get(widget))
+          .filter(name => !!name)
+      };
+    }
+
+    return {
+      type: 'split-area',
+      orientation: area.orientation,
+      sizes: area.sizes,
+      children: area.children.map(serializeArea)
+    };
+  }
+
+  /**
+   * Return a dehydrated, serializable version of the main dock panel.
+   */
+  export
+  function serializeMain(area: ApplicationShell.IMainArea): IMainArea {
+    let dehydrated: IMainArea = {
+      dock: area && area.dock && serializeArea(area.dock.main) || null
+    };
+    if (area && area.currentWidget) {
+      let current = Private.nameProperty.get(area.currentWidget);
+      if (current) {
+        dehydrated.current = current;
+      }
+    }
+    return dehydrated;
+  }
+
+  /**
+   * Deserialize individual areas within the main area.
+   *
+   * #### Notes
+   * Because this data comes from a potentially unreliable foreign source, it is
+   * typed as a `JSONObject`; but the actual expected type is:
+   * `ITabArea | ISplitArea`.
+   *
+   * For fault tolerance, types are manually checked in deserialization.
+   */
+  function deserializeArea(area: JSONObject, names: Map<string, Widget>): ApplicationShell.AreaConfig | null {
+    if (!area) {
+      return null;
+    }
+
+    // Because this data is saved to a foreign data source, its type safety is
+    // not guaranteed when it is retrieved, so exhaustive checks are necessary.
+    const type = (area as any).type as string || 'unknown';
+    if (type === 'unknown' || (type !== 'tab-area' && type !== 'split-area')) {
+      console.warn(`Attempted to deserialize unknown type: ${type}`);
+      return null;
+    }
+
+    if (type === 'tab-area') {
+      const { currentIndex, widgets } = area as ITabArea;
+      let hydrated: ApplicationShell.AreaConfig = {
+        type: 'tab-area',
+        currentIndex: currentIndex || 0,
+        widgets: widgets && widgets.map(widget => names.get(widget))
+            .filter(widget => !!widget) || []
+      };
+
+      // Make sure the current index is within bounds.
+      if (hydrated.currentIndex > hydrated.widgets.length - 1) {
+        hydrated.currentIndex = 0;
+      }
+
+      return hydrated;
+    }
+
+    const { orientation, sizes, children } = area as ISplitArea;
+    let hydrated: ApplicationShell.AreaConfig = {
+      type: 'split-area',
+      orientation: orientation,
+      sizes: sizes || [],
+      children: children &&
+        children.map(child => deserializeArea(child, names))
+           .filter(widget => !!widget) || []
+    };
+
+    return hydrated;
+  }
+
+  /**
+   * Return the hydrated version of the main dock panel, ready to restore.
+   *
+   * #### Notes
+   * Because this data comes from a potentially unreliable foreign source, it is
+   * typed as a `JSONObject`; but the actual expected type is: `IMainArea`.
+   *
+   * For fault tolerance, types are manually checked in deserialization.
+   */
+  export
+  function deserializeMain(area: JSONObject, names: Map<string, Widget>): ApplicationShell.IMainArea | null {
+    if (!area) {
+      return null;
+    }
+
+    const name = (area as any).current || null;
+    const dock = (area as any).dock || null;
+
+    return {
+      currentWidget: name && names.has(name) && names.get(name) || null,
+      dock: dock ? { main: deserializeArea(dock, names) } : null
+    };
+  }
 }

+ 13 - 13
test/src/instancerestorer/instancerestorer.spec.ts

@@ -15,14 +15,14 @@ import {
   Widget
 } from '@phosphor/widgets';
 
-import {
-  IInstanceRestorer, InstanceRestorer
-} from '../../../lib/instancerestorer/instancerestorer';
-
 import {
   ApplicationShell, InstanceTracker
 } from '../../../lib/application';
 
+import {
+  InstanceRestorer
+} from '../../../lib/instancerestorer/instancerestorer';
+
 import {
   StateDB
 } from '../../../lib/statedb/statedb';
@@ -82,8 +82,8 @@ describe('instancerestorer/instancerestorer', () => {
           state: new StateDB({ namespace: NAMESPACE })
         });
         let currentWidget = new Widget();
-        let dehydrated: IInstanceRestorer.ILayout = {
-          currentWidget,
+        let dehydrated: ApplicationShell.ILayout = {
+          mainArea: { currentWidget, dock: null },
           leftArea: { collapsed: true, currentWidget: null, widgets: null },
           rightArea: { collapsed: true, currentWidget: null, widgets: null }
         };
@@ -92,7 +92,7 @@ describe('instancerestorer/instancerestorer', () => {
         restorer.restored.then(() => restorer.save(dehydrated))
           .then(() => restorer.fetch())
           .then(layout => {
-            expect(layout.currentWidget).to.be(currentWidget);
+            expect(layout.mainArea.currentWidget).to.be(currentWidget);
             done();
           }).catch(done);
       });
@@ -122,9 +122,9 @@ describe('instancerestorer/instancerestorer', () => {
         });
         let currentWidget = new Widget();
         // The `fresh` attribute is only here to check against the return value.
-        let dehydrated: IInstanceRestorer.ILayout = {
-          currentWidget: null,
+        let dehydrated: ApplicationShell.ILayout = {
           fresh: false,
+          mainArea: { currentWidget: null, dock: null },
           leftArea: {
             currentWidget,
             collapsed: true,
@@ -187,8 +187,8 @@ describe('instancerestorer/instancerestorer', () => {
           registry: new CommandRegistry(),
           state: new StateDB({ namespace: NAMESPACE })
         });
-        let dehydrated: IInstanceRestorer.ILayout = {
-          currentWidget: null,
+        let dehydrated: ApplicationShell.ILayout = {
+          mainArea: { currentWidget: null, dock: null },
           leftArea: { currentWidget: null, collapsed: true, widgets: null },
           rightArea: { collapsed: true, currentWidget: null, widgets: null }
         };
@@ -206,9 +206,9 @@ describe('instancerestorer/instancerestorer', () => {
         });
         let currentWidget = new Widget();
         // The `fresh` attribute is only here to check against the return value.
-        let dehydrated: IInstanceRestorer.ILayout = {
-          currentWidget: null,
+        let dehydrated: ApplicationShell.ILayout = {
           fresh: false,
+          mainArea: { currentWidget: null, dock: null },
           leftArea: {
             currentWidget,
             collapsed: true,