Forráskód Böngészése

Merge pull request #1126 from afshin/launcher-supplemental

Launcher refactor
Steven Silvester 8 éve
szülő
commit
41dc661d21
5 módosított fájl, 223 hozzáadás és 135 törlés
  1. 1 0
      examples/lab/index.js
  2. 1 1
      jupyterlab/extensions.js
  3. 3 3
      src/launcher/index.css
  4. 180 76
      src/launcher/index.ts
  5. 38 55
      src/launcher/plugin.ts

+ 1 - 0
examples/lab/index.js

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

+ 1 - 1
jupyterlab/extensions.js

@@ -16,7 +16,7 @@ module.exports = [
   require('../lib/imagewidget/plugin').imageHandlerExtension,
   require('../lib/inspector/plugin').inspectorProvider,
   require('../lib/landing/plugin').landingExtension,
-  require('../lib/launcher/plugin').launcherExtension,
+  require('../lib/launcher/plugin').launcherProvider,
   require('../lib/main/plugin').mainExtension,
   require('../lib/mainmenu/plugin').mainMenuProvider,
   require('../lib/markdownwidget/plugin').markdownHandlerExtension,

+ 3 - 3
src/launcher/index.css

@@ -5,6 +5,7 @@
 
 
 .jp-LauncherWidget {
+  background: var(--jp-layout-color1);
   display: flex;
   flex-direction: column;
   flex-wrap: wrap;
@@ -18,7 +19,6 @@
   padding-bottom: 12px;
   font-size: var(--jp-ui-font-size1);
   color: var(--jp-ui-font-color1);
-  background: var(--jp-layout-color1);
   margin-left: auto;
   margin-right: auto;
   text-align: center;
@@ -26,7 +26,6 @@
   align-items: center;
   flex-direction: column;
   flex-wrap: wrap;
-  box-shadow: 0 2px 7px 1px #BDBDBD;
 }
 
 
@@ -51,6 +50,7 @@
   font-weight: 300;
 }
 
+
 .jp-LauncherWidget-header {
   padding-top: 16px;
   padding-bottom: 4px;
@@ -66,7 +66,7 @@
 }
 
 
-.jp-LauncherWidget-column {
+.jp-LauncherWidget-item {
   padding-left: 20px;
   padding-right: 20px;
   display: flex;

+ 180 - 76
src/launcher/index.ts

@@ -2,29 +2,42 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  Token
-} from 'phosphor/lib/core/token';
+  enumerate, IIterator, map, toArray
+} from 'phosphor/lib/algorithm/iteration';
 
 import {
   JSONObject
 } from 'phosphor/lib/algorithm/json';
 
+import {
+  Vector
+} from 'phosphor/lib/collections/vector';
+
 import {
   DisposableDelegate, IDisposable
 } from 'phosphor/lib/core/disposable';
 
 import {
-  h, VNode
-} from 'phosphor/lib/ui/vdom';
+  Message
+} from 'phosphor/lib/core/messaging';
 
 import {
-  JupyterLab
-} from '../application';
+  Token
+} from 'phosphor/lib/core/token';
+
+import {
+  CommandRegistry
+} from 'phosphor/lib/ui/commandregistry';
+
+import {
+  h, VNode
+} from 'phosphor/lib/ui/vdom';
 
 import {
   VDomModel, VDomWidget
 } from '../common/vdom';
 
+
 /* tslint:disable */
 /**
  * The launcher token.
@@ -50,9 +63,9 @@ const IMAGE_CLASS = 'jp-LauncherWidget-image';
 const TEXT_CLASS = 'jp-LauncherWidget-text';
 
 /**
- * The class name added to LauncherWidget column nodes.
+ * The class name added to LauncherWidget item nodes.
  */
-const COLUMN_CLASS = 'jp-LauncherWidget-column';
+const ITEM_CLASS = 'jp-LauncherWidget-item';
 
 /**
  * The class name added to LauncherWidget folder node.
@@ -86,53 +99,45 @@ const DIALOG_CLASS = 'jp-LauncherWidget-dialog';
 export
 interface ILauncher {
   /**
-   * Add a command item to the Launcher, and trigger re-render event for parent
+   * Add a command item to the launcher, and trigger re-render event for parent
    * widget.
    *
-   * @param name - The display name.
-   *
-   * @param action - The command that should be executed on clicking.
-   *
-   * @param args - Arguments to the `action` command.
-   *
-   * @param imgName - The CSS class to attach to the item. Defaults to
-   * 'jp-Image' followed by the `name` with spaces removed. So if the name is
-   * 'Launch New Terminal' the class name will be 'jp-ImageLaunchNewTerminal'.
+   * @param options - The specification options for a launcher item.
    *
    * @returns A disposable that will remove the item from Launcher, and trigger
    * re-render event for parent widget.
    *
    */
