Преглед на файлове

Merge pull request #2580 from ellisonbg/launcher-sections-logos

Finishes refactor/design of launcher
Brian E. Granger преди 7 години
родител
ревизия
a6dc6e6d3a

+ 2 - 1
packages/console-extension/src/index.ts

@@ -171,7 +171,8 @@ function activateConsole(app: JupyterLab, manager: IServiceManager, mainMenu: IM
           name,
           iconClass: 'jp-ImageCodeConsole',
           callback,
-          rank
+          rank,
+          kernelIconUrl: specs.kernelspecs[name].resources["logo-64x64"]
         });
       }
     });

+ 1 - 2
packages/filebrowser-extension/src/index.ts

@@ -372,8 +372,7 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker<FileBrowser>, mai
   app.restored.then(() => {
     if (app.shell.isEmpty('main')) {
       commands.execute('launcher:create', {
-        cwd: mainBrowser.model.path,
-        banner: true
+        cwd: mainBrowser.model.path
       });
     }
   });

+ 1 - 0
packages/launcher-extension/package.json

@@ -18,6 +18,7 @@
     "@jupyterlab/filebrowser": "^0.7.0",
     "@jupyterlab/launcher": "^0.7.0",
     "@jupyterlab/services": "^0.46.0",
+    "@phosphor/coreutils": "^1.1.1",
     "@phosphor/widgets": "^1.3.0"
   },
   "devDependencies": {

+ 6 - 3
packages/launcher-extension/src/index.ts

@@ -15,9 +15,12 @@ import {
 } from '@jupyterlab/apputils';
 
 import {
-  ILauncher, LauncherModel, LauncherWidget
+  ILauncher, LauncherModel, Launcher
 } from '@jupyterlab/launcher';
 
+import {
+  JSONObject
+} from '@phosphor/coreutils';
 
 import {
   Widget
@@ -66,14 +69,14 @@ function activate(app: JupyterLab, services: IServiceManager, palette: ICommandP
 
   commands.addCommand(CommandIDs.create, {
     label: 'New Launcher',
-    execute: (args) => {
+    execute: (args: JSONObject) => {
       let cwd = args['cwd'] ? String(args['cwd']) : '';
       let id = `launcher-${Private.id++}`;
       let callback = (item: Widget) => {
         shell.addToMainArea(item, { ref: id });
         shell.activateById(item.id);
       };
-      let widget = new LauncherWidget({ cwd, callback });
+      let widget = new Launcher({ cwd, callback });
       widget.model = model;
       widget.id = id;
       widget.title.label = 'Launcher';

+ 92 - 68
packages/launcher/src/index.ts → packages/launcher/src/index.tsx

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  ArrayExt, ArrayIterator, IIterator, map, toArray
+  ArrayExt, ArrayIterator, IIterator, map, each, toArray
 } from '@phosphor/algorithm';
 
 import {
@@ -21,9 +21,7 @@ import {
   Widget
 } from '@phosphor/widgets';
 
-import {
-  h, VirtualNode
-} from '@phosphor/virtualdom';
+import * as vdom from '@phosphor/virtualdom';
 
 import {
   VDomModel, VDomRenderer
@@ -50,31 +48,18 @@ export
 const ILauncher = new Token<ILauncher>('jupyter.services.launcher');
 /* tslint:enable */
 
+const h = vdom.h;
 
-/**
- * The class name added to LauncherWidget instances.
- */
-const LAUNCHER_CLASS = 'jp-LauncherWidget';
 
 /**
- * The class name added to LauncherWidget image nodes.
+ * The class name added to Launcher instances.
  */
-const IMAGE_CLASS = 'jp-LauncherWidget-image';
+const LAUNCHER_CLASS = 'jp-Launcher';
 
 /**
- * The class name added to LauncherWidget text nodes.
+ * The class name added to Launcher body nodes.
  */
-const TEXT_CLASS = 'jp-LauncherWidget-text';
-
-/**
- * The class name added to LauncherWidget item nodes.
- */
-const ITEM_CLASS = 'jp-LauncherWidget-item';
-
-/**
- * The class name added to LauncherWidget body nodes.
- */
-const BODY_CLASS = 'jp-LauncherWidget-body';
+// const BODY_CLASS = 'jp-Launcher-body';
 
 
 /**
@@ -165,12 +150,19 @@ interface ILauncherItem {
    * The default rank is `Infinity`.
    */
   rank?: number;
+
+  /**
+   * For items that hava 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;
 }
 
 
 /**
  * LauncherModel keeps track of the path to working directory and has a list of
- * LauncherItems, which the LauncherWidget will render.
+ * LauncherItems, which the Launcher will render.
  */
 export
 class LauncherModel extends VDomModel implements ILauncher {
@@ -219,15 +211,14 @@ class LauncherModel extends VDomModel implements ILauncher {
  * A virtual-DOM-based widget for the Launcher.
  */
 export
-class LauncherWidget extends VDomRenderer<LauncherModel> {
+class Launcher extends VDomRenderer<LauncherModel> {
   /**
    * Construct a new launcher widget.
    */
-  constructor(options: LauncherWidget.IOptions) {
+  constructor(options: Launcher.IOptions) {
     super();
     this.cwd = options.cwd;
     this._callback = options.callback;
-    this._header = options.header;
     this.addClass(LAUNCHER_CLASS);
   }
 
@@ -247,49 +238,68 @@ class LauncherWidget extends VDomRenderer<LauncherModel> {
   /**
    * Render the launcher to virtual DOM nodes.
    */
-  protected render(): VirtualNode | VirtualNode[] {
-    // Create an iterator that yields rendered item nodes.
-    let sorted = toArray(this.model.items()).sort(Private.sortCmp);
-    let items = map(sorted, item => {
-      let onclick = () => {
-        let callback = item.callback;
-        let value = callback(this.cwd, item.name);
-        Promise.resolve(value).then(widget => {
-          let callback = this._callback;
-          callback(widget);
-          this.dispose();
-        });
-      };
-      let imageClass = `${item.iconClass} ${IMAGE_CLASS}`;
-      let icon = h.div({ className: imageClass, onclick }, item.iconLabel);
-      let title = item.displayName + (item.category ? ' ' + item.category : '');
-      let text = h.span({className: TEXT_CLASS, onclick, title }, title);
-      return h.div({
-        className: ITEM_CLASS,
-      }, [icon, text]);
+  protected render(): vdom.VirtualNode | vdom.VirtualNode[] {
+    // First group-by categories
+    let categories = Object.create(null);
+    each(this.model.items(), (item, index) => {
+      let cat = item.category || "Other";
+      if (!(cat in categories)) {
+        categories[cat] = []
+      } 
+      categories[cat].push(item);
     });
-
-    let children: VirtualNode[];
-    if (this._header) {
-      children = [this._header].concat(toArray(items));
-    } else {
-      children = toArray(items);
+    // Within each category sort by rank
+    for (let cat in categories) {
+      categories[cat] = categories[cat].sort(Private.sortCmp);
     }
-    return h.div({ className: BODY_CLASS  }, children);
+
+    // Variable to help create sections
+    let sections: vdom.VirtualNode[] = [];
+    let section: vdom.VirtualNode;
+
+    let knownSections = ['Notebook', 'Console', 'Other'];
+    let kernelSections = ['Notebook', 'Console'];
+
+    each(knownSections, (cat, index) => {
+      let iconClass = `${(categories[cat][0] as ILauncherItem).iconClass} jp-Launcher-sectionIcon jp-Launcher-icon`;
+      let kernel = kernelSections.indexOf(cat) > -1;
+      if (cat in categories) {
+        section = (
+          <div className="jp-Launcher-section">
+            <div className="jp-Launcher-sectionHeader">
+              {kernel && <div className={iconClass} />}
+              <h2 className="jp-Launcher-sectionTitle">{cat}</h2>
+            </div>
+            <div className="jp-Launcher-cardContainer">
+              {toArray(map(categories[cat], item => Card(kernel, (item as ILauncherItem), this, this._callback)))}
+            </div>
+          </div>
+        );
+        sections.push(section);
+      }
+    })
+
+    return (
+      <div className="jp-Launcher-body">
+        <div className="jp-Launcher-content">
+        {sections}
+        </div>
+      </div>  
+    );
+      // vdom.h.div({ className: BODY_CLASS  }, sections);
   }
 
   private _callback: (widget: Widget) => void;
-  private _header: VirtualNode;
 }
 
 
 /**
- * The namespace for `LauncherWidget` class statics.
+ * The namespace for `Launcher` class statics.
  */
 export
-namespace LauncherWidget {
+namespace Launcher {
   /**
-   * The options used to create a LauncherWidget.
+   * The options used to create a Launcher.
    */
   export
   interface IOptions {
@@ -303,10 +313,6 @@ namespace LauncherWidget {
      */
     callback: (widget: Widget) => void;
 
-    /**
-     * An optional header virtual node.
-     */
-    header?: VirtualNode;
   }
 }
 
@@ -342,15 +348,33 @@ namespace Private {
       return r1 < r2 ? -1 : 1;  // Infinity safe
     }
 
-    // Next, compare based on category.
-    let d1 = a.category.localeCompare(b.category);
-    if (d1 !== 0) {
-      return d1;
-    }
-
     // Finally, compare by display name.
     return a.displayName.localeCompare(b.displayName);
   }
 }
 
-
+export
+function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcherCallback: (widget: Widget) => void): vdom.VirtualElement {
+  // Build the onclick handler.
+  let onclick = () => {
+    let callback = item.callback as any;
+    let value = callback(launcher.cwd, item.name);
+    Promise.resolve(value).then(widget => {
+      launcherCallback(widget);
+      launcher.dispose();
+    });
+  };
+  // Add a data attribute for the category
+  let dataset = {category: item.category};
+  // Return the VDOM element.
+  return (
+    <div className="jp-LauncherCard" title={item.displayName} onclick={onclick} dataset={dataset}>
+      <div className="jp-LauncherCard-icon">
+          {(item.kernelIconUrl && kernel) && <img src={item.kernelIconUrl} className="jp-Launcher-kernelIcon" />}
+          {(!item.kernelIconUrl && !kernel) && <div className={`${item.iconClass} jp-Launcher-icon`} />}
+          {(!item.kernelIconUrl && kernel) && <div className="jp-LauncherCard-noKernelIcon">{item.displayName[0].toUpperCase()}</div>}          
+      </div>
+      <div className="jp-LauncherCard-label">{item.displayName}</div>
+    </div>
+  );
+}

+ 4 - 0
packages/launcher/src/typings.d.ts

@@ -0,0 +1,4 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+/// <reference path="../../cells/typings/tsx/tsx.d.ts"/>

+ 134 - 76
packages/launcher/style/index.css

@@ -4,134 +4,192 @@
 |----------------------------------------------------------------------------*/
 
 
-.jp-LauncherWidget {
-  background: var(--md-grey-50);
-  display: flex;
-  flex-direction: column;
-  align-items: center;
+/* Private CSS variables */
+
+
+:root {
+  --jp-private-launcher-top-margin: 16px;
+  --jp-private-launcher-side-margin: 32px;
+  --jp-private-launcher-card-size: 100px;
+  --jp-private-launcher-card-label-height: 25px;
+  --jp-private-launcher-card-icon-height: 75px;
+  --jp-private-launcher-large-icon-size: 52px;
+  --jp-private-launcher-small-icon-size: 32px;
+  --jp-private-launcher-section-margin: 12px;
+}
+
+
+/* Launcher */
+
+.jp-Launcher {
   margin: 0;
   padding: 0;
-  overflow: auto;
   outline: none;
+  background: var(--md-grey-50);
+  box-sizing: border-box;
 }
 
 
-.jp-LauncherWidget::before {
-  content: 'Start a new activity';
+.jp-Launcher::before {
+  content: '';
   display: block;
-  height: 60px;
+  height: var(--jp-toolbar-micro-height);
   width: 100%;
   background: var(--jp-toolbar-background);
   border-bottom: 1px solid var(--jp-toolbar-border-color);
   box-shadow: var(--jp-toolbar-box-shadow);
-  z-index: 10;
-  text-align: center;
-  font-size: 24px;
-  padding-top: 36px;
+  z-index: 1;
 }
 
 
-.jp-LauncherWidget-body {
-  padding-bottom: 12px;
-  font-size: var(--jp-ui-font-size1);
-  color: var(--jp-ui-font-color1);
-  margin-left: auto;
-  margin-right: auto;
-  text-align: center;
-  display: flex;
-  align-items: center;
-  flex-direction: column;
-  flex-wrap: wrap;
+.jp-Launcher-body {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  overflow: auto;
 }
 
 
-.jp-LauncherWidget-logo {
-  flex: 0 0 50px;
-  margin-left: auto;
-  margin-right: auto;
-  width: 100%;
-  background-color: var(--jp-layout-color1);
-  background-size: 232px 50px;
-  background-repeat: no-repeat;
-  background-position: center;
-  margin-top: 20px;
+.jp-Launcher-content {
+  width: 80%;
+  height: 100%;
+  margin-top: var(--jp-private-launcher-top-margin);
+  margin-left: var(--jp-private-launcher-side-margin);
+  margin-right: var(--jp-private-launcher-side-margin);
+  box-sizing: border-box;
 }
 
 
-.jp-LauncherWidget-subtitle {
-  background-color: var(--jp-layout-color1);
-  font-size: var(--jp-ui-font-size2);
+/* Launcher section */
+
+.jp-Launcher-section {
   width: 100%;
-  margin-top: 4px;
-  font-weight: 300;
+  box-sizing: border-box;
+  padding-bottom: var(--jp-private-launcher-section-margin);
+}
+
+
+.jp-Launcher-sectionHeader {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  box-sizing: border-box;
+  /* This is custom tuned to get the section header to align with the cards */
+  padding-left: 5px;
 }
 
 
-.jp-LauncherWidget-header {
-  padding-top: 16px;
-  padding-bottom: 4px;
+.jp-Launcher-sectionHeader .jp-Launcher-sectionIcon {
+  box-sizing: border-box;
+  margin-right: 12px;
+  height: var(--jp-private-launcher-small-icon-size);
+  width: var(--jp-private-launcher-small-icon-size);
+  background-size: var(--jp-private-launcher-small-icon-size) var(--jp-private-launcher-small-icon-size);
 }
 
 
-.jp-LauncherWidget-body {
+.jp-Launcher-sectionTitle {
+  font-size: var(--jp-ui-font-size2);
+  font-weight: normal;
+  color: var(--jp-ui-font-color1);
+  box-sizing: border-box;
+}
+
+
+/* Launcher cards */
+
+.jp-Launcher-cardContainer {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
   display: flex;
   flex-direction: row;
-  margin-top: 8px;
-  justify-content: center;
-  align-items: center;
+  flex-wrap: wrap;
 }
 
 
-.jp-LauncherWidget-item {
+.jp-LauncherCard {
   display: flex;
   flex-direction: column;
   cursor: pointer;
-  width: 100px;
+  width: var(--jp-private-launcher-card-size);
+  height: var(--jp-private-launcher-card-size);
+  margin: 8px;
+  padding: 0px;
   border: 1px solid var(--md-grey-400);
   background: var(--jp-layout-color0);
-  height: 100px;
-  margin: 8px;
   box-shadow: 0px 1px 1px 1px rgba(0,0,0,.08);
-  padding: 12px;
   transition: .2s box-shadow;
 }
 
 
-.jp-LauncherWidget-item:hover {
+.jp-LauncherCard:hover {
   box-shadow: 0px 2px 4px 1px rgba(0,0,0,.20);
 }
 
 
-.jp-LauncherWidget-image {
-  display: block;
-  vertical-align: middle;
-  flex: 0 0 auto;
-  margin: 0 auto;
-  min-height: 68px;
-  min-width: 52px;
-  max-width: 52px;
-  background-size: 52px 68px;
-  background-repeat: no-repeat;
-  cursor: pointer;
+.jp-LauncherCard-icon {
+  width: 100%;
+  height: var(--jp-private-launcher-card-icon-height);
+  box-sizing: border-box;
+  margin: 0px;
+  padding: 0px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 
 
-.jp-LauncherWidget-text {
-  flex: 0 0 auto;
-  text-overflow: ellipsis;
-  order: 1;
-  vertical-align: middle;
-  margin: auto;
+.jp-LauncherCard-noKernelIcon {
+  font-weight: normal;
+  font-size: var(--jp-private-launcher-large-icon-size);
 }
 
 
-.jp-LauncherWidget-tour {
-  margin-top: 14px;
-  height: 14px;
+.jp-LauncherCard[data-category="Notebook"] .jp-LauncherCard-noKernelIcon {
+  /* This color is copied from the notebook icon. */
+  color: #EF6C00;
+}
+
+
+.jp-LauncherCard[data-category="Console"] .jp-LauncherCard-noKernelIcon {
+  /* This color is copied from the console icon. */
+  color: #0288D1;
+}
+
+
+.jp-LauncherCard-label {
   width: 100%;
-  cursor: pointer;
-  padding-bottom: 14px;
-  border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
-  background-position: top center;
+  height: var(--jp-private-launcher-card-label-height);
+  line-height: var(--jp-private-launcher-card-label-height);
+  font-size: var(--jp-ui-font-size1);
+  color: var(--jp-ui-font-color1);
+  box-sizing: border-box;
+  text-overflow: ellipsis;
+  text-align: center;
+}
+
+
+/* Icons, kernel icons */
+
+.jp-Launcher-icon {
+  margin: 0px;
+  padding: 0px;
+  height: var(--jp-private-launcher-large-icon-size);
+  width: var(--jp-private-launcher-large-icon-size);
+  background-size: var(--jp-private-launcher-large-icon-size) var(--jp-private-launcher-large-icon-size);
   background-repeat: no-repeat;
+  cursor: pointer;
+}
+
+
+.jp-Launcher-kernelIcon {
+  width: var(--jp-private-launcher-large-icon-size);
+  height: var(--jp-private-launcher-large-icon-size);
+  margin: 0px;
+  padding: 0px;
 }
+
+
+
+

+ 3 - 1
packages/launcher/tsconfig.json

@@ -9,7 +9,9 @@
     "target": "ES5",
     "outDir": "./lib",
     "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"],
-    "types": []
+    "types": [],
+    "jsx": "react",
+    "jsxFactory": "h"
   },
   "include": ["src/*"]
 }

+ 2 - 1
packages/notebook-extension/src/index.ts

@@ -451,7 +451,8 @@ function activateNotebookHandler(app: JupyterLab, services: IServiceManager, mai
           name,
           iconClass: 'jp-ImageNotebook',
           callback,
-          rank
+          rank,
+          kernelIconUrl: specs.kernelspecs[name].resources["logo-64x64"]
         });
       }
     });