Merge pull request #6625 from brichet/sidepanel_followup

Moves panel related objects to a dedicated module file
pull/6629/head
Jeremy Tuloup 3 years ago committed by GitHub
commit beba237528
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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<void> = {
};
/**
* Plugin to toggle the left or right sidebar's visibility.
* Plugin to toggle the left or right side panel's visibility.
*/
const sidebarVisibility: JupyterFrontEndPlugin<void> = {
id: '@jupyter-notebook/application-extension:sidebar',
const sidePanelVisibility: JupyterFrontEndPlugin<void> = {
id: '@jupyter-notebook/application-extension:sidepanel',
requires: [INotebookShell, ITranslator],
optional: [IMainMenu, ICommandPalette],
autoStart: true,
@ -562,7 +561,7 @@ const sidebarVisibility: JupyterFrontEndPlugin<void> = {
/* 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<void> = {
}
});
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<void> = {
// 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<void> = {
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<any>[] = [
paths,
sessionDialogs,
shell,
sidebarVisibility,
sidePanelVisibility,
status,
tabTitle,
title,

@ -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<Widget>,
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<Widget>, 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<Widget>, 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;
};

@ -3,3 +3,4 @@
export * from './app';
export * from './shell';
export * from './panelhandler';

@ -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<Private.IRankItem>();
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<Widget[]> {
return this._items.map(obj => obj.widget);
}
/**
* Signal fired when a widget is added to the panel
*/
get widgetAdded(): ISignal<SidePanelHandler, Widget> {
return this._widgetAdded;
}
/**
* Signal fired when a widget is removed from the panel
*/
get widgetRemoved(): ISignal<SidePanelHandler, Widget> {
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<SidePanelHandler, Widget> = new Signal(this);
private _widgetRemoved: Signal<SidePanelHandler, Widget> = 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<Widget>,
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<Widget>, 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<Widget>, 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;
}
}

@ -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, void>(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<Widget[]> {
return this._items.map(obj => obj.widget);
}
/**
* Signal fired when a widget is added to the panel
*/
get widgetAdded(): ISignal<SideBarHandler, Widget> {
return this._widgetAdded;
}
/**
* Signal fired when a widget is removed from the panel
*/
get widgetRemoved(): ISignal<SideBarHandler, Widget> {
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.IRankItem>();
private _panel: Panel;
private _widgetPanel: StackedPanel;
private _currentWidget: Widget | null;
private _lastCurrentWidget: Widget | null;
private _widgetAdded: Signal<SideBarHandler, Widget> = new Signal(this);
private _widgetRemoved: Signal<SideBarHandler, Widget> = 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.IRankItem>();
private _panel = new Panel();
}
}

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

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

Loading…
Cancel
Save