Browse Source

Merge pull request #9984 from fasiha/fasiha-header

API for custom toolbars/headers in Notebook widgets
Steven Silvester 4 years ago
parent
commit
bc55a336f1

+ 42 - 1
packages/apputils-extension/src/index.ts

@@ -24,7 +24,8 @@ import {
   Printing,
   sessionContextDialogs,
   ISanitizer,
-  defaultSanitizer
+  defaultSanitizer,
+  MainAreaWidget
 } from '@jupyterlab/apputils';
 
 import { URLExt, PageConfig } from '@jupyterlab/coreutils';
@@ -68,6 +69,8 @@ namespace CommandIDs {
   export const resetOnLoad = 'apputils:reset-on-load';
 
   export const runFirstEnabled = 'apputils:run-first-enabled';
+
+  export const toggleHeader = 'apputils:toggle-header';
 }
 
 /**
@@ -297,6 +300,43 @@ const print: JupyterFrontEndPlugin<void> = {
   }
 };
 
+export const toggleHeader: JupyterFrontEndPlugin<void> = {
+  id: '@jupyterlab/apputils-extension:toggle-header',
+  autoStart: true,
+  requires: [ITranslator],
+  optional: [ICommandPalette],
+  activate: (
+    app: JupyterFrontEnd,
+    translator: ITranslator,
+    palette: ICommandPalette | null
+  ) => {
+    const trans = translator.load('jupyterlab');
+
+    const category: string = trans.__('Main Area');
+    app.commands.addCommand(CommandIDs.toggleHeader, {
+      label: trans.__('Show Header Above Content'),
+      isEnabled: () =>
+        app.shell.currentWidget instanceof MainAreaWidget &&
+        app.shell.currentWidget.contentHeader.widgets.length > 0,
+      isToggled: () => {
+        const widget = app.shell.currentWidget;
+        return widget instanceof MainAreaWidget
+          ? !widget.contentHeader.isHidden
+          : false;
+      },
+      execute: async () => {
+        const widget = app.shell.currentWidget;
+        if (widget instanceof MainAreaWidget) {
+          widget.contentHeader.setHidden(!widget.contentHeader.isHidden);
+        }
+      }
+    });
+    if (palette) {
+      palette.addItem({ command: CommandIDs.toggleHeader, category });
+    }
+  }
+};
+
 /**
  * The default state database for storing application state.
  *
@@ -540,6 +580,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   sessionDialogs,
   themesPlugin,
   themesPaletteMenuPlugin,
+  toggleHeader,
   utilityCommands,
   workspacesPlugin
 ];

+ 32 - 1
packages/apputils/src/mainareawidget.ts

@@ -5,7 +5,7 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
 
 import { Message, MessageLoop } from '@lumino/messaging';
 
-import { BoxLayout, Widget } from '@lumino/widgets';
+import { BoxLayout, BoxPanel, Widget } from '@lumino/widgets';
 
 import { Spinner } from './spinner';
 
@@ -45,12 +45,20 @@ export class MainAreaWidget<T extends Widget = Widget>
     toolbar.node.setAttribute('role', 'navigation');
     toolbar.node.setAttribute('aria-label', trans.__('notebook actions'));
     const spinner = this._spinner;
+    const contentHeader = (this._contentHeader =
+      options.contentHeader ||
+      new BoxPanel({
+        direction: 'top-to-bottom',
+        spacing: 0
+      }));
 
     const layout = (this.layout = new BoxLayout({ spacing: 0 }));
     layout.direction = 'top-to-bottom';
     BoxLayout.setStretch(toolbar, 0);
+    BoxLayout.setStretch(contentHeader, 0);
     BoxLayout.setStretch(content, 1);
     layout.addWidget(toolbar);
+    layout.addWidget(contentHeader);
     layout.addWidget(content);
 
     if (!content.id) {
@@ -131,6 +139,14 @@ export class MainAreaWidget<T extends Widget = Widget>
     return this._toolbar;
   }
 
+  /**
+   * A panel for widgets that sit between the toolbar and the content.
+   * Imagine a formatting toolbar, notification headers, etc.
+   */
+  get contentHeader(): BoxPanel {
+    return this._contentHeader;
+  }
+
   /**
    * Whether the content widget or an error is revealed.
    */
@@ -231,8 +247,17 @@ export class MainAreaWidget<T extends Widget = Widget>
     this.content.activate();
   }
 
+  /*
+  MainAreaWidget's layout:
+  - this.layout, a BoxLayout, from parent
+    - this._toolbar, a Toolbar
+    - this._contentHeader, a BoxPanel, empty by default
+    - this._content
+  */
   private _content: T;
   private _toolbar: Toolbar;
+  private _contentHeader: BoxPanel;
+
   private _changeGuard = false;
   private _spinner = new Spinner();
 
@@ -258,6 +283,12 @@ export namespace MainAreaWidget {
      */
     toolbar?: Toolbar;
 
+    /**
+     * The layout to sit underneath the toolbar and above the content,
+     * and that extensions can populate. Defaults to an empty BoxPanel.
+     */
+    contentHeader?: BoxPanel;
+
     /**
      * An optional promise for when the content is ready to be revealed.
      */

+ 10 - 1
packages/apputils/test/mainareawidget.spec.ts

@@ -5,7 +5,7 @@ import { MainAreaWidget, Toolbar } from '@jupyterlab/apputils';
 
 import { MessageLoop } from '@lumino/messaging';
 
-import { Widget } from '@lumino/widgets';
+import { BoxPanel, Widget } from '@lumino/widgets';
 
 describe('@jupyterlab/apputils', () => {
   describe('MainAreaWidget', () => {
@@ -24,6 +24,15 @@ describe('@jupyterlab/apputils', () => {
         const toolbar = new Toolbar();
         const widget = new MainAreaWidget({ content, toolbar });
         expect(widget.hasClass('jp-MainAreaWidget')).toBe(true);
+        expect(widget.toolbar).toBe(toolbar);
+      });
+    });
+
+    describe('contentHeader', () => {
+      it('should exist and have correct type', () => {
+        const content = new Widget();
+        const widget = new MainAreaWidget({ content });
+        expect(widget.contentHeader).toBeInstanceOf(BoxPanel);
       });
     });