Browse Source

Merge pull request #1291 from afshin/restore

Initial implementation of layout restoration
Steven Silvester 8 năm trước cách đây
mục cha
commit
187a5ce754

+ 1 - 0
examples/lab/index.js

@@ -28,6 +28,7 @@ lab.registerPlugins([
   require('jupyterlab/lib/inspector/plugin').inspectorProvider,
   require('jupyterlab/lib/landing/plugin').landingExtension,
   require('jupyterlab/lib/launcher/plugin').launcherProvider,
+  require('jupyterlab/lib/layoutrestorer/plugin').layoutRestorerProvider,
   require('jupyterlab/lib/main/plugin').mainExtension,
   require('jupyterlab/lib/mainmenu/plugin').mainMenuProvider,
   require('jupyterlab/lib/markdownwidget/plugin').markdownHandlerExtension,

+ 1 - 0
jupyterlab/extensions.js

@@ -19,6 +19,7 @@ module.exports = [
   require('../lib/inspector/plugin').inspectorProvider,
   require('../lib/landing/plugin').landingExtension,
   require('../lib/launcher/plugin').launcherProvider,
+  require('../lib/layoutrestorer/plugin').layoutRestorerProvider,
   require('../lib/main/plugin').mainExtension,
   require('../lib/mainmenu/plugin').mainMenuProvider,
   require('../lib/markdownwidget/plugin').markdownHandlerExtension,

+ 2 - 1
src/about/index.css

@@ -9,7 +9,8 @@
   left: 0;
   width: 100%;
   height: 100%;
-  overflow: scroll;
+  outline: none;
+  overflow-y: auto;
   scroll-snap-points-y: repeat(100%);
   scroll-snap-type: mandatory;
   scroll-snap-destination: 100% 0%;

+ 83 - 91
src/about/index.ts

@@ -1,6 +1,10 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
 import {
   h, VNode
 } from 'phosphor/lib/ui/vdom';
@@ -9,10 +13,6 @@ import {
   VDomModel, VDomWidget
 } from '../common/vdom';
 
-/**
- * The id name added to the About plugin root DOM node.
- */
-const ABOUT_ID = 'about';
 
 /**
  * The class name added to each page in the About plugin.
@@ -157,7 +157,7 @@ class AboutModel extends VDomModel {
     this.title = 'Welcome to the JupyterLab Alpha preview';
     this.headerText = [
       'Click on the Launcher tab for the initial JupyterLab screen.',
-      'This demo gives an Alpha-level developer preview of the JupyterLab enviromnent. ',
+      'This demo gives an Alpha-level developer preview of the JupyterLab environment. ',
       'It is not ready for general usage yet.',
       'We are developing JupyterLab at ',
       'https://github.com/jupyterlab/jupyterlab',
@@ -222,10 +222,19 @@ class AboutModel extends VDomModel {
 export
 class AboutWidget extends VDomWidget<AboutModel> {
   /**
-   * Construct a new about widget.
+   * Handle `'activate-request'` messages.
    */
-  constructor() {
-    super();
+  protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
+    this.node.focus();
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onCloseRequest(msg: Message): void {
+    super.onCloseRequest(msg);
+    this.dispose();
   }
 
   /**
@@ -240,18 +249,17 @@ class AboutWidget extends VDomWidget<AboutModel> {
     let commandPaletteDesc = this.model.commandPaletteDesc;
     let notebookDesc = this.model.notebookDesc;
 
-    let headerRow =
-    h.div({className: ROW_CLASS},
-      h.div({className: COLUMN_CLASS},
-        h.span({className: IMAGE_CLASS + ' ' + LOGO_CLASS}),
-        h.p({className: HEADER_CLASS}, title),
-        h.div({className: DESC_ONE_CLASS},
+    let headerRow = h.div({ className: ROW_CLASS },
+      h.div({ className: COLUMN_CLASS },
+        h.span({ className: IMAGE_CLASS + ' ' + LOGO_CLASS }),
+        h.p({ className: HEADER_CLASS }, title),
+        h.div({ className: DESC_ONE_CLASS },
           h.p(headerText[0]),
           h.p(headerText[1],
             h.b(headerText[2])
           ),
           h.p(headerText[3],
-            h.a({href: headerText[4], target: '_blank'}, headerText[4]),
+            h.a({ href: headerText[4], target: '_blank' }, headerText[4]),
             headerText[5]
           ),
           h.p(headerText[6])
@@ -259,78 +267,72 @@ class AboutWidget extends VDomWidget<AboutModel> {
       )
     );
 
-    let mainAreaCommandPaletteRow =
-    h.div({className: ROW_CLASS},
-      h.div({className: HALF_CLASS + ' ' + COLUMN_CLASS},
-        h.p({className: DESC_TWO_HEADER_CLASS},
-          h.a({href: '#main-area'},
-            h.span({className: IMAGE_CLASS + ' ' + MAIN_AREA_ICON_CLASS}),
+    let mainAreaCommandPaletteRow = h.div({ className: ROW_CLASS },
+      h.div({ className: HALF_CLASS + ' ' + COLUMN_CLASS },
+        h.p({ className: DESC_TWO_HEADER_CLASS },
+          h.a({ href: '#about-main-area' },
+            h.span({ className: IMAGE_CLASS + ' ' + MAIN_AREA_ICON_CLASS }),
             pluginHeaders[0]
           )
         ),
-        h.p({className: DESC_TWO_CLASS}, mainAreaDesc[0])
+        h.p({ className: DESC_TWO_CLASS }, mainAreaDesc[0])
       ),
-      h.div({className: HALF_CLASS + ' ' + COLUMN_CLASS},
-        h.p({className: DESC_TWO_HEADER_CLASS},
-          h.a({href: '#command'},
-            h.span({className: IMAGE_CLASS + ' ' + COMMAND_ICON_CLASS}),
+      h.div({ className: HALF_CLASS + ' ' + COLUMN_CLASS },
+        h.p({ className: DESC_TWO_HEADER_CLASS },
+          h.a({ href: '#about-command' },
+            h.span({ className: IMAGE_CLASS + ' ' + COMMAND_ICON_CLASS }),
             pluginHeaders[1]
           )
         ),
-        h.p({className: DESC_TWO_CLASS}, commandPaletteDesc[0])
+        h.p({ className: DESC_TWO_CLASS }, commandPaletteDesc[0])
       )
     );
 
-    let filebrowserNotebookRow =
-    h.div({className: ROW_CLASS},
-      h.div({className: HALF_CLASS + ' ' + COLUMN_CLASS},
-        h.p({className: DESC_TWO_HEADER_CLASS},
-          h.a({href: '#filebrowser'},
-            h.span({className: IMAGE_CLASS + ' ' + FILEBROWSER_ICON_CLASS}),
+    let filebrowserNotebookRow = h.div({ className: ROW_CLASS },
+      h.div({ className: HALF_CLASS + ' ' + COLUMN_CLASS },
+        h.p({ className: DESC_TWO_HEADER_CLASS },
+          h.a({ href: '#about-filebrowser' },
+            h.span({ className: IMAGE_CLASS + ' ' + FILEBROWSER_ICON_CLASS }),
             pluginHeaders[2]
           )
         ),
-        h.p({className: DESC_TWO_CLASS}, filebrowserDesc[0]),
+        h.p({ className: DESC_TWO_CLASS }, filebrowserDesc[0]),
       ),
-      h.div({className: HALF_CLASS + ' ' + COLUMN_CLASS},
-        h.p({className: DESC_TWO_HEADER_CLASS},
-          h.a({href: '#notebook'},
-            h.span({className: IMAGE_CLASS + ' ' + NOTEBOOK_ICON_CLASS}),
+      h.div({ className: HALF_CLASS + ' ' + COLUMN_CLASS },
+        h.p({ className: DESC_TWO_HEADER_CLASS },
+          h.a({ href: '#about-notebook' },
+            h.span({ className: IMAGE_CLASS + ' ' + NOTEBOOK_ICON_CLASS }),
             pluginHeaders[3]
           )
         ),
-        h.p({className: DESC_TWO_CLASS}, notebookDesc[0])
+        h.p({ className: DESC_TWO_CLASS }, notebookDesc[0])
       )
     );
 
-    let mainAreaPage =
-    h.div({className: SECTION_CLASS},
-      h.a({id: 'main-area'}),
-      h.div({className: SECTION_CENTER_CLASS},
-        h.p({className: HEADER_CLASS + ' ' + CONTENT_CLASS},
-          h.span({className: IMAGE_CLASS + ' ' + MAIN_AREA_ICON_CLASS}),
+    let mainAreaPage = h.div({ className: SECTION_CLASS },
+      h.a({ id: 'about-main-area' }),
+      h.div({ className: SECTION_CENTER_CLASS },
+        h.p({ className: HEADER_CLASS + ' ' + CONTENT_CLASS },
+          h.span({ className: IMAGE_CLASS + ' ' + MAIN_AREA_ICON_CLASS }),
           pluginHeaders[0]
         ),
-        h.span({className: IMAGE_CLASS + ' ' + MAIN_AREA_IMAGE_CLASS}),
-        h.p({className: CONTENT_DESC_CLASS}, mainAreaDesc[1]),
-        h.p({className: CONTENT_DESC_CLASS}, mainAreaDesc[2]),
-        h.a({href: '#command'},
-          h.span({className: NAV_CLASS})
-        )
+        h.span({ className: IMAGE_CLASS + ' ' + MAIN_AREA_IMAGE_CLASS }),
+        h.p({ className: CONTENT_DESC_CLASS }, mainAreaDesc[1]),
+        h.p({ className: CONTENT_DESC_CLASS }, mainAreaDesc[2]),
+        h.a({ href: '#about-command' }, h.span({ className: NAV_CLASS }))
       )
     );
 
-    let commandPalettePage =
-    h.div({className: SECTION_CLASS},
-      h.a({id: 'command'}),
-      h.div({className: SECTION_CENTER_CLASS},
-        h.p({className: HEADER_CLASS + ' ' + CONTENT_CLASS},
-          h.span({className: IMAGE_CLASS + ' ' + COMMAND_ICON_CLASS}),
+    let commandPalettePage = h.div({ className: SECTION_CLASS },
+      h.a({ id: 'about-command' }),
+      h.div({ className: SECTION_CENTER_CLASS },
+        h.p({ className: HEADER_CLASS + ' ' + CONTENT_CLASS },
+          h.span({ className: IMAGE_CLASS + ' ' + COMMAND_ICON_CLASS }),
           pluginHeaders[1]
         ),
-        h.span({className: IMAGE_CLASS + ' ' + COMMAND_IMAGE_CLASS}),
-        h.p({className: CONTENT_DESC_CLASS}, commandPaletteDesc[1]),
-        h.div({className: CONTENT_DESC_CLASS},
+        h.span({ className: IMAGE_CLASS + ' ' + COMMAND_IMAGE_CLASS }),
+        h.p({ className: CONTENT_DESC_CLASS }, commandPaletteDesc[1]),
+        h.div({ className: CONTENT_DESC_CLASS },
           h.p(commandPaletteDesc[2]),
           h.ul(
             h.li(commandPaletteDesc[3]),
@@ -340,53 +342,44 @@ class AboutWidget extends VDomWidget<AboutModel> {
             h.li(commandPaletteDesc[7])
           )
         ),
-        h.a({href: '#filebrowser'},
-          h.span({className: NAV_CLASS})
-        )
+        h.a({ href: '#about-filebrowser' }, h.span({ className: NAV_CLASS }))
       )
     );
 
-    let filebrowserPage =
-    h.div({className: SECTION_CLASS},
-      h.a({id: 'filebrowser'}),
-      h.div({className: SECTION_CENTER_CLASS},
-        h.p({className: HEADER_CLASS + ' ' + CONTENT_CLASS},
-          h.span({className: IMAGE_CLASS + ' ' + FILEBROWSER_ICON_CLASS}),
+    let filebrowserPage = h.div({ className: SECTION_CLASS },
+      h.a({ id: 'about-filebrowser' }),
+      h.div({ className: SECTION_CENTER_CLASS },
+        h.p({ className: HEADER_CLASS + ' ' + CONTENT_CLASS },
+          h.span({ className: IMAGE_CLASS + ' ' + FILEBROWSER_ICON_CLASS }),
           pluginHeaders[2]
         ),
-        h.span({className: IMAGE_CLASS + ' ' + FILEBROWSER_IMAGE_CLASS}),
-        h.p({className: CONTENT_DESC_CLASS}, filebrowserDesc[1]),
-        h.a({href: '#notebook'},
-          h.span({className: NAV_CLASS})
-        )
+        h.span({ className: IMAGE_CLASS + ' ' + FILEBROWSER_IMAGE_CLASS }),
+        h.p({ className: CONTENT_DESC_CLASS }, filebrowserDesc[1]),
+        h.a({ href: '#about-notebook' }, h.span({ className: NAV_CLASS }))
       )
     );
 
-    let notebookPage =
-    h.div({className: SECTION_CLASS},
-      h.a({id: 'notebook'}),
-      h.div({className: SECTION_CENTER_CLASS},
-        h.p({className: HEADER_CLASS + ' ' + CONTENT_CLASS},
-          h.span({className: IMAGE_CLASS + ' ' + NOTEBOOK_ICON_CLASS}),
+    let notebookPage = h.div({ className: SECTION_CLASS },
+      h.a({ id: 'about-notebook' }),
+      h.div({ className: SECTION_CENTER_CLASS },
+        h.p({ className: HEADER_CLASS + ' ' + CONTENT_CLASS },
+          h.span({ className: IMAGE_CLASS + ' ' + NOTEBOOK_ICON_CLASS }),
           pluginHeaders[3]
         ),
-        h.span({className: IMAGE_CLASS + ' ' + NOTEBOOK_IMAGE_CLASS}),
-        h.p({className: CONTENT_DESC_CLASS}, notebookDesc[1])
+        h.span({ className: IMAGE_CLASS + ' ' + NOTEBOOK_IMAGE_CLASS }),
+        h.p({ className: CONTENT_DESC_CLASS }, notebookDesc[1])
       )
     );
 
-    let domTree =
-    h.div({id: ABOUT_ID},
-      h.div({className: SECTION_CLASS},
-        h.div({className: SECTION_CENTER_CLASS},
-          h.div({className: CONTAINER_CLASS},
+    return h.div(
+      h.div({ className: SECTION_CLASS },
+        h.div({ className: SECTION_CENTER_CLASS },
+          h.div({ className: CONTAINER_CLASS },
             headerRow,
             mainAreaCommandPaletteRow,
             filebrowserNotebookRow
           ),
-          h.a({href: '#main-area'},
-            h.span({className: NAV_CLASS})
-          )
+          h.a({ href: '#about-main-area' }, h.span({ className: NAV_CLASS }))
         )
       ),
       mainAreaPage,
@@ -394,6 +387,5 @@ class AboutWidget extends VDomWidget<AboutModel> {
       filebrowserPage,
       notebookPage
     );
-    return domTree;
   }
 }

+ 43 - 12
src/about/plugin.ts

@@ -9,6 +9,18 @@ import {
   ICommandPalette
 } from '../commandpalette';
 
+import {
+  InstanceTracker
+} from '../common/instancetracker';
+
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
+import {
+  IStateDB
+} from '../statedb';
+
 import {
   AboutModel, AboutWidget
 } from './';
@@ -21,28 +33,47 @@ const aboutExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.about',
   activate: activateAbout,
   autoStart: true,
-  requires: [ICommandPalette]
+  requires: [ICommandPalette, IStateDB, ILayoutRestorer]
 };
 
 
-function activateAbout(app: JupyterLab, palette: ICommandPalette): void {
-  let model = new AboutModel();
-  let widget = new AboutWidget();
-  widget.model = model;
-  widget.id = 'about-jupyterlab';
-  widget.title.label = 'About';
-  widget.title.closable = true;
-  widget.node.style.overflowY = 'auto';
+function activateAbout(app: JupyterLab, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
+  const model = new AboutModel();
+  const command = 'about-jupyterlab:show';
+  const category = 'Help';
+  const tracker = new InstanceTracker<AboutWidget>({
+    restore: {
+      state, layout, command,
+      args: widget => null,
+      name: widget => 'about',
+      namespace: 'about',
+      when: app.started,
+      registry: app.commands
+    }
+  });
+
+  let widget: AboutWidget;
+
+  function newWidget(): AboutWidget {
+    let widget = new AboutWidget();
+    widget.model = model;
+    widget.id = 'about';
+    widget.title.label = 'About';
+    widget.title.closable = true;
+    tracker.add(widget);
+    return widget;
+  }
 
-  let command = 'about-jupyterlab:show';
   app.commands.addCommand(command, {
     label: 'About JupyterLab',
     execute: () => {
-      if (!widget.isAttached) {
+      if (!widget || widget.isDisposed) {
+        widget = newWidget();
         app.shell.addToMainArea(widget);
       }
       app.shell.activateMain(widget.id);
     }
   });
-  palette.addItem({ command, category: 'Help' });
+
+  palette.addItem({ command, category });
 }

+ 28 - 0
src/application/shell.ts

@@ -177,6 +177,34 @@ class ApplicationShell extends Widget {
     return this._dockPanel.currentWidget;
   }
 
+  /**
+   * True if main area is empty.
+   */
+  get mainAreaIsEmpty(): boolean {
+    return this._dockPanel.isEmpty;
+  }
+
+  /**
+   * True if top area is empty.
+   */
+  get topAreaIsEmpty(): boolean {
+    return this._topPanel.widgets.length === 0;
+  }
+
+  /**
+   * True if left area is empty.
+   */
+  get leftAreaIsEmpty(): boolean {
+    return this._leftHandler.stackedPanel.widgets.length === 0;
+  }
+
+  /**
+   * True if right area is empty.
+   */
+  get rightAreaIsEmpty(): boolean {
+    return this._rightHandler.stackedPanel.widgets.length === 0;
+  }
+
   /**
    * Add a widget to the top content area.
    *

+ 1 - 1
src/commandlinker/commandlinker.ts

@@ -40,7 +40,7 @@ const ARGS_ATTR = 'commandlinker-args';
  * The command linker token.
  */
 export
-const ICommandLinker = new Token<ICommandLinker>('jupyter.services.commandlinker');
+const ICommandLinker = new Token<ICommandLinker>('jupyter.services.command-linker');
 /* tslint:enable */
 
 

+ 2 - 10
src/commandlinker/plugin.ts

@@ -17,16 +17,8 @@ import {
  */
 export
 const commandLinkerProvider: JupyterLabPlugin<ICommandLinker> = {
-  id: 'jupyter.services.commandlinker',
+  id: 'jupyter.services.command-linker',
   provides: ICommandLinker,
-  activate: activateCommandLinker,
+  activate: (app: JupyterLab) => new CommandLinker({ commands: app.commands }),
   autoStart: true
 };
-
-
-/**
- * Activate the command linker provider.
- */
-function activateCommandLinker(app: JupyterLab): ICommandLinker {
-  return new CommandLinker({ commands: app.commands });
-}

+ 52 - 12
src/common/instancetracker.ts

@@ -1,6 +1,10 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import {
+  utils
+} from '@jupyterlab/services';
+
 import {
   JSONObject
 } from 'phosphor/lib/algorithm/json';
@@ -25,12 +29,15 @@ import {
   Widget
 } from 'phosphor/lib/ui/widget';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IStateDB
 } from '../statedb';
 
 
-
 /**
  * An object that tracks widget instances.
  */
@@ -56,6 +63,12 @@ interface IInstanceTracker<T extends Widget> {
  * #### Notes
  * This is meant to be used in conjunction with a `FocusTracker` and will
  * typically be kept in sync with focus tracking events.
+ *
+ * The API surface area of this concrete implementation is substantially larger
+ * than the instance tracker interface it implements. The interface is intended
+ * for export by JupyterLab plugins that create widgets and have clients who may
+ * wish to keep track of newly created widgets. This class, however, can be used
+ * internally by plugins to restore state as well.
  */
 export
 class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposable {
@@ -64,17 +77,29 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
    */
   constructor(options: InstanceTracker.IOptions<T> = {}) {
     this._restore = options.restore;
-    if (this._restore) {
-      let { command, namespace, registry, state, when } = this._restore;
-      let promises = [state.fetchNamespace(namespace)].concat(when);
-      Promise.all(promises).then(([saved]) => {
-        saved.forEach(args => {
-          // Execute the command and if it fails, delete the state restore data.
-          registry.execute(command, args.value)
-            .catch(() => { state.remove(args.id); });
-        });
-      });
+
+    if (!this._restore) {
+      return;
     }
+
+    let { command, namespace, layout, registry, state, when } = this._restore;
+    let promises = [state.fetchNamespace(namespace)].concat(when);
+
+    // Immediately (synchronously) register the restored promise with the
+    // layout restorer if one is present.
+    if (layout) {
+      layout.await(this._restored.promise);
+    }
+
+    Promise.all(promises).then(([saved]) => {
+      let promises = saved.map(args => {
+        // Execute the command and if it fails, delete the state restore data.
+        return registry.execute(command, args.value)
+          .catch(() => { state.remove(args.id); });
+      });
+      return Promise.all(promises);
+    }).then(() => { this._restored.resolve(void 0); });
+
   }
 
   /**
@@ -111,13 +136,16 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
 
     // Handle widget state restoration.
     if (this._restore) {
-      let { namespace, state } = this._restore;
+      let { layout, namespace, state } = this._restore;
       let widgetName = this._restore.name(widget);
 
       if (widgetName) {
         let name = `${namespace}:${widgetName}`;
         Private.nameProperty.set(widget, name);
         state.save(name, this._restore.args(widget));
+        if (layout) {
+          layout.add(widget, name);
+        }
       }
     }
 
@@ -257,6 +285,7 @@ class InstanceTracker<T extends Widget> implements IInstanceTracker<T>, IDisposa
 
   private _currentWidget: T = null;
   private _restore: InstanceTracker.IRestoreOptions<T> = null;
+  private _restored = new utils.PromiseDelegate<void>();
   private _widgets = new Set<T>();
 }
 
@@ -295,6 +324,17 @@ namespace InstanceTracker {
      */
     namespace: string;
 
+    /**
+     * The layout restorer to use to re-arrange restored tabs.
+     *
+     * #### Notes
+     * If a layout restorer instance is not supplied, widget instances will
+     * still be restored, but their layout within JupyterLab will be arbitrary.
+     * This may be acceptable for widgets that have a pre-defined slot whose
+     * layout cannot be modified.
+     */
+    layout?: ILayoutRestorer;
+
     /**
      * The command registry which holds the restore command.
      */

+ 1 - 1
src/common/vdom.ts

@@ -118,8 +118,8 @@ abstract class VDomWidget<T extends IVDomModel> extends Widget {
     if (this.isDisposed) {
       return;
     }
-    this._model = null;
     super.dispose();
+    this._model = null;
   }
 
   /**

+ 9 - 9
src/console/plugin.ts

@@ -41,6 +41,10 @@ import {
   IInspector
 } from '../inspector';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IMainMenu
 } from '../mainmenu';
@@ -77,7 +81,8 @@ const consoleTrackerProvider: JupyterLabPlugin<IConsoleTracker> = {
     ICommandPalette,
     IPathTracker,
     ConsoleContent.IRenderer,
-    IStateDB
+    IStateDB,
+    ILayoutRestorer
   ],
   activate: activateConsole,
   autoStart: true
@@ -99,11 +104,6 @@ const CONSOLE_ICON_CLASS = 'jp-ImageCodeConsole';
  */
 const CONSOLE_REGEX = /^console-(\d)+-[0-9a-f]+$/;
 
-/**
- * The console plugin state namespace.
- */
-const NAMESPACE = 'consoles';
-
 
 /**
  * The arguments used to create a console.
@@ -119,7 +119,7 @@ interface ICreateConsoleArgs extends JSONObject {
 /**
  * Activate the console extension.
  */
-function activateConsole(app: JupyterLab, services: IServiceManager, rendermime: IRenderMime, mainMenu: IMainMenu, inspector: IInspector, palette: ICommandPalette, pathTracker: IPathTracker, renderer: ConsoleContent.IRenderer, state: IStateDB): IConsoleTracker {
+function activateConsole(app: JupyterLab, services: IServiceManager, rendermime: IRenderMime, mainMenu: IMainMenu, inspector: IInspector, palette: ICommandPalette, pathTracker: IPathTracker, renderer: ConsoleContent.IRenderer, state: IStateDB, layout: ILayoutRestorer): IConsoleTracker {
   let manager = services.sessions;
 
   let { commands, keymap } = app;
@@ -130,11 +130,11 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   // Create an instance tracker for all console panels.
   const tracker = new InstanceTracker<ConsolePanel>({
     restore: {
-      state,
+      state, layout,
       command: 'console:create',
       args: panel => ({ id: panel.content.session.id }),
       name: panel => panel.content.session && panel.content.session.id,
-      namespace: 'consoles',
+      namespace: 'console',
       when: [app.started, manager.ready],
       registry: app.commands
     }

+ 1 - 0
src/csvwidget/index.css

@@ -11,6 +11,7 @@
 .jp-CSVWidget {
   display: flex;
   flex-direction: column;
+  outline: none;
 }
 
 .jp-CSVWidget-toolbar {

+ 8 - 4
src/csvwidget/plugin.ts

@@ -13,6 +13,10 @@ import {
   IDocumentRegistry
 } from '../docregistry';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IStateDB
 } from '../statedb';
@@ -34,7 +38,7 @@ const FACTORY = 'Table';
 export
 const csvHandlerExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.csv-handler',
-  requires: [IDocumentRegistry, IStateDB],
+  requires: [IDocumentRegistry, IStateDB, ILayoutRestorer],
   activate: activateCSVWidget,
   autoStart: true
 };
@@ -43,7 +47,7 @@ const csvHandlerExtension: JupyterLabPlugin<void> = {
 /**
  * Activate the table widget extension.
  */
-function activateCSVWidget(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB): void {
+function activateCSVWidget(app: JupyterLab, registry: IDocumentRegistry, state: IStateDB, layout: ILayoutRestorer): void {
   const factory = new CSVWidgetFactory({
     name: FACTORY,
     fileExtensions: ['.csv'],
@@ -51,11 +55,11 @@ function activateCSVWidget(app: JupyterLab, registry: IDocumentRegistry, state:
   });
   const tracker = new InstanceTracker<CSVWidget>({
     restore: {
-      state,
+      state, layout,
       command: 'file-operations:open',
       args: widget => ({ path: widget.context.path, factory: FACTORY }),
       name: widget => widget.context.path,
-      namespace: 'csvwidgets',
+      namespace: 'csvwidget',
       when: app.started,
       registry: app.commands
     }

+ 1 - 0
src/csvwidget/widget.ts

@@ -130,6 +130,7 @@ class CSVWidget extends Widget {
    * Handle `'activate-request'` messages.
    */
   protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
     this.node.focus();
   }
 

+ 3 - 3
src/docmanager/manager.ts

@@ -341,8 +341,8 @@ class DocumentManager implements IDisposable {
 
     let context: Context<DocumentRegistry.IModel> = null;
 
-    //Handle the load-from-disk case
-    if(which === 'open') {
+    // Handle the load-from-disk case
+    if (which === 'open') {
       // Use an existing context if available.
       context = this._findContext(path, factory.name);
       if (!context) {
@@ -363,7 +363,7 @@ class DocumentManager implements IDisposable {
     } else if (widgetFactory.preferKernel &&
                !(kernel && !kernel.id && !kernel.name) &&
                !context.kernel) {
-      //If the kernel is not the `None` kernel and the widgetFactory wants one
+      // If the kernel is not the `None` kernel and the widgetFactory wants one
       context.startDefaultKernel();
     }
 

+ 23 - 21
src/editorwidget/plugin.ts

@@ -1,6 +1,11 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+import 'codemirror/addon/edit/matchbrackets.js';
+import 'codemirror/addon/edit/closebrackets.js';
+import 'codemirror/addon/comment/comment.js';
+import 'codemirror/keymap/vim.js';
+
 import {
   AttachedProperty
 } from 'phosphor/lib/core/properties';
@@ -13,6 +18,14 @@ import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
 
+import {
+  DEFAULT_CODEMIRROR_THEME
+} from '../codemirror/widget';
+
+import {
+  ICommandPalette
+} from '../commandpalette';
+
 import {
   InstanceTracker
 } from '../common/instancetracker';
@@ -22,12 +35,8 @@ import {
 } from '../docregistry';
 
 import {
-  EditorWidgetFactory, EditorWidget
-} from './widget';
-
-import {
-  ICommandPalette
-} from '../commandpalette';
+  ILayoutRestorer
+} from '../layoutrestorer';
 
 import {
   IMainMenu
@@ -38,17 +47,8 @@ import {
 } from '../statedb';
 
 import {
-  IEditorTracker
-} from './index';
-
-import {
-  DEFAULT_CODEMIRROR_THEME
-} from '../codemirror/widget';
-
-import 'codemirror/addon/edit/matchbrackets.js';
-import 'codemirror/addon/edit/closebrackets.js';
-import 'codemirror/addon/comment/comment.js';
-import 'codemirror/keymap/vim.js';
+  IEditorTracker, EditorWidget, EditorWidgetFactory
+} from './widget';
 
 
 /**
@@ -87,7 +87,9 @@ const cmdIds = {
 export
 const editorHandlerProvider: JupyterLabPlugin<IEditorTracker> = {
   id: 'jupyter.services.editor-handler',
-  requires: [IDocumentRegistry, IMainMenu, ICommandPalette, IStateDB],
+  requires: [
+    IDocumentRegistry, IMainMenu, ICommandPalette, IStateDB, ILayoutRestorer
+  ],
   provides: IEditorTracker,
   activate: activateEditorHandler,
   autoStart: true
@@ -97,7 +99,7 @@ const editorHandlerProvider: JupyterLabPlugin<IEditorTracker> = {
 /**
  * Sets up the editor widget
  */
-function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB): IEditorTracker {
+function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): IEditorTracker {
   const factory = new EditorWidgetFactory({
     name: FACTORY,
     fileExtensions: ['*'],
@@ -105,11 +107,11 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
   });
   const tracker = new InstanceTracker<EditorWidget>({
     restore: {
-      state,
+      state, layout,
       command: 'file-operations:open',
       args: widget => ({ path: widget.context.path, factory: FACTORY }),
       name: widget => widget.context.path,
-      namespace: 'editors',
+      namespace: 'editor',
       when: app.started,
       registry: app.commands
     }

+ 2 - 0
src/faq/index.css

@@ -6,6 +6,8 @@
 
 .jp-FAQ {
   font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+  outline: none;
+  overflow-y: auto;
 }
 
 #faq-header {

+ 48 - 328
src/faq/plugin.ts

@@ -1,367 +1,87 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  h, VNode
-} from 'phosphor/lib/ui/vdom';
-
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
 
+import {
+  ICommandLinker
+} from '../commandlinker';
+
 import {
   ICommandPalette
 } from '../commandpalette';
 
 import {
-  VDomModel, VDomWidget
-} from '../common/vdom';
+  InstanceTracker
+} from '../common/instancetracker';
+
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
+import {
+  IStateDB
+} from '../statedb';
+
+import {
+  FaqModel, FaqWidget
+} from './widget';
 
 
 /**
- * The faq page extension.
+ * The FAQ page extension.
  */
 export
 const faqExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.faq',
-  requires: [ICommandPalette],
+  requires: [ICommandPalette, ICommandLinker, IStateDB, ILayoutRestorer],
   activate: activateFAQ,
   autoStart: true
 };
 
-/**
- * The class name added to the FAQ plugin.
- */
-const FAQ_CLASS = 'jp-FAQ';
-
-/**
- * The id name added to the header section element.
- */
-const HEADER_ID = 'faq-header';
-
-/**
- * The class name added to the title.
- */
-const TITLE_CLASS = 'jp-FAQ-title';
-
-/**
- * The class name added to h1 elements.
- */
-const HEADER_CLASS = 'jp-FAQ-h1';
-
-/**
- * The class name added to h2 elements.
- */
-const SUBHEADER_CLASS = 'jp-FAQ-h2';
-
-/**
- * The class name added for the question mark icon from default-theme.
- */
-const QUESTIONMARK_ICON_CLASS = 'jp-QuestionMark';
-
-/**
- * The class named added the question mark icon.
- */
-const QUESTIONMARK_CLASS = 'jp-FAQ-QuestionMark';
-
-/**
- * The class name added to faq content.
- */
-const CONTENT_CLASS = 'jp-FAQ-content';
-
-/**
- * The class name added to unordered list elements.
- */
-const FAQ_LIST_CLASS = 'jp-FAQ-ul';
-
-/**
- * The class name added to table of contents elements.
- */
-const TOC_CLASS = 'jp-FAQ-toc';
-
-/**
- * The class name added to questions.
- */
-const QUESTION_CLASS = 'jp-FAQ-question';
-
-/**
- * The class name added to answers.
- */
-const ANSWER_CLASS = 'jp-FAQ-answer';
-
-/**
- * The class name added to anchor elements.
- */
-const ANCHOR_CLASS = 'jp-FAQ-a';
-
-/**
- * FaqModel holds data which the FaqWidget will render.
- */
-class FaqModel extends VDomModel {
-  // Title of the FAQ plugin.
-  readonly title: string;
-  // Contain subheadings for each section.
-  readonly subHeadings: string[];
-  // Contain questions for `the basics` section.
-  readonly basicsQuestions: string[];
-  // Contain questions for the `features` section.
-  readonly featuresQuestions: string[];
-  // Contain questions for the `developer` section.
-  readonly developerQuestions: string[];
-
-  /**
-   * Construct a new faq model.
-   */
-  constructor() {
-    super();
-    this.title = 'Frequently Asked Questions';
-    this.subHeadings = [
-      'THE BASICS',
-      'FEATURES',
-      'DEVELOPER'
-    ];
-    this.basicsQuestions = [
-      'What is JupyterLab?',
-      'What is a Jupyter Notebook?',
-      'How stable is JupyterLab?',
-      'I\'m confused with the interface. How do I navigate around JupyterLab?'
-    ];
-    this.featuresQuestions = [
-      'How do I add more kernels/languages to JupyterLab?',
-      'How can I share my notebooks?'
-    ];
-    this.developerQuestions = [
-      'How do I report a bug?',
-      'I have security concerns about JupyterLab.',
-      'How can I contribute?'
-    ];
-  }
-}
 
 /**
- * A virtual-DOM-based widget for the FAQ plugin.
+ * Activate the FAQ plugin.
  */
-class FaqWidget extends VDomWidget<FaqModel> {
-  /**
-   * Construct a new faq widget.
-   */
-  constructor(app: JupyterLab) {
-    super();
-    this._app = app;
-    this.addClass(FAQ_CLASS);
-  }
-
-  /**
-   * Render the faq plugin to virtual DOM nodes.
-   */
-  protected render(): VNode[] {
-    let subHeadings = this.model.subHeadings;
-    let basicsQuestions = this.model.basicsQuestions;
-    let featuresQuestions = this.model.featuresQuestions;
-    let developerQuestions = this.model.developerQuestions;
-
-    // Create Frequently Asked Questions Header Section.
-    let faqHeader =
-    h.section({id: HEADER_ID},
-      h.span({className: QUESTIONMARK_ICON_CLASS + ' ' + QUESTIONMARK_CLASS}),
-      h.h1({className: HEADER_CLASS},
-        h.span({className: TITLE_CLASS},
-          this.model.title
-        )
-      )
-    );
+function activateFAQ(app: JupyterLab, palette: ICommandPalette, linker: ICommandLinker, state: IStateDB, layout: ILayoutRestorer): void {
+  const category = 'Help';
+  const command = 'faq-jupyterlab:show';
+  const model = new FaqModel();
+  const tracker = new InstanceTracker<FaqWidget>({
+    restore: {
+      state, layout, command,
+      args: widget => null,
+      name: widget => 'faq',
+      namespace: 'faq',
+      when: app.started,
+      registry: app.commands
+    }
+  });
 
-    // Create a section element that holds Table of Contents.
-    let questionList =
-    h.section({className: CONTENT_CLASS},
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[0]),
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#basicsQ1'}, basicsQuestions[0])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#basicsQ2'}, basicsQuestions[1])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#basicsQ3'}, basicsQuestions[2])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#basicsQ4'}, basicsQuestions[3])
-        )
-      ),
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[1]),
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#featuresQ1'}, featuresQuestions[0])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#featuresQ2'}, featuresQuestions[1])
-        )
-      ),
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[2]),
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#developerQ1'}, developerQuestions[0])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#developerQ2'}, developerQuestions[1])
-        ),
-        h.li({className: QUESTION_CLASS + ' ' + TOC_CLASS},
-          h.a({href: '#developerQ3'}, developerQuestions[2])
-        )
-      )
-    );
+  let widget: FaqWidget;
 
-    // Create a section element that all other FAQ Content will go under.
-    let questionAnswerList =
-    h.section({className: CONTENT_CLASS},
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[0]),
-      // Create list of questions/answers under the Basics section.
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS, id: 'basicsQ1'}, basicsQuestions[0]),
-        h.li({className: ANSWER_CLASS},
-          'JupyterLab allows users to arrange multiple Jupyter notebooks, '
-          + 'text editors, terminals, output areas, etc. on a single page with multiple '
-          + 'panels and tabs into one application. The codebase and UI of JupyterLab '
-          + 'is based on a flexible plugin system that makes it easy to extend '
-          + 'with new components.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'basicsQ2'}, basicsQuestions[1]),
-        h.li({className: ANSWER_CLASS},
-          'Central to the project is the Jupyter Notebook, a web-based '
-          + 'platform that allows users to combine live code, equations, narrative '
-          + 'text, visualizations, interactive dashboards and other media. Together '
-          + 'these building blocks make science and data reproducible across over '
-          + '40 programming languages and combine to form what we call a computational '
-          + 'narrative.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'basicsQ3'}, basicsQuestions[2]),
-        h.li({className: ANSWER_CLASS},
-          'JupyterLab is currently in a alpha release and not ready for public use '
-          + 'as new features and bug fixes are being added very frequently. We strongly '
-          + 'recommend to backup your work before using JupyterLab. However, testing, '
-          + 'development, and user feedback are greatly appreciated.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'basicsQ4'}, basicsQuestions[3]),
-        h.li({className: ANSWER_CLASS},
-          'Check out the JupyterLab tour ',
-          h.a({className: ANCHOR_CLASS,
-               onclick: () => {
-                 this._app.commands.execute('about-jupyterlab:show', void 0);
-               }},
-            'here'
-          )
-        )
-      ),
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[1]),
-      // Create list of questions/answers under the Features section.
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS, id: 'featuresQ1'}, featuresQuestions[0]),
-        h.li({className: ANSWER_CLASS},
-          'To add more languages to the JupyterLab you must install '
-          + 'a new kernel. Installing a kernel is usually fairly simple and can be '
-          + 'done with a couple terminal commands. However the instructions for installing '
-          + 'kernels is different for each language. For further instructions, click ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'https://jupyter.readthedocs.io/en/latest/install-kernel.html',
-               target: '_blank'},
-            'this'
-          ),
-          ' link.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'featuresQ2'}, featuresQuestions[1]),
-        h.li({className: ANSWER_CLASS},
-          'You can either publish your notebooks on GitHub or use a free service such as ',
-          h.a({className: ANCHOR_CLASS, href: 'https://nbviewer.jupyter.org/', target: '_blank'},
-            'nbviewer.org'
-          ),
-          ' to render your notebooks online.'
-        )
-      ),
-      h.h2({className: SUBHEADER_CLASS}, subHeadings[2]),
-      // Create list of questions/answers under the Developer section.
-      h.ul({className: FAQ_LIST_CLASS},
-        h.li({className: QUESTION_CLASS, id: 'developerQ1'}, developerQuestions[0]),
-        h.li({className: ANSWER_CLASS},
-          'You can open an issue on our ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'https://github.com/jupyterlab/jupyterlab/issues',
-               target: '_blank'},
-            'github repository'
-          ),
-          '. Please check already opened issues before posting.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'developerQ2'}, developerQuestions[1]),
-        h.li({className: ANSWER_CLASS},
-          'If you have any inquiries, concerns, or thought you found a security '
-          + 'vulnerability, please write to use at ',
-          h.a({className: ANCHOR_CLASS, href: 'mailto:security@jupyter.org'},
-            'security@jupyter.org'
-          ),
-          '. We will do our best to repond to you promptly.'
-        ),
-        h.li({className: QUESTION_CLASS, id: 'developerQ3'}, developerQuestions[2]),
-        h.li({className: ANSWER_CLASS},
-          'There are many ways to contribute to JupyterLab. '
-          + 'Whether you are an experienced python programmer or a newcomer, any '
-          + 'interested developers are welcome. You can learn about the JupyterLab '
-          + 'codebase by going through our ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'https://jupyterlab-tutorial.readthedocs.io/en/latest/index.html',
-               target: '_blank'},
-            'tutorial walkthrough'
-          ),
-          ' and ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'https://jupyterlab.github.io/jupyterlab/',
-               target: '_blank'},
-               'documentation'
-          ),
-          '. Also, feel free to ask questions on our ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'https://github.com/jupyterlab/jupyterlab',
-               target: '_blank'},
-               'github'
-          ),
-          ' or through any of our ',
-          h.a({className: ANCHOR_CLASS,
-               href: 'http://jupyter.org/community.html',
-               target: '_blank'},
-            'community resources'
-          ),
-          '.'
-        )
-      )
-    );
-    let domTree = [faqHeader, questionList, questionAnswerList];
-    return domTree;
+  function newWidget(): FaqWidget {
+    let widget = new FaqWidget({ linker });
+    widget.model = model;
+    widget.id = 'faq';
+    widget.title.label = 'FAQ';
+    widget.title.closable = true;
+    tracker.add(widget);
+    return widget;
   }
 
