Browse Source

wip add property inspector

integrity

finish property inspector

lint

integrity

Strict null handling

unused import

Address review comments
Steven Silvester 5 years ago
parent
commit
708944a096

+ 3 - 0
buildutils/src/ensure-package.ts

@@ -193,6 +193,9 @@ export async function ensurePackage(
 
     // write out cssIndexContents, if needed
     const cssIndexPath = path.join(pkgPath, 'style/index.css');
+    if (!fs.existsSync(cssIndexPath)) {
+      fs.ensureFileSync(cssIndexPath);
+    }
     messages.push(...ensureFile(cssIndexPath, cssIndexContents, false));
   }
 

+ 2 - 0
dev_mode/package.json

@@ -143,6 +143,7 @@
     "@jupyterlab/observables": "~3.0.0-alpha.4",
     "@jupyterlab/outputarea": "~2.0.0-alpha.4",
     "@jupyterlab/pdf-extension": "~2.0.0-alpha.4",
+    "@jupyterlab/property-inspector": "~2.0.0-alpha.4",
     "@jupyterlab/rendermime": "~2.0.0-alpha.4",
     "@jupyterlab/rendermime-extension": "~2.0.0-alpha.4",
     "@jupyterlab/rendermime-interfaces": "~2.0.0-alpha.4",
@@ -328,6 +329,7 @@
       "@jupyterlab/observables": "../packages/observables",
       "@jupyterlab/outputarea": "../packages/outputarea",
       "@jupyterlab/pdf-extension": "../packages/pdf-extension",
+      "@jupyterlab/property-inspector": "../packages/property-inspector",
       "@jupyterlab/rendermime": "../packages/rendermime",
       "@jupyterlab/rendermime-extension": "../packages/rendermime-extension",
       "@jupyterlab/rendermime-interfaces": "../packages/rendermime-interfaces",

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

@@ -39,6 +39,7 @@
     "@jupyterlab/application": "^2.0.0-alpha.4",
     "@jupyterlab/apputils": "^2.0.0-alpha.4",
     "@jupyterlab/coreutils": "^4.0.0-alpha.4",
+    "@jupyterlab/property-inspector": "^2.0.0-alpha.4",
     "@lumino/algorithm": "^1.2.1",
     "@lumino/widgets": "^1.9.4",
     "react": "~16.9.0"

+ 25 - 1
packages/application-extension/src/index.tsx

@@ -31,6 +31,11 @@ import {
   URLExt
 } from '@jupyterlab/coreutils';
 
+import {
+  IPropertyInspectorProvider,
+  SideBarPropertyInspectorProvider
+} from '@jupyterlab/property-inspector';
+
 import { each, iter, toArray } from '@lumino/algorithm';
 
 import { Widget, DockLayout } from '@lumino/widgets';
@@ -765,6 +770,24 @@ const paths: JupyterFrontEndPlugin<JupyterFrontEnd.IPaths> = {
   provides: JupyterFrontEnd.IPaths
 };
 
+/**
+ * Initialization data for the property_inspector extension.
+ */
+const propertyInspector: JupyterFrontEndPlugin<IPropertyInspectorProvider> = {
+  id: '@jupyterlab/application-extension:property-inspector',
+  autoStart: true,
+  requires: [ILabShell],
+  provides: IPropertyInspectorProvider,
+  activate: (app: JupyterFrontEnd, labshell: ILabShell) => {
+    const widget = new SideBarPropertyInspectorProvider(labshell);
+    widget.title.iconClass = 'jp-BuildIcon jp-SideBar-tabIcon';
+    widget.title.caption = 'Property Inspector';
+    widget.id = 'jp-property-inspector';
+    labshell.add(widget, 'left');
+    return widget;
+  }
+};
+
 /**
  * Export the plugins as default.
  */
@@ -779,7 +802,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   shell,
   status,
   info,
-  paths
+  paths,
+  propertyInspector
 ];
 
 export default plugins;

+ 5 - 0
packages/application-extension/style/base.css

@@ -0,0 +1,5 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) 2014-2017, Jupyter Development Team.
+|
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/

+ 6 - 2
packages/application-extension/style/index.css

