Sfoglia il codice sorgente

Add file creator handling

Steven Silvester 8 anni fa
parent
commit
71f9bb98a8

+ 9 - 16
src/filebrowser/browser.ts

@@ -211,16 +211,14 @@ class FileBrowserWidget extends Widget {
    * Open a file by path.
    */
   openPath(path: string, widgetName='default'): Widget {
-    let model = this.model;
-    let widget = this._manager.findWidget(path, widgetName);
-    if (!widget) {
-      widget = this._manager.open(path, widgetName);
-      let context = this._manager.contextForWidget(widget);
-      context.populated.connect(() => model.refresh() );
-      context.kernelChanged.connect(() => model.refresh() );
-    }
-    this._opener.open(widget);
-    return widget;
+    return this._buttons.open(path, widgetName);
+  }
+
+  /**
+   * Create a file from a creator.
+   */
+  createFrom(creatorName: string): Promise<Widget> {
+    return this._buttons.createFrom(creatorName);
   }
 
   /**
@@ -229,12 +227,7 @@ class FileBrowserWidget extends Widget {
   createNew(options: IContents.ICreateOptions): Promise<Widget> {
     let model = this.model;
     return model.newUntitled(options).then(contents => {
-      let widget = this._manager.createNew(contents.path);
-      let context = this._manager.contextForWidget(widget);
-      context.populated.connect(() => model.refresh() );
-      context.kernelChanged.connect(() => model.refresh() );
-      this._opener.open(widget);
-      return widget;
+      this._buttons.createNew(contents.path);
     });
   }
 

+ 31 - 51
src/filebrowser/buttons.ts

@@ -41,6 +41,10 @@ import {
   IWidgetOpener
 } from './browser';
 
+import {
+  createFromDialog
+} from './dialogs';
+
 import {
   FileBrowserModel
 } from './model';
@@ -157,28 +161,46 @@ class FileButtons extends Widget {
     return this._manager;
   }
 
+  /**
+   * Create a file from a creator.
+   */
+  createFrom(creatorName: string): Promise<Widget> {
+    return createFromDialog(this.model, this.manager, creatorName).then(widget => {
+      if (widget) {
+        return this._open(widget);
+      }
+    });
+  }
+
   /**
    * Open a file by path.
    */
-  open(path: string, widgetName='default', kernel?: IKernel.IModel): void {
-    let widget = this._manager.open(path, widgetName, kernel);
-    let opener = this._opener;
-    opener.open(widget);
-    let context = this._manager.contextForWidget(widget);
-    context.populated.connect(() => this.model.refresh() );
-    context.kernelChanged.connect(() => this.model.refresh() );
+  open(path: string, widgetName='default', kernel?: IKernel.IModel): Widget {
+    let widget = this._manager.findWidget(path, widgetName);
+    if (!widget) {
+      widget = this._manager.open(path, widgetName, kernel);
+    }
+    return this._open(widget);
   }
 
   /**
    * Create a new file by path.
    */
-  createNew(path: string, widgetName='default', kernel?: IKernel.IModel): void {
+  createNew(path: string, widgetName='default', kernel?: IKernel.IModel): Widget {
     let widget = this._manager.createNew(path, widgetName, kernel);
+    return this._open(widget);
+  }
+
+  /**
+   * Open a widget and attach listeners.
+   */
+  private _open(widget: Widget): Widget {
     let opener = this._opener;
     opener.open(widget);
     let context = this._manager.contextForWidget(widget);
     context.populated.connect(() => this.model.refresh() );
     context.kernelChanged.connect(() => this.model.refresh() );
+    return widget;
   }
 
   /**
@@ -389,18 +411,6 @@ namespace Private {
     return input;
   }
 
-  /**
-   * Create a new source file.
-   */
-  export
-  function createNewFile(widget: FileButtons): void {
-    widget.model.newUntitled({ type: 'file' }).then(contents => {
-      return widget.open(contents.path);
-    }).catch(error => {
-      utils.showErrorMessage(widget, 'New File Error', error);
-    });
-  }
-
   /**
    * Create a new folder.
    */
@@ -413,21 +423,6 @@ namespace Private {
     });
   }
 
-  /**
-   * Create a new item using a file creator.
-   */
-  function createNewItem(widget: FileButtons, fileType: IFileType, widgetName: string, kernelName?: string): void {
-    let kernel: IKernel.IModel;
-    if (kernelName) {
-      kernel = { name: kernelName };
-    }
-    widget.model.newUntitled(
-      { type: fileType.fileType, ext: fileType.extension })
-    .then(contents => {
-      widget.createNew(contents.path, widgetName, kernel);
-    });
-  }
-
   /**
    * Create a new dropdown menu for the create new button.
    */
@@ -443,13 +438,6 @@ namespace Private {
     // Remove all the commands associated with this menu upon disposal.
     menu.disposed.connect(() => disposables.dispose());
 
-    command = `${prefix}:new-text-file`;
-    disposables.add(commands.addCommand(command, {
-      execute: () => { createNewFile(widget); },
-      label: 'Text File'
-    }));
-    menu.addItem({ command });
-
     command = `${prefix}:new-text-folder`;
     disposables.add(commands.addCommand(command, {
       execute: () => { createNewFolder(widget); },
@@ -457,19 +445,11 @@ namespace Private {
     }));
     menu.addItem({ command });
 
-
-    if (creators) {
-      menu.addItem({ type: 'separator' });
-    }
     for (let creator of creators) {
-      let fileType = registry.getFileType(creator.fileType);
-
       command = `${prefix}:new-${creator.name}`;
       disposables.add(commands.addCommand(command, {
         execute: () => {
-          let widgetName = creator.widgetName || 'default';
-          let kernelName = creator.kernelName;
-          createNewItem(widget, fileType, widgetName, kernelName);
+          widget.createFrom(creator.name);
         },
         label: creator.name
       }));

+ 161 - 4
src/filebrowser/dialogs.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  IKernel, ISession
+  IContents, IKernel, ISession
 } from 'jupyter-js-services';
 
 import {
@@ -25,12 +25,38 @@ import {
   FileBrowserModel
 } from './model';
 
+
+/**
+ * The class name added to file dialogs.
+ */
+const FILE_DIALOG_CLASS = 'jp-FileDialog';
+
 /**
  * The class name added for a file conflict.
  */
 const FILE_CONFLICT_CLASS = 'jp-mod-conflict';
 
 
+/**
+ * Create a file using a file creator.
+ */
+export
+function createFromDialog(model: FileBrowserModel, manager: DocumentManager, creatorName: string): Promise<Widget> {
+  let handler = new CreateFromHandler(model, manager, creatorName);
+  return handler.populate().then(() => {
+    return showDialog({
+      title: `Create New ${creatorName}`,
+      body: handler.node,
+      okText: 'CREATE'
+    }).then(result => {
+      if (result.text === 'CREATE') {
+        return handler.open();
+      }
+    });
+  });
+}
+
+
 /**
  * Open a file using a dialog.
  */
@@ -171,6 +197,122 @@ class OpenWithHandler extends Widget {
 }
 
 
+/**
+ * A widget used to create a file using a creator.
+ */
+class CreateFromHandler extends Widget {
+  /**
+   * Construct a new "create from" dialog.
+   */
+  constructor(model: FileBrowserModel, manager: DocumentManager, creatorName: string) {
+    super({ node: Private.createCreateFromNode() });
+    this.addClass(FILE_DIALOG_CLASS);
+    this._model = model;
+    this._manager = manager;
+    this._creatorName = creatorName;
+
+    // Check for name conflicts when the input changes.
+    this.input.addEventListener('input', () => {
+      let value = this.input.value;
+      if (value !== this._orig) {
+        for (let item of this._model.items) {
+          if (item.name === value) {
+            this.addClass(FILE_CONFLICT_CLASS);
+            return;
+          }
+        }
+      }
+      this.removeClass(FILE_CONFLICT_CLASS);
+    });
+  }
+
+  /**
+   * Dispose of the resources used by the widget.
+   */
+  dispose(): void {
+    this._model = null;
+    this._manager = null;
+    super.dispose();
+  }
+
+  /**
+   * Get the input text node.
+   */
+  get input(): HTMLInputElement {
+    return this.node.getElementsByTagName('input')[0] as HTMLInputElement;
+  }
+
+  /**
+   * Get the kernel dropdown node.
+   */
+  get kernelDropdown(): HTMLSelectElement {
+    return this.node.getElementsByTagName('select')[0] as HTMLSelectElement;
+  }
+
+  /**
+   * Populate the create from widget.
+   */
+  populate(): Promise<void> {
+    let model = this._model;
+    let manager = this._manager;
+    let registry = manager.registry;
+    let creator = registry.getCreator(this._creatorName);
+    let { fileType, widgetName, kernelName } = creator;
+    let fType = registry.getFileType(fileType);
+    let ext = '.txt';
+    let type: IContents.FileType = 'file';
+    if (fType) {
+      ext = fType.extension;
+      type = fType.fileType || 'file';
+    }
+    if (!widgetName || widgetName === 'default') {
+      this._widgetName = widgetName = registry.defaultWidgetFactory(ext);
+    }
+
+    // Handle the kernel preferences.
+    let preference = registry.getKernelPreference(ext, widgetName);
+    if (preference.canStartKernel) {
+      Private.updateKernels(preference, this.kernelDropdown, this._manager.kernelspecs, this._sessions, kernelName);
+    } else {
+      this.node.removeChild(this.kernelDropdown);
+    }
+
+    return manager.listSessions().then(sessions => {
+      this._sessions = sessions;
+      return model.newUntitled({ ext, type });
+    }).then(contents => {
+      this.input.value = this._orig = contents.name;
+    });
+  }
+
+  /**
+   * Open the file and return the document widget.
+   */
+  open(): Promise<Widget> {
+    let path = this.input.value;
+    let widgetName = this._widgetName;
+    let kernelValue = this.kernelDropdown ? this.kernelDropdown.value : 'null';
+    let kernelId: IKernel.IModel;
+    if (kernelValue !== 'null') {
+      kernelId = JSON.parse(kernelValue) as IKernel.IModel;
+    }
+    if (path !== this._orig) {
+      return this._model.rename(this._orig, path).then(() => {
+        return this._manager.createNew(path, widgetName, kernelId);
+      });
+    }
+    return Promise.resolve(this._manager.createNew(path, widgetName, kernelId));
+  }
+
+  private _model: FileBrowserModel = null;
+  private _creatorName: string;
+  private _widgetName: string;
+  private _orig: string;
+  private _manager: DocumentManager;
+  private _sessions: ISession.IModel[] = [];
+}
+
+
 /**
  * A widget used to create new files.
  */
@@ -400,11 +542,24 @@ namespace Private {
     return body;
   }
 
+  /**
+   * Create the node for a create from handler.
+   */
+  export
+  function createCreateFromNode(): HTMLElement {
+    let body = document.createElement('div');
+    let name = document.createElement('input');
+    let kernelDropdown = document.createElement('select');
+    body.appendChild(name);
+    body.appendChild(kernelDropdown);
+    return body;
+  }
+
   /**
    * Update a kernel listing based on a kernel preference.
    */
   export
-  function updateKernels(preference: IKernelPreference, node: HTMLSelectElement, specs: IKernel.ISpecModels, running: ISession.IModel[]): void {
+  function updateKernels(preference: IKernelPreference, node: HTMLSelectElement, specs: IKernel.ISpecModels, sessions: ISession.IModel[], preferredKernel?: string): void {
     if (!preference.canStartKernel) {
       while (node.firstChild) {
         node.removeChild(node.firstChild);
@@ -412,9 +567,11 @@ namespace Private {
       node.disabled = true;
       return;
     }
-    let lang = preference.language;
+    let preferredLanguage = preference.language;
     node.disabled = false;
-    populateKernels(node, specs, running, lang);
+    populateKernels(node,
+      { specs, sessions, preferredLanguage, preferredKernel }
+    );
     // Select the "null" valued kernel if we do not prefer a kernel.
     if (!preference.preferKernel) {
       node.value = 'null';

+ 5 - 0
src/filebrowser/index.css

@@ -263,3 +263,8 @@
   min-height: 120px;
   outline: none;
 }
+
+
+.jp-FileDialog.jp-mod-conflict input {
+  color: red;
+}

+ 38 - 20
src/filebrowser/plugin.ts

@@ -67,8 +67,6 @@ const fileBrowserProvider: JupyterLabPlugin<IPathTracker> = {
  * The map of command ids used by the file browser.
  */
 const cmdIds = {
-  newText: 'file-operations:new-text-file',
-  newNotebook: 'file-operations:new-notebook',
   save: 'file-operations:save',
   restoreCheckpoint: 'file-operations:restore-checkpoint',
   saveAs: 'file-operations:saveAs',
@@ -111,6 +109,21 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, registry
     opener: opener
   });
 
+  let category = 'File Operations';
+  let creators = registry.listCreators();
+  let creatorCmds: string[] = [];
+  for (let creator of creators) {
+    let command = `file-operations:new-${creator.name}`;
+    creatorCmds.push(command);
+    commands.addCommand(command, {
+      execute: () => {
+        fbWidget.createFrom(creator.name);
+      },
+      label: `New ${creator.name}`
+    });
+    palette.addItem({ command, category });
+  }
+
   // Add a context menu to the dir listing.
   let node = fbWidget.node.getElementsByClassName('jp-DirListing-content')[0];
   node.addEventListener('contextmenu', (event: MouseEvent) => {
@@ -144,10 +157,7 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, registry
 
   addCommands(app, tracker, fbWidget, docManager);
 
-  let category = 'File Operations';
   [
-    cmdIds.newText,
-    cmdIds.newNotebook,
     cmdIds.save,
     cmdIds.restoreCheckpoint,
     cmdIds.saveAs,
@@ -155,13 +165,31 @@ function activateFileBrowser(app: JupyterLab, manager: IServiceManager, registry
     cmdIds.closeAllFiles,
   ].forEach(command => palette.addItem({ command, category }));
 
-  mainMenu.addMenu(createMenu(app), {rank: 1});
+  let menu = createMenu(app, creatorCmds);
+  mainMenu.addMenu(menu, {rank: 1});
 
   fbWidget.title.label = 'Files';
   fbWidget.id = 'file-browser';
   app.shell.addToLeftArea(fbWidget, { rank: 40 });
   app.commands.execute(cmdIds.showBrowser, void 0);
 
+  registry.changed.connect((sender, args) => {
+    if (args.type === 'fileCreator' && args.change === 'added') {
+      menu.dispose();
+      let command = `file-operations:new-${args.name}`;
+      creatorCmds.push(command);
+      commands.addCommand(command, {
+        execute: () => {
+          fbWidget.createFrom(args.name);
+        },
+        label: `New ${args.name}`
+      });
+      palette.addItem({ command, category });
+      menu = createMenu(app, creatorCmds);
+      mainMenu.addMenu(menu, {rank: 1});
+    }
+  });
+
   return fbModel;
 }
 
@@ -173,14 +201,6 @@ function addCommands(app: JupyterLab, tracker: FocusTracker<Widget>, fbWidget: F
   let commands = app.commands;
   let fbModel = fbWidget.model;
 
-  commands.addCommand(cmdIds.newText, {
-    label: 'New File',
-    execute: () => fbWidget.createNew({ type: 'file' })
-  });
-  commands.addCommand(cmdIds.newNotebook, {
-    label: 'New Notebook',
-    execute: () => fbWidget.createNew({ type: 'notebook' })
-  });
   commands.addCommand(cmdIds.save, {
     label: 'Save',
     caption: 'Save and create checkpoint',
@@ -264,20 +284,18 @@ function addCommands(app: JupyterLab, tracker: FocusTracker<Widget>, fbWidget: F
 /**
  * Create a top level menu for the file browser.
  */
-function createMenu(app: JupyterLab): Menu {
+function createMenu(app: JupyterLab, creatorCmds: string[]): Menu {
   let { commands, keymap } = app;
   let menu = new Menu({ commands, keymap });
   menu.title.label = 'File';
-
+  creatorCmds.forEach(command => { menu.addItem({ command }); });
   [
-    cmdIds.newText,
-    cmdIds.newNotebook,
     cmdIds.save,
     cmdIds.restoreCheckpoint,
     cmdIds.saveAs,
     cmdIds.close,
     cmdIds.closeAllFiles,
-  ].forEach(command => menu.addItem({ command }));
+  ].forEach(command => { menu.addItem({ command }); });
 
   return menu;
 }
@@ -294,7 +312,7 @@ function createContextMenu(fbWidget: FileBrowserWidget, openWith: Menu):  Menu {
   let command: string;
 
   // // Remove all the commands associated with this menu upon disposal.
-  menu.disposed.connect(() => disposables.dispose());
+  menu.disposed.connect(() => { disposables.dispose(); });
 
   command = `${prefix}:open`;
   disposables.add(commands.addCommand(command, {