-  private _app: JupyterLab;
-}
-
-/**
- * Activate the faq plugin.
- */
-function activateFAQ(app: JupyterLab, palette: ICommandPalette): void {
-  let faqModel = new FaqModel();
-  let widget = new FaqWidget(app);
-  let commandId = 'faq-jupyterlab:show';
-  widget.model = faqModel;
-  widget.id = 'faq-jupyterlab';
-  widget.title.label = 'FAQ';
-  widget.title.closable = true;
-  widget.node.style.overflowY = 'auto';
-
-  app.commands.addCommand(commandId, {
+  app.commands.addCommand(command, {
     label: 'Frequently Asked Questions',
     execute: () => {
-      if (!widget.isAttached) {
+      if (!widget || widget.isDisposed) {
+        widget = newWidget();
         app.shell.addToMainArea(widget);
       }
       app.shell.activateMain(widget.id);
     }
   });
 
-  palette.addItem({ command: commandId, category: 'Help' });
+  palette.addItem({ command, category });
 }

+ 383 - 0
src/faq/widget.ts

@@ -0,0 +1,383 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
+import {
+  h, VNode
+} from 'phosphor/lib/ui/vdom';
+
+import {
+  ICommandLinker
+} from '../commandlinker';
+
+import {
+  VDomModel, VDomWidget
+} from '../common/vdom';
+
+
+/**
+ * The class name added to the FAQ plugin.
+ */
+const FAQ_CLASS = 'jp-FAQ';
+
+/**
+ * The id name added to the header section element.
+ */
+const HEADER_ID = 'faq-header';
+
+/**
+ * The class name added to the title.
+ */
+const TITLE_CLASS = 'jp-FAQ-title';
+
+/**
+ * The class name added to h1 elements.
+ */
+const HEADER_CLASS = 'jp-FAQ-h1';
+
+/**
+ * The class name added to h2 elements.
+ */
+const SUBHEADER_CLASS = 'jp-FAQ-h2';
+
+/**
+ * The class name added for the question mark icon from default-theme.
+ */
+const QUESTIONMARK_ICON_CLASS = 'jp-QuestionMark';
+
+/**
+ * The class named added the question mark icon.
+ */
+const QUESTIONMARK_CLASS = 'jp-FAQ-QuestionMark';
+
+/**
+ * The class name added to faq content.
+ */
+const CONTENT_CLASS = 'jp-FAQ-content';
+
+/**
+ * The class name added to unordered list elements.
+ */
+const FAQ_LIST_CLASS = 'jp-FAQ-ul';
+
+/**
+ * The class name added to table of contents elements.
+ */
+const TOC_CLASS = 'jp-FAQ-toc';
+
+/**
+ * The class name added to questions.
+ */
+const QUESTION_CLASS = 'jp-FAQ-question';
+
+/**
+ * The class name added to answers.
+ */
+const ANSWER_CLASS = 'jp-FAQ-answer';
+
+/**
+ * The class name added to anchor elements.
+ */
+const ANCHOR_CLASS = 'jp-FAQ-a';
+
+/**
+ * FaqModel holds data which the FaqWidget will render.
+ */
+export
+class FaqModel extends VDomModel {
+  // Title of the FAQ plugin.
+  readonly title: string;
+  // Contain subheadings for each section.
+  readonly subHeadings: string[];
+  // Contain questions for `the basics` section.
+  readonly basicsQuestions: string[];
+  // Contain questions for the `features` section.
+  readonly featuresQuestions: string[];
+  // Contain questions for the `developer` section.
+  readonly developerQuestions: string[];
+
+  /**
+   * Construct a new faq model.
+   */
+  constructor() {
+    super();
+    this.title = 'Frequently Asked Questions';
+    this.subHeadings = [
+      'THE BASICS',
+      'FEATURES',
+      'DEVELOPER'
+    ];
+    this.basicsQuestions = [
+      'What is JupyterLab?',
+      'What is a Jupyter Notebook?',
+      'How stable is JupyterLab?',
+      'I\'m confused with the interface. How do I navigate around JupyterLab?'
+    ];
+    this.featuresQuestions = [
+      'How do I add more kernels/languages to JupyterLab?',
+      'How can I share my notebooks?'
+    ];
+    this.developerQuestions = [
+      'How do I report a bug?',
+      'I have security concerns about JupyterLab.',
+      'How can I contribute?'
+    ];
+  }
+}
+
+/**
+ * A virtual-DOM-based widget for the FAQ plugin.
+ */
+export
+class FaqWidget extends VDomWidget<FaqModel> {
+  /**
+   * Construct a new faq widget.
+   */
+  constructor(options: FaqWidget.IOptions) {
+    super();
+    this._linker = options.linker;
+    this.addClass(FAQ_CLASS);
+  }
+
+  /**
+   * Render the faq plugin to virtual DOM nodes.
+   */
+  protected render(): VNode[] {
+    let subHeadings = this.model.subHeadings;
+    let basicsQuestions = this.model.basicsQuestions;
+    let featuresQuestions = this.model.featuresQuestions;
+    let developerQuestions = this.model.developerQuestions;
+
+    // Create Frequently Asked Questions Header Section.
+    let faqHeader =
+    h.section({ id: HEADER_ID },
+      h.span({
+        className: QUESTIONMARK_ICON_CLASS + ' ' + QUESTIONMARK_CLASS
+      }),
+      h.h1({ className: HEADER_CLASS },
+        h.span({ className: TITLE_CLASS }, this.model.title)
+      )
+    );
+
+    // Create a section element that holds Table of Contents.
+    let questionList =
+    h.section({ className: CONTENT_CLASS },
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[0]),
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#basicsQ1' }, basicsQuestions[0])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#basicsQ2' }, basicsQuestions[1])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#basicsQ3' }, basicsQuestions[2])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#basicsQ4' }, basicsQuestions[3])
+        )
+      ),
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[1]),
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#featuresQ1' }, featuresQuestions[0])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#featuresQ2' }, featuresQuestions[1])
+        )
+      ),
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[2]),
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#developerQ1' }, developerQuestions[0])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#developerQ2' }, developerQuestions[1])
+        ),
+        h.li({ className: QUESTION_CLASS + ' ' + TOC_CLASS },
+          h.a({ href: '#developerQ3' }, developerQuestions[2])
+        )
+      )
+    );
+
+    // Create a section element that all other FAQ Content will go under.
+    let questionAnswerList =
+    h.section({ className: CONTENT_CLASS },
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[0]),
+      // Create list of questions/answers under the Basics section.
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({ className: QUESTION_CLASS, id: 'basicsQ1' }, basicsQuestions[0]),
+        h.li({ className: ANSWER_CLASS },
+          `JupyterLab allows users to arrange multiple Jupyter notebooks,
+          text editors, terminals, output areas, etc. on a single page with
+          multiple panels and tabs into one application. The codebase and UI of
+          JupyterLab is based on a flexible plugin system that makes it easy to
+          extend with new components.`
+        ),
+        h.li({ className: QUESTION_CLASS, id: 'basicsQ2' }, basicsQuestions[1]),
+        h.li({ className: ANSWER_CLASS },
+          `Central to the project is the Jupyter Notebook, a web-based
+          platform that allows users to combine live code, equations, narrative
+          text, visualizations, interactive dashboards and other media. Together
+          these building blocks make science and data reproducible across over
+          40 programming languages and combine to form what we call a
+          computational narrative.`
+        ),
+        h.li({ className: QUESTION_CLASS, id: 'basicsQ3' }, basicsQuestions[2]),
+        h.li({ className: ANSWER_CLASS },
+          `JupyterLab is currently in an alpha release and not ready for public
+          use as new features and bug fixes are being added very frequently. We
+          strongly recommend to back up your work before using JupyterLab.
+          However, testing, development, and user feedback are greatly
+          appreciated.`
+        ),
+        h.li({ className: QUESTION_CLASS, id: 'basicsQ4' }, basicsQuestions[3]),
+        h.li({ className: ANSWER_CLASS },
+          'Check out the JupyterLab tour ',
+          h.a(this._linker.populateVNodeAttrs({
+            className: ANCHOR_CLASS
+          }, 'about-jupyterlab:show', null), 'here')
+        )
+      ),
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[1]),
+      // Create list of questions/answers under the Features section.
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({
+          className: QUESTION_CLASS,
+          id: 'featuresQ1'
+        }, featuresQuestions[0]),
+        h.li({ className: ANSWER_CLASS },
+          `To add more languages to the JupyterLab you must install a new
+          kernel. Installing a kernel is usually fairly simple and can be done
+          with a couple terminal commands. However the instructions for
+          installing kernels is different for each language. For further
+          instructions, click`,
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://jupyter.readthedocs.io/en/latest/install-kernel.html',
+            target: '_blank'
+          }, 'this'),
+          ' link.'
+        ),
+        h.li({
+          className: QUESTION_CLASS,
+          id: 'featuresQ2'
+        }, featuresQuestions[1]),
+        h.li({ className: ANSWER_CLASS },
+          `You can either publish your notebooks on GitHub or use a free service
+          such as `,
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://nbviewer.jupyter.org/',
+            target: '_blank'
+          }, 'nbviewer.org'),
+          ' to render your notebooks online.'
+        )
+      ),
+      h.h2({ className: SUBHEADER_CLASS }, subHeadings[2]),
+      // Create list of questions/answers under the Developer section.
+      h.ul({ className: FAQ_LIST_CLASS },
+        h.li({
+          className: QUESTION_CLASS,
+          id: 'developerQ1'
+        }, developerQuestions[0]),
+        h.li({ className: ANSWER_CLASS },
+          'You can open an issue on our ',
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://github.com/jupyterlab/jupyterlab/issues',
+            target: '_blank'
+          }, 'github repository'),
+          '. Please check already opened issues before posting.'
+        ),
+        h.li({
+          className: QUESTION_CLASS,
+          id: 'developerQ2'
+        }, developerQuestions[1]),
+        h.li({ className: ANSWER_CLASS },
+          `If you have any inquiries, concerns, or thought you found a security
+          vulnerability, please write to use at `,
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'mailto:security@jupyter.org'
+          }, 'security@jupyter.org'),
+          '. We will do our best to repond to you promptly.'
+        ),
+        h.li({
+          className: QUESTION_CLASS,
+          id: 'developerQ3'
+        }, developerQuestions[2]),
+        h.li({ className: ANSWER_CLASS },
+          `There are many ways to contribute to JupyterLab. Whether you are an
+          experienced Python programmer or a newcomer, any interested developers
+          are welcome. You can learn about the JupyterLab codebase by going
+          through our`,
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://jupyterlab-tutorial.readthedocs.io/en/latest/index.html',
+            target: '_blank'
+          }, 'tutorial walkthrough' ),
+          ' and ',
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://jupyterlab.github.io/jupyterlab/',
+            target: '_blank'
+          }, 'documentation'),
+          '. Also, feel free to ask questions on our ',
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'https://github.com/jupyterlab/jupyterlab',
+            target: '_blank'
+          }, 'github'),
+          ' or through any of our ',
+          h.a({
+            className: ANCHOR_CLASS,
+            href: 'http://jupyter.org/community.html',
+            target: '_blank'
+          }, 'community resources'),
+          '.'
+        )
+      )
+    );
+    return [faqHeader, questionList, questionAnswerList];
+  }
+
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
+    this.node.focus();
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onCloseRequest(msg: Message): void {
+    super.onCloseRequest(msg);
+    this.dispose();
+  }
+
+  private _linker: ICommandLinker;
+}
+
+
+/**
+ * A namespace for `FaqWidget` statics.
+ */
+export
+namespace FaqWidget {
+  /**
+   * Instantiation options for the FAQ widget.
+   */
+  export
+  interface IOptions {
+    /**
+     * A command linker instance.
+     */
+    linker: ICommandLinker;
+  }
+}

