UNPKG

@jupyterlab/application

Version:
2,152 lines 82.5 kB
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DocumentWidget } from '@jupyterlab/docregistry';
import { nullTranslator } from '@jupyterlab/translation';
import { classes, 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';
import { OptimizedDockPanelSvg } from './dockpanel';
/**
 * 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 default relative size of the down area when it is expanded.
 */
const DEFAULT_DOWN_AREA_SIZE = 0.25;
/**
 * 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._lastDownAreaSize = DEFAULT_DOWN_AREA_SIZE;
        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 OptimizedDockPanelSvg({
            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({
            side: 'left',
            host: hboxPanel
        }));
        const rightHandler = (this._rightHandler = new Private.SideBarHandler({
            side: 'right',
            host: hboxPanel
        }));
        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';
        leftHandler.area.addClass('jp-SideArea');
        leftHandler.area.node.setAttribute('data-side', 'left');
        rightHandler.sideBar.addClass(SIDEBAR_CLASS);
        rightHandler.sideBar.addClass('jp-mod-right');
        rightHandler.sideBar.node.setAttribute('role', 'complementary');
        rightHandler.stackedPanel.id = 'jp-right-stack';
        rightHandler.area.addClass('jp-SideArea');
        rightHandler.area.node.setAttribute('data-side', 'right');
        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.area, 0);
        SplitPanel.setStretch(downPanel, 0);
        SplitPanel.setStretch(dockPanel, 1);
        SplitPanel.setStretch(rightHandler.area, 0);
        BoxPanel.setStretch(leftHandler.sideBar, 0);
        BoxPanel.setStretch(hsplitPanel, 1);
        BoxPanel.setStretch(rightHandler.sideBar, 0);
        SplitPanel.setStretch(vsplitPanel, 1);
        hsplitPanel.addWidget(leftHandler.area);
        hsplitPanel.addWidget(dockPanel);
        hsplitPanel.addWidget(rightHandler.area);
        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;
    }
    /**
     * Whether the down area is collapsed.
     */
    get downCollapsed() {
        return this._downPanel.isHidden;
    }
    /**
     * 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) {
        var _a;
        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) {
            const wasHidden = this._downPanel.isHidden;
            this._downPanel.currentIndex = tabIndex;
            if (wasHidden) {
                this._showDownPanel();
                this._onLayoutModified();
            }
            (_a = this._downPanel.currentWidget) === null || _a === void 0 ? void 0 : _a.activate();
            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') {
        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
        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;
        if ((options === null || options === void 0 ? void 0 : options.rank) !== undefined) {
            this._sideOptionsCache.set(widget, {
                ...this._sideOptionsCache.get(widget),
                rank: options.rank
            });
        }
        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 to a new area and update the shell user layout.
     *
     * The widget is reparented to `area` immediately. The type used as the
     * user-layout key is determined from `widget.id`, falling back to
     * `widget.id` itself.
     *
     * #### Notes
     * If `mode` is undefined, both modes are updated in the user layout.
     * When `mode` is set, only that mode's user layout is updated, but the
     * live widget is still reparented regardless of mode.
     *
     * The new layout is stored in the shell user layout. Callers are
     * responsible for persisting it when needed.
     *
     * @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, _b;
        const type = (_a = this._idTypeMap.get(widget.id)) !== null && _a !== void 0 ? _a : widget.id;
        const rank = (_b = this._sideOptionsCache.get(widget)) === null || _b === void 0 ? void 0 : _b.rank;
        for (const m of ['single-document', 'multiple-document'].filter(c => !mode || c === mode)) {
            const position = this._userLayout[m][type];
            this._userLayout[m][type] = {
                ...position,
                area,
                ...(rank !== undefined
                    ? {
                        options: {
                            ...position === null || position === void 0 ? void 0 : position.options,
                            rank
                        }
                    }
                    : {})
            };
        }
        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();
    }
    /**
     * Collapse the down area.
     */
    collapseDown() {
        if (!this._downPanel.isHidden) {
            this._hideDownPanel();
            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();
    }
    /**
     * Expand the down area.
     */
    expandDown() {
        var _a;
        if (this._downPanel.stackedPanel.widgets.length === 0) {
            return;
        }
        this._showDownPanel();
        (_a = this._downPanel.currentWidget) === null || _a === void 0 ? void 0 : _a.activate();
        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, _e, _f, _g, _h;
        // 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 collapsed = (_c = downArea.collapsed) !== null && _c !== void 0 ? _c : !size;
            const widgetIds = (_d = widgets === null || widgets === void 0 ? void 0 : widgets.map(widget => widget.id)) !== null && _d !== void 0 ? _d : [];
            const otherAreaWidgetIds = new Set();
            const collectMainWidgetIds = (area) => {
                if (!area) {
                    return;
                }
                if (area.type === 'tab-area') {
                    area.widgets.forEach(widget => {
                        otherAreaWidgetIds.add(widget.id);
                    });
                    return;
                }
                area.children.forEach(collectMainWidgetIds);
            };
            collectMainWidgetIds((_e = mainArea === null || mainArea === void 0 ? void 0 : mainArea.dock) === null || _e === void 0 ? void 0 : _e.main);
            (_f = leftArea === null || leftArea === void 0 ? void 0 : leftArea.widgets) === null || _f === void 0 ? void 0 : _f.forEach(widget => {
                otherAreaWidgetIds.add(widget.id);
            });
            (_g = rightArea === null || rightArea === void 0 ? void 0 : rightArea.widgets) === null || _g === void 0 ? void 0 : _g.forEach(widget => {
                otherAreaWidgetIds.add(widget.id);
            });
            // Remove absent widgets
            this._downPanel.tabBar.titles
                .filter(title => !widgetIds.includes(title.owner.id))
                .forEach(title => {
                if (otherAreaWidgetIds.has(title.owner.id)) {
                    title.owner.parent = null;
                }
                else {
                    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 >= 0) {
                    this._downPanel.currentIndex = index;
                }
            }
            if (!collapsed && (widgets === null || widgets === void 0 ? void 0 : widgets.length) && size && size > 0.0) {
                this._showDownPanel(size);
                (_h = this._downPanel.currentWidget) === null || _h === void 0 ? void 0 : _h.activate();
            }
            else {
                this._hideDownPanel();
                if (size && size > 0.0) {
                    // Remember the saved size so a later expand restores the user's
                    // previous height. `_hideDownPanel` seeds `_lastDownAreaSize`
                    // from the current splitter, which at cold startup reflects the
                    // default layout rather than the persisted value.
                    this._lastDownAreaSize = size;
                }
            }
        }
        // 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: {
                collapsed: this.downCollapsed,
                currentWidget: this._downPanel.currentWidget,
                widgets: Array.from(this._downPanel.stackedPanel.widgets),
                size: this._downPanel.stackedPanel.widgets.length === 0
                    ? 0
                    : this.downCollapsed
                        ? this._lastDownAreaSize
                        : 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;
            }
        }
        if (config.dockPanelPadding !== undefined) {
            if (config.dockPanelPadding === false) {
                this._dockPanel.node.style.setProperty('--jp-private-dock-panel-padding', '0px');
            }
            else {
                this._dockPanel.node.style.removeProperty('--jp-private-dock-panel-padding');
            }
            this._dockPanel.fit();
        }
        if (config.optimizeResize !== undefined) {
            this._dockPanel.optimizeResize = config.optimizeResize;
        }
        if (config.activityBarPosition !== undefined) {
            this._setActivityBarPosition('left', config.activityBarPosition);
            this._setActivityBarPosition('right', config.activityBarPosition);
        }
    }
    /**
     * Move the activity bar of a side area to a new position.
     *
     * @param side - The side area to update.
     * @param position - The new position of the activity bar.
     */
    _setActivityBarPosition(side, position) {
        const handler = side === 'left' ? this._leftHandler : this._rightHandler;
        if (handler.position === position) {
            return;
        }
        handler.position = position;
        this._onLayoutModified();
    }
    /**
     * 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();
            case 'down':
                return this._downPanel.stackedPanel.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 ? TabBarSvg.titleLabel(current.title) : '';
        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;
        }
        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);
        if (this._downPanel.isHidden) {
            this._showDownPanel();
        }
        this._onLayoutModified();
    }
    _showDownPanel(size = this._lastDownAreaSize) {
        const downSize = size > 0.0 ? size : DEFAULT_DOWN_AREA_SIZE;
        this._lastDownAreaSize = downSize;
        this._vsplitPanel.setRelativeSizes([
            Math.max(1.0 - downSize, 0.0),
            downSize
        ]);
        this._downPanel.show();
    }
    _hideDownPanel() {
        const size = this._vsplitPanel.relativeSizes()[1];
        if (size > 0.0) {
            this._lastDownAreaSize = size;
        }
        this._downPanel.hide();
    }
    /*
     * 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._hideDownPanel();
        }
        this._onLayoutModified();
    }
    /**
     * Handle a change to the layout.
     */
    _onLayoutModified() {
        void this._layoutDebouncer.invoke();
    }
    /**
     * Move a widget to the main dock panel.
     */
    moveTab(widget, direction) {
        const ref = widget;
        const modeMap = {
            left: 'split-left',
            right: 'split-right',
            top: 'split-top',
            bottom: 'split-bottom'
        };
        const mode = modeMap[direction];
        this._dockPanel.addWidget(widget, {
            mode,
            ref
        });
    }
    /**
     *  Find the tab bar containing a given widget.
     */
    getMainAreaTabBar(widget) {
        for (const tabBar of this._dockPanel.tabBars()) {
            if (tabBar.titles.includes(widget.title)) {
                return tabBar;
            }
        }
        return null;
    }
}
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 Panel();
            MessageLoop.installMessageHook(this._panel, this._panelChildHook);
        }
        /**
         * Get the panel managed by the handler.
         */
        get panel() {
            return this._panel;
        }
        /**
         * Add a widget to the panel.
         *
         * If the widget is already added, it will be moved.
         */
        addWidget(widget, rank) {
            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);
        }
    }
    Private.PanelHandler = PanelHandler;
    /**
     * A class which manages a side bar and related stacked panel.
     */
    class SideBarHandler {
        /**
         * Construct a new side bar handler.
         */
        constructor(options) {
            this._isHiddenByUser = false;
            this._isCollapsedByUser = false;
            this._items = new Array();
            this._position = 'side';
            this._updated = new Signal(this);
            this._side = options.side;
            this._host = options.host;
            this._sideBar = new TabBar({
                insertBehavior: 'none',
                removeBehavior: 'none',
                allowDeselect: true,
                orientation: 'vertical'
            });
            // Mirror the initial position on the bar via `data-side`. The setter
            // keeps it in sync on subsequent transitions.
            this._sideBar.node.setAttribute('data-side', this._side);
            this._stackedPanel = new StackedPanel();
            this._area = new BoxPanel({ direction: 'top-to-bottom', spacing: 0 });
            // The stacked panel always lives inside the area wrapper. It is the
            // only stretchable child so the activity bar (when inside the wrapper)
            // takes only its natural size at the top or bottom.
            BoxPanel.setStretch(this._stackedPanel, 1);
            BoxPanel.setStretch(this._sideBar, 0);
            this._area.addWidget(this._stackedPanel);
            this._sideBar.hide();
            this._stackedPanel.hide();
            this._lastCurrent = null;
            this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
            this._sideBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
            this._sideBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
            this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
        }
        /**
         * Whether the side bar is visible
         */
        get isVisible() {
            return this._sideBar.isVisible;
        }
        /**
         * Get the tab bar managed by the handler.
         */
        get sideBar() {
            return this._sideBar;
        }
        /**
         * Get the stacked panel managed by the handler
         */
        get stackedPanel() {
            return this._stackedPanel;
        }
        /**
         * Get the wrapper panel for this side area.
         *
         * The wrapper always contains the stacked panel. When the activity bar is
         * positioned inside the area (top or bottom), it is also a child of this
         * wrapper. The wrapper is what gets added to the main horizontal split.
         */
        get area() {
            return this._area;
        }
        /**
         * Get the current position of the activity bar.
         */
        get position() {
            return this._position;
        }
        /**
         * Set the position of the activity bar relative to the area.
         *
         * In `'side'` mode the activity bar is reattached to the host panel
         * (e.g. the shell's hboxPanel) on its natural side. In `'top'` or
         * `'bottom'` mode the activity bar is reparented inside the area wrapper
         * above or below the stacked panel and laid out horizontally.
         */
        set position(value) {
            if (this._position === value) {
                return;
            }
            this._position = value;
            // The user-collapse flag only has meaning in horizontal mode and should
            // not carry over from a prior interaction in a different mode.
            this._isCollapsedByUser = false;
            // Detach the activity bar from its current parent before reattaching
            // it to the new one (host or area wrapper).
            this._sideBar.parent = null;
            // Update the orientation and the position-related attributes on the
            // activity bar. The icon/label rotation depends on these.
            const isLeft = this._side === 'left';
            this._sideBar.orientation = value === 'side' ? 'vertical' : 'horizontal';
            // Keep `jp-mod-left`/`jp-mod-right` for backward compatibility with
            // existing themes that target those classes.
            this._sideBar.toggleClass('jp-mod-left', isLeft && value === 'side');
            this._sideBar.toggleClass('jp-mod-right', !isLeft && value === 'side');
            // `data-side` mirrors the `data-orientation` attribute set by Lumino
            // and is the canonical hook for new CSS rules.
            const dataSide = value === 'side' ? (isLeft ? 'left' : 'right') : value;
            this._sideBar.node.setAttribute('data-side', dataSide);
            // In 'side' mode, clicking the active tab collapses the area (the
            // activity bar stays as a thin strip). That collapse UX makes no sense
            // when the activity bar is at the top or bottom — clicking would just
            // leave an empty horizontal strip — so disable deselection there.
            this._sideBar.allowDeselect = value === 'side';
            if (value === 'side') {
                const index = isLeft ? 0 : this._host.widgets.length;
                this._host.insertWidget(index, this._sideBar);
            }
            else if (value === 'top') {
                this._area.insertWidget(0, this._sideBar);
            }
            else {
                this._area.addWidget(this._sideBar);
            }
            this._refreshVisibility();
        }
        /**
         * Signal fires when the stack panel or the sidebar changes
         */
        get updated() {
            return this._updated;
        }
        /**
         * Handles a movement to the handles of a widget
         */
        _onHandleMoved() {
            return this._refreshVisibility();
        }
        /**
         * Handles changes to the expansion status of a widget
         */
        _onExpansionToggle(sender, index) {
            return this._refreshVisibility();
        }
        /**
         * Expand the sidebar.
         *
         * #### Notes
         * This will open the most recently used tab, or the first tab
         * if there is no most recently used. In `'top'` or `'bottom'` mode it
         * also re-shows the wrapper area if a previous `collapse()` call hid it.
         */
        expand() {
            this._isCollapsedByUser = false;
            const previous = this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
            if (previous) {
                this.activate(previous.id);
            }
            else {
                // No tab to activate, but we still need to reflect the cleared
                // collapse flag (relevant in 'top'/'bottom' mode).
                this._refreshVisibility();
            }
        }
        /**
         * Activate a widget residing in the side bar by ID.
         *
         * @param id - The widget's unique ID.
         */
        activate(id) {
            const widget = this._findWidgetByID(id);
            if (widget) {
                this._isCollapsedByUser = false;
                this._sideBar.currentTitle = widget.title;
                widget.activate();
            }
        }
        /**
         * Test whether the sidebar has the given widget by id.
         */
        has(id) {
            return this._findWidgetByID(id) !== null;
        }
        /**
         * Collapse the sidebar so no items are expanded.
         *
         * #### Notes
         * In `'side'` mode this only deselects the active tab, leaving the
         * activity bar visible. In `'top'` or `'bottom'` mode it also hides the
         * wrapper area entirely so the column can reclaim its space.
         */
        collapse() {
            this._isCollapsedByUser = true;
            this._sideBar.currentTitle = null;
            this._refreshVisibility();
        }
        /**
         * Add a widget and its title to the stacked panel and side bar.
         *
         * If the widget is already added, it will be moved.
         */
        addWidget(widget, rank) {
            var _a, _b, _c, _d;
            widget.parent = null;
            widget.hide();
            const item = { widget, rank };
            const index = this._findInsertIndex(item);
            ArrayExt.insert(this._items, index, item);
            this._stackedPanel.insertWidget(index, widget);
            const title = this._sideBar.insertTab(index, widget.title);
            // Store the parent id in the title dataset
            // in order to dispatch click events to the right widget.
            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: 'sideBar'
                });
            }
            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', 'jp-Icon-20');
            }
            else if (!title.icon && !title.label) {
                // add a fallback icon if there is no title label nor icon
                title.icon = tabIcon.bindprops({
                    stylesheet: 'sideBar'
                });
            }
            // @ts-expect-error sometimes widget is an Accordion Panel
            (_b = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.expansionToggled) === null || _b === void 0 ? void 0 : _b.connect(this._onExpansionToggle, this);
            // @ts-expect-error sometimes widget is a SidePanel
            (_d = (_c = widget.content) === null || _c === void 0 ? void 0 : _c.handleMoved) === null || _d === void 0 ? void 0 : _d.connect(this._onHandleMoved, this);
            this._refreshVisibility();
        }
        /**
         * Dehydrate the side bar data.
         */
        dehydrate() {
            const collapsed = this._position === 'side'
                ? this._sideBar.currentTitle === null
                : this._isCollapsedByUser;
            const widgets = Array.from(this._stackedPanel.widgets);
            const currentWidget = widgets[this._sideBar.currentIndex];
            const widgetStates = {};
            this._stackedPanel.widgets.forEach((w) => {
                if (w.id && w.content instanceof SplitPanel) {
                    widgetStates[w.id] = {
                        sizes: w.content.relativeSizes(),
                        expansionStates: w.content.widgets.map(wi => wi.isVisible)
                    };
                }
            });
            return {
                collapsed,
                currentWidget,
                visible: !this._isHiddenByUser,
                widgets,
                widgetStates
            };
        }
        /**
         * Rehydrate the side bar.
         */
        rehydrate(data) {
            if (Array.isArray(data.widgets)) {
                const widgetIds = data.widgets.map(widget => widget.id);
                const widgetIdSet = new Set(widgetIds);
                // Add widgets that are in the saved layout but not currently
                // in the sidebar.
                const currentIds = this._stackedPanel.widgets.map(widget => widget.id);
                data.widgets
                    .filter(widget => !currentIds.includes(widget.id))
                    .forEach(widget => {
                    this.addWidget(widget, DEFAULT_RANK);
                });
                // Merge the saved order into the current sidebar slots so widgets
                // absent from the saved layout keep their rank-relative positions.
                let savedIndex = 0;
                const targetIds = this._stackedPanel.widgets.map(widget => widgetIdSet.has(widget.id) ? widgetIds[savedIndex++] : widget.id);
                targetIds.forEach((id, targetIndex) => {
                    const currentIndex = this._stackedPanel.widgets.findIndex(widget => widget.id === id);
                    if (currentIndex >= 0 && currentIndex !== targetIndex) {
                        const widget = this._stackedPanel.widgets[currentIndex];
                        ArrayExt.move(this._items, currentIndex, targetIndex);
                        this._stackedPanel.insertWidget(targetIndex, widget);
                        this._sideBar.insertTab(targetIndex, widget.title);
                    }
                });
            }
            if (data.visible) {
                this.show();
            }
            else {
                this.hide();
            }
            if (data.currentWidget) {
                this.activate(data.currentWidget.id);
            }
            else if (data.collapsed) {
                this.collapse();
            }
            if (data.widgetStates) {
                this._stackedPanel.widgets.forEach((w) => {
                    var _a;
                    if (w.id && w.content instanceof SplitPanel) {
                        const state = (_a = data.widgetStates[w.id]) !== null && _a !== void 0 ? _a : {};
                        w.content.widgets.forEach((wi, widx) => {
                            var _a;
                            const expansion = ((_a = state.expansionStates) !== null && _a !== void 0 ? _a : [])[widx];
                            if (typeof expansion === 'boolean' &&
                                w.content instanceof AccordionPanel) {
                                if (expansion) {
                                    w.content.expand(widx);
                                }
                                else {
                                    w.content.collapse(widx);
                                }
                            }
                        });
                        if (state.sizes) {
                            w.content.setRelativeSizes(state.sizes);
                        }
                    }
                });
            }
        }
        /**
         * Hide the side bar even if it contains widgets
         */
        hide() {
            this._isHiddenByUser = true;
            this._refreshVisibility();
        }
        /**
         * Show the side bar if it contains widgets
         */
        show() {
            this._isHiddenByUser = false;
            this._isCollapsedByUser = false;
            this._refreshVisibility();
        }
        /**
         * Find the insertion index for a rank item.
         */
        _findInsertIndex(item) {
            return ArrayExt.upperBound(this._items, item, Private.itemCmp);
        }
        /**
         * Find the index of the item with the given widget, or `-1`.
         */
        _findWidgetIndex(widget) {
            return ArrayExt.findFirstIndex(this._items, i => i.widget === widget);
        }
        /**
         * Find the widget which owns the given title, or `null`.
         */
        _findWidgetByTitle(title) {
            const item = find(this._items, value => value.widget.title === title);
            return item ? item.widget : null;
        }
        /**
         * Find the widget with the given id, or `null`.
         */
        _findWidgetByID(id) {
            const item = find(this._items, value => value.widget.id === id);
            return item ? item.widget : null;
        }
        /**
         * Refresh the visibility of the side bar and stacked panel.
         */
        _refreshVisibility() {
            if (this._position === 'side') {
                // In 'side' mode the activity bar lives outside the wrapper and the
                // wrapper holds only the stack panel. Hiding the stack panel when no
                // widget is current collapses the area down to the activity bar.
                this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
            }
            else {
                // In 'top'/'bottom' mode the activity bar lives inside the wrapper
                // alongside the stack panel. The stack panel stays visible (and
                // stretches as an empty area when no widget is current) so the
                // activity bar stays anchored at the top or bottom of the wrapper.
                this._stackedPanel.setHidden(this._items.length === 0);
            }
            this._sideBar.setHidden(this._isHiddenByUser || this._sideBar.titles.length === 0);
            // Hide the wrapper area so the parent split panel can reclaim its
            // allocated width when no contents would be visible.
            if (this._position === 'side') {
                // In 'side' mode wrapper visibility tracks the stack panel.
                this._area.setHidden(this._stackedPanel.isHidden);
            }
            else {
                // In 'top'/'bottom' mode the wrapper hides only when the user has
                // explicitly collapsed the side area or there are no widgets at all.
                // Toggling the activity bar visibility ('Show Left/Right Activity Bar')
                // hides only the bar — the stack panel and selected widget stay
                // visible inside the wrapper, matching the 'side' mode semantic.
                this._area.setHidden(this._isCollapsedByUser || this._sideBar.titles.length === 0);
            }
            this._updated.emit();
        }
        /**
         * Handle the `currentChanged` signal from the sidebar.
         */
        _onCurrentChanged(sender, args) {
            const oldWidget = args.previousTitle
                ? this._findWidgetByTitle(args.previousTitle)
                : null;
            const newWidget = args.currentTitle
                ? this._findWidgetByTitle(args.currentTitle)
                : null;
            if (oldWidget) {
                oldWidget.hide();
            }
            if (newWidget) {
                newWidget.show();
            }
            this._lastCurrent = newWidget || oldWidget;
            this._refreshVisibility();
        }
        /**
         * Handle a `tabActivateRequest` signal from the sidebar.
         */
        _onTabActivateRequested(sender, args) {
            args.title.owner.activate();
        }
        /**
         * Handle a `tabCloseRequested` signal from the sidebar.
         */
        _onTabCloseRequested(sender, args) {
            args.title.owner.close();
        }
        /*
         * Handle the `widgetRemoved` signal from the stacked panel.
         */
        _onWidgetRemoved(sender, widget) {
            if (widget === this._lastCurrent) {
                this._lastCurrent = null;
            }
            ArrayExt.removeAt(this._items, this._findWidgetIndex(widget));
            this._sideBar.removeTab(widget.title);
            this._refreshVisibility();
        }
    }
    Private.SideBarHandler = SideBarHandler;
    class SkipLinkWidget extends Widget {
        /**
         * Construct a new skipLink widget.
         */
        constructor(shell) {
            super();
            this.addClass('jp-skiplink');
            this.id = 'jp-skiplink';
            this._shell = shell;
            this._createSkipLink('Skip to main panel', 'main');
        }
        handleEvent(event) {
            var _a, _b;
            switch (event.type) {
                case 'click':
                    if (event.target instanceof HTMLElement) {
                        this._shell.activateArea((_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.dataset) === null || _b === void 0 ? void 0 : _b.targetarea);
                    }
                    break;
            }
        }
        /**
         * Handle `after-attach` messages for the widget.
         */
        onAfterAttach(msg) {
            super.onAfterAttach(msg);
            this.node.addEventListener('click', this);
        }
        /**
         * A message handler invoked on a `'before-detach'`
         * message
         */
        onBeforeDetach(msg) {
            this.node.removeEventListener('click', this);
            super.onBeforeDetach(msg);
        }
        _createSkipLink(skipLinkText, area) {
            const skipLink = document.createElement('a');
            skipLink.href = '#';
            skipLink.tabIndex = 0;
            skipLink.text = skipLinkText;
            skipLink.className = 'skip-link';
            skipLink.dataset['targetarea'] = area;
            this.node.appendChild(skipLink);
        }
    }
    Private.SkipLinkWidget = SkipLinkWidget;
    class TitleHandler extends Widget {
        /**
         * Construct a new title handler.
         */
        constructor(shell) {
            super();
            this._selected = false;
            const inputElement = document.createElement('input');
            inputElement.type = 'text';
            this.node.appendChild(inputElement);
            this._shell = shell;
            this.id = 'jp-title-panel-title';
        }
        /**
         * Handle `after-attach` messages for the widget.
         */
        onAfterAttach(msg) {
            super.onAfterAttach(msg);
            this.inputElement.addEventListener('keyup', this);
            this.inputElement.addEventListener('click', this);
            this.inputElement.addEventListener('blur', this);
        }
        /**
         * Handle `before-detach` messages for the widget.
         */
        onBeforeDetach(msg) {
            super.onBeforeDetach(msg);
            this.inputElement.removeEventListener('keyup', this);
            this.inputElement.removeEventListener('click', this);
            this.inputElement.removeEventListener('blur', this);
        }
        handleEvent(event) {
            switch (event.type) {
                case 'keyup':
                    void this._evtKeyUp(event);
                    break;
                case 'click':
                    this._evtClick(event);
                    break;
                case 'blur':
                    this._selected = false;
                    break;
            }
        }
        /**
         * Handle `keyup` events on the handler.
         */
        async _evtKeyUp(event) {
            if (event.key == 'Enter') {
                const widget = this._shell.currentWidget;
                if (widget == null) {
                    return;
                }
                const oldName = TabBarSvg.titleLabel(widget.title);
                const inputElement = this.inputElement;
                const newName = inputElement.value;
                inputElement.blur();
                if (newName !== oldName) {
                    widget.title.label = newName;
                }
                else {
                    inputElement.value = oldName;
                }
            }
        }
        /**
         * Handle `click` events on the handler.
         */
        _evtClick(event) {
            // only handle primary button clicks
            if (event.button !== 0 || this._selected) {
                return;
            }
            const inputElement = this.inputElement;
            event.preventDefault();
            event.stopPropagation();
            this._selected = true;
            const selectEnd = inputElement.value.indexOf('.');
            if (selectEnd === -1) {
                inputElement.select();
            }
            else {
                inputElement.setSelectionRange(0, selectEnd);
            }
        }
        /**
         * The input element containing the parent widget's title.
         */
        get inputElement() {
            return this.node.children[0];
        }
    }
    Private.TitleHandler = TitleHandler;
    class RestorableSplitPanel extends SplitPanel {
        /**
         * Construct a new RestorableSplitPanel.
         */
        constructor(options = {}) {
            super(options);
            this._updated = new Signal(this);
        }
        /**
         * A signal emitted when the split panel is updated.
         */
        get updated() {
            return this._updated;
        }
        /**
         * Emit 'updated' signal on 'update' requests.
         */
        onUpdateRequest(msg) {
            super.onUpdateRequest(msg);
            this._updated.emit();
        }
    }
    Private.RestorableSplitPanel = RestorableSplitPanel;
})(Private || (Private = {}));
//# sourceMappingURL=shell.js.map