@@ -1,7 +1,11 @@
 /*-----------------------------------------------------------------------------
-| Copyright (c) 2014-2017, Jupyter Development Team.
-|
+| Copyright (c) Jupyter Development Team.
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
+/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
+@import url('~@lumino/widgets/style/index.css');
 @import url('~@jupyterlab/application/style/index.css');
+@import url('~@jupyterlab/property-inspector/style/index.css');
+
+@import url('./base.css');

+ 3 - 0
packages/application-extension/tsconfig.json

@@ -14,6 +14,9 @@
     },
     {
       "path": "../coreutils"
+    },
+    {
+      "path": "../property-inspector"
     }
   ]
 }

+ 1 - 0
packages/metapackage/package.json

@@ -86,6 +86,7 @@
     "@jupyterlab/observables": "^3.0.0-alpha.4",
     "@jupyterlab/outputarea": "^2.0.0-alpha.4",
     "@jupyterlab/pdf-extension": "^2.0.0-alpha.4",
+    "@jupyterlab/property-inspector": "^2.0.0-alpha.4",
     "@jupyterlab/rendermime": "^2.0.0-alpha.4",
     "@jupyterlab/rendermime-extension": "^2.0.0-alpha.4",
     "@jupyterlab/rendermime-interfaces": "^2.0.0-alpha.4",

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -168,6 +168,9 @@
     {
       "path": "../pdf-extension"
     },
+    {
+      "path": "../property-inspector"
+    },
     {
       "path": "../rendermime"
     },

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

@@ -46,6 +46,7 @@
     "@jupyterlab/launcher": "^2.0.0-alpha.4",
     "@jupyterlab/mainmenu": "^2.0.0-alpha.4",
     "@jupyterlab/notebook": "^2.0.0-alpha.4",
+    "@jupyterlab/property-inspector": "^2.0.0-alpha.4",
     "@jupyterlab/rendermime": "^2.0.0-alpha.4",
     "@jupyterlab/services": "^5.0.0-alpha.4",
     "@jupyterlab/statusbar": "^2.0.0-alpha.4",

+ 16 - 46
packages/notebook-extension/src/index.ts

@@ -70,13 +70,15 @@ import { ServiceManager } from '@jupyterlab/services';
 
 import { IStatusBar } from '@jupyterlab/statusbar';
 
-import { ReadonlyJSONObject, JSONValue } from '@lumino/coreutils';
+import { JSONValue } from '@lumino/coreutils';
 
 import { Message, MessageLoop } from '@lumino/messaging';
 
 import { Panel, Menu } from '@lumino/widgets';
 import { CommandRegistry } from '@lumino/commands';
 
+import { IPropertyInspectorProvider } from '@jupyterlab/property-inspector';
+
 /**
  * The command IDs used by the notebook plugin.
  */
@@ -226,11 +228,6 @@ const NOTEBOOK_ICON_CLASS = 'jp-NotebookIcon';
  */
 const FACTORY = 'Notebook';
 