+ 0 - 1
src/filebrowser/listing.ts

@@ -967,7 +967,6 @@ class DirListing extends Widget {
   private _startDrag(index: number, clientX: number, clientY: number): void {
     let selectedNames = Object.keys(this._selection);
     let source = this._items.at(index);
-    let model = this._model;
     let items = this._sortedItems;
     let item: Contents.IModel = null;
 

+ 25 - 44
src/filebrowser/plugin.ts

@@ -13,10 +13,6 @@ import {
   Menu
 } from 'phosphor/lib/ui/menu';
 
-import {
-  Widget
-} from 'phosphor/lib/ui/widget';
-
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
@@ -25,10 +21,6 @@ import {
   ICommandPalette
 } from '../commandpalette';
 
-import {
-  InstanceTracker
-} from '../common/instancetracker';
-
 import {
   IDocumentManager
 } from '../docmanager';
@@ -62,7 +54,12 @@ const fileBrowserProvider: JupyterLabPlugin<IPathTracker> = {
   id: 'jupyter.services.file-browser',
   provides: IPathTracker,
   requires: [
-    IServiceManager, IDocumentManager, IDocumentRegistry, IMainMenu, ICommandPalette, IStateDB
+    IServiceManager,
+    IDocumentManager,
+    IDocumentRegistry,
+    IMainMenu,
+    ICommandPalette,
+    IStateDB
   ],
   activate: activateFileBrowser,
   autoStart: true
@@ -94,8 +91,6 @@ const NAMESPACE = 'filebrowser';
  * Activate the file browser.
  */
 function activateFileBrowser(app: JupyterLab, manager: IServiceManager, documentManager: IDocumentManager, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB): IPathTracker {
-  let id = 0;
-  
   let { commands, keymap } = app;
   let fbModel = new FileBrowserModel({ manager });
   let fbWidget = new FileBrowser({
@@ -107,7 +102,6 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
 
   let category = 'File Operations';
   let creatorCmds: { [key: string]: DisposableSet } = Object.create(null);
-
   let addCreator = (name: string) => {
     let disposables = creatorCmds[name] = new DisposableSet();
     let command = Private.commandForName(name);
@@ -136,9 +130,7 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
     });
   });
 
-  each(registry.creators(), creator => {
-    addCreator(creator.name);
-  });
+  each(registry.creators(), creator => { addCreator(creator.name); });
 
   // Add a context menu to the dir listing.
   let node = fbWidget.node.getElementsByClassName('jp-DirListing-content')[0];
@@ -147,9 +139,7 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
     let path = fbWidget.pathForClick(event) || '';
     let ext = '.' + path.split('.').pop();
     let factories = registry.preferredWidgetFactories(ext);
-    let widgetNames = toArray(map(factories, factory => {
-      return factory.name;
-    }));
+    let widgetNames = toArray(map(factories, factory => factory.name));
     let prefix = `file-browser-contextmenu-${++Private.id}`;
     let openWith: Menu = null;
     if (path && widgetNames.length > 1) {
@@ -158,7 +148,7 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
 
       openWith = new Menu({ commands, keymap });
       openWith.title.label = 'Open With...';
-      openWith.disposed.connect(() => disposables.dispose());
+      openWith.disposed.connect(() => { disposables.dispose(); });
 
       for (let widgetName of widgetNames) {
         command = `${prefix}:${widgetName}`;
@@ -182,10 +172,10 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
     cmdIds.saveAs,
     cmdIds.close,
     cmdIds.closeAllFiles,
-  ].forEach(command => palette.addItem({ command, category }));
+  ].forEach(command => { palette.addItem({ command, category }); });
 
   let menu = createMenu(app, Object.keys(creatorCmds));
-  mainMenu.addMenu(menu, {rank: 1});
+  mainMenu.addMenu(menu, { rank: 1 });
 
   fbWidget.title.label = 'Files';
   fbWidget.id = 'file-browser';
@@ -204,7 +194,7 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
         delete creatorCmds[name];
       }
       menu = createMenu(app, Object.keys(creatorCmds));
-      mainMenu.addMenu(menu, {rank: 1});
+      mainMenu.addMenu(menu, { rank: 1 });
     }
   });
 
