diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index 7bb96f36c4..65e4021bd7 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -35,8 +35,9 @@ import { NotebookApp, NotebookShell, INotebookShell, - SideBarPanel, - SideBarHandler + SidePanel, + SidePanelHandler, + SidePanelPalette } from '@jupyter-notebook/application'; import { jupyterIcon } from '@jupyter-notebook/ui-components'; @@ -51,8 +52,6 @@ import { import { Menu, Widget } from '@lumino/widgets'; -import { SideBarPalette } from './sidebarpalette'; - /** * A regular expression to match path to notebooks and documents */ @@ -73,7 +72,7 @@ namespace CommandIDs { export const toggleTop = 'application:toggle-top'; /** - * Toggle sidebar visibility + * Toggle side panel visibility */ export const togglePanel = 'application:toggle-panel'; @@ -543,10 +542,10 @@ const topVisibility: JupyterFrontEndPlugin = { }; /** - * Plugin to toggle the left or right sidebar's visibility. + * Plugin to toggle the left or right side panel's visibility. */ -const sidebarVisibility: JupyterFrontEndPlugin = { - id: '@jupyter-notebook/application-extension:sidebar', +const sidePanelVisibility: JupyterFrontEndPlugin = { + id: '@jupyter-notebook/application-extension:sidepanel', requires: [INotebookShell, ITranslator], optional: [IMainMenu, ICommandPalette], autoStart: true, @@ -562,7 +561,7 @@ const sidebarVisibility: JupyterFrontEndPlugin = { /* Arguments for togglePanel command: * side, left or right area * title, widget title to show in the menu - * id, widget ID to activate in the sidebar + * id, widget ID to activate in the side panel */ app.commands.addCommand(CommandIDs.togglePanel, { label: args => args['title'] as string, @@ -643,33 +642,33 @@ const sidebarVisibility: JupyterFrontEndPlugin = { } }); - const sideBarMenu: { [area in SideBarPanel.Area]: IDisposable | null } = { + const sidePanelMenu: { [area in SidePanel.Area]: IDisposable | null } = { left: null, right: null }; /** - * The function which adds entries to the View menu for each widget of a sidebar. + * The function which adds entries to the View menu for each widget of a side panel. * - * @param area - 'left' or 'right', the area of the side bar. - * @param entryLabel - the name of the main entry in the View menu for that sidebar. + * @param area - 'left' or 'right', the area of the side panel. + * @param entryLabel - the name of the main entry in the View menu for that side panel. * @returns - The disposable menu added to the View menu or null. */ - const updateMenu = (area: SideBarPanel.Area, entryLabel: string) => { + const updateMenu = (area: SidePanel.Area, entryLabel: string) => { if (menu === null) { return null; } - // Remove the previous menu entry for this sidebar. - sideBarMenu[area]?.dispose(); + // Remove the previous menu entry for this side panel. + sidePanelMenu[area]?.dispose(); - // Creates a new menu entry and populates it with sidebar widgets. + // Creates a new menu entry and populates it with side panel widgets. const newMenu = new Menu({ commands: app.commands }); newMenu.title.label = entryLabel; const widgets = notebookShell.widgets(area); let menuToAdd = false; - for (let widget of widgets) { + for (const widget of widgets) { newMenu.addItem({ command: CommandIDs.togglePanel, args: { @@ -683,7 +682,7 @@ const sidebarVisibility: JupyterFrontEndPlugin = { // If there are widgets, add the menu to the main menu entry. if (menuToAdd) { - sideBarMenu[area] = menu.viewMenu.addItem({ + sidePanelMenu[area] = menu.viewMenu.addItem({ type: 'submenu', submenu: newMenu }); @@ -693,63 +692,65 @@ const sidebarVisibility: JupyterFrontEndPlugin = { app.restored.then(() => { // Create menu entries for the left and right panel. if (menu) { - const getSideBarLabel = (area: SideBarPanel.Area): string => { + const getSidePanelLabel = (area: SidePanel.Area): string => { if (area === 'left') { - return trans.__(`Left Sidebar`); + return trans.__('Left Sidebar'); } else { - return trans.__(`Right Sidebar`); + return trans.__('Right Sidebar'); } }; const leftArea = notebookShell.leftHandler.area; - const leftLabel = getSideBarLabel(leftArea); + const leftLabel = getSidePanelLabel(leftArea); updateMenu(leftArea, leftLabel); const rightArea = notebookShell.rightHandler.area; - const rightLabel = getSideBarLabel(rightArea); + const rightLabel = getSidePanelLabel(rightArea); updateMenu(rightArea, rightLabel); - const handleSideBarChange = ( - sidebar: SideBarHandler, + const handleSidePanelChange = ( + sidePanel: SidePanelHandler, widget: Widget ) => { - const label = getSideBarLabel(sidebar.area); - updateMenu(sidebar.area, label); + const label = getSidePanelLabel(sidePanel.area); + updateMenu(sidePanel.area, label); }; - notebookShell.leftHandler.widgetAdded.connect(handleSideBarChange); - notebookShell.leftHandler.widgetRemoved.connect(handleSideBarChange); - notebookShell.rightHandler.widgetAdded.connect(handleSideBarChange); - notebookShell.rightHandler.widgetRemoved.connect(handleSideBarChange); + notebookShell.leftHandler.widgetAdded.connect(handleSidePanelChange); + notebookShell.leftHandler.widgetRemoved.connect(handleSidePanelChange); + notebookShell.rightHandler.widgetAdded.connect(handleSidePanelChange); + notebookShell.rightHandler.widgetRemoved.connect(handleSidePanelChange); } // Add palette entries for side panels. if (palette) { - const sideBarPalette = new SideBarPalette({ + const sidePanelPalette = new SidePanelPalette({ commandPalette: palette as ICommandPalette, command: CommandIDs.togglePanel }); notebookShell.leftHandler.widgets.forEach(widget => { - sideBarPalette.addItem(widget, notebookShell.leftHandler.area); + sidePanelPalette.addItem(widget, notebookShell.leftHandler.area); }); notebookShell.rightHandler.widgets.forEach(widget => { - sideBarPalette.addItem(widget, notebookShell.rightHandler.area); + sidePanelPalette.addItem(widget, notebookShell.rightHandler.area); }); - // Update menu and palette when widgets are added or removed from sidebars. - notebookShell.leftHandler.widgetAdded.connect((sidebar, widget) => { - sideBarPalette.addItem(widget, sidebar.area); - }); - notebookShell.leftHandler.widgetRemoved.connect((sidebar, widget) => { - sideBarPalette.removeItem(widget, sidebar.area); + // Update menu and palette when widgets are added or removed from side panels. + notebookShell.leftHandler.widgetAdded.connect((sidePanel, widget) => { + sidePanelPalette.addItem(widget, sidePanel.area); }); - notebookShell.rightHandler.widgetAdded.connect((sidebar, widget) => { - sideBarPalette.addItem(widget, sidebar.area); + notebookShell.leftHandler.widgetRemoved.connect((sidePanel, widget) => { + sidePanelPalette.removeItem(widget, sidePanel.area); }); - notebookShell.rightHandler.widgetRemoved.connect((sidebar, widget) => { - sideBarPalette.removeItem(widget, sidebar.area); + notebookShell.rightHandler.widgetAdded.connect((sidePanel, widget) => { + sidePanelPalette.addItem(widget, sidePanel.area); }); + notebookShell.rightHandler.widgetRemoved.connect( + (sidePanel, widget) => { + sidePanelPalette.removeItem(widget, sidePanel.area); + } + ); } }); } @@ -908,7 +909,7 @@ const plugins: JupyterFrontEndPlugin[] = [ paths, sessionDialogs, shell, - sidebarVisibility, + sidePanelVisibility, status, tabTitle, title, diff --git a/packages/application-extension/src/sidebarpalette.ts b/packages/application-extension/src/sidebarpalette.ts deleted file mode 100644 index e47c33be6c..0000000000 --- a/packages/application-extension/src/sidebarpalette.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ICommandPalette } from '@jupyterlab/apputils'; -import { IDisposable } from '@lumino/disposable'; -import { Widget } from '@lumino/widgets'; - -/** - * A class to manages the palette entries associated to the side bar. - */ -export class SideBarPalette { - /** - * Construct a new side bar palette. - */ - constructor(options: SideBarPaletteOption) { - this._commandPalette = options.commandPalette; - this._command = options.command; - } - - /** - * Get a command palette item from the widget id and the area. - */ - getItem( - widget: Readonly, - area: 'left' | 'right' - ): SideBarPaletteItem | null { - const itemList = this._items; - for (let i = 0; i < itemList.length; i++) { - const item = itemList[i]; - if (item.widgetId == widget.id && item.area == area) { - return item; - } - } - return null; - } - - /** - * Add an item to the command palette. - */ - addItem(widget: Readonly, area: 'left' | 'right'): void { - // Check if the item does not already exist. - if (this.getItem(widget, area)) { - return; - } - - // Add a new item in command palette. - const disposableDelegate = this._commandPalette.addItem({ - command: this._command, - category: 'View', - args: { - side: area, - title: `Show ${widget.title.caption}`, - id: widget.id - } - }); - - // Keep the disposableDelegate objet to be able to dispose of the item if the widget - // is remove from the side bar. - this._items.push({ - widgetId: widget.id, - area: area, - disposable: disposableDelegate - }); - } - - /** - * Remove an item from the command palette. - */ - removeItem(widget: Readonly, area: 'left' | 'right'): void { - const item = this.getItem(widget, area); - if (item) { - item.disposable.dispose(); - } - } - - _command: string; - _commandPalette: ICommandPalette; - _items: SideBarPaletteItem[] = []; -} - -type SideBarPaletteItem = { - /** - * The ID of the widget associated to the command palette. - */ - widgetId: string; - - /** - * The area of the panel associated to the command palette. - */ - area: 'left' | 'right'; - - /** - * The disposable object to remove the item from command palette. - */ - disposable: IDisposable; -}; - -/** - * An interface for the options to include in SideBarPalette constructor. - */ -type SideBarPaletteOption = { - /** - * The commands palette. - */ - commandPalette: ICommandPalette; - - /** - * The command to call from each sidebar menu entry. - * - * ### Notes - * That command required 3 args : - * side: 'left' | 'right', the area to toggle - * title: string, label of the command - * id: string, id of the widget to activate - */ - command: string; -}; diff --git a/packages/application/src/index.ts b/packages/application/src/index.ts index 9bbd605f96..c87df8bd53 100644 --- a/packages/application/src/index.ts +++ b/packages/application/src/index.ts @@ -3,3 +3,4 @@ export * from './app'; export * from './shell'; +export * from './panelhandler'; diff --git a/packages/application/src/panelhandler.ts b/packages/application/src/panelhandler.ts new file mode 100644 index 0000000000..77c08dcd6d --- /dev/null +++ b/packages/application/src/panelhandler.ts @@ -0,0 +1,446 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { ICommandPalette } from '@jupyterlab/apputils'; +import { closeIcon } from '@jupyterlab/ui-components'; +import { ArrayExt, find } from '@lumino/algorithm'; +import { IDisposable } from '@lumino/disposable'; +import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Panel, StackedPanel, Widget } from '@lumino/widgets'; + +/** + * A class which manages a panel and sorts its widgets by rank. + */ +export class PanelHandler { + constructor() { + MessageLoop.installMessageHook(this._panel, this._panelChildHook); + } + + /** + * Get the panel managed by the handler. + */ + get panel(): Panel { + return this._panel; + } + + /** + * Add a widget to the panel. + * + * If the widget is already added, it will be moved. + */ + addWidget(widget: Widget, rank: number): void { + widget.parent = null; + const item = { widget, rank }; + const index = ArrayExt.upperBound(this._items, item, Private.itemCmp); + ArrayExt.insert(this._items, index, item); + this._panel.insertWidget(index, widget); + } + + /** + * A message hook for child add/remove messages on the main area dock panel. + */ + private _panelChildHook = ( + handler: IMessageHandler, + msg: Message + ): boolean => { + switch (msg.type) { + case 'child-added': + { + const widget = (msg as Widget.ChildMessage).child; + // If we already know about this widget, we're done + if (this._items.find(v => v.widget === widget)) { + break; + } + + // Otherwise, add to the end by default + const rank = this._items[this._items.length - 1].rank; + this._items.push({ widget, rank }); + } + break; + case 'child-removed': + { + const widget = (msg as Widget.ChildMessage).child; + ArrayExt.removeFirstWhere(this._items, v => v.widget === widget); + } + break; + default: + break; + } + return true; + }; + + protected _items = new Array(); + protected _panel = new Panel(); +} + +/** + * A class which manages a side panel that can show at most one widget at a time. + */ +export class SidePanelHandler extends PanelHandler { + /** + * Construct a new side panel handler. + */ + constructor(area: SidePanel.Area) { + super(); + this._area = area; + this._panel.hide(); + + this._currentWidget = null; + this._lastCurrentWidget = null; + + this._widgetPanel = new StackedPanel(); + this._widgetPanel.widgetRemoved.connect(this._onWidgetRemoved, this); + + const closeButton = document.createElement('button'); + closeIcon.element({ + container: closeButton, + height: '16px', + width: 'auto' + }); + closeButton.onclick = () => { + this.collapse(); + this.hide(); + }; + closeButton.className = 'jp-Button jp-SidePanel-collapse'; + const icon = new Widget({ node: closeButton }); + this._panel.addWidget(icon); + this._panel.addWidget(this._widgetPanel); + } + + /** + * Get the current widget in the sidebar panel. + */ + get currentWidget(): Widget | null { + return ( + this._currentWidget || + this._lastCurrentWidget || + (this._items.length > 0 ? this._items[0].widget : null) + ); + } + + /** + * Get the area of the side panel + */ + get area(): SidePanel.Area { + return this._area; + } + + /** + * Whether the panel is visible + */ + get isVisible(): boolean { + return this._panel.isVisible; + } + + /** + * Get the stacked panel managed by the handler + */ + get panel(): Panel { + return this._panel; + } + + /** + * Get the widgets list. + */ + get widgets(): Readonly { + return this._items.map(obj => obj.widget); + } + + /** + * Signal fired when a widget is added to the panel + */ + get widgetAdded(): ISignal { + return this._widgetAdded; + } + + /** + * Signal fired when a widget is removed from the panel + */ + get widgetRemoved(): ISignal { + return this._widgetRemoved; + } + + /** + * Expand the sidebar. + * + * #### Notes + * This will open the most recently used widget, or the first widget + * if there is no most recently used. + */ + expand(id?: string): void { + if (this._currentWidget) { + this.collapse(); + } + if (id) { + this.activate(id); + } else { + const visibleWidget = this.currentWidget; + if (visibleWidget) { + this._currentWidget = visibleWidget; + this.activate(visibleWidget.id); + } + } + } + + /** + * Activate a widget residing in the stacked panel by ID. + * + * @param id - The widget's unique ID. + */ + activate(id: string): void { + const widget = this._findWidgetByID(id); + if (widget) { + this._currentWidget = widget; + widget.show(); + widget.activate(); + } + } + + /** + * Test whether the sidebar has the given widget by id. + */ + has(id: string): boolean { + return this._findWidgetByID(id) !== null; + } + + /** + * Collapse the sidebar so no items are expanded. + */ + collapse(): void { + this._currentWidget?.hide(); + this._currentWidget = null; + } + + /** + * Add a widget and its title to the stacked panel. + * + * If the widget is already added, it will be moved. + */ + addWidget(widget: Widget, rank: number): void { + widget.parent = null; + widget.hide(); + const item = { widget, rank }; + const index = this._findInsertIndex(item); + ArrayExt.insert(this._items, index, item); + this._widgetPanel.insertWidget(index, widget); + + this._refreshVisibility(); + + this._widgetAdded.emit(widget); + } + + /** + * Hide the side panel + */ + hide(): void { + this._isHiddenByUser = true; + this._refreshVisibility(); + } + + /** + * Show the side panel + */ + show(): void { + this._isHiddenByUser = false; + this._refreshVisibility(); + } + + /** + * Find the insertion index for a rank item. + */ + private _findInsertIndex(item: Private.IRankItem): number { + return ArrayExt.upperBound(this._items, item, Private.itemCmp); + } + + /** + * Find the index of the item with the given widget, or `-1`. + */ + private _findWidgetIndex(widget: Widget): number { + return ArrayExt.findFirstIndex(this._items, i => i.widget === widget); + } + + /** + * Find the widget with the given id, or `null`. + */ + private _findWidgetByID(id: string): Widget | null { + const item = find(this._items, value => value.widget.id === id); + return item ? item.widget : null; + } + + /** + * Refresh the visibility of the stacked panel. + */ + private _refreshVisibility(): void { + this._panel.setHidden(this._isHiddenByUser); + } + + /* + * Handle the `widgetRemoved` signal from the panel. + */ + private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void { + if (widget === this._lastCurrentWidget) { + this._lastCurrentWidget = null; + } + ArrayExt.removeAt(this._items, this._findWidgetIndex(widget)); + + this._refreshVisibility(); + + this._widgetRemoved.emit(widget); + } + + private _area: SidePanel.Area; + private _isHiddenByUser = false; + private _widgetPanel: StackedPanel; + private _currentWidget: Widget | null; + private _lastCurrentWidget: Widget | null; + private _widgetAdded: Signal = new Signal(this); + private _widgetRemoved: Signal = new Signal(this); +} + +/** + * A name space for SideBarPanel functions. + */ +export namespace SidePanel { + /** + * The areas of the sidebar panel + */ + export type Area = 'left' | 'right'; +} + +/** + * A class to manages the palette entries associated to the side panels. + */ +export class SidePanelPalette { + /** + * Construct a new side panel palette. + */ + constructor(options: SidePanelPaletteOption) { + this._commandPalette = options.commandPalette; + this._command = options.command; + } + + /** + * Get a command palette item from the widget id and the area. + */ + getItem( + widget: Readonly, + area: 'left' | 'right' + ): SidePanelPaletteItem | null { + const itemList = this._items; + for (let i = 0; i < itemList.length; i++) { + const item = itemList[i]; + if (item.widgetId === widget.id && item.area === area) { + return item; + } + } + return null; + } + + /** + * Add an item to the command palette. + */ + addItem(widget: Readonly, area: 'left' | 'right'): void { + // Check if the item does not already exist. + if (this.getItem(widget, area)) { + return; + } + + // Add a new item in command palette. + const disposableDelegate = this._commandPalette.addItem({ + command: this._command, + category: 'View', + args: { + side: area, + title: `Show ${widget.title.caption}`, + id: widget.id + } + }); + + // Keep the disposableDelegate objet to be able to dispose of the item if the widget + // is remove from the side panel. + this._items.push({ + widgetId: widget.id, + area: area, + disposable: disposableDelegate + }); + } + + /** + * Remove an item from the command palette. + */ + removeItem(widget: Readonly, area: 'left' | 'right'): void { + const item = this.getItem(widget, area); + if (item) { + item.disposable.dispose(); + } + } + + _command: string; + _commandPalette: ICommandPalette; + _items: SidePanelPaletteItem[] = []; +} + +type SidePanelPaletteItem = { + /** + * The ID of the widget associated to the command palette. + */ + widgetId: string; + + /** + * The area of the panel associated to the command palette. + */ + area: 'left' | 'right'; + + /** + * The disposable object to remove the item from command palette. + */ + disposable: IDisposable; +}; + +/** + * An interface for the options to include in SideBarPalette constructor. + */ +type SidePanelPaletteOption = { + /** + * The commands palette. + */ + commandPalette: ICommandPalette; + + /** + * The command to call from each side panel menu entry. + * + * ### Notes + * That command required 3 args : + * side: 'left' | 'right', the area to toggle + * title: string, label of the command + * id: string, id of the widget to activate + */ + command: string; +}; + +/** + * A namespace for private module data. + */ +namespace Private { + /** + * An object which holds a widget and its sort rank. + */ + export interface IRankItem { + /** + * The widget for the item. + */ + widget: Widget; + + /** + * The sort rank of the widget. + */ + rank: number; + } + /** + * A less-than comparison function for side bar rank items. + */ + export function itemCmp(first: IRankItem, second: IRankItem): number { + return first.rank - second.rank; + } +} diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index fc97e4393a..58b3e0406a 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -3,20 +3,13 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { closeIcon } from '@jupyterlab/ui-components'; -import { ArrayExt, find } from '@lumino/algorithm'; +import { find } from '@lumino/algorithm'; import { PromiseDelegate, Token } from '@lumino/coreutils'; -import { Message, MessageLoop, IMessageHandler } from '@lumino/messaging'; import { ISignal, Signal } from '@lumino/signaling'; -import { - BoxLayout, - Panel, - SplitPanel, - StackedPanel, - Widget -} from '@lumino/widgets'; +import { BoxLayout, Panel, SplitPanel, Widget } from '@lumino/widgets'; +import { PanelHandler, SidePanelHandler } from './panelhandler'; /** * The Jupyter Notebook application shell token. @@ -43,10 +36,10 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { super(); this.id = 'main'; - this._topHandler = new Private.PanelHandler(); - this._menuHandler = new Private.PanelHandler(); - this._leftHandler = new SideBarHandler('left'); - this._rightHandler = new SideBarHandler('right'); + this._topHandler = new PanelHandler(); + this._menuHandler = new PanelHandler(); + this._leftHandler = new SidePanelHandler('left'); + this._rightHandler = new SidePanelHandler('right'); this._main = new Panel(); const topWrapper = (this._topWrapper = new Panel()); const menuWrapper = (this._menuWrapper = new Panel()); @@ -147,14 +140,14 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { /** * Get the left area handler */ - get leftHandler(): SideBarHandler { + get leftHandler(): SidePanelHandler { return this._leftHandler; } /** * Get the right area handler */ - get rightHandler(): SideBarHandler { + get rightHandler(): SidePanelHandler { return this._rightHandler; } @@ -319,11 +312,11 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { } private _topWrapper: Panel; - private _topHandler: Private.PanelHandler; + private _topHandler: PanelHandler; private _menuWrapper: Panel; - private _menuHandler: Private.PanelHandler; - private _leftHandler: SideBarHandler; - private _rightHandler: SideBarHandler; + private _menuHandler: PanelHandler; + private _leftHandler: SidePanelHandler; + private _rightHandler: SidePanelHandler; private _spacer: Widget; private _main: Panel; private _currentChanged = new Signal(this); @@ -339,330 +332,3 @@ export namespace Shell { */ export type Area = 'main' | 'top' | 'left' | 'right' | 'menu'; } - -/** - * A name space for SideBarPanel functions. - */ -export namespace SideBarPanel { - /** - * The areas of the sidebar panel - */ - export type Area = 'left' | 'right'; -} - -/** - * A class which manages a side bar that can show at most one widget at a time. - */ -export class SideBarHandler { - /** - * Construct a new side bar handler. - */ - constructor(area: SideBarPanel.Area) { - this._area = area; - this._panel = new Panel(); - this._panel.hide(); - - this._currentWidget = null; - this._lastCurrentWidget = null; - - this._widgetPanel = new StackedPanel(); - this._widgetPanel.widgetRemoved.connect(this._onWidgetRemoved, this); - - const closeButton = document.createElement('button'); - closeIcon.element({ - container: closeButton, - height: '16px', - width: 'auto' - }); - closeButton.onclick = () => { - this.collapse(); - this.hide(); - }; - closeButton.className = 'jp-Button jp-SidePanel-collapse'; - const icon = new Widget({ node: closeButton }); - this._panel.addWidget(icon); - this._panel.addWidget(this._widgetPanel); - } - - /** - * Get the current widget in the sidebar panel. - */ - get currentWidget(): Widget | null { - return ( - this._currentWidget || - this._lastCurrentWidget || - (this._items.length > 0 ? this._items[0].widget : null) - ); - } - - /** - * Get the area of the side panel - */ - get area(): SideBarPanel.Area { - return this._area; - } - - /** - * Whether the panel is visible - */ - get isVisible(): boolean { - return this._panel.isVisible; - } - - /** - * Get the stacked panel managed by the handler - */ - get panel(): Panel { - return this._panel; - } - - /** - * Get the widgets list. - */ - get widgets(): Readonly { - return this._items.map(obj => obj.widget); - } - - /** - * Signal fired when a widget is added to the panel - */ - get widgetAdded(): ISignal { - return this._widgetAdded; - } - - /** - * Signal fired when a widget is removed from the panel - */ - get widgetRemoved(): ISignal { - return this._widgetRemoved; - } - - /** - * Expand the sidebar. - * - * #### Notes - * This will open the most recently used widget, or the first widget - * if there is no most recently used. - */ - expand(id?: string): void { - if (this._currentWidget) { - this.collapse(); - } - if (id) { - this.activate(id); - } else { - const visibleWidget = this.currentWidget; - if (visibleWidget) { - this._currentWidget = visibleWidget; - this.activate(visibleWidget.id); - } - } - } - - /** - * Activate a widget residing in the stacked panel by ID. - * - * @param id - The widget's unique ID. - */ - activate(id: string): void { - const widget = this._findWidgetByID(id); - if (widget) { - this._currentWidget = widget; - widget.show(); - widget.activate(); - } - } - - /** - * Test whether the sidebar has the given widget by id. - */ - has(id: string): boolean { - return this._findWidgetByID(id) !== null; - } - - /** - * Collapse the sidebar so no items are expanded. - */ - collapse(): void { - this._currentWidget?.hide(); - this._currentWidget = null; - } - - /** - * Add a widget and its title to the stacked panel. - * - * If the widget is already added, it will be moved. - */ - addWidget(widget: Widget, rank: number): void { - widget.parent = null; - widget.hide(); - const item = { widget, rank }; - const index = this._findInsertIndex(item); - ArrayExt.insert(this._items, index, item); - this._widgetPanel.insertWidget(index, widget); - - this._refreshVisibility(); - - this._widgetAdded.emit(widget); - } - - /** - * Hide the side panel - */ - hide(): void { - this._isHiddenByUser = true; - this._refreshVisibility(); - } - - /** - * Show the side panel - */ - show(): void { - this._isHiddenByUser = false; - this._refreshVisibility(); - } - - /** - * Find the insertion index for a rank item. - */ - private _findInsertIndex(item: Private.IRankItem): number { - return ArrayExt.upperBound(this._items, item, Private.itemCmp); - } - - /** - * Find the index of the item with the given widget, or `-1`. - */ - private _findWidgetIndex(widget: Widget): number { - return ArrayExt.findFirstIndex(this._items, i => i.widget === widget); - } - - /** - * Find the widget with the given id, or `null`. - */ - private _findWidgetByID(id: string): Widget | null { - const item = find(this._items, value => value.widget.id === id); - return item ? item.widget : null; - } - - /** - * Refresh the visibility of the stacked panel. - */ - private _refreshVisibility(): void { - this._panel.setHidden(this._isHiddenByUser); - } - - /* - * Handle the `widgetRemoved` signal from the panel. - */ - private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void { - if (widget === this._lastCurrentWidget) { - this._lastCurrentWidget = null; - } - ArrayExt.removeAt(this._items, this._findWidgetIndex(widget)); - - this._refreshVisibility(); - - this._widgetRemoved.emit(widget); - } - - private _area: SideBarPanel.Area; - private _isHiddenByUser = false; - private _items = new Array(); - private _panel: Panel; - private _widgetPanel: StackedPanel; - private _currentWidget: Widget | null; - private _lastCurrentWidget: Widget | null; - private _widgetAdded: Signal = new Signal(this); - private _widgetRemoved: Signal = new Signal(this); -} - -/** - * A namespace for private module data. - */ -namespace Private { - /** - * An object which holds a widget and its sort rank. - */ - export interface IRankItem { - /** - * The widget for the item. - */ - widget: Widget; - - /** - * The sort rank of the widget. - */ - rank: number; - } - /** - * A less-than comparison function for side bar rank items. - */ - export function itemCmp(first: IRankItem, second: IRankItem): number { - return first.rank - second.rank; - } - - /** - * A class which manages a panel and sorts its widgets by rank. - */ - export class PanelHandler { - constructor() { - MessageLoop.installMessageHook(this._panel, this._panelChildHook); - } - - /** - * Get the panel managed by the handler. - */ - get panel(): Panel { - return this._panel; - } - - /** - * Add a widget to the panel. - * - * If the widget is already added, it will be moved. - */ - addWidget(widget: Widget, rank: number): void { - widget.parent = null; - const item = { widget, rank }; - const index = ArrayExt.upperBound(this._items, item, Private.itemCmp); - ArrayExt.insert(this._items, index, item); - this._panel.insertWidget(index, widget); - } - - /** - * A message hook for child add/remove messages on the main area dock panel. - */ - private _panelChildHook = ( - handler: IMessageHandler, - msg: Message - ): boolean => { - switch (msg.type) { - case 'child-added': - { - const widget = (msg as Widget.ChildMessage).child; - // If we already know about this widget, we're done - if (this._items.find(v => v.widget === widget)) { - break; - } - - // Otherwise, add to the end by default - const rank = this._items[this._items.length - 1].rank; - this._items.push({ widget, rank }); - } - break; - case 'child-removed': - { - const widget = (msg as Widget.ChildMessage).child; - ArrayExt.removeFirstWhere(this._items, v => v.widget === widget); - } - break; - default: - break; - } - return true; - }; - - private _items = new Array(); - private _panel = new Panel(); - } -} diff --git a/ui-tests/test/notebook.spec.ts b/ui-tests/test/notebook.spec.ts index 3bfbbf095f..78a9e4c50e 100644 --- a/ui-tests/test/notebook.spec.ts +++ b/ui-tests/test/notebook.spec.ts @@ -127,7 +127,7 @@ test.describe('Notebook', () => { ) ).toHaveCount(3); - const imageName = `toc-left-panel.png`; + const imageName = 'toc-left-panel.png'; expect(await panel.screenshot()).toMatchSnapshot(imageName); }); @@ -152,7 +152,7 @@ test.describe('Notebook', () => { await page.isVisible('#notebook-tools.jp-NotebookTools > #add-tag.tag'); - const imageName = `notebooktools-right-panel.png`; + const imageName = 'notebooktools-right-panel.png'; expect(await panel.screenshot()).toMatchSnapshot(imageName); }); }); diff --git a/ui-tests/test/smoke.spec.ts b/ui-tests/test/smoke.spec.ts index c9e944eac4..1e3c5e3371 100644 --- a/ui-tests/test/smoke.spec.ts +++ b/ui-tests/test/smoke.spec.ts @@ -53,7 +53,7 @@ math.pi`); '.jp-Cell-inputArea >> .cm-editor >> .cm-content[contenteditable="true"]' ) .nth(1) - .type(`import this`); + .type('import this'); // Run the cell runAndAdvance(notebook);