浏览代码

Refactor launcher to use CommandRegistry.

Ian Rose 6 年之前
父节点
当前提交
0a49161927

+ 17 - 14
packages/console-extension/src/index.ts

@@ -6,7 +6,7 @@ import {
 } from '@jupyterlab/application';
 
 import {
-  Dialog, ICommandPalette, InstanceTracker, showDialog
+  Dialog, IClientSession, ICommandPalette, InstanceTracker, showDialog
 } from '@jupyterlab/apputils';
 
 import {
@@ -164,11 +164,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand
     when: manager.ready
   });
 
-  // The launcher callback.
-  let callback = (cwd: string, name: string) => {
-    return createConsole({ basePath: cwd, kernelPreference: { name }});
-  };
-
   // Add a launcher item if the launcher is available.
   if (launcher) {
     manager.ready.then(() => {
@@ -178,7 +173,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand
       }
       let baseUrl = PageConfig.getBaseUrl();
       for (let name in specs.kernelspecs) {
-        let displayName = specs.kernelspecs[name].display_name;
         let rank = name === specs.default ? 0 : Infinity;
         let kernelIconUrl = specs.kernelspecs[name].resources['logo-64x64'];
         if (kernelIconUrl) {
@@ -186,11 +180,9 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand
           kernelIconUrl = baseUrl + kernelIconUrl.slice(index);
         }
         launcher.add({
-          displayName,
+          command: CommandIDs.create,
+          args: { isLauncher: true, kernelPreference: { name } },
           category: 'Console',
-          name,
-          iconClass: 'jp-CodeConsoleIcon',
-          callback,
           rank,
           kernelIconUrl
         });
@@ -265,9 +257,20 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand
 
   command = CommandIDs.create;
   commands.addCommand(command, {
-    label: args => args['isPalette'] ? 'New Console' : 'Console',
-    execute: (args: Partial<ConsolePanel.IOptions>) => {
-      let basePath = args.basePath || browserFactory.defaultBrowser.model.path;
+    label: args => {
+      if (args['isPalette']) {
+        return 'New Console';
+      } else if (args['isLauncher'] && args['kernelPreference']) {
+        const kernelPreference =
+          args['kernelPreference'] as IClientSession.IKernelPreference;
+        return manager.specs.kernelspecs[kernelPreference.name].display_name;
+      }
+      return 'Console';
+    },
+    iconClass: 'jp-CodeConsoleIcon',
+    execute: args => {
+      let basePath = args['basePath'] as string || args['cwd'] as string ||
+        browserFactory.defaultBrowser.model.path;
       return createConsole({ basePath, ...args });
     }
   });

+ 5 - 6
packages/fileeditor-extension/src/index.ts

@@ -376,20 +376,19 @@ function activate(app: JupyterLab, consoleTracker: IConsoleTracker, editorServic
   commands.addCommand(CommandIDs.createNew, {
     label: 'Text File',
     caption: 'Create a new text file',
-    execute: () => {
-      let cwd = browserFactory.defaultBrowser.model.path;
-      return createNew(cwd);
+    iconClass: EDITOR_ICON_CLASS,
+    execute: args => {
+      let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path;
+      return createNew(cwd as string);
     }
   });
 
   // Add a launcher item if the launcher is available.
   if (launcher) {
     launcher.add({
-      displayName: 'Text Editor',
+      command: CommandIDs.createNew,
       category: 'Other',
       rank: 1,
-      iconClass: EDITOR_ICON_CLASS,
-      callback: createNew
     });
   }
 

+ 1 - 1
packages/launcher-extension/src/index.ts

@@ -70,7 +70,7 @@ function activate(app: JupyterLab, palette: ICommandPalette): ILauncher {
       const callback = (item: Widget) => {
         shell.addToMainArea(item, { ref: id });
       };
-      const launcher = new Launcher({ cwd, callback });
+      const launcher = new Launcher({ cwd, callback, commands });
 
       launcher.model = model;
       launcher.title.label = 'Launcher';

+ 1 - 0
packages/launcher/package.json

@@ -32,6 +32,7 @@
   "dependencies": {
     "@jupyterlab/apputils": "^0.16.3",
     "@phosphor/algorithm": "^1.1.2",
+    "@phosphor/commands": "^1.5.0",
     "@phosphor/coreutils": "^1.3.0",
     "@phosphor/disposable": "^1.1.2",
     "@phosphor/properties": "^1.1.2",

+ 111 - 125
packages/launcher/src/index.tsx

@@ -10,7 +10,11 @@ import {
 } from '@phosphor/algorithm';
 
 import {
-  Token
+  CommandRegistry
+} from '@phosphor/commands';
+
+import {
+  Token, ReadonlyJSONObject
 } from '@phosphor/coreutils';
 
 import {
@@ -47,17 +51,6 @@ const KNOWN_CATEGORIES = ['Notebook', 'Console', 'Other'];
 const KERNEL_CATEGORIES = ['Notebook', 'Console'];
 
 
-/**
- * The command IDs used by the launcher plugin.
- */
-export
-namespace CommandIDs {
-  export
-  const show: string = 'launcher:show';
-}
-
-
-
 /* tslint:disable */
 /**
  * The launcher token.
@@ -82,91 +75,7 @@ interface ILauncher {
    * re-render event for parent widget.
    *
    */
-  add(options: ILauncherItem): IDisposable;
-}
-
-
-/**
- * The specification for a launcher item.
- */
-export
-interface ILauncherItem {
-  /**
-   * The display name for the launcher item.
-   */
-  displayName: string;
-
-  /**
-   * The callback invoked to launch the item.
-   *
-   * The callback is invoked with a current working directory and the
-   * name of the selected launcher item.  When the function returns
-   * the launcher will close.
-   *
-   * #### Notes
-   * The callback must return the widget that was created so the launcher
-   * can replace itself with the created widget.
-   */
-  callback: (cwd: string, name: string) => Widget | Promise<Widget>;
-
-  /**
-   * The icon class for the launcher item.
-   *
-   * #### Notes
-   * This class name will be added to the icon node for the visual
-   * representation of the launcher item.
-   *
-   * Multiple class names can be separated with white space.
-   *
-   * The default value is an empty string.
-   */
-  iconClass?: string;
-
-  /**
-   * The icon label for the launcher item.
-   *
-   * #### Notes
-   * This label will be added as text to the icon node for the visual
-   * representation of the launcher item.
-   *
-   * The default value is an empty string.
-   */
-  iconLabel?: string;
-
-  /**
-   * The identifier for the launcher item.
-   *
-   * The default value is the displayName.
-   */
-  name?: string;
-
-  /**
-   * The category for the launcher item.
-   *
-   * The default value is the an empty string.
-   */
-  category?: string;
-
-  /**
-   * The rank for the launcher item.
-   *
-   * The rank is used when ordering launcher items for display. After grouping
-   * into categories, items are sorted in the following order:
-   *   1. Rank (lower is better)
-   *   3. Display Name (locale order)
-   *
-   * The default rank is `Infinity`.
-   */
-  rank?: number;
-
-  /**
-   * For items that have a kernel associated with them, the URL of the kernel
-   * icon.
-   *
-   * This is not a CSS class, but the URL that points to the icon in the kernel
-   * spec.
-   */
-  kernelIconUrl?: string;
+  add(options: ILauncher.IItemOptions): IDisposable;
 }
 
 
@@ -193,7 +102,7 @@ class LauncherModel extends VDomModel implements ILauncher {
    * re-render event for parent widget.
    *
    */
-  add(options: ILauncherItem): IDisposable {
+  add(options: ILauncher.IItemOptions): IDisposable {
     // Create a copy of the options to circumvent mutations to the original.
     let item = Private.createItem(options);
 
@@ -209,11 +118,11 @@ class LauncherModel extends VDomModel implements ILauncher {
   /**
    * Return an iterator of launcher items.
    */
-  items(): IIterator<ILauncherItem> {
+  items(): IIterator<ILauncher.IItemOptions> {
     return new ArrayIterator(this._items);
   }
 
-  private _items: ILauncherItem[] = [];
+  private _items: ILauncher.IItemOptions[] = [];
 }
 
 
@@ -225,10 +134,11 @@ class Launcher extends VDomRenderer<LauncherModel> {
   /**
    * Construct a new launcher widget.
    */
-  constructor(options: Launcher.IOptions) {
+  constructor(options: ILauncher.IOptions) {
     super();
     this._cwd = options.cwd;
     this._callback = options.callback;
+    this._commands = options.commands;
     this.addClass(LAUNCHER_CLASS);
   }
 
@@ -273,7 +183,10 @@ class Launcher extends VDomRenderer<LauncherModel> {
     });
     // Within each category sort by rank
     for (let cat in categories) {
-      categories[cat] = categories[cat].sort(Private.sortCmp);
+      categories[cat] = categories[cat]
+        .sort((a: ILauncher.IItemOptions, b: ILauncher.IItemOptions) => {
+          return Private.sortCmp(a, b, this._cwd, this._commands);
+        });
     }
 
     // Variable to help create sections
@@ -293,8 +206,10 @@ class Launcher extends VDomRenderer<LauncherModel> {
     }
 
     // Now create the sections for each category
-    each(orderedCategories, cat => {
-      let iconClass = `${(categories[cat][0] as ILauncherItem).iconClass} ` +
+    orderedCategories.forEach(cat => {
+      const item = categories[cat][0] as ILauncher.IItemOptions;
+      let iconClass =
+        `${this._commands.iconClass(item.command, {...item.args, cwd: this.cwd})} ` +
         'jp-Launcher-sectionIcon jp-Launcher-icon';
       let kernel = KERNEL_CATEGORIES.indexOf(cat) > -1;
       if (cat in categories) {
@@ -305,8 +220,8 @@ class Launcher extends VDomRenderer<LauncherModel> {
               <h2 className='jp-Launcher-sectionTitle'>{cat}</h2>
             </div>
             <div className='jp-Launcher-cardContainer'>
-              {toArray(map(categories[cat], (item: ILauncherItem) => {
-                return Card(kernel, item, this, this._callback);
+              {toArray(map(categories[cat], (item: ILauncher.IItemOptions) => {
+                return Card(kernel, item, this, this._commands, this._callback);
               }))}
             </div>
           </div>
@@ -328,6 +243,7 @@ class Launcher extends VDomRenderer<LauncherModel> {
     );
   }
 
+  private _commands: CommandRegistry;
   private _callback: (widget: Widget) => void;
   private _pending = false;
   private _cwd = '';
@@ -335,10 +251,10 @@ class Launcher extends VDomRenderer<LauncherModel> {
 
 
 /**
- * The namespace for `Launcher` class statics.
+ * The namespace for `ILauncher` class statics.
  */
 export
-namespace Launcher {
+namespace ILauncher {
   /**
    * The options used to create a Launcher.
    */
@@ -349,11 +265,75 @@ namespace Launcher {
      */
     cwd: string;
 
+    /**
+     * The command registry used by the launcher.
+     */
+    commands: CommandRegistry;
+
     /**
      * The callback used when an item is launched.
      */
     callback: (widget: Widget) => void;
   }
+
+  /**
+   * The options used to create a launcher item.
+   */
+  export
+  interface IItemOptions {
+    /**
+     * The command ID for the launcher item.
+     *
+     * #### Notes
+     * The command's `execute` method should return
+     * the widget that was created so that the launcher
+     * can replace itself with the widget in the correct
+     * location.
+     */
+    command: string;
+
+    /**
+     * The arguments given to the command for
+     * creating the launcher item.
+     *
+     * ### Notes
+     * The launcher will also add the current working
+     * directory of the filebrowser in the `cwd` field
+     * of the args, which a command may use to create
+     * the activity with respect to the right directory.
+     */
+    args?: ReadonlyJSONObject;
+
+    /**
+     * The category for the launcher item.
+     *
+     * The default value is the an empty string.
+     */
+    category?: string;
+
+    /**
+     * The rank for the launcher item.
+     *
+     * The rank is used when ordering launcher items for display. After grouping
+     * into categories, items are sorted in the following order:
+     *   1. Rank (lower is better)
+     *   3. Display Name (locale order)
+     *
+     * The default rank is `Infinity`.
+     */
+    rank?: number;
+
+    /**
+     * For items that have a kernel associated with them, the URL of the kernel
+     * icon.
+     *
+     * This is not a CSS class, but the URL that points to the icon in the kernel
+     * spec.
+     */
+    kernelIconUrl?: string;
+  }
+
+
 }
 
 
@@ -370,7 +350,12 @@ namespace Launcher {
  *
  * @returns a vdom `VirtualElement` for the launcher card.
  */
-function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcherCallback: (widget: Widget) => void): React.ReactElement<any> {
+function Card(kernel: boolean, item: ILauncher.IItemOptions, launcher: Launcher, commands: CommandRegistry, launcherCallback: (widget: Widget) => void): React.ReactElement<any> {
+  // Get some properties of the command
+  const command = item.command;
+  const args = {...item.args, cwd: launcher.cwd};
+  const label = commands.label(command, args);
+
   // Build the onclick handler.
   let onclick = () => {
     // If an item has already been launched,
@@ -379,11 +364,13 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher
       return;
     }
     launcher.pending = true;
-    let callback = item.callback as any;
-    let value = callback(launcher.cwd, item.name);
+    let value = commands.execute(command, {
+      ...item.args,
+      cwd: launcher.cwd
+    });
     Promise.resolve(value).then(widget => {
       if (!widget) {
-        throw new Error('Launcher callbacks must resolve with a widget');
+        throw new Error('Launcher commands must resolve with a widget');
       }
       launcherCallback(widget);
       launcher.dispose();
@@ -396,7 +383,7 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher
   // Return the VDOM element.
   return (
     <div className='jp-LauncherCard'
-      title={item.displayName}
+      title={label}
       onClick={onclick}
       data-category={item.category || 'Other'}
       key={Private.keyProperty.get(item)}>
@@ -404,14 +391,14 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher
           {(item.kernelIconUrl && kernel) &&
             <img src={item.kernelIconUrl} className='jp-Launcher-kernelIcon' />}
           {(!item.kernelIconUrl && !kernel) &&
-            <div className={`${item.iconClass} jp-Launcher-icon`} />}
+            <div className={`${commands.iconClass(command, args)} jp-Launcher-icon`} />}
           {(!item.kernelIconUrl && kernel) &&
             <div className='jp-LauncherCard-noKernelIcon'>
-              {item.displayName[0].toUpperCase()}
+              {label[0].toUpperCase()}
             </div>}
       </div>
-      <div className='jp-LauncherCard-label' title={item.displayName}>
-        {item.displayName}
+      <div className='jp-LauncherCard-label' title={label}>
+        {label}
       </div>
     </div>
   );
@@ -431,22 +418,19 @@ namespace Private {
    * An attached property for an item's key.
    */
   export
-  const keyProperty = new AttachedProperty<ILauncherItem, number>({
+  const keyProperty = new AttachedProperty<ILauncher.IItemOptions, number>({
     name: 'key',
     create: () => id++
   });
 
   /**
-   * Create an item given item options.
+   * Create a fully specified item given item options.
    */
   export
-  function createItem(options: ILauncherItem): ILauncherItem {
+  function createItem(options: ILauncher.IItemOptions): ILauncher.IItemOptions {
     return {
       ...options,
       category: options.category || '',
-      name: options.name || options.name,
-      iconClass: options.iconClass || '',
-      iconLabel: options.iconLabel || '',
       rank: options.rank !== undefined ? options.rank : Infinity
     };
   }
@@ -455,7 +439,7 @@ namespace Private {
    * A sort comparison function for a launcher item.
    */
   export
-  function sortCmp(a: ILauncherItem, b: ILauncherItem): number {
+  function sortCmp(a: ILauncher.IItemOptions, b: ILauncher.IItemOptions, cwd: string, commands: CommandRegistry): number {
     // First, compare by rank.
     let r1 = a.rank;
     let r2 = b.rank;
@@ -464,6 +448,8 @@ namespace Private {
     }
 
     // Finally, compare by display name.
-    return a.displayName.localeCompare(b.displayName);
+    const aLabel = commands.label(a.command, { ...a.args, cwd });
+    const bLabel = commands.label(a.command, { ...b.args, cwd });
+    return aLabel.localeCompare(bLabel);
   }
 }

+ 15 - 10
packages/notebook-extension/src/index.ts

@@ -515,14 +515,22 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette:
     });
   };
 
-  // Add a command for creating a new notebook in the File Menu.
+  // Add a command for creating a new notebook.
   commands.addCommand(CommandIDs.createNew, {
-    label: 'Notebook',
+    label: args => {
+      const kernelName = args['kernelName'] as string || '';
+      if (args['isLauncher'] && args['kernelName']) {
+        return services.specs.kernelspecs[kernelName].display_name;
+      }
+      return 'Notebook';
+    },
     caption: 'Create a new notebook',
-    execute: () => {
-      let cwd = browserFactory ?
+    iconClass: 'jp-NotebookIcon',
+    execute: args => {
+      const cwd = args['cwd'] || browserFactory ?
         browserFactory.defaultBrowser.model.path : '';
-      return createNew(cwd);
+      const kernelName = args['kernelName'] as string || '';
+      return createNew(cwd, kernelName);
     }
   });
 
@@ -534,7 +542,6 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette:
       const baseUrl = PageConfig.getBaseUrl();
 
       for (let name in specs.kernelspecs) {
-        let displayName = specs.kernelspecs[name].display_name;
         let rank = name === specs.default ? 0 : Infinity;
         let kernelIconUrl = specs.kernelspecs[name].resources['logo-64x64'];
         if (kernelIconUrl) {
@@ -542,11 +549,9 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette:
           kernelIconUrl = baseUrl + kernelIconUrl.slice(index);
         }
         launcher.add({
-          displayName,
+          command: CommandIDs.createNew,
+          args: { isLauncher: true, kernelName: name },
           category: 'Notebook',
-          name,
-          iconClass: 'jp-NotebookIcon',
-          callback: createNew,
           rank,
           kernelIconUrl
         });

+ 3 - 4
packages/terminal-extension/src/index.ts

@@ -79,7 +79,7 @@ export default plugin;
  * Activate the terminal plugin.
  */
 function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, restorer: ILayoutRestorer, launcher: ILauncher | null): ITerminalTracker {
-  const { commands, serviceManager } = app;
+  const { serviceManager } = app;
   const category = 'Terminal';
   const namespace = 'terminal';
   const tracker = new InstanceTracker<MainAreaWidget<Terminal>>({ namespace });
@@ -124,11 +124,9 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette
   // Add a launcher item if the launcher is available.
   if (launcher) {
     launcher.add({
-      displayName: 'Terminal',
+      command: CommandIDs.createNew,
       category: 'Other',
       rank: 0,
-      iconClass: TERMINAL_ICON_CLASS,
-      callback: () => commands.execute(CommandIDs.createNew)
     });
   }
 
@@ -157,6 +155,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Instanc
   commands.addCommand(CommandIDs.createNew, {
     label: args => args['isPalette'] ? 'New Terminal' : 'Terminal',
     caption: 'Start a new terminal session',
+    iconClass: TERMINAL_ICON_CLASS,
     execute: args => {
       const name = args['name'] as string;
       const initialCommand = args['initialCommand'] as string;