@@ -217,13 +207,10 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, document
  */
 function addCommands(app: JupyterLab, fbWidget: FileBrowser, docManager: IDocumentManager): void {
   let commands = app.commands;
-
-  let isEnabled: () => boolean = () => {
-      if (app.shell.currentWidget && docManager.contextForWidget(app.shell.currentWidget)) {
-        return true;
-      }
-      return false;
-    };
+  let isEnabled = () => {
+    let currentWidget = app.shell.currentWidget;
+    return !!(currentWidget && docManager.contextForWidget(currentWidget));
+  };
 
   commands.addCommand(cmdIds.save, {
     label: 'Save',
@@ -232,9 +219,7 @@ function addCommands(app: JupyterLab, fbWidget: FileBrowser, docManager: IDocume
     execute: () => {
       if (isEnabled()) {
         let context = docManager.contextForWidget(app.shell.currentWidget);
-        return context.save().then(() => {
-          return context.createCheckpoint();
-        });
+        return context.save().then(() => context.createCheckpoint());
       }
     }
   });
@@ -246,9 +231,7 @@ function addCommands(app: JupyterLab, fbWidget: FileBrowser, docManager: IDocume
     execute: () => {
       if (isEnabled()) {
         let context = docManager.contextForWidget(app.shell.currentWidget);
-        context.restoreCheckpoint().then(() => {
-          context.revert();
-        });        
+        return context.restoreCheckpoint().then(() => context.revert());
       }
     }
   });
@@ -286,13 +269,11 @@ function addCommands(app: JupyterLab, fbWidget: FileBrowser, docManager: IDocume
 
   commands.addCommand(cmdIds.closeAllFiles, {
     label: 'Close All',
-    execute: () => {
-      app.shell.closeAll();
-    }
+    execute: () => { app.shell.closeAll(); }
   });
 
   commands.addCommand(cmdIds.showBrowser, {
-    execute: () => app.shell.activateLeft(fbWidget.id)
+    execute: () => { app.shell.activateLeft(fbWidget.id); }
   });
 
   commands.addCommand(cmdIds.hideBrowser, {
@@ -352,7 +333,7 @@ function createContextMenu(fbWidget: FileBrowser, openWith: Menu):  Menu {
 
   command = `${prefix}:open`;
   disposables.add(commands.addCommand(command, {
-    execute: () => fbWidget.open(),
+    execute: () => { fbWidget.open(); },
     icon: 'fa fa-folder-open-o',
     label: 'Open',
     mnemonic: 0
@@ -391,7 +372,7 @@ function createContextMenu(fbWidget: FileBrowser, openWith: Menu):  Menu {
 
   command = `${prefix}:cut`;
   disposables.add(commands.addCommand(command, {
-    execute: () => fbWidget.cut(),
+    execute: () => { fbWidget.cut(); },
     icon: 'fa fa-cut',
     label: 'Cut'
   }));
@@ -399,7 +380,7 @@ function createContextMenu(fbWidget: FileBrowser, openWith: Menu):  Menu {
 
   command = `${prefix}:copy`;
   disposables.add(commands.addCommand(command, {
-    execute: () => fbWidget.copy(),
+    execute: () => { fbWidget.copy(); },
     icon: 'fa fa-copy',
     label: 'Copy',
     mnemonic: 0
@@ -417,7 +398,7 @@ function createContextMenu(fbWidget: FileBrowser, openWith: Menu):  Menu {
 
   command = `${prefix}:download`;
   disposables.add(commands.addCommand(command, {
-    execute: () => fbWidget.download(),
+    execute: () => { fbWidget.download(); },
     icon: 'fa fa-download',
     label: 'Download'
   }));
@@ -431,7 +412,7 @@ function createContextMenu(fbWidget: FileBrowser, openWith: Menu):  Menu {
   }));
   menu.addItem({ command });
 
-  menu.disposed.connect(() => disposables.dispose());
+  menu.disposed.connect(() => { disposables.dispose(); });
 
   return menu;
 }

+ 5 - 1
src/help/plugin.ts

@@ -172,11 +172,15 @@ namespace Private {
     let { commands, keymap } = app;
     let menu = new Menu({ commands, keymap });
     menu.title.label = 'Help';
+
     menu.addItem({ command: 'about-jupyterlab:show' });
     menu.addItem({ command: 'faq-jupyterlab:show' });
-    menu.addItem({ command: 'classic-notebook:open'})
+    menu.addItem({ command: 'classic-notebook:open' });
 
     COMMANDS.forEach(item => menu.addItem({ command: item.id }));
+
+    menu.addItem({ command: 'statedb:clear' });
+
     return menu;
   }
 

+ 8 - 4
src/imagewidget/plugin.ts

@@ -17,6 +17,10 @@ import {
   IDocumentRegistry
 } from '../docregistry';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IStateDB
 } from '../statedb';
@@ -43,7 +47,7 @@ const FACTORY = 'Image';
 export
 const imageHandlerExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.image-handler',
-  requires: [IDocumentRegistry, ICommandPalette, IStateDB],
+  requires: [IDocumentRegistry, ICommandPalette, IStateDB, ILayoutRestorer],
   activate: activateImageWidget,
   autoStart: true
 };
@@ -52,7 +56,7 @@ const imageHandlerExtension: JupyterLabPlugin<void> = {
 /**
  * Activate the image widget extension.
  */
-function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palette: ICommandPalette, state: IStateDB): void {
+function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
   let zoomInImage = 'image-widget:zoom-in';
   let zoomOutImage = 'image-widget:zoom-out';
   let resetZoomImage = 'image-widget:reset-zoom';
@@ -66,11 +70,11 @@ function activateImageWidget(app: JupyterLab, registry: IDocumentRegistry, palet
 
   const tracker = new InstanceTracker<ImageWidget>({
     restore: {
-      state,
+      state, layout,
       command: 'file-operations:open',
       args: widget => ({ path: widget.context.path, factory: FACTORY }),
       name: widget => widget.context.path,
-      namespace: 'images',
+      namespace: 'imagewidget',
       when: app.started,
       registry: app.commands
     }

+ 2 - 1
src/inspector/index.css

@@ -17,8 +17,9 @@
 | Inspector
 |----------------------------------------------------------------------------*/
 
-.jp-Inspector .p-TabPanel-stackedPanel {
+.jp-Inspector {
   border: none;
+  outline: none;
 }
 
 

+ 9 - 1
src/inspector/inspector.ts

@@ -155,7 +155,7 @@ class Inspector extends TabPanel implements IInspector {
     }
 
     // Dispose the inspector child items.
-    Object.keys(this._items).forEach(i => this._items[i].dispose());
+    Object.keys(this._items).forEach(i => { this._items[i].dispose(); });
     this._items = null;
 
     // Disconnect from source.
@@ -164,6 +164,14 @@ class Inspector extends TabPanel implements IInspector {
     super.dispose();
   }
 
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
+    this.node.focus();
+  }
+
   /**
    * Handle `'close-request'` messages.
    */

+ 31 - 13
src/inspector/plugin.ts

@@ -10,6 +10,18 @@ import {
   ICommandPalette
 } from '../commandpalette';
 
+import {
+  InstanceTracker
+} from '../common/instancetracker';
+
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
+import {
+  IStateDB
+} from '../statedb';
+
 import {
   IInspector, Inspector
 } from './';
@@ -21,7 +33,7 @@ import {
 export
 const inspectorProvider: JupyterLabPlugin<IInspector> = {
   id: 'jupyter.services.inspector',
-  requires: [ICommandPalette],
+  requires: [ICommandPalette, IStateDB, ILayoutRestorer],
   provides: IInspector,
   activate: activateInspector
 };
@@ -83,9 +95,21 @@ class InspectorManager implements IInspector {
 /**
  * Activate the console extension.
  */
-function activateInspector(app: JupyterLab, palette: ICommandPalette): IInspector {
-  let manager = new InspectorManager();
-  let openInspectorCommand = 'inspector:open';
+function activateInspector(app: JupyterLab, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): IInspector {
+  const category = 'Inspector';
+  const command = 'inspector:open';
+  const label = 'Open Inspector';
+  const manager = new InspectorManager();
+  const tracker = new InstanceTracker<Inspector>({
+    restore: {
+      state, layout, command,
+      args: widget => null,
+      name: widget => 'inspector',
+      namespace: 'inspector',
+      when: app.started,
+      registry: app.commands
+    }
+  });
 
   function newInspector(): Inspector {
     let inspector = new Inspector({ items: Private.defaultInspectorItems });
@@ -97,6 +121,7 @@ function activateInspector(app: JupyterLab, palette: ICommandPalette): IInspecto
         manager.inspector = null;
       }
     });
+    tracker.add(inspector);
     return inspector;
   }
 
@@ -110,15 +135,8 @@ function activateInspector(app: JupyterLab, palette: ICommandPalette): IInspecto
     }
   }
 
-  app.commands.addCommand(openInspectorCommand, {
-    execute: openInspector,
-    label: 'Open Inspector'
-  });
-
-  palette.addItem({
-    command: openInspectorCommand,
-    category: 'Inspector'
-  });
+  app.commands.addCommand(command, { execute: openInspector, label });
+  palette.addItem({ command, category });
 
   return manager;
 }

+ 1 - 0
src/landing/index.css

@@ -5,6 +5,7 @@
 
 
 .jp-Landing {
+  outline: none;
   position: absolute;
   display: flex;
   flex-direction: column;

+ 58 - 225
src/landing/plugin.ts

@@ -1,10 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  h, VNode
-} from 'phosphor/lib/ui/vdom';
-
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
@@ -14,17 +10,29 @@ import {
 } from '../commandpalette';
 
 import {
-  VDomModel, VDomWidget
-} from '../common/vdom';
+  InstanceTracker
+} from '../common/instancetracker';
 
 import {
   IPathTracker
 } from '../filebrowser';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IServiceManager
 } from '../services';
 
+import {
+  IStateDB
+} from '../statedb';
+
+import {
+  LandingModel, LandingWidget
+} from './widget';
+
 
 /**
  * The landing page extension.
@@ -32,7 +40,9 @@ import {
 export
 const landingExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.landing',
-  requires: [IPathTracker, ICommandPalette, IServiceManager],
+  requires: [
+    IPathTracker, ICommandPalette, IServiceManager, IStateDB, ILayoutRestorer
+  ],
   activate: activateLanding,
   autoStart: true
 };
@@ -42,237 +52,60 @@ const landingExtension: JupyterLabPlugin<void> = {
  */
 const LANDING_CLASS = 'jp-Landing';
 
-/**
- * The class name added to the dialog.
- */
-const LANDING_DIALOG_CLASS = 'jp-Landing-dialog';
-
-/**
- * The class name for the JupyterLab icon from default-theme.
- */
-const JUPYTERLAB_ICON_CLASS = 'jp-ImageJupyterLab';
-
-/**
- * The class name added to specify size of the JupyterLab logo.
- */
-const LANDING_LOGO_CLASS = 'jp-Landing-logo';
-
-/**
- * The class name added to the preview message subtitle.
- */
-const LANDING_SUBTITLE_CLASS = 'jp-Landing-subtitle';
-
-/**
- * The class name added for the tour icon from default-theme.
- */
-const TOUR_ICON_CLASS = 'jp-Landing-tour';
-
-/**
- * The class name added to the header text.
- */
-const LANDING_HEADER_CLASS = 'jp-Landing-header';
-
-/**
- * The class name added to the dialog body.
- */
-const LANDING_BODY_CLASS = 'jp-Landing-body';
-
-/**
- * The class name added to the column of the dialog.
- */
-const LANDING_COLUMN_CLASS = 'jp-Landing-column';
-
-/**
- * The class name added to specify size of activity icons.
- */
-const LANDING_ICON_CLASS = 'jp-Landing-image';
-
-/**
- * The class name added to the image text of an activity.
- */
-const LANDING_TEXT_CLASS = 'jp-Landing-text';
-
-/**
- * The class name added to the current working directory.
- */
-const LANDING_CWD_CLASS = 'jp-Landing-cwd';
-
-/**
- * The class name added to Landing folder node.
- */
-const FOLDER_CLASS = 'jp-Landing-folder';
-
-/**
- * The class name added for the folder icon from default-theme.
- */
-const FOLDER_ICON_CLASS = 'jp-FolderIcon';
-
-/**
- * The class name added to the current working directory path.
- */
-const LANDING_PATH_CLASS = 'jp-Landing-path';
-
-
-/**
- * LandingModel keeps track of the path to working directory and has text data,
- * which the LandingWidget will render.
- */
-class LandingModel extends VDomModel {
-  // Contains a preview messages.
-  readonly previewMessage: string;
-  // Contains text to `Start a new activity`.
-  readonly headerText: string;
-  // Contains the names of activities and their associated commands.
-  readonly activities: string[][];
-
-  /**
-   * Construct a new landing model.
-   */
-  constructor(terminalsAvailable = false) {
-    super();
-    let previewMessages = ['super alpha preview', 'very alpha preview', 'extremely alpha preview', 'exceedingly alpha preview', 'alpha alpha preview'];
-    this.previewMessage = previewMessages[(Math.floor(Math.random() * previewMessages.length))];
-    this.headerText = 'Start a new activity';
-    this.activities =
-    [['Notebook', 'file-operations:new-notebook'],
-     ['Code Console', `console:create`],
-     ['Text Editor', 'file-operations:new-text-file']];
-
-    if (terminalsAvailable) {
-      this.activities.push(
-        ['Terminal', 'terminal:create-new']
-      );
-    }
-    this._path = 'home';
-  }
-
-  /**
-   * Get the path of the current working directory.
-   */
-  get path(): string {
-    return this._path;
-  }
-
-  /**
-   * Set the path of the current working directory.
-   */
-  set path(value: string) {
-    this._path = value;
-    this.stateChanged.emit(void 0);
-  }
-
-  private _path: string;
-}
-
-/**
- * A virtual-DOM-based widget for the Landing plugin.
- */
-class LandingWidget extends VDomWidget<LandingModel> {
-  /**
-   * Construct a new landing widget.
-   */
-  constructor(app: JupyterLab) {
-    super();
-    this._app = app;
-  }
 
-  /**
-   * Render the landing plugin to virtual DOM nodes.
-   */
-  protected render(): VNode {
-    let activitiesList: VNode[] = [];
-    let activites = this.model.activities;
-    for (let activityName of activites) {
-      let imgName = activityName[0].replace(' ', '');
-      let column =
-      h.div({className: LANDING_COLUMN_CLASS},
-        h.span({className: LANDING_ICON_CLASS + ` jp-Image${imgName}` ,
-                onclick: () => {
-                  this._app.commands.execute(activityName[1], void 0);
-                }}
-        ),
-        h.span({className: LANDING_TEXT_CLASS}, activityName[0])
-      );
-      activitiesList.push(column);
+function activateLanding(app: JupyterLab, pathTracker: IPathTracker, palette: ICommandPalette, services: IServiceManager, state: IStateDB, layout: ILayoutRestorer): void {
+  const category = 'Help';
+  const command = 'jupyterlab-landing:show';
+  const model = new LandingModel(services.terminals.isAvailable());
+  const tracker = new InstanceTracker<LandingWidget>({
+    restore: {
+      state, layout, command,
+      args: widget => null,
+      name: widget => 'landing',
+      namespace: 'landing',
+      when: app.started,
+      registry: app.commands
     }
+  });
 
-    let logo = h.span({className: JUPYTERLAB_ICON_CLASS + ' ' + LANDING_LOGO_CLASS});
-    let subtitle =
-    h.span({className: LANDING_SUBTITLE_CLASS},
-      this.model.previewMessage
-    );
-    let tour =
-    h.span({className: TOUR_ICON_CLASS,
-            onclick: () => {
-              this._app.commands.execute('about-jupyterlab:show', void 0);
-            }}
-    );
-    let header =
-    h.span({className: LANDING_HEADER_CLASS},
-      this.model.headerText
-    );
-    let body =
-    h.div({className: LANDING_BODY_CLASS},
-      activitiesList
-    );
-
-    let dialog =
-    h.div({className: LANDING_DIALOG_CLASS},
-      logo,
-      subtitle,
-      tour,
-      header,
-      body,
-      h.div({className: LANDING_CWD_CLASS},
-        h.span({className: FOLDER_ICON_CLASS + ' ' + FOLDER_CLASS}),
-        h.span({className: LANDING_PATH_CLASS}, this.model.path
-        )
-      )
-    );
-    return dialog;
+  let widget: LandingWidget;
+
+  function newWidget(): LandingWidget {
+    let widget = new LandingWidget(app);
+    widget.model = model;
+    widget.id = 'landing-jupyterlab';
+    widget.title.label = 'Launcher';
+    widget.title.closable = true;
+    widget.addClass(LANDING_CLASS);
+    tracker.add(widget);
+    return widget;
   }
 
-  private _app: JupyterLab;
-}
-
-
-function activateLanding(app: JupyterLab, pathTracker: IPathTracker, palette: ICommandPalette, services: IServiceManager): void {
-  let landingModel = new LandingModel(services.terminals.isAvailable());
-  let widget = new LandingWidget(app);
-  widget.model = landingModel;
-  widget.id = 'landing-jupyterlab';
-  widget.title.label = 'Launcher';
-  widget.title.closable = true;
-  widget.addClass(LANDING_CLASS);
-
-  let path = 'home';
-  pathTracker.pathChanged.connect(() => {
-    if (pathTracker.path.length > 0) {
-      path = 'home > ';
-      let path2 = pathTracker.path;
-      path2 = path2.replace('/', ' > ');
-      path += path2;
-    } else {
-      path = 'home';
-    }
-    landingModel.path = path;
-  });
-
-  app.commands.addCommand('jupyterlab-landing:show', {
+  app.commands.addCommand(command, {
     label: 'Show Landing',
     execute: () => {
-      if (!widget.isAttached) {
+      if (!widget || widget.isDisposed) {
+        widget = newWidget();
         app.shell.addToMainArea(widget);
       }
       app.shell.activateMain(widget.id);
     }
   });
 
-  palette.addItem({
-    command: 'jupyterlab-landing:show',
-    category: 'Help'
+  pathTracker.pathChanged.connect(() => {
+    if (pathTracker.path.length) {
+      model.path = 'home > ' + pathTracker.path.replace('/', ' > ');
+    } else {
+      model.path = 'home';
+    }
   });
 
-  app.shell.addToMainArea(widget);
-  app.shell.activateMain(widget.id);
+  palette.addItem({ category, command });
+
+  // Only create a landing page if there are no other tabs open.
+  layout.restored.then(() => {
+    if (app.shell.mainAreaIsEmpty) {
+      app.commands.execute(command, void 0);
+    }
+  });
 }

+ 245 - 0
src/landing/widget.ts

@@ -0,0 +1,245 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
+import {
+  h, VNode
+} from 'phosphor/lib/ui/vdom';
+
+import {
+  JupyterLab
+} from '../application';
+
+import {
+  VDomModel, VDomWidget
+} from '../common/vdom';
+
+
+/**
+ * The class name added to the dialog.
+ */
+const LANDING_DIALOG_CLASS = 'jp-Landing-dialog';
+
+/**
+ * The class name for the JupyterLab icon from default-theme.
+ */
+const JUPYTERLAB_ICON_CLASS = 'jp-ImageJupyterLab';
+
+/**
+ * The class name added to specify size of the JupyterLab logo.
+ */
+const LANDING_LOGO_CLASS = 'jp-Landing-logo';
+
+/**
+ * The class name added to the preview message subtitle.
+ */
+const LANDING_SUBTITLE_CLASS = 'jp-Landing-subtitle';
+
+/**
+ * The class name added for the tour icon from default-theme.
+ */
+const TOUR_ICON_CLASS = 'jp-Landing-tour';
+
+/**
+ * The class name added to the header text.
+ */
+const LANDING_HEADER_CLASS = 'jp-Landing-header';
+
+/**
+ * The class name added to the dialog body.
+ */
+const LANDING_BODY_CLASS = 'jp-Landing-body';
+
+/**
+ * The class name added to the column of the dialog.
+ */
+const LANDING_COLUMN_CLASS = 'jp-Landing-column';
+
+/**
+ * The class name added to specify size of activity icons.
+ */
+const LANDING_ICON_CLASS = 'jp-Landing-image';
+
+/**
+ * The class name added to the image text of an activity.
+ */
+const LANDING_TEXT_CLASS = 'jp-Landing-text';
+
+/**
+ * The class name added to the current working directory.
+ */
+const LANDING_CWD_CLASS = 'jp-Landing-cwd';
+
+/**
+ * The class name added to Landing folder node.
+ */
+const FOLDER_CLASS = 'jp-Landing-folder';
+
+/**
+ * The class name added for the folder icon from default-theme.
+ */
+const FOLDER_ICON_CLASS = 'jp-FolderIcon';
+
+/**
+ * The class name added to the current working directory path.
+ */
+const LANDING_PATH_CLASS = 'jp-Landing-path';
+
+/**
+ * The list of preview messages.
+ */
+const previewMessages = [
+  'super alpha preview',
+  'very alpha preview',
+  'extremely alpha preview',
+  'exceedingly alpha preview',
+  'alpha alpha preview'
+];
+
+
+/**
+ * LandingModel keeps track of the path to working directory and has text data,
+ * which the LandingWidget will render.
+ */
+export
+class LandingModel extends VDomModel {
+  /**
+   * Preview messages.
+   */
+  readonly previewMessage: string;
+
+  /**
+   * The `Start a new activity` text.
+   */
+  readonly headerText: string;
+
+  /**
+   * The names of activities and their associated commands.
+   */
+  readonly activities: string[][];
+
+  /**
+   * Construct a new landing model.
+   */
+  constructor(terminalsAvailable = false) {
+    super();
+    this.previewMessage = previewMessages[
+      Math.floor(Math.random() * previewMessages.length)
+    ];
+    this.headerText = 'Start a new activity';
+    this.activities =
+    [['Notebook', 'file-operations:new-notebook'],
+     ['Code Console', `console:create`],
+     ['Text Editor', 'file-operations:new-text-file']];
+
+    if (terminalsAvailable) {
+      this.activities.push(
+        ['Terminal', 'terminal:create-new']
+      );
+    }
+    this._path = 'home';
+  }
+
+  /**
+   * Get the path of the current working directory.
+   */
+  get path(): string {
+    return this._path;
+  }
+
+  /**
+   * Set the path of the current working directory.
+   */
+  set path(value: string) {
+    this._path = value;
+    this.stateChanged.emit(void 0);
+  }
+
+  private _path: string;
+}
+
+/**
+ * A virtual-DOM-based widget for the Landing plugin.
+ */
+export
+class LandingWidget extends VDomWidget<LandingModel> {
+  /**
+   * Construct a new landing widget.
+   */
+  constructor(app: JupyterLab) {
+    super();
+    this._app = app;
+  }
+
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
+    this.node.focus();
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onCloseRequest(msg: Message): void {
+    super.onCloseRequest(msg);
+    this.dispose();
+  }
+
+  /**
+   * Render the landing plugin to virtual DOM nodes.
+   */
+  protected render(): VNode {
+    let activitiesList: VNode[] = [];
+    let activites = this.model.activities;
+    for (let activityName of activites) {
+      let imgName = activityName[0].replace(' ', '');
+      let column =
+      h.div({className: LANDING_COLUMN_CLASS},
+        h.span({className: LANDING_ICON_CLASS + ` jp-Image${imgName}` ,
+                onclick: () => {
+                  this._app.commands.execute(activityName[1], void 0);
+                }}
+        ),
+        h.span({className: LANDING_TEXT_CLASS}, activityName[0])
+      );
+      activitiesList.push(column);
+    }
+
+    let logo = h.span({className: JUPYTERLAB_ICON_CLASS + ' ' + LANDING_LOGO_CLASS});
+    let subtitle =
+    h.span({className: LANDING_SUBTITLE_CLASS},
+      this.model.previewMessage
+    );
+    let tour =
+    h.span({className: TOUR_ICON_CLASS,
+      onclick: () => {
+        this._app.commands.execute('about-jupyterlab:show', void 0);
+      }}
+    );
+    let header = h.span({
+      className: LANDING_HEADER_CLASS
+    }, this.model.headerText);
+    let body = h.div({className: LANDING_BODY_CLASS}, activitiesList);
+
+    let dialog = h.div({className: LANDING_DIALOG_CLASS},
+      logo,
+      subtitle,
+      tour,
+      header,
+      body,
+      h.div({className: LANDING_CWD_CLASS},
+        h.span({className: FOLDER_ICON_CLASS + ' ' + FOLDER_CLASS}),
+        h.span({className: LANDING_PATH_CLASS}, this.model.path
+        )
+      )
+    );
+    return dialog;
+  }
+
+  private _app: JupyterLab;
+}

+ 8 - 0
src/layoutrestorer/index.ts

@@ -0,0 +1,8 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+export {
+  ILayoutRestorer
+} from './layoutrestorer';

+ 260 - 0
src/layoutrestorer/layoutrestorer.ts

@@ -0,0 +1,260 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import {
+  utils
+} from '@jupyterlab/services';
+
+import {
+  AttachedProperty
+} from 'phosphor/lib/core/properties';
+
+import {
+  defineSignal, ISignal
+} from 'phosphor/lib/core/signaling';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  Token
+} from 'phosphor/lib/core/token';
+
+import {
+  IStateDB
+} from '../statedb';
+
+
+/* tslint:disable */
+/**
+ * The layout restorer token.
+ */
+export
+const ILayoutRestorer = new Token<ILayoutRestorer>('jupyter.services.layout-restorer');
+/* tslint:enable */
+
+
+/**
+ * A static class that restores the layout of the application when it reloads.
+ */
+export
+interface ILayoutRestorer {
+  /**
+   * A promise resolved when the layout restorer is ready to receive signals.
+   */
+  restored: Promise<void>;
+
+  /**
+   * Add a widget to be tracked by the layout restorer.
+   */
+  add(widget: Widget, name: string): void;
+
+  /**
+   * Wait for the given promise to resolve before restoring layout.
+   *
+   * #### Notes
+   * This function should only be called before the `first` promise passed in
+   * at instantiation has resolved. See the notes for `LayoutRestorer.IOptions`.
+   */
+  await(promise: Promise<any>): void;
+}
+
+
+/**
+ * The state database key for restorer data.
+ */
+const KEY = 'layout-restorer:data';
+
+
+/**
+ * The default implementation of a layout restorer.
+ *
+ * #### Notes
+ * The layout restorer requires all of the tabs that will be rearranged and
+ * focused to already exist, it does not rehydrate them.
+ */
+export
+class LayoutRestorer implements ILayoutRestorer {
+  /**
+   * Create a layout restorer.
+   */
+  constructor(options: LayoutRestorer.IOptions) {
+    this._state = options.state;
+    options.first.then(() => Promise.all(this._promises))
+      .then(() => {
+        // Release the promises held in memory.
+        this._promises = null;
+        // Restore the application state.
+        return this._restore();
+      })
+      .then(() => { this._restored.resolve(void 0); });
+  }
+
+  /**
+   * A signal emitted when a widget should be activated.
+   */
+  readonly activated: ISignal<this, string>;
+
+  /**
+   * A promise resolved when the layout restorer is ready to receive signals.
+   */
+  get restored(): Promise<void> {
+    return this._restored.promise;
+  }
+
+  /**
+   * Add a widget to be tracked by the layout restorer.
+   */
+  add(widget: Widget, name: string): void {
+    Private.nameProperty.set(widget, name);
+    this._widgets.set(name, widget);
+    widget.disposed.connect(() => { this._widgets.delete(name); });
+  }
+
+  /**
+   * Wait for the given promise to resolve before restoring layout.
+   *
+   * #### Notes
+   * This function should only be called before the `first` promise passed in
+   * at instantiation has resolved. See the notes for `LayoutRestorer.IOptions`.
+   */
+  await(promise: Promise<any>): void {
+    if (!this._promises) {
+      console.warn('await can only be called before app has started.');
+      return;
+    }
+
+    this._promises.push(promise);
+  }
+
+  /**
+   * Save the layout state for the application.
+   */
+  save(data: LayoutRestorer.IRestorable): Promise<void> {
+    // If there are promises that are unresolved, bail.
+    if (this._promises) {
+      return Promise.resolve(void 0);
+    }
+    let promise: Promise<void>;
+    if (data.currentWidget) {
+      let name = Private.nameProperty.get(data.currentWidget);
+      if (name) {
+        promise = this._state.save(KEY, { currentWidget: name });
+      }
+    }
+    return promise || this._state.remove(KEY);
+  }
+
+  /**
+   * Restore the application state.
+   */
+  private _restore(): Promise<void> {
+    return this._state.fetch(KEY).then(data => {
+      if (!data) {
+        return;
+      }
+
+      let name = data['currentWidget'] as string;
+      if (!name) {
+        return;
+      }
+
+      let widget = this._widgets.get(name);
+      if (widget) {
+        this.activated.emit(widget.id);
+      }
+    });
+  }
+
+  private _promises: Promise<any>[] = [];
+  private _restored = new utils.PromiseDelegate<void>();
+  private _state: IStateDB = null;
+  private _widgets = new Map<string, Widget>();
+}
+
+
+// Define the signals for the `LayoutRestorer` class.
+defineSignal(LayoutRestorer.prototype, 'activated');
+
+
+/**
+ * A namespace for `LayoutRestorer` statics.
+ */
+export
+namespace LayoutRestorer {
+  /**
+   * The configuration options for layout restorer instantiation.
+   */
+  export
+  interface IOptions {
+    /**
+     * The initial promise that has to be resolved before layout restoration.
+     *
+     * #### Notes
+     * The lifecycle for state and layout restoration is subtle. This promise
+     * is intended to equal the JupyterLab application `started` notifier.
+     * The sequence of events is as follows:
+     *
+     * 1. The layout restorer plugin is instantiated.
+     *
+     * 2. Other plugins that care about state and layout restoration require
+     *    the layout restorer as a dependency.
+     *
+     * 3. As each load-time plugin initializes (which happens before the lab
+     *    application has `started`), it instructs the layout restorer whether
+     *    the restorer ought to `await` its state restoration.
+     *
+     * 4. After all the load-time plugins have finished initializing, the lab
+     *    application `started` promise will resolve. This is the `first`
+     *    promise that the layout restorer waits for. By this point, all of the
+     *    plugins that care about layout restoration will have instructed the
+     *    layout restorer to `await` their restoration.
+     *
+     * 5. Each plugin will then proceed to restore its state and reinstantiate
+     *    whichever widgets it wants to restore.
+     *
+     * 6. As each plugin finishes restoring, it resolves the promise that it
+     *    instructed the layout restorer to `await` (in step 3).
+     *
+     * 7. After all of the promises that the restorer is awaiting have resolved,
+     *    the restorer then proceeds to reconstruct the saved layout.
+     *
+     * Of particular note are steps 5 and 6: since state restoration of plugins
+     * is accomplished by executing commands, the command that is used to
+     * restore the state of each plugin must return a promise that only resolves
+     * when the widget has been created and added to the plugin's instance
+     * tracker.
+     */
+    first: Promise<any>;
+
+    /**
+     * The state database instance.
+     */
+    state: IStateDB;
+  }
+
+  /**
+   * A restorable user interface.
+   */
+  export
+  interface IRestorable {
+    /**
+     * The current widget that has application focus.
+     */
+    currentWidget: Widget;
+  }
+}
+
+/*
+ * A namespace for private data.
+ */
+namespace Private {
+  /**
+   * An attached property for a widget's ID in the state database.
+   */
+  export
+  const nameProperty = new AttachedProperty<Widget, string>({ name: 'name' });
+}

+ 42 - 0
src/layoutrestorer/plugin.ts

@@ -0,0 +1,42 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import {
+  JupyterLab, JupyterLabPlugin
+} from '../application';
+
+import {
+  IStateDB
+} from '../statedb';
+
+import {
+  ILayoutRestorer, LayoutRestorer
+} from './layoutrestorer';
+
+
+/**
+ * The default layout restorer provider.
+ */
+export
+const layoutRestorerProvider: JupyterLabPlugin<ILayoutRestorer> = {
+  id: 'jupyter.services.layout-restorer',
+  requires: [IStateDB],
+  activate: (app: JupyterLab, state: IStateDB) => {
+    let layout = new LayoutRestorer({ first: app.started, state });
+    // Activate widgets that have been restored if necessary.
+    layout.activated.connect((sender, id) => {
+      app.shell.activateMain(id);
+    });
+    // After restoration is complete, listen to the shell for updates.
+    layout.restored.then(() => {
+      app.shell.currentChanged.connect((sender, args) => {
+        layout.save({ currentWidget: args.newValue });
+      });
+    });
+    return layout;
+  },
+  autoStart: true,
+  provides: ILayoutRestorer
+};

+ 1 - 1
src/markdownwidget/index.css

@@ -3,9 +3,9 @@
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
-
 .jp-MarkdownWidget {
   border-top: var(--jp-border-width) solid var(--jp-border-color2);
   padding: 14px;
+  outline: none;
   overflow: auto;
 }

+ 7 - 3
src/markdownwidget/plugin.ts

@@ -13,6 +13,10 @@ import {
   IDocumentRegistry
 } from '../docregistry';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IRenderMime
 } from '../rendermime';
@@ -48,8 +52,8 @@ const FACTORY = 'Rendered Markdown';
 export
 const markdownHandlerExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.rendered-markdown',
-  requires: [IDocumentRegistry, IRenderMime, IStateDB],
-  activate: (app: JupyterLab, registry: IDocumentRegistry, rendermime: IRenderMime, state: IStateDB) => {
+  requires: [IDocumentRegistry, IRenderMime, IStateDB, ILayoutRestorer],
+  activate: (app: JupyterLab, registry: IDocumentRegistry, rendermime: IRenderMime, state: IStateDB, layout: ILayoutRestorer) => {
     const factory = new MarkdownWidgetFactory({
       name: FACTORY,
       fileExtensions: ['.md'],
@@ -58,7 +62,7 @@ const markdownHandlerExtension: JupyterLabPlugin<void> = {
 
     const tracker = new InstanceTracker<MarkdownWidget>({
       restore: {
-        state,
+        state, layout,
         command: 'file-operations:open',
         args: widget => ({ path: widget.context.path, factory: FACTORY }),
         name: widget => widget.context.path,

+ 8 - 4
src/markdownwidget/widget.ts

@@ -1,10 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  Kernel
-} from '@jupyterlab/services';
-
 import {
   Message
 } from 'phosphor/lib/core/messaging';
@@ -85,6 +81,14 @@ class MarkdownWidget extends Widget {
     super.dispose();
   }
 
+  /**
+   * Handle `'activate-request'` messages.
+   */
+  protected onActivateRequest(msg: Message): void {
+    this.node.tabIndex = -1;
+    this.node.focus();
+  }
+
   /**
    * Handle an `after-attach` message to the widget.
    */

+ 9 - 4
src/notebook/plugin.ts

@@ -30,6 +30,10 @@ import {
   IInspector
 } from '../inspector';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IRenderMime
 } from '../rendermime';
@@ -126,7 +130,8 @@ const notebookTrackerProvider: JupyterLabPlugin<INotebookTracker> = {
     ICommandPalette,
     IInspector,
     NotebookPanel.IRenderer,
-    IStateDB
+    IStateDB,
+    ILayoutRestorer
   ],
   activate: activateNotebookHandler,
   autoStart: true
@@ -136,7 +141,7 @@ const notebookTrackerProvider: JupyterLabPlugin<INotebookTracker> = {
 /**
  * Activate the notebook handler extension.
  */
-function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, services: IServiceManager, rendermime: IRenderMime, clipboard: IClipboard, mainMenu: IMainMenu, palette: ICommandPalette, inspector: IInspector, renderer: NotebookPanel.IRenderer, state: IStateDB): INotebookTracker {
+function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, services: IServiceManager, rendermime: IRenderMime, clipboard: IClipboard, mainMenu: IMainMenu, palette: ICommandPalette, inspector: IInspector, renderer: NotebookPanel.IRenderer, state: IStateDB, layout: ILayoutRestorer): INotebookTracker {
   const factory = new NotebookWidgetFactory({
     name: FACTORY,
     fileExtensions: ['.ipynb'],
@@ -151,11 +156,11 @@ function activateNotebookHandler(app: JupyterLab, registry: IDocumentRegistry, s
 
   const tracker = new NotebookTracker({
     restore: {
-      state,
+      state, layout,
       command: 'file-operations:open',
       args: widget => ({ path: widget.context.path, factory: FACTORY }),
       name: widget => widget.context.path,
-      namespace: 'notebooks',
+      namespace: 'notebook',
       when: [app.started, services.ready],
       registry: app.commands
     }

+ 19 - 4
src/statedb/plugin.ts

@@ -6,9 +6,13 @@ import {
 } from 'phosphor/lib/algorithm/json';
 
 import {
-  JupyterLabPlugin
+  JupyterLab, JupyterLabPlugin
 } from '../application';
 
+import {
+  ICommandPalette
+} from '../commandpalette';
+
 import {
   IStateDB
 } from './index';
@@ -26,16 +30,19 @@ const stateProvider: JupyterLabPlugin<IStateDB> = {
   id: 'jupyter.services.statedb',
   activate: activateState,
   autoStart: true,
-  provides: IStateDB
+  provides: IStateDB,
+  requires: [ICommandPalette]
 };
 
 
 /**
  * Activate the state database.
  */
-function activateState(): Promise<IStateDB> {
+function activateState(app: JupyterLab, palette: ICommandPalette): Promise<IStateDB> {
   let state = new StateDB();
   let version = (window as any).jupyter.version;
+  let command = 'statedb:clear';
+  let category = 'Help';
   let key = 'statedb:version';
   let fetch = state.fetch(key);
   let save = () => state.save(key, { version });
@@ -43,9 +50,17 @@ function activateState(): Promise<IStateDB> {
   let check = (value: JSONObject) => {
     let old = value && (value as any).version;
     if (!old || old !== version) {
-      console.log(`Upgraded: ${old || 'unknown'} to ${version}. Resetting DB.`);
+      console.log(`Upgraded: ${old || 'unknown'} to ${version}; Resetting DB.`);
       return reset();
     }
   };
+
+  app.commands.addCommand(command, {
+    label: 'Clear Application Restore State',
+    execute: () => state.clear()
+  });
+
+  palette.addItem({ command, category });
+
   return fetch.then(check, reset).then(() => state);
 }

+ 13 - 7
src/terminal/plugin.ts

@@ -21,6 +21,10 @@ import {
   ICommandPalette
 } from '../commandpalette';
 
+import {
+  ILayoutRestorer
+} from '../layoutrestorer';
+
 import {
   IMainMenu
 } from '../mainmenu';
@@ -55,13 +59,15 @@ const TERMINAL_ICON_CLASS = 'jp-ImageTerminal';
 export
 const terminalExtension: JupyterLabPlugin<void> = {
   id: 'jupyter.extensions.terminal',
-  requires: [IServiceManager, IMainMenu, ICommandPalette, IStateDB],
+  requires: [
+    IServiceManager, IMainMenu, ICommandPalette, IStateDB, ILayoutRestorer
+  ],
   activate: activateTerminal,
   autoStart: true
 };
 
 
-function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB): void {
+function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB, layout: ILayoutRestorer): void {
   // Bail if there are no terminals available.
   if (!services.terminals.isAvailable()) {
     console.log('Disabling terminals plugin because they are not available on the server');
@@ -82,11 +88,11 @@ function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu:
   // Create an instance tracker for all terminal widgets.
   const tracker = new InstanceTracker<TerminalWidget>({
     restore: {
-      state,
-      command: 'terminal:create-new',
+      state, layout,
+      command: newTerminalId,
       args: widget => ({ name: widget.session.name }),
       name: widget => widget.session && widget.session.name,
-      namespace: 'terminals',
+      namespace: 'terminal',
       when: app.started,
       registry: app.commands
     }
@@ -109,8 +115,8 @@ function activateTerminal(app: JupyterLab, services: IServiceManager, mainMenu:
       } else {
         promise = services.terminals.startNew();
       }
-      promise.then(session => {
-        session.ready.then(() => {
+      return promise.then(session => {
+        return session.ready.then(() => {
           let term = new TerminalWidget(options);
           term.session = session;
           term.title.closable = true;