-  add(name: string, action: string, args?: JSONObject, imgName?: string): IDisposable ;
+  add(options: ILauncherItem): IDisposable;
 }
 
 
 /**
- * Simple encapsulation of name and callback of launcher entries. This is an
- * implementation detail of the LauncherModel. You should not need to use
- * this class directly, but use the `LauncherModel.add` method instead.
+ * The specification for a launcher item.
  */
 export
-class LauncherItem {
-
-  readonly name: string;
+interface ILauncherItem {
+  /**
+   * The display name of the launcher item.
+   */
+  name: string;
 
-  readonly clickCallback: () => void;
+  /**
+   * The ID of the command that is called to launch the item.
+   */
+  command: string;
 
-  readonly imgName: string;
+  /**
+   * The command arguments, if any, needed to launch the item.
+   */
+  args?: JSONObject;
 
   /**
-   * Construct a new launcher item.
+   * The image class name to attach to the launcher item. Defaults to
+   * 'jp-Image' followed by the `name` with spaces removed. So if the name is
+   * 'Launch New Terminal' the class name will be 'jp-ImageLaunchNewTerminal'.
    */
-  constructor(name: string, clickCallback: () => void, imgName?: string) {
-    this.name = name;
-    this.clickCallback = clickCallback;
-    if (imgName) {
-      this.imgName = imgName;
-    } else {
-      this.imgName = 'jp-Image' + name.replace(/\ /g, '');
-    }
-  }
+  imgClassName?: string;
 }
 
 
@@ -142,22 +147,20 @@ class LauncherItem {
  */
 export
 class LauncherModel extends VDomModel implements ILauncher {
-  items: LauncherItem[] = [];
-
-  add(name: string, action: string, args?: JSONObject, imgName?: string) : IDisposable {
-    let clickCallback = () => { this.app.commands.execute( action, args); };
-    let item = new LauncherItem(name, clickCallback, imgName);
-    this.items.push(item);
-    this.stateChanged.emit(void 0);
+  /**
+   * Create a new launcher model.
+   */
+  constructor(options: LauncherModel.IOptions) {
+    super();
+    this._commands = options.commands;
+    this._items = new Vector<ILauncherItem>();
+  }
 
-    return new DisposableDelegate(() => {
-      // Remove the item from the list of items.
-      let index = this.items.indexOf(item, 0);
-      if (index > -1) {
-          this.items.splice(index, 1);
-          this.stateChanged.emit(void 0);
-      }
-    });
+  /**
+   * The command registry.
+   */
+  get commands(): CommandRegistry {
+    return this._commands;
   }
 
   /**
@@ -167,22 +170,81 @@ class LauncherModel extends VDomModel implements ILauncher {
     return this._path;
   }
   set path(path: string) {
+    if (path === this._path) {
+      return;
+    }
     this._path = path;
     this.stateChanged.emit(void 0);
   }
 
   /**
-   * The JupyterLab application this launcher will use when executing
-   * commands.
+   * Add a command item to the launcher, and trigger re-render event for parent
+   * widget.
+   *
+   * @param options - The specification options for a launcher item.
+   *
+   * @returns A disposable that will remove the item from Launcher, and trigger
+   * re-render event for parent widget.
+   *
+   */
+  add(options: ILauncherItem): IDisposable {
+    // Create a copy of the options to circumvent mutations to the original.
+    let item = JSON.parse(JSON.stringify(options));
+
+    // If image class name is not set, use the default value.
+    item.imgClassName = item.imgClassName ||
+      `jp-Image${item.name.replace(/\ /g, '')}`;
+
+    this._items.pushBack(item);
+    this.stateChanged.emit(void 0);
+
+    return new DisposableDelegate(() => {
+      this._items.remove(item);
+      this.stateChanged.emit(void 0);
+    });
+  }
+
+  /**
+   * Execute the command of the launcher item at a specific index.
+   *
+   * @param index - The index of the launcher item to execute.
    */
-  get app() : JupyterLab {
-    return this._app;
+  execute(index: number): void {
+    let item = this._items.at(index);
+    if (!item) {
+      return;
+    }
+    this.commands.execute(item.command, item.args);
   }
-  set app(app: JupyterLab) {
-    this._app = app;
+
+  /**
+   * Return an iterator of launcher items.
+   */
+  items(): IIterator<ILauncherItem> {
+    return this._items.iter();
   }
+
+  private _commands: CommandRegistry = null;
+  private _items: Vector<ILauncherItem> = null;
   private _path: string = 'home';
-  private _app: JupyterLab;
+}
+
+
+/**
+ * A namespace for launcher model statics.
+ */
+export
+namespace LauncherModel {
+  /**
+   * The instantiation options for a launcher model.
+   */
+  export
+  interface IOptions {
+    /**
+     * The command registry instance that all launcher commands should use.
+     */
+    commands: CommandRegistry;
+  }
 }
 
 
@@ -191,7 +253,6 @@ class LauncherModel extends VDomModel implements ILauncher {
  */
 export
 class LauncherWidget extends VDomWidget<LauncherModel> {
-
   /**
    * Construct a new launcher widget.
    */
@@ -201,31 +262,74 @@ class LauncherWidget extends VDomWidget<LauncherModel> {
   }
 
   /**
-   * Render the launcher to virtual DOM nodes.
+   * Handle the DOM events for launcher widget.
+   *
+   * @param event - The DOM event sent to the widget.
+   *
+   * #### Notes
+   * This method implements the DOM `EventListener` interface and is
+   * called in response to events on the panel's DOM node. It should
+   * not be called directly by user code.
    */
-  protected render(): VNode | VNode[] {
-    let children : VNode[] = [];
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'click':
+      this._evtClick(event as MouseEvent);
+      break;
+    default:
+      return;
+    }
+  }
 
-    for (let item of this.model.items) {
-      let img = h.span({className: item.imgName + ' ' + IMAGE_CLASS});
-      let text = h.span({className:  TEXT_CLASS }, item.name);
+  /**
+   * Handle `after_attach` messages for the widget.
+   */
+  protected onAfterAttach(msg: Message): void {
+    super.onAfterAttach(msg);
+    this.node.addEventListener('click', this);
+  }
 
-      let column = h.div({
-        className: COLUMN_CLASS,
-        'onclick': item.clickCallback
-      }, [img, text]);
-      children.push(column);
-    }
+  /**
+   * Handle `before_detach` messages for the widget.
+   */
+  protected onBeforeDetach(msg: Message): void {
+    super.onBeforeDetach(msg);
+    this.node.removeEventListener('click', this);
+  }
+
+  /**
+   * Render the launcher to virtual DOM nodes.
+   */
+  protected render(): VNode | VNode[] {
+    // Create an iterator that yields rendered item nodes.
+    let children = map(enumerate(this.model.items()), ([index, item]) => {
+      let img = h.span({className: item.imgClassName + ' ' + IMAGE_CLASS});
+      let text = h.span({className: TEXT_CLASS }, item.name);
+      return h.div({ className: ITEM_CLASS, dataset: { index } }, [img, text]);
+    });
 
     let folderImage = h.span({ className: FOLDER_CLASS });
     let p = this.model.path;
-    let pathName = p.length ? 'home > ' + p.replace(/\//g, ' > ') : 'home';
+    let pathName = p.length ? `home > ${p.replace(/\//g, ' > ')}` : 'home';
     let path = h.span({ className: PATH_CLASS }, pathName );
-
     let cwd = h.div({ className: CWD_CLASS }, [folderImage, path]);
-    let body = h.div({ className: BODY_CLASS  }, children);
+    let body = h.div({ className: BODY_CLASS  }, toArray(children));
 
-    return h.div({ className: DIALOG_CLASS}, [ cwd, body ]);
+    return h.div({ className: DIALOG_CLASS }, [cwd, body]);
+  }
 
+  /**
+   * Handle click events on the widget.
+   */
+  private _evtClick(event: MouseEvent) {
+    let target = event.target as HTMLElement;
+    while (target !== this.node) {
+      if (target.classList.contains(ITEM_CLASS)) {
+        let index = parseInt(target.getAttribute('data-index'), 10);
+        this.model.execute(index);
+        return;
+      }
+      target = target.parentElement;
+    }
   }
 }

+ 38 - 55
src/launcher/plugin.ts

@@ -1,10 +1,6 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import {
-  JSONObject
-} from 'phosphor/lib/algorithm/json';
-
 import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
@@ -22,7 +18,7 @@ import {
 } from '../services';
 
 import {
-  ILauncher, LauncherModel, LauncherWidget
+  ILauncher, ILauncherItem, LauncherModel, LauncherWidget
 } from './';
 
 
@@ -30,8 +26,8 @@ import {
  * A service providing an interface to the the launcher.
  */
 export
-const launcherExtension: JupyterLabPlugin<ILauncher> = {
-  id: 'jupyter.extensions.launcher',
+const launcherProvider: JupyterLabPlugin<ILauncher> = {
+  id: 'jupyter.services.launcher',
   requires: [IServiceManager, IPathTracker, ICommandPalette],
   provides: ILauncher,
   activate: activateLauncher,
@@ -43,67 +39,54 @@ const launcherExtension: JupyterLabPlugin<ILauncher> = {
  * Activate the launcher.
  */
 function activateLauncher(app: JupyterLab, services: IServiceManager, pathTracker: IPathTracker, palette: ICommandPalette): ILauncher {
-  let launcherModel = new LauncherModel();
+  let model = new LauncherModel({ commands: app.commands });
 
-  launcherModel.path = pathTracker.path;
-  launcherModel.app = app;
-
-  pathTracker.pathChanged.connect(() => {
-    launcherModel.path = pathTracker.path;
-  });
+  // Set launcher path and track the path as it changes.
+  model.path = pathTracker.path;
+  pathTracker.pathChanged.connect(() => { model.path = pathTracker.path; });
 
-  let launcherWidget = new LauncherWidget();
+  let widget = new LauncherWidget();
 
-  launcherWidget.model = launcherModel;
-  launcherWidget.id = 'launcher-jupyterlab-widget';
-  launcherWidget.title.label = 'Launcher';
+  widget.model = model;
+  widget.id = 'launcher';
+  widget.title.label = 'Launcher';
 
   // Hardcoded defaults.
-  let names = [
-    'Notebook',
-    'Code Console',
-    'Terminal',
-    'Text Editor',
-  ];
-
-  let actions = [
-    'file-operations:new-notebook',
-    'console:create-new',
-    'terminal:create-new',
-    'file-operations:new-text-file',
-  ];
-
-  app.commands.addCommand('jupyterlab-launcher:add-item', {
-    label: 'Add Launcher Item',
-    execute: (args) => {
-      launcherModel.add(args['name'] as string, args['action'] as string,
-                        args['args'] as JSONObject, args['imgName'] as string);
+  let defaults: ILauncherItem[] = [
+    {
+      name: 'Notebook',
+      command: 'file-operations:new-notebook'
+    },
+    {
+      name: 'Code Console',
+      command: 'console:create'
+    },
+    {
+      name: 'Terminal',
+      command: 'terminal:create-new'
+    },
+    {
+      name: 'Text Editor',
+      command: 'file-operations:new-text-file'
     }
-  });
-
-  for (let i in names) {
-    // Note: we do not retain a handle on the items added by default, which
-    // means we have to way of removing them after the fact.
-    launcherModel.add(names[i], actions[i]);
-  }
+  ];
 
+  // Note: we do not retain a handle on the items added by default, which
+  // means we have to way of removing them after the fact.
+  defaults.forEach(options => { model.add(options); });
 
-  app.commands.addCommand('jupyterlab-launcher:show', {
+  app.commands.addCommand('launcher:show', {
     label: 'Show Launcher',
     execute: () => {
-      if (!launcherWidget.isAttached) {
-        app.shell.addToLeftArea(launcherWidget);
+      if (!widget.isAttached) {
+        app.shell.addToLeftArea(widget);
       }
-      app.shell.activateLeft(launcherWidget.id);
+      app.shell.activateLeft(widget.id);
     }
   });
+  palette.addItem({ command: 'launcher:show', category: 'Help' });
 
-  palette.addItem({
-    command: 'jupyterlab-launcher:show',
-    category: 'Help'
-  });
+  app.shell.addToLeftArea(widget);
 
-  app.shell.addToLeftArea(launcherWidget);
-  return launcherModel;
+  return model;
 }
-