Browse Source

Remove the document wrapper in favor of a private document widget manager

Steven Silvester 8 years ago
parent
commit
0f0de6ca21

+ 93 - 13
src/docmanager/context.ts

@@ -54,8 +54,15 @@ class Context implements IDocumentContext<IDocumentModel> {
   /**
    * A signal emitted when the model is saved or reverted.
    */
-  get dirtyCleared(): ISignal<Context, void> {
-    return Private.dirtyClearedSignal.bind(this);
+  get contentsModelChanged(): ISignal<Context, IContentsModel> {
+    return Private.contentsModelChangedSignal.bind(this);
+  }
+
+  /**
+   * A signal emitted when the context is fully populated for the first time.
+   */
+  get populated(): ISignal<IDocumentContext<IDocumentModel>, void> {
+    return Private.populatedSignal.bind(this);
   }
 
   /**
@@ -120,7 +127,17 @@ class Context implements IDocumentContext<IDocumentModel> {
   }
 
   /**
-   * Get whether the context has been disposed.
+   * Test whether the context is fully populated.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get isPopulated(): boolean {
+    return this._manager.isPopulated(this._id);
+  }
+
+  /**
+   * Test whether the context has been disposed.
    */
   get isDisposed(): boolean {
     return this._manager === null;
@@ -200,11 +217,11 @@ class ContextManager implements IDisposable {
   /**
    * Construct a new context manager.
    */
-  constructor(contentsManager: IContentsManager, sessionManager: ISession.IManager,  kernelspecs: IKernel.ISpecModels, opener: (id: string, widget: Widget) => IDisposable) {
-    this._contentsManager = contentsManager;
-    this._sessionManager = sessionManager;
-    this._opener = opener;
-    this._kernelspecids = kernelspecs;
+  constructor(options: ContextManager.IOptions) {
+    this._contentsManager = options.contentsManager;
+    this._sessionManager = options.sessionManager;
+    this._opener = options.opener;
+    this._kernelspecids = options.kernelspecs;
   }
 
   /**
@@ -250,7 +267,8 @@ class ContextManager implements IDisposable {
       modelName: factory.name,
       opts: factory.contentsOptions,
       contentsModel: null,
-      session: null
+      session: null,
+      isPopulated: false
     };
     return id;
   }
@@ -368,7 +386,7 @@ class ContextManager implements IDisposable {
    *
    * @param newPath - The new path.
    */
-  rename(oldPath: string, newPath: string): void {
+  handleRename(oldPath: string, newPath: string): void {
     // Update all of the paths, but only update one session
     // so there is only one REST API call.
     let ids = this.getIdsForPath(oldPath);
@@ -451,11 +469,34 @@ class ContextManager implements IDisposable {
       } else {
         model.fromString(contents.content);
       }
-      contextEx.contentsModel = this._copyContentsModel(contents);
+      let contentsModel = this._copyContentsModel(contents);
+      // TODO: use deepEqual to check for equality
+      contextEx.contentsModel = contentsModel;
+      contextEx.context.contentsModelChanged.emit(contentsModel);
       model.dirty = false;
     });
   }
 
+  /**
+   * Test whether the context is fully populated.
+   */
+  isPopulated(id: string): boolean {
+    let contextEx = this._contexts[id];
+    return contextEx.isPopulated;
+  }
+
+  /**
+   * Finalize a context.
+   */
+  finalize(id: string): void {
+    let contextEx = this._contexts[id];
+    if (contextEx.isPopulated) {
+      return;
+    }
+    contextEx.isPopulated = true;
+    this._contexts[id].context.populated.emit(void 0);
+  }
+
   /**
    * Get the list of running sessions.
    */
@@ -520,6 +561,38 @@ class ContextManager implements IDisposable {
 }
 
 
+/**
+ * A namespace for ContextManager statics.
+ */
+export namespace ContextManager {
+  /**
+   * The options used to initialize a context manager.
+   */
+  export
+  interface IOptions {
+    /**
+     * A contents manager instance.
+     */
+    contentsManager: IContentsManager;
+
+    /**
+     * A session manager instance.
+     */
+    sessionManager: ISession.IManager;
+
+    /**
+     * The system kernelspec information.
+     */
+    kernelspecs: IKernel.ISpecModels;
+
+    /**
+     * A callback for opening sibling widgets.
+     */
+    opener: (id: string, widget: Widget) => IDisposable;
+  }
+}
+
+
 /**
  * A namespace for private data.
  */
@@ -536,6 +609,7 @@ namespace Private {
     path: string;
     contentsModel: IContentsModel;
     modelName: string;
+    isPopulated: boolean;
   }
 
   /**
@@ -551,8 +625,14 @@ namespace Private {
   const pathChangedSignal = new Signal<Context, string>();
 
   /**
-   * A signal emitted when the model is saved or reverted.
+   * A signal emitted when the contentsModel changes.
+   */
+  export
+  const contentsModelChangedSignal = new Signal<Context, IContentsModel>();
+
+  /**
+   * A signal emitted when the context is fully populated for the first time.
    */
   export
-  const dirtyClearedSignal = new Signal<Context, void>();
+  const populatedSignal = new Signal<Context, void>();
 }

+ 82 - 305
src/docmanager/manager.ts

@@ -6,47 +6,29 @@ import {
 } from 'jupyter-js-services';
 
 import {
-  IDisposable, DisposableDelegate, DisposableSet
+  IDisposable, DisposableDelegate
 } from 'phosphor-disposable';
 
-import {
-  Message
-} from 'phosphor-messaging';
-
-import {
-  PanelLayout
-} from 'phosphor-panel';
-
-import {
-  ISignal, Signal
-} from 'phosphor-signaling';
-
 import {
   Widget
 } from 'phosphor-widget';
 
-import {
-  showDialog
-} from '../dialog';
-
 import {
   IWidgetOpener
 } from '../filebrowser/browser';
 
 import {
-  DocumentRegistry, IDocumentContext, IWidgetFactory, IWidgetFactoryOptions,
-  IDocumentModel
+  DocumentRegistry, IWidgetFactory, IWidgetFactoryOptions,
+  IDocumentModel, IDocumentContext
 } from '../docregistry';
 
 import {
   ContextManager
 } from './context';
 
-
-/**
- * The class name added to a document wrapper widgets.
- */
-const DOCUMENT_CLASS = 'jp-DocumentWrapper';
+import {
+  DocumentWidgetManager
+} from './widgetmanager';
 
 
 /**
@@ -64,18 +46,27 @@ class DocumentManager implements IDisposable {
   /**
    * Construct a new document manager.
    */
-  constructor(registry: DocumentRegistry, contentsManager: IContentsManager, sessionManager: ISession.IManager, kernelspecs: IKernel.ISpecModels, opener: IWidgetOpener) {
-    this._registry = registry;
-    this._contentsManager = contentsManager;
-    this._sessionManager = sessionManager;
-    this._specs = kernelspecs;
-    this._contextManager = new ContextManager(contentsManager, sessionManager, kernelspecs, (id: string, widget: Widget) => {
-      let parent = this._createWidget('', id);
-      parent.setContent(widget);
-      opener.open(parent);
-      return new DisposableDelegate(() => {
-        parent.close();
-      });
+  constructor(options: DocumentManager.IOptions) {
+    this._registry = options.registry;
+    this._contentsManager = options.contentsManager;
+    this._sessionManager = options.sessionManager;
+    this._specs = options.kernelspecs;
+    let opener = options.opener;
+    this._contextManager = new ContextManager({
+      contentsManager: this._contentsManager,
+      sessionManager: this._sessionManager,
+      kernelspecs: this._specs,
+      opener: (id: string, widget: Widget) => {
+        this._widgetManager.adoptWidget(id, widget);
+        opener.open(widget);
+        return new DisposableDelegate(() => {
+          widget.close();
+        });
+      }
+    });
+    this._widgetManager = new DocumentWidgetManager({
+      contextManager: this._contextManager,
+      registry: this._registry
     });
   }
 
@@ -113,16 +104,12 @@ class DocumentManager implements IDisposable {
     if (this.isDisposed) {
       return;
     }
-    for (let id in this._widgets) {
-      for (let widget of this._widgets[id]) {
-        widget.dispose();
-      }
-    }
-    this._widgets = null;
     this._contentsManager = null;
     this._sessionManager = null;
     this._contextManager.dispose();
     this._contextManager = null;
+    this._widgetManager.dispose();
+    this._widgetManager = null;
   }
 
   /**
@@ -134,7 +121,7 @@ class DocumentManager implements IDisposable {
    *
    * @param kernel - An optional kernel name/id to override the default.
    */
-  open(path: string, widgetName='default', kernel?: IKernel.IModel): DocumentWrapper {
+  open(path: string, widgetName='default', kernel?: IKernel.IModel): Widget {
     let registry = this._registry;
     if (widgetName === 'default') {
       let parts = path.split('.');
@@ -150,23 +137,20 @@ class DocumentManager implements IDisposable {
     if (!mFactory) {
       return;
     }
-    let widget: DocumentWrapper;
     // Use an existing context if available.
     let id = this._contextManager.findContext(path, mFactory.name);
     if (id) {
-      widget = this._createWidget(widgetName, id);
-      this._populateWidget(widget, kernel);
-      return widget;
+      return this._widgetManager.createWidget(widgetName, id, kernel);
     }
     let lang = mFactory.preferredLanguage(path);
     let model = mFactory.createNew(lang);
     id = this._contextManager.createNew(path, model, mFactory);
-    widget = this._createWidget(widgetName, id);
     // Load the contents from disk.
     this._contextManager.revert(id).then(() => {
-      this._populateWidget(widget, kernel);
+      model.dirty = false;
+      this._contextManager.finalize(id);
     });
-    return widget;
+    return this._widgetManager.createWidget(widgetName, id, kernel);
   }
 
   /**
@@ -178,7 +162,7 @@ class DocumentManager implements IDisposable {
    *
    * @param kernel - An optional kernel name/id to override the default.
    */
-  createNew(path: string, widgetName='default', kernel?: IKernel.IModel): DocumentWrapper {
+  createNew(path: string, widgetName='default', kernel?: IKernel.IModel): Widget {
     let registry = this._registry;
     if (widgetName === 'default') {
       let parts = path.split('.');
@@ -197,13 +181,11 @@ class DocumentManager implements IDisposable {
     let lang = mFactory.preferredLanguage(path);
     let model = mFactory.createNew(lang);
     let id = this._contextManager.createNew(path, model, mFactory);
-    let widget = this._createWidget(widgetName, id);
-    // Save the contents to disk to get a valid contentsModel for the
-    // context.
     this._contextManager.save(id).then(() => {
-      this._populateWidget(widget, kernel);
+      model.dirty = false;
+      this._contextManager.finalize(id);
     });
-    return widget;
+    return this._widgetManager.createWidget(widgetName, id, kernel);
   }
 
   /**
@@ -221,7 +203,7 @@ class DocumentManager implements IDisposable {
    * @param newPath - The new path.
    */
   handleRename(oldPath: string, newPath: string): void {
-    this._contextManager.rename(oldPath, newPath);
+    this._contextManager.handleRename(oldPath, newPath);
   }
 
   /**
@@ -238,18 +220,18 @@ class DocumentManager implements IDisposable {
    * This can be used to use an existing widget instead of opening
    * a new widget.
    */
-  findWidget(path: string, widgetName='default'): DocumentWrapper {
-    let ids = this._contextManager.getIdsForPath(path);
+  findWidget(path: string, widgetName='default'): Widget {
     if (widgetName === 'default') {
       widgetName = this._registry.defaultWidgetFactory;
     }
-    for (let id of ids) {
-      for (let widget of this._widgets[id]) {
-        if (widget.name === widgetName) {
-          return widget;
-        }
-      }
-    }
+    return this._widgetManager.findWidget(path, widgetName);
+  }
+
+  /**
+   * Get the document context for a widget.
+   */
+  contextForWidget(widget: Widget): IDocumentContext<IDocumentModel> {
+    return this._widgetManager.contextForWidget(widget);
   }
 
   /**
@@ -259,267 +241,68 @@ class DocumentManager implements IDisposable {
    * This will create a new widget with the same model and context
    * as this widget.
    */
-  clone(widget: DocumentWrapper): DocumentWrapper {
-    let parent = this._createWidget(widget.name, widget.context.id);
-    this._populateWidget(parent);
-    return parent;
+  clone(widget: Widget): Widget {
+    return this._widgetManager.clone(widget);
   }
 
   /**
    * Close the widgets associated with a given path.
    */
   closeFile(path: string): void {
-    let ids = this._contextManager.getIdsForPath(path);
-    for (let id of ids) {
-      let widgets: DocumentWrapper[] = this._widgets[id] || [];
-      for (let w of widgets) {
-        w.close();
-      }
-    }
+    this._widgetManager.closeFile(path);
   }
 
   /**
    * Close all of the open documents.
    */
   closeAll(): void {
-    for (let id in this._widgets) {
-      for (let w of this._widgets[id]) {
-        w.close();
-      }
-    }
+    this._widgetManager.closeAll();
   }
 
-  /**
-   * Create a container widget and handle its lifecycle.
-   */
-  private _createWidget(name: string, id: string): DocumentWrapper {
-    let factory = this._registry.getWidgetFactory(name);
-    let widget = new DocumentWrapper(name, id, this._contextManager, factory, this._widgets);
-    if (!(id in this._widgets)) {
-      this._widgets[id] = [];
-    }
-    this._widgets[id].push(widget);
-    return widget;
-  }
-
-  /**
-   * Create a content widget and add it to the container widget.
-   */
-  private _populateWidget(parent: DocumentWrapper, kernel?: IKernel.IModel): void {
-    let factory = this._registry.getWidgetFactory(parent.name);
-    let id = parent.context.id;
-    let model = this._contextManager.getModel(id);
-    model.initialize();
-    let context = this._contextManager.getContext(id);
-    let child = factory.createNew(context, kernel);
-    parent.setContent(child);
-    // Handle widget extensions.
-    let disposables = new DisposableSet();
-    for (let extender of this._registry.getWidgetExtensions(parent.name)) {
-      disposables.add(extender.createNew(child, context));
-    }
-    parent.disposed.connect(() => {
-      disposables.dispose();
-    });
-  }
-
-  private _widgets: { [key: string]: DocumentWrapper[] } = Object.create(null);
   private _contentsManager: IContentsManager = null;
   private _sessionManager: ISession.IManager = null;
   private _contextManager: ContextManager = null;
+  private _widgetManager: DocumentWidgetManager = null;
   private _specs: IKernel.ISpecModels = null;
   private _registry: DocumentRegistry = null;
 }
 
 
 /**
- * A container widget for documents.
+ * A namespace for document manager statics.
  */
 export
-class DocumentWrapper extends Widget {
-  /**
-   * A signal emitted when the document widget is populated.
-   */
-  get populated(): ISignal<DocumentWrapper, Widget> {
-    return Private.populatedSignal.bind(this);
-  }
-
-  /**
-   * Construct a new document widget.
-   */
-  constructor(name: string, id: string, manager: ContextManager, factory: IWidgetFactory<Widget, IDocumentModel>, widgets: { [key: string]: DocumentWrapper[] }) {
-    super();
-    this.addClass(DOCUMENT_CLASS);
-    this.layout = new PanelLayout();
-    this._name = name;
-    this._id = id;
-    this._manager = manager;
-    this._widgets = widgets;
-    this._factory = factory;
-    this.title.closable = true;
-  }
-
-  /**
-   * Get the name of the widget.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get name(): string {
-    return this._name;
-  }
-
-  /**
-   * The context for the widget.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get context(): IDocumentContext<IDocumentModel> {
-    return this._manager.getContext(this._id);
-  }
-
-  /**
-   * The content widget used by the document widget.
-   */
-  get content(): Widget {
-    let layout = this.layout as PanelLayout;
-    return layout.childAt(0);
-  }
-
-  /**
-   * Dispose of the resources held by the widget.
-   */
-  dispose(): void {
-    if (this.isDisposed) {
-      return;
-    }
-    // Remove the widget from the widget registry.
-    let id = this._id;
-    let index = this._widgets[id].indexOf(this);
-    this._widgets[id].splice(index, 1);
-    // Dispose of the context if this is the last widget using it.
-    if (!this._widgets[id].length) {
-      this._manager.removeContext(id);
-    }
-    this._manager = null;
-    this._factory = null;
-    this._widgets = null;
-    super.dispose();
-  }
-
-  /**
-   * Set the child the widget.
-   *
-   * #### Notes
-   * This function is not intended to be called by user code.
-   */
-  setContent(child: Widget): void {
-    let layout = this.layout as PanelLayout;
-    if (layout.childAt(0)) {
-      throw new Error('Content already set');
-    }
-    this.title.text = child.title.text;
-    this.title.icon = child.title.icon;
-    this.title.className = child.title.className;
-    // Mirror this title based on the child.
-    child.title.changed.connect(() => {
-      this.title.text = child.title.text;
-      this.title.icon = child.title.icon;
-      this.title.className = child.title.className;
-    });
-    // Add the child widget to the layout.
-    (this.layout as PanelLayout).addChild(child);
-    this.populated.emit(child);
-  }
-
+namespace DocumentManager {
   /**
-   * Handle `'close-request'` messages.
+   * The options used to initialize a document manager.
    */
-  protected onCloseRequest(msg: Message): void {
-    let model = this._manager.getModel(this._id);
-    let layout = this.layout as PanelLayout;
-    let child = layout.childAt(0);
-    // Handle dirty state.
-    this._maybeClose(model.dirty).then(result => {
-      if (result) {
-        // Let the widget factory handle closing.
-        return this._factory.beforeClose(child, this.context);
-      }
-      return result;
-    }).then(result => {
-      if (result) {
-        // Perform close tasks.
-        return this._actuallyClose();
-      }
-      return result;
-    }).then(result => {
-      if (result) {
-        // Dispose of document widgets when they are closed.
-        this.dispose();
-      }
-    }).catch(() => {
-      this.dispose();
-    });
-  }
-
-  /**
-   * Ask the user whether to close an unsaved file.
-   */
-  private _maybeClose(dirty: boolean): Promise<boolean> {
-    // Bail if the model is not dirty or other widgets are using the model.
-    let widgets = this._widgets[this._id];
-    if (!dirty || widgets.length > 1) {
-      return Promise.resolve(true);
-    }
-    return showDialog({
-      title: 'Close without saving?',
-      body: `File "${this.title.text}" has unsaved changes, close without saving?`,
-      host: this.node
-    }).then(value => {
-      if (value && value.text === 'OK') {
-        return true;
-      }
-      return false;
-    });
-  }
-
-  /**
-   * Perform closing tasks for the widget.
-   */
-  private _actuallyClose(): Promise<boolean> {
-    // Check for a dangling kernel.
-    let widgets = this._widgets[this._id];
-    let kernelId = this.context.kernel ? this.context.kernel.id : '';
-    if (!kernelId || widgets.length > 1) {
-      return Promise.resolve(true);
-    }
-    for (let id in this._widgets) {
-      for (let widget of this._widgets[id]) {
-        let kId = widget.context.kernel || widget.context.kernel.id;
-        if (widget !== this && kId === kernelId) {
-          return Promise.resolve(true);
-        }
-      }
-    }
-    return showDialog({
-      title: 'Shut down kernel?',
-      body: `Shut down ${this.context.kernel.name}?`,
-      host: this.node
-    }).then(value => {
-      if (value && value.text === 'OK') {
-        return this.context.kernel.shutdown();
-      }
-    }).then(() => {
-      return true;
-    });
+  export
+  interface IOptions {
+    /**
+     * A document registry instance.
+     */
+    registry: DocumentRegistry;
+
+    /**
+     * A contents manager instance.
+     */
+    contentsManager: IContentsManager;
+
+    /**
+     * A session manager instance.
+     */
+    sessionManager: ISession.IManager;
+
+    /**
+     * The system kernelspec information.
+     */
+    kernelspecs: IKernel.ISpecModels;
+
+    /**
+     * A widget opener for sibling widgets.
+     */
+    opener: IWidgetOpener;
   }
-
-  private _manager: ContextManager = null;
-  private _factory: IWidgetFactory<Widget, IDocumentModel> = null;
-  private _id = '';
-  private _name = '';
-  private _widgets: { [key: string]: DocumentWrapper[] } = null;
 }
 
 
@@ -527,12 +310,6 @@ class DocumentWrapper extends Widget {
  * A private namespace for DocumentManager data.
  */
 namespace Private {
-  /**
-   * A signal emitted when the document widget is populated.
-   */
-  export
-  const populatedSignal = new Signal<DocumentWrapper, Widget>();
-
   /**
    * An extended interface for a widget factory and its options.
    */

+ 326 - 0
src/docmanager/widgetmanager.ts

@@ -0,0 +1,326 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  IKernel
+} from 'jupyter-js-services';
+
+import {
+  DisposableSet
+} from 'phosphor-disposable';
+
+import {
+  IMessageHandler, Message, installMessageFilter
+} from 'phosphor-messaging';
+
+import {
+  Property
+} from 'phosphor-properties';
+
+import {
+  Widget
+} from 'phosphor-widget';
+
+import {
+  showDialog
+} from '../dialog';
+
+import {
+  DocumentRegistry, IDocumentContext, IDocumentModel
+} from '../docregistry';
+
+import {
+  ContextManager
+} from './context';
+
+
+/**
+ * The class name added to document widgets.
+ */
+const DOCUMENT_CLASS = 'jp-Document';
+
+
+/**
+ * A class that maintains the lifecyle of file-backed widgets.
+ */
+export
+class DocumentWidgetManager {
+  /**
+   * Construct a new document widget manager.
+   */
+  constructor(options: DocumentWidgetManager.IOptions) {
+    this._contextManager = options.contextManager;
+    this._registry = options.registry;
+  }
+
+  /**
+   * Dispose of the resources used by the widget manager.
+   */
+  dispose(): void {
+    this._registry = null;
+    this._contextManager = null;
+    for (let id in this._widgets) {
+      for (let widget of this._widgets[id]) {
+        widget.dispose();
+      }
+    }
+  }
+
+  /**
+   * Create a widget for a document and handle its lifecycle.
+   */
+  createWidget(name: string, id: string, kernel?: IKernel.IModel): Widget {
+    let factory = this._registry.getWidgetFactory(name);
+    let context = this._contextManager.getContext(id);
+    let widget = factory.createNew(context, kernel);
+    Private.nameProperty.set(widget, name);
+
+    // Handle widget extensions.
+    let disposables = new DisposableSet();
+    for (let extender of this._registry.getWidgetExtensions(name)) {
+      disposables.add(extender.createNew(widget, context));
+    }
+    widget.disposed.connect(() => {
+      disposables.dispose();
+    });
+    this.adoptWidget(id, widget);
+    return widget;
+  }
+
+  /**
+   * Install the message filter for the widget and add to list
+   * of known widgets.
+   */
+  adoptWidget(id: string, widget: Widget): void {
+    if (!(id in this._widgets)) {
+      this._widgets[id] = [];
+    }
+    this._widgets[id].push(widget);
+    installMessageFilter(widget, this);
+    widget.addClass(DOCUMENT_CLASS);
+    widget.title.closable = true;
+    widget.disposed.connect(() => {
+      // Remove the widget from the widget registry.
+      let index = this._widgets[id].indexOf(widget);
+      this._widgets[id].splice(index, 1);
+      // Dispose of the context if this is the last widget using it.
+      if (!this._widgets[id].length) {
+        this._contextManager.removeContext(id);
+      }
+    });
+    Private.idProperty.set(widget, id);
+  }
+
+  /**
+   * See if a widget already exists for the given path and widget name.
+   *
+   * #### Notes
+   * This can be used to use an existing widget instead of opening
+   * a new widget.
+   */
+  findWidget(path: string, widgetName: string): Widget {
+    let ids = this._contextManager.getIdsForPath(path);
+    for (let id of ids) {
+      for (let widget of this._widgets[id]) {
+        let name = Private.nameProperty.get(widget);
+        if (name === widgetName) {
+          return widget;
+        }
+      }
+    }
+  }
+
+  /**
+   * Get the document context for a widget.
+   */
+  contextForWidget(widget: Widget): IDocumentContext<IDocumentModel> {
+    let id = Private.idProperty.get(widget);
+    return this._contextManager.getContext(id);
+  }
+
+  /**
+   * Clone a widget.
+   *
+   * #### Notes
+   * This will create a new widget with the same model and context
+   * as this widget.
+   */
+  clone(widget: Widget): Widget {
+    let id = Private.idProperty.get(widget);
+    let name = Private.nameProperty.get(widget);
+    let newWidget = this.createWidget(name, id);
+    this.adoptWidget(id, newWidget);
+    return widget;
+  }
+
+  /**
+   * Close the widgets associated with a given path.
+   */
+  closeFile(path: string): void {
+    let ids = this._contextManager.getIdsForPath(path);
+    for (let id of ids) {
+      let widgets: Widget[] = this._widgets[id] || [];
+      for (let w of widgets) {
+        w.close();
+      }
+    }
+  }
+
+  /**
+   * Close all of the open documents.
+   */
+  closeAll(): void {
+    for (let id in this._widgets) {
+      for (let w of this._widgets[id]) {
+        w.close();
+      }
+    }
+  }
+
+  /**
+   * Filter a message sent to a message handler.
+   *
+   * @param handler - The target handler of the message.
+   *
+   * @param msg - The message dispatched to the handler.
+   *
+   * @returns `true` if the message should be filtered, of `false`
+   *   if the message should be dispatched to the handler as normal.
+   */
+  filterMessage(handler: IMessageHandler, msg: Message): boolean {
+    if (msg.type === 'close-request') {
+      // TODO: allow the original close method to be called eventually.
+      this.onClose(handler as Widget);
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onClose(widget: Widget): void {
+    // Handle dirty state.
+    this._maybeClose(widget).then(result => {
+      if (result) {
+        // Perform close tasks.
+        return this._actuallyClose(widget);
+      }
+      return result;
+    }).then(result => {
+      if (result) {
+        // Dispose of document widgets when they are closed.
+        this.dispose();
+      }
+    }).catch(() => {
+      this.dispose();
+    });
+  }
+
+  /**
+   * Ask the user whether to close an unsaved file.
+   */
+  private _maybeClose(widget: Widget): Promise<boolean> {
+    // Bail if the model is not dirty or other widgets are using the model.
+    let id = Private.idProperty.get(widget);
+    let widgets = this._widgets[id];
+    let model = this._contextManager.getModel(id);
+    if (!model.dirty || widgets.length > 1) {
+      return Promise.resolve(true);
+    }
+    return showDialog({
+      title: 'Close without saving?',
+      body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
+      host: widget.node
+    }).then(value => {
+      if (value && value.text === 'OK') {
+        return true;
+      }
+      return false;
+    });
+  }
+
+  /**
+   * Perform closing tasks for the widget.
+   */
+  private _actuallyClose(widget: Widget): Promise<boolean> {
+    let id = Private.idProperty.get(widget);
+    let context = this._contextManager.getContext(id);
+    // Check for a dangling kernel.
+    let widgets = this._widgets[id];
+    let kernelId = context.kernel ? context.kernel.id : '';
+    if (!kernelId || widgets.length > 1) {
+      return Promise.resolve(true);
+    }
+    for (let otherId in this._widgets) {
+      if (otherId === id) {
+        continue;
+      }
+      let otherContext = this._contextManager.getContext(id);
+      let kId = otherContext.kernel || otherContext.kernel.id;
+      if (kId === kernelId) {
+        return Promise.resolve(true);
+      }
+    }
+    return showDialog({
+      title: 'Shut down kernel?',
+      body: `Shut down ${context.kernel.name}?`,
+      host: widget.node
+    }).then(value => {
+      if (value && value.text === 'OK') {
+        return context.kernel.shutdown();
+      }
+    }).then(() => {
+      return true;
+    });
+  }
+
+  private _contextManager: ContextManager = null;
+  private _registry: DocumentRegistry = null;
+  private _widgets: { [key: string]: Widget[] } = Object.create(null);
+}
+
+
+/**
+ * A namespace for document widget manager statics.
+ */
+export
+namespace DocumentWidgetManager {
+  /**
+   * The options used to initialize a document widget manager.
+   */
+  export
+  interface IOptions {
+    /**
+     * A document registry instance.
+     */
+    registry: DocumentRegistry;
+
+    /**
+     * A context manager instance.
+     */
+    contextManager: ContextManager;
+  }
+}
+
+
+/**
+ * A private namespace for DocumentManager data.
+ */
+namespace Private {
+  /**
+   * A private attached property for a widget context id.
+   */
+  export
+  const idProperty = new Property<Widget, string>({
+    name: 'id'
+  });
+
+  /**
+   * A private attached property for a widget factory name.
+   */
+  export
+  const nameProperty = new Property<Widget, string>({
+    name: 'name'
+  });
+}

+ 0 - 18
src/docregistry/default.ts

@@ -154,13 +154,6 @@ class DocumentModel implements IDocumentModel {
     this.fromString(JSON.parse(value));
   }
 
-  /**
-   * Initialize the model state.
-   */
-  initialize(): void {
-    this.dirty = false;
-  }
-
   private _text = '';
   private _defaultLang = '';
   private _dirty = false;
@@ -284,17 +277,6 @@ abstract class ABCWidgetFactory implements IWidgetFactory<Widget, IDocumentModel
    */
   abstract createNew(context: IDocumentContext<IDocumentModel>, kernel?: IKernel.IModel): Widget;
 
-  /**
-   * Take an action on a widget before closing it.
-   *
-   * @returns A promise that resolves to true if the document should close
-   *   and false otherwise.
-   */
-  beforeClose(widget: Widget, context: IDocumentContext<IDocumentModel>): Promise<boolean> {
-    // There is nothing specific to do.
-    return Promise.resolve(true);
-  }
-
   private _isDisposed = false;
 }
 

+ 27 - 21
src/docregistry/interfaces.ts

@@ -92,11 +92,6 @@ interface IDocumentModel extends IDisposable {
    * Should emit a [contentChanged] signal.
    */
   fromJSON(value: any): void;
-
-  /**
-   * Initialize the model state.
-   */
-  initialize(): void;
 }
 
 
@@ -104,6 +99,26 @@ interface IDocumentModel extends IDisposable {
  * The document context object.
  */
 export interface IDocumentContext<T extends IDocumentModel> extends IDisposable {
+  /**
+   * A signal emitted when the kernel changes.
+   */
+  kernelChanged: ISignal<IDocumentContext<T>, IKernel>;
+
+  /**
+   * A signal emitted when the path changes.
+   */
+  pathChanged: ISignal<IDocumentContext<T>, string>;
+
+  /**
+   * A signal emitted when the contentsModel changes.
+   */
+  contentsModelChanged: ISignal<IDocumentContext<T>, IContentsModel>;
+
+  /**
+   * A signal emitted when the context is fully populated for the first time.
+   */
+  populated: ISignal<IDocumentContext<T>, void>;
+
   /**
    * The unique id of the context.
    *
@@ -141,7 +156,8 @@ export interface IDocumentContext<T extends IDocumentModel> extends IDisposable
    *
    * #### Notes
    * This is a read-only property.  The model will have an
-   * empty `contents` field.
+   * empty `contents` field.  It will be `null` until the
+   * first save or load to disk.
    */
   contentsModel: IContentsModel;
 
@@ -154,14 +170,12 @@ export interface IDocumentContext<T extends IDocumentModel> extends IDisposable
   kernelspecs: IKernel.ISpecModels;
 
   /**
-   * A signal emitted when the kernel changes.
-   */
-  kernelChanged: ISignal<IDocumentContext<T>, IKernel>;
-
-  /**
-   * A signal emitted when the path changes.
+   * Test whether the context is fully populated.
+   *
+   * #### Notes
+   * This is a read-only property.
    */
-  pathChanged: ISignal<IDocumentContext<T>, string>;
+  isPopulated: boolean;
 
   /**
    * Change the current kernel associated with the document.
@@ -258,14 +272,6 @@ interface IWidgetFactory<T extends Widget, U extends IDocumentModel> extends IDi
    * Create a new widget.
    */
   createNew(context: IDocumentContext<U>, kernel?: IKernel.IModel): T;
-
-  /**
-   * Take an action on a widget before closing it.
-   *
-   * @returns A promise that resolves to true if the document should close
-   *   and false otherwise.
-   */
-  beforeClose(widget: T, context: IDocumentContext<U>): Promise<boolean>;
 }
 
 

+ 6 - 11
src/filebrowser/browser.ts

@@ -1,13 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  IContentsModel
-} from 'jupyter-js-services';
-
-import * as arrays
-  from 'phosphor-arrays';
-
 import {
   Message
 } from 'phosphor-messaging';
@@ -184,8 +177,9 @@ class FileBrowserWidget extends Widget {
     let widget = this._manager.findWidget(path);
     if (!widget) {
       widget = this._manager.open(path);
-      widget.populated.connect(() => model.refresh() );
-      widget.context.kernelChanged.connect(() => model.refresh() );
+      let context = this._manager.contextForWidget(widget);
+      context.populated.connect(() => model.refresh() );
+      context.kernelChanged.connect(() => model.refresh() );
     }
     this._opener.open(widget);
     return widget;
@@ -198,8 +192,9 @@ class FileBrowserWidget extends Widget {
     let model = this.model;
     return model.newUntitled(type, ext).then(contents => {
       let widget = this._manager.createNew(contents.path);
-      widget.populated.connect(() => model.refresh() );
-      widget.context.kernelChanged.connect(() => model.refresh() );
+      let context = this._manager.contextForWidget(widget);
+      context.populated.connect(() => model.refresh() );
+      context.kernelChanged.connect(() => model.refresh() );
       this._opener.open(widget);
       return widget;
     });

+ 6 - 4
src/filebrowser/buttons.ts

@@ -148,8 +148,9 @@ class FileButtons extends Widget {
     let widget = this._manager.open(path, widgetName, kernel);
     let opener = this._opener;
     opener.open(widget);
-    widget.populated.connect(() => this.model.refresh() );
-    widget.context.kernelChanged.connect(() => this.model.refresh() );
+    let context = this._manager.contextForWidget(widget);
+    context.populated.connect(() => this.model.refresh() );
+    context.kernelChanged.connect(() => this.model.refresh() );
   }
 
   /**
@@ -159,8 +160,9 @@ class FileButtons extends Widget {
     let widget = this._manager.createNew(path, widgetName, kernel);
     let opener = this._opener;
     opener.open(widget);
-    widget.populated.connect(() => this.model.refresh() );
-    widget.context.kernelChanged.connect(() => this.model.refresh() );
+    let context = this._manager.contextForWidget(widget);
+    context.populated.connect(() => this.model.refresh() );
+    context.kernelChanged.connect(() => this.model.refresh() );
   }
 
   /**

+ 5 - 5
src/filebrowser/dialogs.ts

@@ -14,7 +14,7 @@ import {
 } from '../dialog';
 
 import {
-  DocumentManager, DocumentWrapper
+  DocumentManager
 } from '../docmanager';
 
 import {
@@ -35,7 +35,7 @@ const FILE_CONFLICT_CLASS = 'jp-mod-conflict';
  * Open a file using a dialog.
  */
 export
-function openWithDialog(path: string, manager: DocumentManager, host?: HTMLElement): Promise<DocumentWrapper> {
+function openWithDialog(path: string, manager: DocumentManager, host?: HTMLElement): Promise<Widget> {
   let handler: OpenWithHandler;
   return manager.listSessions().then(sessions => {
     handler = new OpenWithHandler(path, manager, sessions);
@@ -57,7 +57,7 @@ function openWithDialog(path: string, manager: DocumentManager, host?: HTMLEleme
  * Create a new file using a dialog.
  */
 export
-function createNewDialog(model: FileBrowserModel, manager: DocumentManager, host?: HTMLElement): Promise<DocumentWrapper> {
+function createNewDialog(model: FileBrowserModel, manager: DocumentManager, host?: HTMLElement): Promise<Widget> {
   let handler: CreateNewHandler;
   return manager.listSessions().then(sessions => {
     handler = new CreateNewHandler(model, manager, sessions);
@@ -143,7 +143,7 @@ class OpenWithHandler extends Widget {
   /**
    * Open the file and return the document widget.
    */
-  open(): DocumentWrapper {
+  open(): Widget {
     let path = this.input.textContent;
     let widgetName = this.widgetDropdown.value;
     let kernelValue = this.kernelDropdown.value;
@@ -287,7 +287,7 @@ class CreateNewHandler extends Widget {
   /**
    * Open the file and return the document widget.
    */
-  open(): DocumentWrapper {
+  open(): Widget {
     let path = this.input.textContent;
     let widgetName = this.widgetDropdown.value;
     let kernelValue = this.kernelDropdown.value;

+ 12 - 8
src/filebrowser/listing.ts

@@ -896,9 +896,10 @@ class DirListing extends Widget {
       return;
     }
 
-    let item = this._model.sortedItems[i];
+    let model = this._model;
+    let item = model.sortedItems[i];
     if (item.type === 'directory') {
-      this._model.cd(item.name).catch(error =>
+      model.cd(item.name).catch(error =>
         showErrorMessage(this, 'Open directory', error)
       );
     } else {
@@ -906,8 +907,9 @@ class DirListing extends Widget {
       let widget = this._manager.findWidget(path);
       if (!widget) {
         widget = this._manager.open(item.path);
-        widget.populated.connect(() => this.model.refresh() );
-        widget.context.kernelChanged.connect(() => this.model.refresh() );
+        let context = this._manager.contextForWidget(widget);
+        context.populated.connect(() => model.refresh() );
+        context.kernelChanged.connect(() => model.refresh() );
       }
       this._opener.open(widget);
     }
@@ -1028,7 +1030,8 @@ class DirListing extends Widget {
   private _startDrag(index: number, clientX: number, clientY: number): void {
     let selectedNames = Object.keys(this._selection);
     let source = this._items[index];
-    let items = this._model.sortedItems;
+    let model = this._model;
+    let items = model.sortedItems;
     let item: IContentsModel = null;
 
     // If the source node is not selected, use just that node.
@@ -1041,7 +1044,7 @@ class DirListing extends Widget {
     }
 
     // Create the drag image.
-    var dragImage = source.cloneNode(true) as HTMLElement;
+    let dragImage = source.cloneNode(true) as HTMLElement;
     dragImage.removeChild(dragImage.lastChild);
     if (selectedNames.length > 1) {
       let text = utils.findElement(dragImage, ITEM_TEXT_CLASS);
@@ -1062,8 +1065,9 @@ class DirListing extends Widget {
         let widget = this._manager.findWidget(path);
         if (!widget) {
           widget = this._manager.open(item.path);
-          widget.populated.connect(() => this.model.refresh() );
-          widget.context.kernelChanged.connect(() => this.model.refresh() );
+          let context = this._manager.contextForWidget(widget);
+          context.populated.connect(() => model.refresh() );
+          context.kernelChanged.connect(() => model.refresh() );
         }
         return widget;
       });

+ 23 - 13
src/filebrowser/plugin.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  FileBrowserWidget
+  IWidgetOpener, FileBrowserWidget
 } from './browser';
 
 import {
@@ -10,7 +10,7 @@ import {
 } from './model';
 
 import {
-  DocumentManager, DocumentWrapper
+  DocumentManager
 } from '../docmanager';
 
 import {
@@ -29,6 +29,10 @@ import {
   TabPanel
 } from 'phosphor-tabs';
 
+import {
+  Widget
+} from 'phosphor-widget';
+
 import {
   JupyterServices
 } from '../services/plugin';
@@ -51,12 +55,12 @@ const fileBrowserExtension = {
 function activateFileBrowser(app: Application, provider: JupyterServices, registry: DocumentRegistry): Promise<void> {
   let contents = provider.contentsManager;
   let sessions = provider.sessionManager;
-  let widgets: DocumentWrapper[] = [];
-  let activeWidget: DocumentWrapper;
+  let widgets: Widget[] = [];
+  let activeWidget: Widget;
   let id = 0;
 
-  let opener = {
-    open: (widget: DocumentWrapper) => {
+  let opener: IWidgetOpener = {
+    open: (widget) => {
       if (!widget.id) {
         widget.id = `document-manager-${++id}`;
       }
@@ -73,8 +77,8 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
         tabs.currentWidget = widget;
       }
       activeWidget = widget;
-      widget.disposed.connect((w: DocumentWrapper) => {
-        let index = widgets.indexOf(w);
+      widget.disposed.connect(() => {
+        let index = widgets.indexOf(widget);
         widgets.splice(index, 1);
       });
     }
@@ -91,9 +95,13 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
     }
   });
 
-  let docManager = new DocumentManager(
-    registry, contents, sessions, provider.kernelspecs, opener
-  );
+  let docManager = new DocumentManager({
+    registry,
+    contentsManager: contents,
+    sessionManager: sessions,
+    kernelspecs: provider.kernelspecs,
+    opener
+  });
   let model = new FileBrowserModel(contents, sessions, provider.kernelspecs);
   let widget = new FileBrowserWidget(model, docManager, opener);
   let menu = createMenu(widget);
@@ -136,7 +144,8 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
         if (!activeWidget) {
           return;
         }
-        activeWidget.context.save();
+        let context = docManager.contextForWidget(activeWidget);
+        context.save();
       }
     }
   ]);
@@ -159,7 +168,8 @@ function activateFileBrowser(app: Application, provider: JupyterServices, regist
         if (!activeWidget) {
           return;
         }
-        activeWidget.context.revert();
+        let context = docManager.contextForWidget(activeWidget);
+        context.revert();
       }
     }
   ]);

+ 7 - 1
src/imagewidget/widget.ts

@@ -48,6 +48,9 @@ class ImageWidget extends Widget {
     context.model.contentChanged.connect(() => {
       this.update();
     });
+    context.contentsModelChanged.connect(() => {
+      this.update();
+    });
   }
 
   /**
@@ -67,8 +70,11 @@ class ImageWidget extends Widget {
   protected onUpdateRequest(msg: Message): void {
     this.title.text = this._context.path.split('/').pop();
     let node = this.node as HTMLImageElement;
-    let content = this._context.model.toString();
     let cm = this._context.contentsModel;
+    if (cm === null) {
+      return;
+    }
+    let content = this._context.model.toString();
     node.src = `data:${cm.mimetype};${cm.format},${content}`;
   }
 

+ 0 - 8
src/notebook/notebook/model.ts

@@ -358,14 +358,6 @@ class NotebookModel extends DocumentModel implements INotebookModel {
     this.dirty = true;
   }
 
-  /**
-   * Initialize the model state.
-   */
-  initialize(): void {
-    this._cells.clearUndo();
-    this.dirty = false;
-  }
-
   /**
    * Get a metadata cursor for the notebook.
    *

+ 16 - 1
src/notebook/notebook/panel.ts

@@ -257,6 +257,16 @@ class NotebookPanel extends Widget {
     this.title.text = path.split('/').pop();
   }
 
+  /**
+   * Handle a context population.
+   */
+  protected onPopulated(sender: IDocumentContext<INotebookModel>, args: void): void {
+    // Clear the undo state of the cells.
+    if (sender.model) {
+      sender.model.cells.clearUndo();
+    }
+  }
+
   /**
    * Handle a change in the context.
    */
@@ -264,6 +274,7 @@ class NotebookPanel extends Widget {
     if (oldValue) {
       oldValue.kernelChanged.disconnect(this._onKernelChanged, this);
       oldValue.pathChanged.disconnect(this.onPathChanged, this);
+      oldValue.populated.disconnect(this.onPopulated, this);
       if (oldValue.model) {
         oldValue.model.stateChanged.disconnect(this.onModelStateChanged, this);
       }
@@ -277,7 +288,11 @@ class NotebookPanel extends Widget {
     this._content.model = newValue.model;
     this._handleDirtyState();
     newValue.model.stateChanged.connect(this.onModelStateChanged, this);
-
+    if (newValue.isPopulated) {
+      this.onPopulated(newValue, void 0);
+    } else {
+      newValue.populated.connect(this.onPopulated, this);
+    }
     // Handle the document title.
     this.onPathChanged(context, context.path);
     context.pathChanged.connect(this.onPathChanged, this);

+ 3 - 3
src/shortcuts/plugin.ts

@@ -62,17 +62,17 @@ const SHORTCUTS = [
   },
   {
     command: 'file-operations:save',
-    selector: '.jp-DocumentWrapper',
+    selector: '.jp-Document',
     sequence: ['Accel S']
   },
   {
     command: 'file-operations:close',
-    selector: '.jp-DocumentWrapper',
+    selector: '.jp-Document',
     sequence: ['Ctrl Q']
   },
   {
     command: 'file-operations:close-all',
-    selector: '.jp-DocumentWrapper',
+    selector: '.jp-Document',
     sequence: ['Ctrl Shift Q']
   },
   {

+ 0 - 10
tutorial/documents.md

@@ -91,13 +91,3 @@ given model.  They are tied to a model and can be shared between widgets.
 The reason for a separate context and model is so that it is easy to create
 model factories and the heavy lifting of the context is left to the Document
 Manager.
-
-### [Document Wrappers](http://jupyter.org/jupyterlab/classes/_docmanager_manager_.documentwrapper.html)
-
-The top level widget created by the Document Manager that wraps the widget
-returned by the widget factory.
-
-Document wrappers are used because they are created synchronously; while,
-the widgets created using the widget factory are created asynchronously after
-potentially loading data from disk. Some interfaces (like drag and drop)
-require a widget to be returned synchronously.