ソースを参照

wip content widget refactor

Steven Silvester 9 年 前
コミット
a4c7ca259c
2 ファイル変更176 行追加255 行削除
  1. 9 2
      src/docmanager/context.ts
  2. 167 253
      src/docmanager/index.ts

+ 9 - 2
src/docmanager/context.ts

@@ -144,6 +144,13 @@ class Context implements IDocumentContext {
     return this._manager.save(this._id);
   }
 
+  /**
+   * Save the document to a different path.
+   */
+  saveAs(path: string): Promise<void> {
+    return this._manager.saveAs(this._id, path);
+  }
+
   /**
    * Revert the document contents to disk contents.
    */
@@ -226,7 +233,7 @@ class ContextManager implements IDisposable {
   /**
    * Create a new context.
    */
-  createNew(path: string, model: IDocumentModel, options: IModelFactoryOptions, contents: IContentsModel): string {
+  createNew(path: string, model: IDocumentModel, options: IModelFactoryOptions): string {
     let context = new Context(this);
     let id = context.id;
     this._contexts[id] = {
@@ -235,7 +242,7 @@ class ContextManager implements IDisposable {
       model,
       modelName: options.name,
       opts: options.contentsOptions,
-      contentsModel: this._copyContentsModel(contents),
+      contentsModel: null,
       session: null
     };
     // Handle the session - use one created for another model on this

+ 167 - 253
src/docmanager/index.ts

@@ -8,15 +8,12 @@ import {
   IKernelSpecIds, ISessionId
 } from 'jupyter-js-services';
 
-import * as utils
-  from 'jupyter-js-utils';
-
 import {
   IDisposable, DisposableDelegate
 } from 'phosphor-disposable';
 
 import {
-  IMessageHandler, Message, installMessageFilter
+  Message
 } from 'phosphor-messaging';
 
 import {
@@ -24,11 +21,7 @@ import {
 } from 'phosphor-panel';
 
 import {
-  Property
-} from 'phosphor-properties';
-
-import {
-  ISignal, Signal
+  ISignal
 } from 'phosphor-signaling';
 
 import {
@@ -48,6 +41,12 @@ import {
 } from './context';
 
 
+/**
+ * The class name added to a document container widgets.
+ */
+const DOCUMENT_CLASS = 'jp-DocumentWidget';
+
+
 /**
  * The interface for a document model.
  */
@@ -186,6 +185,11 @@ export interface IDocumentContext extends IDisposable {
    */
   save(): Promise<void>;
 
+  /**
+   * Save the document to a different path.
+   */
+  saveAs(path: string): Promise<void>;
+
   /**
    * Revert the document contents to disk contents.
    */
@@ -356,10 +360,8 @@ class DocumentManager implements IDisposable {
     this._contentsManager = contentsManager;
     this._sessionManager = sessionManager;
     this._contextManager = new ContextManager(contentsManager, sessionManager, kernelSpecs, (id: string, widget: Widget) => {
-      let parent = new Widget();
-      this._attachChild(parent, widget);
-      Private.contextProperty.set(parent, id);
-      this._widgets[id].push(parent);
+      let parent = this._createWidget('', id);
+      parent.setContent(widget);
       opener.open(parent);
       return new DisposableDelegate(() => {
         parent.close();
@@ -549,29 +551,28 @@ class DocumentManager implements IDisposable {
    * @param kernel - An optional kernel name/id to override the default.
    */
   open(path: string, widgetName='default', kernel?: IKernelId): Widget {
-    let widget = new Widget();
-    let manager = this._contentsManager;
     if (widgetName === 'default') {
       widgetName = this.listWidgetFactories(path)[0];
     }
     let mFactoryEx = this._getModelFactoryEx(widgetName);
-    let lang = mFactoryEx.factory.preferredLanguage(path);
+    if (!mFactoryEx) {
+      return;
+    }
+    let widget: DocumentWidget;
+    // Use an existing context if available.
     let id = this._contextManager.findContext(path, mFactoryEx.name);
     if (id) {
-      this._createWidget(id, widgetName, widget, kernel);
+      widget = this._createWidget(widgetName, id);
+      this._populateWidget(widget, kernel);
       return widget;
     }
+    let lang = mFactoryEx.factory.preferredLanguage(path);
     let model = mFactoryEx.factory.createNew(lang);
-    let opts = mFactoryEx.contentsOptions;
-    manager.get(path, opts).then(contents => {
-      if (contents.format === 'json') {
-        model.fromJSON(contents.content);
-      } else {
-        model.fromString(contents.content);
-      }
-      model.dirty = false;
-      id = this._createContext(path, model, widgetName, contents);
-      this._createWidget(id, widgetName, widget, kernel);
+    id = this._contextManager.createNew(path, model, mFactoryEx);
+    widget = this._createWidget(widgetName, id);
+    // Load the contents from disk.
+    this._contextManager.revert(id).then(() => {
+      this._populateWidget(widget, kernel);
     });
     return widget;
   }
@@ -586,8 +587,6 @@ class DocumentManager implements IDisposable {
    * @param kernel - An optional kernel name/id to override the default.
    */
   createNew(path: string, widgetName='default', kernel?: IKernelId): Widget {
-    let widget = new Widget();
-    let manager = this._contentsManager;
     if (widgetName === 'default') {
       widgetName = this.listWidgetFactories(path)[0];
     }
@@ -597,79 +596,16 @@ class DocumentManager implements IDisposable {
     }
     let lang = mFactoryEx.factory.preferredLanguage(path);
     let model = mFactoryEx.factory.createNew(lang);
-    let opts = utils.copy(mFactoryEx.contentsOptions);
-    if (opts.format === 'json') {
-      opts.content = model.toJSON();
-    } else {
-      opts.content = model.toString();
-    }
-    manager.save(path, opts).then(contents => {
-      let id = this._createContext(path, model, widgetName, contents);
-      this._createWidget(id, widgetName, widget, kernel);
+    let id = this._contextManager._createNew(path, model, mFactoryEx);
+    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);
     });
     return widget;
   }
 
-  /**
-   * Get the path given a widget.
-   */
-  getPath(widget: Widget): string {
-    let id = Private.contextProperty.get(widget);
-    return this._contextManager.getPath(id);
-  }
-
-  /**
-   * Clone a widget.
-   *
-   * #### Notes
-   * This will create a new widget with the same model and context
-   * as the existing widget.
-   */
-  clone(widget: Widget): Widget {
-    let parent = new Widget();
-    let id = Private.contextProperty.get(widget);
-    let name = Private.nameProperty.get(widget);
-    this._createWidget(id, name, parent);
-    return parent;
-  }
-
-  /**
-   * Filter messages on the widget.
-   */
-  filterMessage(handler: IMessageHandler, msg: Message): boolean {
-    if (msg.type !== 'close-request') {
-      return false;
-    }
-    if (this._closeGuard) {
-      // Allow the close to propagate to the widget and its layout.
-      this._closeGuard = false;
-      return false;
-    }
-    let widget = handler as Widget;
-    let id = Private.contextProperty.get(widget);
-    let model = this._contextManager.getModel(id);
-    let context = this._contextManager.getContext(id);
-    let child = (widget.layout as PanelLayout).childAt(0);
-    let name = Private.nameProperty.get(widget);
-    // Check for a sibling widget.
-    if (!name) {
-      // Do not filter the message.
-      return false;
-    }
-    let factory = this._widgetFactories[name].factory;
-    this._maybeClose(widget, model.dirty).then(result => {
-      if (!result) {
-        return result;
-      }
-      return factory.beforeClose(model, context, child);
-    }).then(result => {
-      if (result) {
-        return this._cleanupWidget(widget);
-      }
-    });
-    return true;
-  }
-
   /**
    * Update the path of an open document.
    *
@@ -681,21 +617,6 @@ class DocumentManager implements IDisposable {
     this._contextManager.rename(oldPath, newPath);
   }
 
-  /**
-   * Handle a file deletion on the currently open widgets.
-   *
-   * @param path - The path of the file to delete.
-   */
-  deleteFile(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) {
-        this._cleanupWidget(w);
-      }
-    }
-  }
-
   /**
    * See if a widget already exists for the given path and widget name.
    *
@@ -710,7 +631,7 @@ class DocumentManager implements IDisposable {
     }
     for (let id of ids) {
       for (let widget of this._widgets[id]) {
-        if (Private.nameProperty.get(widget) === widgetName) {
+        if (widget.name === widgetName) {
           return widget;
         }
       }
@@ -718,42 +639,16 @@ class DocumentManager implements IDisposable {
   }
 
   /**
-   * Save the document contents to disk.
-   *
-   * #### Notes
-   * This will affect the contents of all other widgets
-   * that share the same model as the given widget.
-   */
-  save(widget: Widget): Promise<void> {
-    let id = Private.contextProperty.get(widget);
-    return this._contextManager.save(id);
-  }
-
-  /**
-   * Save a widget to a different file name.
-   *
-   * #### Notes
-   * It is assumed that all other widgets associated with the new path
-   * have been closed and that the path is either not in conflict
-   * or the user has chosen to overwrite the file.
-   * This will affect the contents of all other widgets
-   * that share the same model as the given widget.
-   */
-  saveAs(widget: Widget, path: string): Promise<void> {
-    let id = Private.contextProperty.get(widget);
-    return this._contextManager.saveAs(id, path);
-  }
-
-  /**
-   * Revert the document contents to disk contents.
+   * Clone a widget.
    *
    * #### Notes
-   * This will affect the contents of all other widgets
-   * that share the same model as the given widget.
+   * This will create a new widget with the same model and context
+   * as this widget.
    */
-  revert(widget: Widget): Promise<void> {
-    let id = Private.contextProperty.get(widget);
-    return this._contextManager.revert(id);
+  clone(widget: Widget): DocumentWidget {
+    let parent = this._createWidget(widget.name, widget.context.id);
+    this._populateWidget(parent);
+    return parent;
   }
 
   /**
@@ -781,97 +676,31 @@ class DocumentManager implements IDisposable {
   }
 
   /**
-   * Create a context or reuse an existing one.
-   */
-  private _createContext(path: string, model: IDocumentModel, widgetName: string, contents: IContentsModel): string {
-    let mFactoryEx = this._getModelFactoryEx(widgetName);
-    let id = this._contextManager.findContext(path, mFactoryEx.name);
-    if (id) {
-      return id;
-    } else {
-      return this._contextManager.createNew(path, model, mFactoryEx, contents);
-    }
-  }
-
-  /**
-   * Create a widget from a context and attach it to the parent.
+   * Create a container widget and handle its lifecycle.
    */
-  private _createWidget(contextId: string, widgetName: string, parent: Widget, kernel?: IKernelId): void {
-    let wFactoryEx = this._getWidgetFactoryEx(widgetName);
-    if (!(contextId in this._widgets)) {
-      this._widgets[contextId] = [];
+  private _createWidget(name: string, id: string): DocumentWidget {
+    let context = this._contextManager.getContext(id);
+    let widget = new DocumentWidget(name, context);
+    if (!(id in this._widgets)) {
+      this._widgets[id] = [];
     }
-    this._widgets[contextId].push(parent);
-    let context = this._contextManager.getContext(contextId);
-    let model = this._contextManager.getModel(contextId);
-    // Create the child widget using the factory.
-    let child = wFactoryEx.factory.createNew(model, context, kernel);
-    this._attachChild(parent, child);
-    Private.nameProperty.set(parent, widgetName);
-    Private.contextProperty.set(parent, contextId);
-    installMessageFilter(parent, this);
-  }
-
-  /**
-   * Attach a child widget to a parent container.
-   */
-  private _attachChild(parent: Widget, child: Widget) {
-    parent.layout = new PanelLayout();
-    parent.title.closable = true;
-    parent.title.text = child.title.text;
-    parent.title.icon = child.title.icon;
-    parent.title.className = child.title.className;
-    // Mirror the parent title based on the child.
-    child.title.changed.connect(() => {
-      child.parent.title.text = child.title.text;
-      child.parent.title.icon = child.title.icon;
-      child.parent.title.className = child.title.className;
+    this._widgets[id].push(child);
+    widget.disposed.connect(() => {
+      let index = this._widgets[id].indexOf(widget);
+      this._widgets[id] = this._widgets[id].splice(index, 1);
     });
-    // Add the child widget to the parent widget.
-    (parent.layout as PanelLayout).addChild(child);
   }
 
   /**
-   * Ask the user whether to close an unsaved file.
+   * Create a content widget and add it to the container widget.
    */
-  private _maybeClose(widget: Widget, dirty: boolean): Promise<boolean> {
-    if (!dirty) {
-      return Promise.resolve(true);
-    }
-    let host = widget.isAttached ? widget.node : document.body;
-    return showDialog({
-      title: 'Close without saving?',
-      body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
-      host
-    }).then(value => {
-      if (value && value.text === 'OK') {
-        return true;
-      }
-      return false;
-    });
-  }
-
-  /**
-   * Clean up the data associated with a widget.
-   */
-  private _cleanupWidget(widget: Widget): void {
-    // Remove the widget from our internal storage.
-    let id = Private.contextProperty.get(widget);
-    let index = this._widgets[id].indexOf(widget);
-    this._widgets[id] = this._widgets[id].splice(index, 1);
-    this._closeGuard = true;
-    // If this is the last widget in that context, remove the context.
-    if (!this._widgets[id]) {
-      let session = this._contextManager.removeContext(id);
-      if (session) {
-        // TODO: show a dialog asking whether to shut down the kernel.
-        widget.close();
-        widget.dispose();
-      }
-    } else {
-      widget.close();
-      widget.dispose();
-    }
+  private _populateWidget(parent: DocumentWidget, kernel?: IKernel): void {
+    let factory = this._widgetFactories[parent.name].factory;
+    let id = parent.context.id;
+    let model = this._contextManager.getModel(id);
+    let context = this._contextManager.getContext(id);
+    let child = factory.createNew(model, context, kernel);
+    parent.setContent(child);
   }
 
   /**
@@ -906,49 +735,134 @@ class DocumentManager implements IDisposable {
   private _contentsManager: IContentsManager = null;
   private _sessionManager: INotebookSessionManager = null;
   private _contextManager: ContextManager = null;
-  private _closeGuard = false;
 }
 
 
 /**
- * A private namespace for DocumentManager data.
+ * A container widget for documents.
  */
-namespace Private {
+export
+class DocumentWidget {
   /**
-   * A signal emitted when a file is opened.
+   * Construct a new document widget.
    */
-  export
-  const openedSignal = new Signal<DocumentManager, Widget>();
+  constructor(name: string, context: IDocumentContext) {
+    super();
+    this.addClass(DOCUMENT_CLASS);
+    this.layout = new PanelLayout();
+    this._name = name;
+    this._context = context;
+    this.title.closable = true;
+  }
 
   /**
-   * An extended interface for a model factory and its options.
+   * Get the name of the widget.
+   *
+   * #### Notes
+   * This is a read-only property.
    */
-  export
-  interface IModelFactoryEx extends IModelFactoryOptions {
-    factory: IModelFactory;
+  get name(): string {
+    return this._name;
   }
 
   /**
-   * An extended interface for a widget factory and its options.
+   * The context for the widget.
+   *
+   * #### Notes
+   * This is a read-only property.
    */
-  export
-  interface IWidgetFactoryEx extends IWidgetFactoryOptions {
-    factory: IWidgetFactory<Widget>;
+  get context(): IDocumentContext {
+    return this._context;
   }
 
   /**
-   * The widget factory name used to create a widget.
+   * Dispose of the resources held by the widget.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._context = null;
+    super.dispose();
+  }
+
+  /**
+   * Set the child and context of 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.
+    this.title.changed.connect(() => {
+      this.parent.title.text = child.title.text;
+      this.parent.title.icon = child.title.icon;
+      this.parent.title.className = child.title.className;
+    });
+    // Add the child widget to the layout.
+    (this.layout as PanelLayout).addChild(child);
+  }
+
+  /**
+   * Handle `'close-request'` messages.
+   */
+  protected onCloseRequest(msg: Message): void {
+    // Handle dirty.
+    // Call the beforeClose on the widget factory.
+    // Check for dangling kernel.
+    // THEN,
+    super.onCloseRequest(msg);
+  }
+
+  /**
+   * Ask the user whether to close an unsaved file.
+   */
+  private _maybeClose(widget: Widget, dirty: boolean): Promise<boolean> {
+    if (!dirty) {
+      return Promise.resolve(true);
+    }
+    let host = widget.isAttached ? widget.node : document.body;
+    return showDialog({
+      title: 'Close without saving?',
+      body: `File "${widget.title.text}" has unsaved changes, close without saving?`,
+      host
+    }).then(value => {
+      if (value && value.text === 'OK') {
+        return true;
+      }
+      return false;
+    });
+  }
+
+  private _context = IDocumentContext;
+  private _name = '';
+}
+
+
+/**
+ * A private namespace for DocumentManager data.
+ */
+namespace Private {
+  /**
+   * An extended interface for a model factory and its options.
    */
   export
-  const nameProperty = new Property<Widget, string>({
-    name: 'name'
-  });
+  interface IModelFactoryEx extends IModelFactoryOptions {
+    factory: IModelFactory;
+  }
 
   /**
-   * The context id associated with a widget.
+   * An extended interface for a widget factory and its options.
    */
   export
-  const contextProperty = new Property<Widget, string>({
-    name: 'context'
-  });
+  interface IWidgetFactoryEx extends IWidgetFactoryOptions {
+    factory: IWidgetFactory<Widget>;
+  }
 }