-/**
- * The rank of the notebook tools tab in the sidebar
- */
-const NOTEBOOK_TOOLS_RANK = 400;
-
 /**
  * The exluded Export To ...
  * (returned from nbconvert's export list)
@@ -302,8 +299,12 @@ const tools: JupyterFrontEndPlugin<INotebookTools> = {
   provides: INotebookTools,
   id: '@jupyterlab/notebook-extension:tools',
   autoStart: true,
-  requires: [INotebookTracker, IEditorServices, IStateDB],
-  optional: [ILabShell]
+  requires: [
+    INotebookTracker,
+    IEditorServices,
+    IStateDB
+  ],
+  optional: [ILabShell, IPropertyInspectorProvider]
 };
 
 /**
@@ -405,7 +406,8 @@ function activateNotebookTools(
   tracker: INotebookTracker,
   editorServices: IEditorServices,
   state: IStateDB,
-  labShell: ILabShell | null
+  labShell: ILabShell | null,
+  inspectorProvider: IPropertyInspectorProvider | null
 ): INotebookTools {
   const id = 'notebook-tools';
   const notebookTools = new NotebookTools({ tracker });
@@ -475,44 +477,12 @@ function activateNotebookTools(
 
   MessageLoop.installMessageHook(notebookTools, hook);
 
-  // Wait until the application has finished restoring before rendering.
-  void Promise.all([state.fetch(id), app.restored]).then(([value]) => {
-    const open = !!(
-      value && ((value as ReadonlyJSONObject)['open'] as boolean)
-    );
-
-    // After initial restoration, check if the notebook tools should render.
-    if (tracker.size) {
-      app.shell.add(notebookTools, 'left', { rank: NOTEBOOK_TOOLS_RANK });
-      if (open) {
-        app.shell.activateById(notebookTools.id);
-      }
-    }
-
-    const updateTools = () => {
-      // If there are any open notebooks, add notebook tools to the side panel if
-      // it is not already there.
-      if (labShell && tracker.size) {
-        if (!notebookTools.isAttached) {
-          labShell.add(notebookTools, 'left', { rank: NOTEBOOK_TOOLS_RANK });
-        }
-        return;
-      }
-      // If there are no notebooks, close notebook tools.
-      notebookTools.close();
-    };
-
-    // For all subsequent widget changes, check if the notebook tools should render.
-    if (labShell) {
-      labShell.currentChanged.connect((sender, args) => {
-        updateTools();
-      });
-    }
-    // A notebook widget could be closed without a change to labShell.currentWidget
-    tracker.currentChanged.connect((sender, args) => {
-      updateTools();
+  if (inspectorProvider) {
+    tracker.widgetAdded.connect((sender, panel) => {
+      const inspector = inspectorProvider.register(panel);
+      inspector.render(notebookTools);
     });
-  });
+  }
 
   return notebookTools;
 }

+ 3 - 0
packages/notebook-extension/tsconfig.json

@@ -36,6 +36,9 @@
     {
       "path": "../notebook"
     },
+    {
+      "path": "../property-inspector"
+    },
     {
       "path": "../rendermime"
     },

+ 51 - 0
packages/property-inspector/package.json

@@ -0,0 +1,51 @@
+{
+  "name": "@jupyterlab/property-inspector",
+  "version": "2.0.0-alpha.4",
+  "description": "A property inspector display for widgets",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "files": [
+    "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
+    "schema/*.json",
+    "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
+  ],
+  "sideEffects": [
+    "style/**/*"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "style": "style/index.css",
+  "directories": {
+    "lib": "lib/"
+  },
+  "scripts": {
+    "build": "tsc",
+    "clean": "rimraf lib",
+    "prepublishOnly": "npm run build",
+    "watch": "tsc -w --listEmittedFiles"
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^2.0.0-alpha.4",
+    "@jupyterlab/apputils": "^2.0.0-alpha.4",
+    "@lumino/coreutils": "^1.4.0",
+    "@lumino/disposable": "^1.3.2",
+    "@lumino/signaling": "^1.3.2",
+    "@lumino/widgets": "^1.9.4",
+    "react": "~16.9.0"
+  },
+  "devDependencies": {
+    "rimraf": "~3.0.0",
+    "typescript": "~3.7.3"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 304 - 0
packages/property-inspector/src/index.ts

@@ -0,0 +1,304 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { ILabShell } from '@jupyterlab/application';
+
+import { ReactWidget } from '@jupyterlab/apputils';
+
+import { Signal, ISignal } from '@lumino/signaling';
+
+import { Widget, FocusTracker, SingletonLayout } from '@lumino/widgets';
+
+import * as React from 'react';
+
+import { IPropertyInspector, IPropertyInspectorProvider } from './token';
+
+export { IPropertyInspector, IPropertyInspectorProvider };
+
+/**
+ * The implementation of the PropertyInspector.
+ */
+abstract class PropertyInspectorProvider extends Widget
+  implements IPropertyInspectorProvider {
+  /**
+   * Construct a new Property Inspector.
+   */
+  constructor() {
+    super();
+    this.addClass('jp-PropertyInspector');
+    this._tracker = new FocusTracker();
+    this._tracker.currentChanged.connect(this._onCurrentChanged, this);
+  }
+
+  /**
+   * Register a widget in the property inspector provider.
+   *
+   * @param widget The owner widget to register.
+   */
+  register(widget: Widget): IPropertyInspector {
+    if (this._inspectors.has(widget)) {
+      throw new Error('Widget is already registered');
+    }
+    const inspector = new Private.PropertyInspector(widget);
+    widget.disposed.connect(this._onWidgetDisposed, this);
+    this._inspectors.set(widget, inspector);
+    inspector.onAction.connect(this._onInspectorAction, this);
+    this._tracker.add(widget);
+    return inspector;
+  }
+
+  /**
+   * The current widget being tracked by the inspector.
+   */
+  protected get currentWidget(): Widget | null {
+    return this._tracker.currentWidget;
+  }
+
+  /**
+   * Refresh the content for the current widget.
+   */
+  protected refresh(): void {
+    const current = this._tracker.currentWidget;
+    if (!current) {
+      this.setContent(null);
+      return;
+    }
+    const inspector = this._inspectors.get(current);
+    if (inspector) {
+      this.setContent(inspector.content);
+    }
+  }
+
+  /**
+   * Show the provider panel.
+   */
+  protected abstract showPanel(): void;
+
+  /**
+   * Set the content of the provider.
+   */
+  protected abstract setContent(content: Widget | null): void;
+
+  /**
+   * Handle the disposal of a widget.
+   */
+  private _onWidgetDisposed(sender: Widget): void {
+    const inspector = this._inspectors.get(sender);
+    if (inspector) {
+      inspector.dispose();
+      this._inspectors.delete(sender);
+    }
+  }
+
+  /**
+   * Handle inspector actions.
+   */
+  private _onInspectorAction(
+    sender: Private.PropertyInspector,
+    action: Private.PropertyInspectorAction
+  ) {
+    const owner = sender.owner;
+    const current = this._tracker.currentWidget;
+    switch (action) {
+      case 'content':
+        if (current === owner) {
+          this.setContent(sender.content);
+        }
+        break;
+      case 'dispose':
+        if (owner) {
+         this._tracker.remove(owner);
+         this._inspectors.delete(owner);
+        }
+        break;
+      case 'show-panel':
+        if (current === owner) {
+          this.showPanel();
+        }
+        break;
+      default:
+        throw new Error('Unsupported inspector action');
+    }
+  }
+
+  /**
+   * Handle a change to the current widget in the tracker.
+   */
+  private _onCurrentChanged(): void {
+    const current = this._tracker.currentWidget;
+    if (current) {
+      const inspector = this._inspectors.get(current);
+      const content = inspector!.content;
+      this.setContent(content);
+    } else {
+      this.setContent(null);
+    }
+  }
+
+  private _tracker = new FocusTracker();
+  private _inspectors = new Map<Widget, Private.PropertyInspector>();
+}
+
+/**
+ * A class that adds a property inspector provider to the
+ * JupyterLab sidebar.
+ */
+export class SideBarPropertyInspectorProvider extends PropertyInspectorProvider {
+/**
+ * Construct a new Side Bar Property Inspector.
+ */
+  constructor(labshell: ILabShell, placeholder?: Widget) {
+    super();
+    this._labshell = labshell;
+    const layout = (this.layout = new SingletonLayout());
+    if (placeholder) {
+      this._placeholder = placeholder;
+    } else {
+      this._placeholder = new Widget();
+      this._placeholder.node.textContent = 'No properties to inspect.';
+      this._placeholder.addClass('jp-PropertyInspector-placeholder');
+    }
+    layout.widget = this._placeholder;
+    labshell.currentChanged.connect(this._onShellCurrentChanged, this);
+  }
+
+  /**
+   * Set the content of the sidebar panel.
+   */
+  protected setContent(content: Widget | null): void {
+    const layout = this.layout as SingletonLayout;
+    if (layout.widget) {
+      layout.widget.removeClass('jp-PropertyInspector-content');
+      layout.removeWidget(layout.widget);
+    }
+    if (!content) {
+      content = this._placeholder;
+    }
+    content.addClass('jp-PropertyInspector-content');
+    layout.widget = content;
+  }
+
+  /**
+   * Show the sidebar panel.
+   */
+  showPanel(): void {
+    this._labshell.activateById(this.id);
+  }
+
+  /**
+   * Handle the case when the current widget is not in our tracker.
+   */
+  private _onShellCurrentChanged(): void {
+    const current = this.currentWidget;
+    if (!current) {
+      this.setContent(null);
+      return;
+    }
+    const currentShell = this._labshell.currentWidget;
+    if (currentShell?.node.contains(current.node)) {
+      this.refresh();
+    } else {
+      this.setContent(null);
+    }
+  }
+
+  private _labshell: ILabShell;
+  private _placeholder: Widget;
+}
+
+/**
+ * A namespace for module private data.
+ */
+namespace Private {
+  /**
+   * A type alias for the actions a property inspector can take.
+   */
+  export type PropertyInspectorAction = 'content' | 'dispose' | 'show-panel';
+
+  /**
+   * An implementation of the property inspector used by the
+   * property inspector provider.
+   */
+  export class PropertyInspector implements IPropertyInspector {
+    /**
+     * Construct a new property inspector.
+     */
+    constructor(owner: Widget) {
+      this._owner = owner;
+    }
+
+    /**
+     * The owner widget for the property inspector.
+     */
+    get owner(): Widget | null {
+      return this._owner;
+    }
+
+    /**
+     * The current content for the property inspector.
+     */
+    get content(): Widget | null {
+      return this._content;
+    }
+
+    /**
+     * Whether the property inspector is disposed.
+     */
+    get isDisposed() {
+      return this._isDisposed;
+    }
+
+    /**
+     * A signal used for actions related to the property inspector.
+     */
+    get onAction(): ISignal<PropertyInspector, PropertyInspectorAction> {
+      return this._onAction;
+    }
+
+    /**
+     * Show the property inspector panel.
+     */
+    showPanel(): void {
+      if (this._isDisposed) {
+        return;
+      }
+      this._onAction.emit('show-panel');
+    }
+
+    /**
+     * Render the property inspector content.
+     */
+    render(widget: Widget | React.ReactElement): void {
+      if (this._isDisposed) {
+        return;
+      }
+      if (widget instanceof Widget) {
+        this._content = widget;
+      } else {
+        this._content = ReactWidget.create(widget);
+      }
+      this._onAction.emit('content');
+    }
+
+    /**
+     * Dispose of the property inspector.
+     */
+    dispose(): void {
+      if (this._isDisposed) {
+        return;
+      }
+      this._isDisposed = true;
+      this._content = null;
+      this._owner = null;
+      Signal.clearData(this);
+    }
+
+    private _isDisposed = false;
+    private _content: Widget | null = null;
+    private _owner: Widget | null = null;
+    private _onAction = new Signal<
+      PropertyInspector,
+      Private.PropertyInspectorAction
+    >(this);
+  }
+}

+ 62 - 0
packages/property-inspector/src/token.ts

@@ -0,0 +1,62 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { Token } from '@lumino/coreutils';
+
+import { IDisposable } from '@lumino/disposable';
+
+import { Widget } from '@lumino/widgets';
+
+import * as React from 'react';
+
+/**
+ * A property inspector interface provided when registering
+ * to a property inspector provider.  Allows an owner widget
+ * to set the property inspector content for itself.
+ */
+export interface IPropertyInspector extends IDisposable {
+  /*
+   * Render the property inspector content.
+   *
+   * If the owner widget is not the most recently focused,
+   * The content will not be shown until that widget
+   * is focused.
+   *
+   * @param content - the widget or react element to render.
+   */
+  render(content: Widget | React.ReactElement): void;
+
+  /**
+   * Show the property inspector panel.
+   *
+   * If the owner widget is not the most recently focused,
+   * this is a no-op.  It should be triggered by a user
+   * action.
+   */
+  showPanel(): void;
+}
+
+/**
+ * A provider for property inspectors.
+ */
+export interface IPropertyInspectorProvider {
+  /**
+   * Register a widget in the property inspector provider.
+   *
+   * @param widget The owner widget whose properties will be inspected.
+   *
+   * ## Notes
+   * Only one property inspector can be provided for each widget.
+   * Registering the same widget twice will result in an error.
+   * A widget can be unregistered by disposing of its property
+   * inspector.
+   */
+  register(widget: Widget): IPropertyInspector;
+}
+
+/**
+ * The property inspector provider token.
+ */
+export const IPropertyInspectorProvider = new Token<IPropertyInspectorProvider>(
+  '@jupyterlab/property-inspector:IPropertyInspectorProvider'
+);

+ 17 - 0
packages/property-inspector/style/base.css

@@ -0,0 +1,17 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) 2014-2019, Jupyter Development Team.
+|
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+.jp-PropertyInspector {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--jp-ui-font-color1);
+  background: var(--jp-layout-color1);
+}
+
+.jp-PropertyInspector-placeholder {
+  padding: 8px;
+}

+ 11 - 0
packages/property-inspector/style/index.css

@@ -0,0 +1,11 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
+@import url('~@lumino/widgets/style/index.css');
+@import url('~@jupyterlab/apputils/style/index.css');
+@import url('~@jupyterlab/application/style/index.css');
+
+@import url('./base.css');

+ 16 - 0
packages/property-inspector/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../application"
+    },
+    {
+      "path": "../apputils"
+    }
+  ]
+}