You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
444 lines
10 KiB
444 lines
10 KiB
// 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 remove messages on the panel handler.
|
|
*/
|
|
private _panelChildHook = (
|
|
handler: IMessageHandler,
|
|
msg: Message
|
|
): boolean => {
|
|
switch (msg.type) {
|
|
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);
|
|
|
|
this._closeButton = document.createElement('button');
|
|
closeIcon.element({
|
|
container: this._closeButton,
|
|
height: '16px',
|
|
width: 'auto'
|
|
});
|
|
this._closeButton.onclick = () => {
|
|
this.collapse();
|
|
this.hide();
|
|
};
|
|
this._closeButton.className = 'jp-Button jp-SidePanel-collapse';
|
|
this._closeButton.title = 'Collapse side panel';
|
|
|
|
const icon = new Widget({ node: this._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;
|
|
}
|
|
|
|
/**
|
|
* Get the close button element.
|
|
*/
|
|
get closeButton(): HTMLButtonElement {
|
|
return this._closeButton;
|
|
}
|
|
|
|
/**
|
|
* 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 _closeButton: HTMLButtonElement;
|
|
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;
|
|
}
|
|
}
|