|
@@ -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);
|
|
|
+ }
|
|
|
+}
|