UNPKG

@jupyterlab/application

Version:
1,336 lines 66.3 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { DocumentWidget } from '@jupyterlab/docregistry'; import { nullTranslator } from '@jupyterlab/translation'; import { classes, DockPanelSvg, LabIcon, TabBarSvg, tabIcon, TabPanelSvg } from '@jupyterlab/ui-components'; import { ArrayExt, find, map } from '@lumino/algorithm'; import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils'; import { MessageLoop } from '@lumino/messaging'; import { Debouncer } from '@lumino/polling'; import { Signal } from '@lumino/signaling'; import { AccordionPanel, BoxLayout, BoxPanel, FocusTracker, Panel, SplitPanel, StackedPanel, TabBar, Widget } from '@lumino/widgets'; /** * The class name added to AppShell instances. */ const APPLICATION_SHELL_CLASS = 'jp-LabShell'; /** * The class name added to side bar instances. */ const SIDEBAR_CLASS = 'jp-SideBar'; /** * The class name added to the current widget's title. */ const CURRENT_CLASS = 'jp-mod-current'; /** * The class name added to the active widget's title. */ const ACTIVE_CLASS = 'jp-mod-active'; /** * The default rank of items added to a sidebar. */ const DEFAULT_RANK = 900; const ACTIVITY_CLASS = 'jp-Activity'; /** * The JupyterLab application shell token. */ export const ILabShell = new Token('@jupyterlab/application:ILabShell', 'A service for interacting with the JupyterLab shell. The top-level ``application`` object also has a reference to the shell, but it has a restricted interface in order to be agnostic to different shell implementations on the application. Use this to get more detailed information about currently active widgets and layout state.'); /** * The application shell for JupyterLab. */ export class LabShell extends Widget { /** * Construct a new application shell. */ constructor(options) { super(); /** * A message hook for child add/remove messages on the main area dock panel. */ this._dockChildHook = (handler, msg) => { switch (msg.type) { case 'child-added': msg.child.addClass(ACTIVITY_CLASS); this._tracker.add(msg.child); break; case 'child-removed': msg.child.removeClass(ACTIVITY_CLASS); this._tracker.remove(msg.child); break; default: break; } return true; }; this._activeChanged = new Signal(this); this._cachedLayout = null; this._currentChanged = new Signal(this); this._currentPath = ''; this._currentPathChanged = new Signal(this); this._modeChanged = new Signal(this); this._isRestored = false; this._layoutModified = new Signal(this); this._layoutDebouncer = new Debouncer(() => { this._layoutModified.emit(undefined); }, 0); this._restored = new PromiseDelegate(); this._tracker = new FocusTracker(); this._topHandlerHiddenByUser = false; this._idTypeMap = new Map(); this._mainOptionsCache = new Map(); this._sideOptionsCache = new Map(); this._delayedWidget = new Array(); this.addClass(APPLICATION_SHELL_CLASS); this.id = 'main'; if ((options === null || options === void 0 ? void 0 : options.waitForRestore) === false) { this._userLayout = { 'multiple-document': {}, 'single-document': {} }; } // Skip Links const skipLinkWidget = (this._skipLinkWidget = new Private.SkipLinkWidget(this)); this._skipLinkWidget.show(); // Wrap the skip widget to customize its position and size const skipLinkWrapper = new Panel(); skipLinkWrapper.addClass('jp-skiplink-wrapper'); skipLinkWrapper.addWidget(skipLinkWidget); const headerPanel = (this._headerPanel = new BoxPanel()); const menuHandler = (this._menuHandler = new Private.PanelHandler()); menuHandler.panel.node.setAttribute('role', 'navigation'); const topHandler = (this._topHandler = new Private.PanelHandler()); topHandler.panel.node.setAttribute('role', 'banner'); const bottomPanel = (this._bottomPanel = new BoxPanel()); bottomPanel.node.setAttribute('role', 'contentinfo'); const hboxPanel = new BoxPanel(); const vsplitPanel = (this._vsplitPanel = new Private.RestorableSplitPanel()); const dockPanel = (this._dockPanel = new DockPanelSvg({ hiddenMode: Widget.HiddenMode.Display })); MessageLoop.installMessageHook(dockPanel, this._dockChildHook); const hsplitPanel = (this._hsplitPanel = new Private.RestorableSplitPanel()); const downPanel = (this._downPanel = new TabPanelSvg({ tabsMovable: true })); const leftHandler = (this._leftHandler = new Private.SideBarHandler()); const rightHandler = (this._rightHandler = new Private.SideBarHandler()); const rootLayout = new BoxLayout(); headerPanel.id = 'jp-header-panel'; menuHandler.panel.id = 'jp-menu-panel'; topHandler.panel.id = 'jp-top-panel'; bottomPanel.id = 'jp-bottom-panel'; hboxPanel.id = 'jp-main-content-panel'; vsplitPanel.id = 'jp-main-vsplit-panel'; dockPanel.id = 'jp-main-dock-panel'; hsplitPanel.id = 'jp-main-split-panel'; downPanel.id = 'jp-down-stack'; leftHandler.sideBar.addClass(SIDEBAR_CLASS); leftHandler.sideBar.addClass('jp-mod-left'); leftHandler.sideBar.node.setAttribute('role', 'complementary'); leftHandler.stackedPanel.id = 'jp-left-stack'; rightHandler.sideBar.addClass(SIDEBAR_CLASS); rightHandler.sideBar.addClass('jp-mod-right'); rightHandler.sideBar.node.setAttribute('role', 'complementary'); rightHandler.stackedPanel.id = 'jp-right-stack'; dockPanel.node.setAttribute('role', 'main'); hboxPanel.spacing = 0; vsplitPanel.spacing = 1; dockPanel.spacing = 5; hsplitPanel.spacing = 1; headerPanel.direction = 'top-to-bottom'; vsplitPanel.orientation = 'vertical'; hboxPanel.direction = 'left-to-right'; hsplitPanel.orientation = 'horizontal'; bottomPanel.direction = 'bottom-to-top'; SplitPanel.setStretch(leftHandler.stackedPanel, 0); SplitPanel.setStretch(downPanel, 0); SplitPanel.setStretch(dockPanel, 1); SplitPanel.setStretch(rightHandler.stackedPanel, 0); BoxPanel.setStretch(leftHandler.sideBar, 0); BoxPanel.setStretch(hsplitPanel, 1); BoxPanel.setStretch(rightHandler.sideBar, 0); SplitPanel.setStretch(vsplitPanel, 1); hsplitPanel.addWidget(leftHandler.stackedPanel); hsplitPanel.addWidget(dockPanel); hsplitPanel.addWidget(rightHandler.stackedPanel); vsplitPanel.addWidget(hsplitPanel); vsplitPanel.addWidget(downPanel); hboxPanel.addWidget(leftHandler.sideBar); hboxPanel.addWidget(vsplitPanel); hboxPanel.addWidget(rightHandler.sideBar); rootLayout.direction = 'top-to-bottom'; rootLayout.spacing = 0; // TODO make this configurable? // Use relative sizing to set the width of the side panels. // This will still respect the min-size of children widget in the stacked // panel. The default sizes will be overwritten during layout restoration. vsplitPanel.setRelativeSizes([3, 1]); hsplitPanel.setRelativeSizes([1, 2.5, 1]); BoxLayout.setStretch(headerPanel, 0); BoxLayout.setStretch(menuHandler.panel, 0); BoxLayout.setStretch(topHandler.panel, 0); BoxLayout.setStretch(hboxPanel, 1); BoxLayout.setStretch(bottomPanel, 0); rootLayout.addWidget(skipLinkWrapper); rootLayout.addWidget(headerPanel); rootLayout.addWidget(topHandler.panel); rootLayout.addWidget(hboxPanel); rootLayout.addWidget(bottomPanel); // initially hiding header and bottom panel when no elements inside, this._headerPanel.hide(); this._bottomPanel.hide(); this._downPanel.hide(); this.layout = rootLayout; // Connect change listeners. this._tracker.currentChanged.connect(this._onCurrentChanged, this); this._tracker.activeChanged.connect(this._onActiveChanged, this); // Connect main layout change listener. this._dockPanel.layoutModified.connect(this._onLayoutModified, this); // Connect vsplit layout change listener this._vsplitPanel.updated.connect(this._onLayoutModified, this); // Connect down panel change listeners this._downPanel.currentChanged.connect(this._onLayoutModified, this); this._downPanel.tabBar.tabMoved.connect(this._onTabPanelChanged, this); this._downPanel.stackedPanel.widgetRemoved.connect(this._onTabPanelChanged, this); // Catch current changed events on the side handlers. this._leftHandler.updated.connect(this._onLayoutModified, this); this._rightHandler.updated.connect(this._onLayoutModified, this); // Catch update events on the horizontal split panel this._hsplitPanel.updated.connect(this._onLayoutModified, this); // Setup single-document-mode title bar const titleHandler = (this._titleHandler = new Private.TitleHandler(this)); this.add(titleHandler, 'top', { rank: 100 }); if (this._dockPanel.mode === 'multiple-document') { this._topHandler.addWidget(this._menuHandler.panel, 100); titleHandler.hide(); } else { rootLayout.insertWidget(3, this._menuHandler.panel); } this.translator = nullTranslator; // Wire up signals to update the title panel of the simple interface mode to // follow the title of this.currentWidget this.currentChanged.connect((sender, args) => { let newValue = args.newValue; let oldValue = args.oldValue; // Stop watching the title of the previously current widget if (oldValue) { oldValue.title.changed.disconnect(this._updateTitlePanelTitle, this); if (oldValue instanceof DocumentWidget) { oldValue.context.pathChanged.disconnect(this._updateCurrentPath, this); } } // Start watching the title of the new current widget if (newValue) { newValue.title.changed.connect(this._updateTitlePanelTitle, this); this._updateTitlePanelTitle(); if (newValue instanceof DocumentWidget) { newValue.context.pathChanged.connect(this._updateCurrentPath, this); } } this._updateCurrentPath(); }); } /** * A signal emitted when main area's active focus changes. */ get activeChanged() { return this._activeChanged; } /** * The active widget in the shell's main area. */ get activeWidget() { return this._tracker.activeWidget; } /** * Whether the add buttons for each main area tab bar are enabled. */ get addButtonEnabled() { return this._dockPanel.addButtonEnabled; } set addButtonEnabled(value) { this._dockPanel.addButtonEnabled = value; } /** * A signal emitted when the add button on a main area tab bar is clicked. */ get addRequested() { return this._dockPanel.addRequested; } /** * A signal emitted when main area's current focus changes. */ get currentChanged() { return this._currentChanged; } /** * Current document path. */ // FIXME deprecation `undefined` is to ensure backward compatibility in 4.x get currentPath() { return this._currentPath; } /** * A signal emitted when the path of the current document changes. * * This also fires when the current document itself changes. */ get currentPathChanged() { return this._currentPathChanged; } /** * The current widget in the shell's main area. */ get currentWidget() { return this._tracker.currentWidget; } /** * A signal emitted when the main area's layout is modified. */ get layoutModified() { return this._layoutModified; } /** * Whether the left area is collapsed. */ get leftCollapsed() { return !this._leftHandler.sideBar.currentTitle; } /** * Whether the left area is collapsed. */ get rightCollapsed() { return !this._rightHandler.sideBar.currentTitle; } /** * Whether JupyterLab is in presentation mode with the * `jp-mod-presentationMode` CSS class. */ get presentationMode() { return this.hasClass('jp-mod-presentationMode'); } set presentationMode(value) { this.toggleClass('jp-mod-presentationMode', value); } /** * The main dock area's user interface mode. */ get mode() { return this._dockPanel.mode; } set mode(mode) { const dock = this._dockPanel; if (mode === dock.mode) { return; } const applicationCurrentWidget = this.currentWidget; if (mode === 'single-document') { // Cache the current multi-document layout before changing the mode. this._cachedLayout = dock.saveLayout(); dock.mode = mode; // In case the active widget in the dock panel is *not* the active widget // of the application, defer to the application. if (this.currentWidget) { dock.activateWidget(this.currentWidget); } // Adjust menu and title this.layout.insertWidget(3, this._menuHandler.panel); this._titleHandler.show(); this._updateTitlePanelTitle(); if (this._topHandlerHiddenByUser) { this._topHandler.panel.hide(); } } else { // Cache a reference to every widget currently in the dock panel before // changing its mode. const widgets = Array.from(dock.widgets()); dock.mode = mode; // Restore cached layout if possible. if (this._cachedLayout) { // Remove any disposed widgets in the cached layout and restore. Private.normalizeAreaConfig(dock, this._cachedLayout.main); dock.restoreLayout(this._cachedLayout); this._cachedLayout = null; } // If layout restoration has been deferred, restore layout now. if (this._layoutRestorer.isDeferred) { this._layoutRestorer .restoreDeferred() .then(mainArea => { if (mainArea) { const { currentWidget, dock } = mainArea; if (dock) { this._dockPanel.restoreLayout(dock); } if (currentWidget) { this.activateById(currentWidget.id); } } }) .catch(reason => { console.error('Failed to restore the deferred layout.'); console.error(reason); }); } // Add any widgets created during single document mode, which have // subsequently been removed from the dock panel after the multiple document // layout has been restored. If the widget has add options cached for // the widget (i.e., if it has been placed with respect to another widget), // then take that into account. widgets.forEach(widget => { if (!widget.parent) { this._addToMainArea(widget, { ...this._mainOptionsCache.get(widget), activate: false }); } }); this._mainOptionsCache.clear(); // In case the active widget in the dock panel is *not* the active widget // of the application, defer to the application. if (applicationCurrentWidget) { dock.activateWidget(applicationCurrentWidget); } // Adjust menu and title this.add(this._menuHandler.panel, 'top', { rank: 100 }); this._titleHandler.hide(); } // Set the mode data attribute on the applications shell node. this.node.dataset.shellMode = mode; this._downPanel.fit(); // Emit the mode changed signal this._modeChanged.emit(mode); } /** * A signal emitted when the shell/dock panel change modes (single/multiple document). */ get modeChanged() { return this._modeChanged; } /** * Promise that resolves when state is first restored, returning layout * description. */ get restored() { return this._restored.promise; } get translator() { var _a; return (_a = this._translator) !== null && _a !== void 0 ? _a : nullTranslator; } set translator(value) { if (value !== this._translator) { this._translator = value; // Set translator for tab bars TabBarSvg.translator = value; const trans = value.load('jupyterlab'); this._menuHandler.panel.node.setAttribute('aria-label', trans.__('main menu')); this._leftHandler.sideBar.node.setAttribute('aria-label', trans.__('main sidebar')); this._leftHandler.sideBar.contentNode.setAttribute('aria-label', trans.__('main sidebar')); this._rightHandler.sideBar.node.setAttribute('aria-label', trans.__('alternate sidebar')); this._rightHandler.sideBar.contentNode.setAttribute('aria-label', trans.__('alternate sidebar')); this._topHandler.panel.node.setAttribute('aria-label', trans.__('Top Bar')); this._bottomPanel.node.setAttribute('aria-label', trans.__('Bottom Panel')); this._dockPanel.node.setAttribute('aria-label', trans.__('Main Content')); } } /** * User customized shell layout. */ get userLayout() { return JSONExt.deepCopy(this._userLayout); } /** * Activate a widget in its area. */ activateById(id) { if (this._leftHandler.has(id)) { this._leftHandler.activate(id); return; } if (this._rightHandler.has(id)) { this._rightHandler.activate(id); return; } const tabIndex = this._downPanel.tabBar.titles.findIndex(title => title.owner.id === id); if (tabIndex >= 0) { this._downPanel.currentIndex = tabIndex; return; } const dock = this._dockPanel; const widget = find(dock.widgets(), value => value.id === id); if (widget) { dock.activateWidget(widget); } } /** * Activate widget in specified area. * * ### Notes * The alpha version of this method only supports activating the "main" area. * * @alpha * @param area Name of area to activate */ activateArea(area = 'main') { switch (area) { case 'main': { const current = this._currentTabBar(); if (!current) { return; } if (current.currentTitle) { current.currentTitle.owner.activate(); } } return; case 'left': case 'right': case 'header': case 'top': case 'menu': case 'bottom': console.debug(`Area: ${area} activation not yet implemented`); break; default: throw new Error(`Invalid area: ${area}`); } } /** * Activate the next Tab in the active TabBar. */ activateNextTab() { const current = this._currentTabBar(); if (!current) { return; } const ci = current.currentIndex; if (ci === -1) { return; } if (ci < current.titles.length - 1) { current.currentIndex += 1; if (current.currentTitle) { current.currentTitle.owner.activate(); } return; } if (ci === current.titles.length - 1) { const nextBar = this._adjacentBar('next'); if (nextBar) { nextBar.currentIndex = 0; if (nextBar.currentTitle) { nextBar.currentTitle.owner.activate(); } } } } /** * Activate the previous Tab in the active TabBar. */ activatePreviousTab() { const current = this._currentTabBar(); if (!current) { return; } const ci = current.currentIndex; if (ci === -1) { return; } if (ci > 0) { current.currentIndex -= 1; if (current.currentTitle) { current.currentTitle.owner.activate(); } return; } if (ci === 0) { const prevBar = this._adjacentBar('previous'); if (prevBar) { const len = prevBar.titles.length; prevBar.currentIndex = len - 1; if (prevBar.currentTitle) { prevBar.currentTitle.owner.activate(); } } } } /** * Activate the next TabBar. */ activateNextTabBar() { const nextBar = this._adjacentBar('next'); if (nextBar) { if (nextBar.currentTitle) { nextBar.currentTitle.owner.activate(); } } } /** * Activate the next TabBar. */ activatePreviousTabBar() { const nextBar = this._adjacentBar('previous'); if (nextBar) { if (nextBar.currentTitle) { nextBar.currentTitle.owner.activate(); } } } /** * Add a widget to the JupyterLab shell * * @param widget Widget * @param area Area * @param options Options */ add(widget, area = 'main', options) { var _a; if (!this._userLayout) { this._delayedWidget.push({ widget, area, options }); return; } let userPosition; if ((options === null || options === void 0 ? void 0 : options.type) && this._userLayout[this.mode][options.type]) { userPosition = this._userLayout[this.mode][options.type]; this._idTypeMap.set(widget.id, options.type); } else { userPosition = this._userLayout[this.mode][widget.id]; } if (options === null || options === void 0 ? void 0 : options.type) { this._idTypeMap.set(widget.id, options.type); widget.disposed.connect(() => { this._idTypeMap.delete(widget.id); }); } area = (_a = userPosition === null || userPosition === void 0 ? void 0 : userPosition.area) !== null && _a !== void 0 ? _a : area; options = options || (userPosition === null || userPosition === void 0 ? void 0 : userPosition.options) ? { ...options, ...userPosition === null || userPosition === void 0 ? void 0 : userPosition.options } : undefined; switch (area || 'main') { case 'bottom': return this._addToBottomArea(widget, options); case 'down': return this._addToDownArea(widget, options); case 'header': return this._addToHeaderArea(widget, options); case 'left': return this._addToLeftArea(widget, options); case 'main': return this._addToMainArea(widget, options); case 'menu': return this._addToMenuArea(widget, options); case 'right': return this._addToRightArea(widget, options); case 'top': return this._addToTopArea(widget, options); default: throw new Error(`Invalid area: ${area}`); } } /** * Move a widget type to a new area. * * The type is determined from the `widget.id` and fallback to `widget.id`. * * #### Notes * If `mode` is undefined, both mode are updated. * The new layout is now persisted. * * @param widget Widget to move * @param area New area * @param mode Mode to change * @returns The new user layout */ move(widget, area, mode) { var _a; const type = (_a = this._idTypeMap.get(widget.id)) !== null && _a !== void 0 ? _a : widget.id; for (const m of ['single-document', 'multiple-document'].filter(c => !mode || c === mode)) { this._userLayout[m][type] = { ...this._userLayout[m][type], area }; } this.add(widget, area); return this._userLayout; } /** * Collapse the left area. */ collapseLeft() { this._leftHandler.collapse(); this._onLayoutModified(); } /** * Collapse the right area. */ collapseRight() { this._rightHandler.collapse(); this._onLayoutModified(); } /** * Dispose the shell. */ dispose() { if (this.isDisposed) { return; } this._layoutDebouncer.dispose(); super.dispose(); } /** * Expand the left area. * * #### Notes * This will open the most recently used tab, * or the first tab if there is no most recently used. */ expandLeft() { this._leftHandler.expand(); this._onLayoutModified(); } /** * Expand the right area. * * #### Notes * This will open the most recently used tab, * or the first tab if there is no most recently used. */ expandRight() { this._rightHandler.expand(); this._onLayoutModified(); } /** * Close all widgets in the main and down area. */ closeAll() { // Make a copy of all the widget in the dock panel (using `Array.from()`) // before removing them because removing them while iterating through them // modifies the underlying data of the iterator. Array.from(this._dockPanel.widgets()).forEach(widget => widget.close()); this._downPanel.stackedPanel.widgets.forEach(widget => widget.close()); } /** * Whether an side tab bar is visible or not. * * @param side Sidebar of interest * @returns Side tab bar visibility */ isSideTabBarVisible(side) { switch (side) { case 'left': return this._leftHandler.isVisible; case 'right': return this._rightHandler.isVisible; } } /** * Whether the top bar in simple mode is visible or not. * * @returns Top bar visibility */ isTopInSimpleModeVisible() { return !this._topHandlerHiddenByUser; } /** * True if the given area is empty. */ isEmpty(area) { switch (area) { case 'bottom': return this._bottomPanel.widgets.length === 0; case 'down': return this._downPanel.stackedPanel.widgets.length === 0; case 'header': return this._headerPanel.widgets.length === 0; case 'left': return this._leftHandler.stackedPanel.widgets.length === 0; case 'main': return this._dockPanel.isEmpty; case 'menu': return this._menuHandler.panel.widgets.length === 0; case 'right': return this._rightHandler.stackedPanel.widgets.length === 0; case 'top': return this._topHandler.panel.widgets.length === 0; default: return true; } } /** * Restore the layout state and configuration for the application shell. * * #### Notes * This should only be called once. */ async restoreLayout(mode, layoutRestorer, configuration = {}) { var _a, _b, _c, _d; // Set the configuration and add widgets added before the shell was ready. this._userLayout = { 'single-document': (_a = configuration['single-document']) !== null && _a !== void 0 ? _a : {}, 'multiple-document': (_b = configuration['multiple-document']) !== null && _b !== void 0 ? _b : {} }; this._delayedWidget.forEach(({ widget, area, options }) => { this.add(widget, area, options); }); this._delayedWidget.length = 0; this._layoutRestorer = layoutRestorer; // Get the layout from the restorer const layout = await layoutRestorer.fetch(); // Reset the layout const { mainArea, downArea, leftArea, rightArea, topArea, relativeSizes } = layout; // Rehydrate the main area. if (mainArea) { const { currentWidget, dock } = mainArea; if (dock && mode === 'multiple-document') { this._dockPanel.restoreLayout(dock); } if (mode) { this.mode = mode; } if (currentWidget) { this.activateById(currentWidget.id); } } else { // This is needed when loading in an empty workspace in single doc mode if (mode) { this.mode = mode; } } if ((topArea === null || topArea === void 0 ? void 0 : topArea.simpleVisibility) !== undefined) { this._topHandlerHiddenByUser = !topArea.simpleVisibility; if (this.mode === 'single-document') { this._topHandler.panel.setHidden(this._topHandlerHiddenByUser); } } // Rehydrate the down area if (downArea) { const { currentWidget, widgets, size } = downArea; const widgetIds = (_c = widgets === null || widgets === void 0 ? void 0 : widgets.map(widget => widget.id)) !== null && _c !== void 0 ? _c : []; // Remove absent widgets this._downPanel.tabBar.titles .filter(title => !widgetIds.includes(title.owner.id)) .map(title => title.owner.close()); // Add new widgets const titleIds = this._downPanel.tabBar.titles.map(title => title.owner.id); widgets === null || widgets === void 0 ? void 0 : widgets.filter(widget => !titleIds.includes(widget.id)).map(widget => this._downPanel.addWidget(widget)); // Reorder tabs while (!ArrayExt.shallowEqual(widgetIds, this._downPanel.tabBar.titles.map(title => title.owner.id))) { this._downPanel.tabBar.titles.forEach((title, index) => { const position = widgetIds.findIndex(id => title.owner.id == id); if (position >= 0 && position != index) { this._downPanel.tabBar.insertTab(position, title); } }); } if (currentWidget) { const index = this._downPanel.stackedPanel.widgets.findIndex(widget => widget.id === currentWidget.id); if (index) { this._downPanel.currentIndex = index; (_d = this._downPanel.currentWidget) === null || _d === void 0 ? void 0 : _d.activate(); } } if (size && size > 0.0) { this._vsplitPanel.setRelativeSizes([1.0 - size, size]); } else { // Close all tabs and hide the panel this._downPanel.stackedPanel.widgets.forEach(widget => widget.close()); this._downPanel.hide(); } } // Rehydrate the left area. if (leftArea) { this._leftHandler.rehydrate(leftArea); } else { if (mode === 'single-document') { this.collapseLeft(); } } // Rehydrate the right area. if (rightArea) { this._rightHandler.rehydrate(rightArea); } else { if (mode === 'single-document') { this.collapseRight(); } } // Restore the relative sizes. if (relativeSizes) { this._hsplitPanel.setRelativeSizes(relativeSizes); } if (!this._isRestored) { // Make sure all messages in the queue are finished before notifying // any extensions that are waiting for the promise that guarantees the // application state has been restored. MessageLoop.flush(); this._restored.resolve(layout); } } /** * Save the dehydrated state of the application shell. */ saveLayout() { // If the application is in single document mode, use the cached layout if // available. Otherwise, default to querying the dock panel for layout. const layout = { mainArea: { currentWidget: this._tracker.currentWidget, dock: this.mode === 'single-document' ? this._cachedLayout || this._dockPanel.saveLayout() : this._dockPanel.saveLayout() }, downArea: { currentWidget: this._downPanel.currentWidget, widgets: Array.from(this._downPanel.stackedPanel.widgets), size: this._vsplitPanel.relativeSizes()[1] }, leftArea: this._leftHandler.dehydrate(), rightArea: this._rightHandler.dehydrate(), topArea: { simpleVisibility: !this._topHandlerHiddenByUser }, relativeSizes: this._hsplitPanel.relativeSizes() }; return layout; } /** * Toggle top header visibility in simple mode * * Note: Does nothing in multi-document mode */ toggleTopInSimpleModeVisibility() { if (this.mode === 'single-document') { if (this._topHandler.panel.isVisible) { this._topHandlerHiddenByUser = true; this._topHandler.panel.hide(); } else { this._topHandlerHiddenByUser = false; this._topHandler.panel.show(); this._updateTitlePanelTitle(); } this._onLayoutModified(); } } /** * Toggle side tab bar visibility * * @param side Sidebar of interest */ toggleSideTabBarVisibility(side) { if (side === 'right') { if (this._rightHandler.isVisible) { this._rightHandler.hide(); } else { this._rightHandler.show(); } } else { if (this._leftHandler.isVisible) { this._leftHandler.hide(); } else { this._leftHandler.show(); } } } /** * Update the shell configuration. * * @param config Shell configuration */ updateConfig(config) { if (config.hiddenMode) { switch (config.hiddenMode) { case 'display': this._dockPanel.hiddenMode = Widget.HiddenMode.Display; break; case 'scale': this._dockPanel.hiddenMode = Widget.HiddenMode.Scale; break; case 'contentVisibility': this._dockPanel.hiddenMode = Widget.HiddenMode.ContentVisibility; break; } } } /** * Returns the widgets for an application area. */ widgets(area) { switch (area !== null && area !== void 0 ? area : 'main') { case 'main': return this._dockPanel.widgets(); case 'left': return map(this._leftHandler.sideBar.titles, t => t.owner); case 'right': return map(this._rightHandler.sideBar.titles, t => t.owner); case 'header': return this._headerPanel.children(); case 'top': return this._topHandler.panel.children(); case 'menu': return this._menuHandler.panel.children(); case 'bottom': return this._bottomPanel.children(); default: throw new Error(`Invalid area: ${area}`); } } /** * Handle `after-attach` messages for the application shell. */ onAfterAttach(msg) { this.node.dataset.shellMode = this.mode; } /** * Update the title panel title based on the title of the current widget. */ _updateTitlePanelTitle() { let current = this.currentWidget; const inputElement = this._titleHandler.inputElement; inputElement.value = current ? current.title.label : ''; inputElement.title = current ? current.title.caption : ''; } /** * The path of the current widget changed, fire the _currentPathChanged signal. */ _updateCurrentPath() { let current = this.currentWidget; let newValue = ''; if (current && current instanceof DocumentWidget) { newValue = current.context.path; } this._currentPathChanged.emit({ newValue: newValue, oldValue: this._currentPath }); this._currentPath = newValue; } /** * Add a widget to the left content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToLeftArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || this._sideOptionsCache.get(widget) || {}; this._sideOptionsCache.set(widget, options); const rank = 'rank' in options ? options.rank : DEFAULT_RANK; this._leftHandler.addWidget(widget, rank); this._onLayoutModified(); } /** * Add a widget to the main content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. * All widgets added to the main area should be disposed after removal * (disposal before removal will remove the widget automatically). * * In the options, `ref` defaults to `null`, `mode` defaults to `'tab-after'`, * and `activate` defaults to `true`. */ _addToMainArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || {}; const dock = this._dockPanel; const mode = options.mode || 'tab-after'; let ref = this.currentWidget; if (options.ref) { ref = find(dock.widgets(), value => value.id === options.ref) || null; } const { title } = widget; // Add widget ID to tab so that we can get a handle on the tab's widget // (for context menu support) title.dataset = { ...title.dataset, id: widget.id }; if (title.icon instanceof LabIcon) { // bind an appropriate style to the icon title.icon = title.icon.bindprops({ stylesheet: 'mainAreaTab' }); } else if (typeof title.icon === 'string' || !title.icon) { // add some classes to help with displaying css background imgs title.iconClass = classes(title.iconClass, 'jp-Icon'); } dock.addWidget(widget, { mode, ref }); // The dock panel doesn't account for placement information while // in single document mode, so upon rehydrating any widgets that were // added will not be in the correct place. Cache the placement information // here so that we can later rehydrate correctly. if (dock.mode === 'single-document') { this._mainOptionsCache.set(widget, options); } if (options.activate !== false) { dock.activateWidget(widget); } } /** * Add a widget to the right content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToRightArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || this._sideOptionsCache.get(widget) || {}; const rank = 'rank' in options ? options.rank : DEFAULT_RANK; this._sideOptionsCache.set(widget, options); this._rightHandler.addWidget(widget, rank); this._onLayoutModified(); } /** * Add a widget to the top content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToTopArea(widget, options) { var _a; if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || {}; const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : DEFAULT_RANK; this._topHandler.addWidget(widget, rank); this._onLayoutModified(); if (this._topHandler.panel.isHidden) { this._topHandler.panel.show(); } } /** * Add a widget to the title content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToMenuArea(widget, options) { var _a; if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || {}; const rank = (_a = options.rank) !== null && _a !== void 0 ? _a : DEFAULT_RANK; this._menuHandler.addWidget(widget, rank); this._onLayoutModified(); if (this._menuHandler.panel.isHidden) { this._menuHandler.panel.show(); } } /** * Add a widget to the header content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToHeaderArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } // Temporary: widgets are added to the panel in order of insertion. this._headerPanel.addWidget(widget); this._onLayoutModified(); if (this._headerPanel.isHidden) { this._headerPanel.show(); } } /** * Add a widget to the bottom content area. * * #### Notes * Widgets must have a unique `id` property, which will be used as the DOM id. */ _addToBottomArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } // Temporary: widgets are added to the panel in order of insertion. this._bottomPanel.addWidget(widget); this._onLayoutModified(); if (this._bottomPanel.isHidden) { this._bottomPanel.show(); } } _addToDownArea(widget, options) { if (!widget.id) { console.error('Widgets added to app shell must have unique id property.'); return; } options = options || {}; const { title } = widget; // Add widget ID to tab so that we can get a handle on the tab's widget // (for context menu support) title.dataset = { ...title.dataset, id: widget.id }; if (title.icon instanceof LabIcon) { // bind an appropriate style to the icon title.icon = title.icon.bindprops({ stylesheet: 'mainAreaTab' }); } else if (typeof title.icon === 'string' || !title.icon) { // add some classes to help with displaying css background imgs title.iconClass = classes(title.iconClass, 'jp-Icon'); } this._downPanel.addWidget(widget); this._onLayoutModified(); if (this._downPanel.isHidden) { this._downPanel.show(); } } /* * Return the tab bar adjacent to the current TabBar or `null`. */ _adjacentBar(direction) { const current = this._currentTabBar(); if (!current) { return null; } const bars = Array.from(this._dockPanel.tabBars()); const len = bars.length; const index = bars.indexOf(current); if (direction === 'previous') { return index > 0 ? bars[index - 1] : index === 0 ? bars[len - 1] : null; } // Otherwise, direction is 'next'. return index < len - 1 ? bars[index + 1] : index === len - 1 ? bars[0] : null; } /* * Return the TabBar that has the currently active Widget or null. */ _currentTabBar() { const current = this._tracker.currentWidget; if (!current) { return null; } const title = current.title; const bars = this._dockPanel.tabBars(); return find(bars, bar => bar.titles.indexOf(title) > -1) || null; } /** * Handle a change to the dock area active widget. */ _onActiveChanged(sender, args) { if (args.newValue) { args.newValue.title.className += ` ${ACTIVE_CLASS}`; } if (args.oldValue) { args.oldValue.title.className = args.oldValue.title.className.replace(ACTIVE_CLASS, ''); } this._activeChanged.emit(args); } /** * Handle a change to the dock area current widget. */ _onCurrentChanged(sender, args) { if (args.newValue) { args.newValue.title.className += ` ${CURRENT_CLASS}`; } if (args.oldValue) { args.oldValue.title.className = args.oldValue.title.className.replace(CURRENT_CLASS, ''); } this._currentChanged.emit(args); this._onLayoutModified(); } /** * Handle a change on the down panel widgets */ _onTabPanelChanged() { if (this._downPanel.stackedPanel.widgets.length === 0) { this._downPanel.hide(); } this._onLayoutModified(); } /** * Handle a change to the layout. */ _onLayoutModified() { void this._layoutDebouncer.invoke(); } } var Private; (function (Private) { /** * A less-than comparison function for side bar rank items. */ function itemCmp(first, second) { return first.rank - second.rank; } Private.itemCmp = itemCmp; /** * Removes widgets that have been disposed from an area config, mutates area. */ function normalizeAreaConfig(parent, area) { if (!area) { return; } if (area.type === 'tab-area') { area.widgets = area.widgets.filter(widget => !widget.isDisposed && widget.parent === parent); return; } area.children.forEach(child => { normalizeAreaConfig(parent, child); }); } Private.normalizeAreaConfig = normalizeAreaConfig; /** * A class which manages a panel and sorts its widgets by rank. */ class PanelHandler { constructor() { /** * A message hook for child add/remove messages on the main area dock panel. */ this._panelChildHook = (handler, msg) => { switch (msg.type) { case 'child-added': { const widget = msg.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.child; ArrayExt.removeFirstWhere(this._items, v => v.widget === widget); } break; default: break; } return true; }; this._items = new Array(); this._panel = new Pa