@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
2,112 lines • 88.8 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
var ApplicationShell_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApplicationShell = exports.WidgetAreaResolver = exports.MAXIMIZED_CLASS = exports.DockPanelRenderer = exports.DockPanelRendererFactory = exports.ApplicationShellOptions = exports.applicationShellLayoutVersion = exports.BOTTOM_AREA_CLASS = exports.MAIN_AREA_CLASS = exports.BOTTOM_PANEL_TOGGLE_ID = exports.MAIN_BOTTOM_AREA_CLASS = exports.APPLICATION_SHELL_CLASS = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const algorithm_1 = require("@lumino/algorithm");
const widgets_1 = require("@lumino/widgets");
const common_1 = require("../../common");
const browser_1 = require("../browser");
const saveable_1 = require("../saveable");
const status_bar_1 = require("../status-bar/status-bar");
const theia_dock_panel_1 = require("./theia-dock-panel");
const side_panel_handler_1 = require("./side-panel-handler");
const tab_bars_1 = require("./tab-bars");
const split_panels_1 = require("./split-panels");
const frontend_application_state_1 = require("../frontend-application-state");
const tab_bar_toolbar_1 = require("./tab-bar-toolbar");
const context_key_service_1 = require("../context-key-service");
const event_1 = require("../../common/event");
const widgets_2 = require("../widgets");
const core_preferences_1 = require("../../common/core-preferences");
const breadcrumbs_renderer_1 = require("../breadcrumbs/breadcrumbs-renderer");
const promise_util_1 = require("../../common/promise-util");
const saveable_service_1 = require("../saveable-service");
const nls_1 = require("../../common/nls");
const secondary_window_handler_1 = require("../secondary-window-handler");
const uri_1 = require("../../common/uri");
const opener_service_1 = require("../opener-service");
const previewable_widget_1 = require("../widgets/previewable-widget");
const window_service_1 = require("../window/window-service");
const theia_split_panel_1 = require("./theia-split-panel");
const logger_1 = require("../../common/logger");
/** The class name added to ApplicationShell instances. */
exports.APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
/** The class name added to the main and bottom area panels. */
exports.MAIN_BOTTOM_AREA_CLASS = 'theia-app-centers';
/** Status bar entry identifier for the bottom panel toggle button. */
exports.BOTTOM_PANEL_TOGGLE_ID = 'bottom-panel-toggle';
/** The class name added to the main area panel. */
exports.MAIN_AREA_CLASS = 'theia-app-main';
/** The class name added to the bottom area panel. */
exports.BOTTOM_AREA_CLASS = 'theia-app-bottom';
/**
* When a version is increased, make sure to introduce a migration (ApplicationShellLayoutMigration) to this version.
*/
exports.applicationShellLayoutVersion = 5.0;
exports.ApplicationShellOptions = Symbol('ApplicationShellOptions');
exports.DockPanelRendererFactory = Symbol('DockPanelRendererFactory');
/**
* A renderer for dock panels that supports context menus on tabs.
*/
let DockPanelRenderer = class DockPanelRenderer {
constructor(tabBarRendererFactory, tabBarToolbarRegistry, tabBarToolbarFactory, breadcrumbsRendererFactory, corePreferences) {
this.tabBarRendererFactory = tabBarRendererFactory;
this.tabBarToolbarRegistry = tabBarToolbarRegistry;
this.tabBarToolbarFactory = tabBarToolbarFactory;
this.breadcrumbsRendererFactory = breadcrumbsRendererFactory;
this.corePreferences = corePreferences;
this.tabBarClasses = [];
this.onDidCreateTabBarEmitter = new event_1.Emitter();
}
get onDidCreateTabBar() {
return this.onDidCreateTabBarEmitter.event;
}
createTabBar() {
const getDynamicTabOptions = () => {
if (this.corePreferences.get('workbench.tab.shrinkToFit.enabled')) {
return {
minimumTabSize: this.corePreferences.get('workbench.tab.shrinkToFit.minimumSize'),
defaultTabSize: this.corePreferences.get('workbench.tab.shrinkToFit.defaultSize')
};
}
else {
return undefined;
}
};
const renderer = this.tabBarRendererFactory();
const tabBar = new tab_bars_1.ToolbarAwareTabBar(this.tabBarToolbarRegistry, this.tabBarToolbarFactory, this.breadcrumbsRendererFactory, {
document: this.document,
renderer
}, {
// Scroll bar options
handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
useBothWheelAxes: true,
scrollXMarginOffset: 4,
suppressScrollY: true
}, getDynamicTabOptions());
this.tabBarClasses.forEach(c => tabBar.addClass(c));
renderer.tabBar = tabBar;
renderer.contextMenuPath = tab_bars_1.SHELL_TABBAR_CONTEXT_MENU;
tabBar.currentChanged.connect(this.onCurrentTabChanged, this);
const prefChangeDisposable = this.corePreferences.onPreferenceChanged(change => {
if (change.preferenceName === 'workbench.tab.shrinkToFit.enabled' ||
change.preferenceName === 'workbench.tab.shrinkToFit.minimumSize' ||
change.preferenceName === 'workbench.tab.shrinkToFit.defaultSize') {
tabBar.dynamicTabOptions = getDynamicTabOptions();
}
});
tabBar.disposed.connect(() => {
prefChangeDisposable.dispose();
renderer.dispose();
});
this.onDidCreateTabBarEmitter.fire(tabBar);
return tabBar;
}
createHandle() {
return widgets_1.DockPanel.defaultRenderer.createHandle();
}
onCurrentTabChanged(sender, { currentIndex }) {
if (currentIndex >= 0) {
sender.revealTab(currentIndex);
}
}
};
exports.DockPanelRenderer = DockPanelRenderer;
exports.DockPanelRenderer = DockPanelRenderer = tslib_1.__decorate([
(0, inversify_1.injectable)(),
tslib_1.__param(0, (0, inversify_1.inject)(tab_bars_1.TabBarRendererFactory)),
tslib_1.__param(1, (0, inversify_1.inject)(tab_bar_toolbar_1.TabBarToolbarRegistry)),
tslib_1.__param(2, (0, inversify_1.inject)(tab_bar_toolbar_1.TabBarToolbarFactory)),
tslib_1.__param(3, (0, inversify_1.inject)(breadcrumbs_renderer_1.BreadcrumbsRendererFactory)),
tslib_1.__param(4, (0, inversify_1.inject)(core_preferences_1.CorePreferences)),
tslib_1.__metadata("design:paramtypes", [Function, tab_bar_toolbar_1.TabBarToolbarRegistry, Function, Function, Object])
], DockPanelRenderer);
exports.MAXIMIZED_CLASS = 'theia-maximized';
exports.WidgetAreaResolver = Symbol('WidgetAreaResolver');
/**
* The application shell manages the top-level widgets of the application. Use this class to
* add, remove, or activate a widget.
*/
let ApplicationShell = ApplicationShell_1 = class ApplicationShell extends widgets_1.Widget {
fireDidAddWidget(widget) {
this.onDidAddWidgetEmitter.fire(widget);
}
fireDidRemoveWidget(widget) {
this.onDidRemoveWidgetEmitter.fire(widget);
}
get mainPanelRenderer() {
return this._mainPanelRenderer;
}
/**
* Construct a new application shell.
*/
constructor(dockPanelRendererFactory, statusBar, sidePanelHandlerFactory, splitPositionHandler, applicationStateService, options = {}, corePreferences, saveableService, secondaryWindowHandler, windowService) {
super(options);
this.dockPanelRendererFactory = dockPanelRendererFactory;
this.statusBar = statusBar;
this.sidePanelHandlerFactory = sidePanelHandlerFactory;
this.splitPositionHandler = splitPositionHandler;
this.applicationStateService = applicationStateService;
this.corePreferences = corePreferences;
this.saveableService = saveableService;
this.secondaryWindowHandler = secondaryWindowHandler;
this.windowService = windowService;
/**
* The current state of the bottom panel.
*/
this.bottomPanelState = {
empty: true,
expansion: side_panel_handler_1.SidePanel.ExpansionState.collapsed,
pendingUpdate: Promise.resolve()
};
this.tracker = new widgets_1.FocusTracker();
this.onDidAddWidgetEmitter = new event_1.Emitter();
this.onDidAddWidget = this.onDidAddWidgetEmitter.event;
this.onDidRemoveWidgetEmitter = new event_1.Emitter();
this.onDidRemoveWidget = this.onDidRemoveWidgetEmitter.event;
this.onDidChangeActiveWidgetEmitter = new event_1.Emitter();
this.onDidChangeActiveWidget = this.onDidChangeActiveWidgetEmitter.event;
this.onDidChangeCurrentWidgetEmitter = new event_1.Emitter();
this.onDidChangeCurrentWidget = this.onDidChangeCurrentWidgetEmitter.event;
this.onDidDoubleClickMainAreaEmitter = new event_1.Emitter();
this.onDidDoubleClickMainArea = this.onDidDoubleClickMainAreaEmitter.event;
this.initializedDeferred = new promise_util_1.Deferred();
this.initialized = this.initializedDeferred.promise;
this.toDisposeOnActiveChanged = new common_1.DisposableCollection();
this.activationTimeout = 2000;
this.toDisposeOnActivationCheck = new common_1.DisposableCollection();
this.onDidToggleMaximizedEmitter = new event_1.Emitter();
this.onDidToggleMaximized = this.onDidToggleMaximizedEmitter.event;
this.maximizedElement = this.node.ownerDocument.createElement('div');
this.maximizedElement.style.position = 'fixed';
this.maximizedElement.style.display = 'none';
this.maximizedElement.style.left = '0px';
this.maximizedElement.style.bottom = '0px';
this.maximizedElement.style.right = '0px';
this.maximizedElement.style.background = 'var(--theia-editor-background)';
this.maximizedElement.style.zIndex = '2000';
this.node.ownerDocument.body.appendChild(this.maximizedElement);
// Merge the user-defined application options with the default options
this.options = {
bottomPanel: {
...ApplicationShell_1.DEFAULT_OPTIONS.bottomPanel,
...options?.bottomPanel || {}
},
leftPanel: {
...ApplicationShell_1.DEFAULT_OPTIONS.leftPanel,
...options?.leftPanel || {}
},
rightPanel: {
...ApplicationShell_1.DEFAULT_OPTIONS.rightPanel,
...options?.rightPanel || {}
}
};
if (corePreferences) {
corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.handleMenuBarVisibility(corePreferences['window.menuBarVisibility']);
}
});
}
}
init() {
this.initializeShell();
this.initSidebarVisibleKeyContext();
this.initFocusKeyContexts();
if (!common_1.environment.electron.is()) {
this.corePreferences.ready.then(() => {
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
});
this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.setTopPanelVisibility(this.corePreferences['window.menuBarVisibility']);
}
});
}
this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.tabbar.enhancedPreview') {
this.allTabBars.forEach(tabBar => {
tabBar.update();
});
}
});
this.refreshBottomPanelToggleButton();
this.initializedDeferred.resolve();
}
initializeShell() {
this.addClass(exports.APPLICATION_SHELL_CLASS);
this.addClass('monaco-workbench'); // needed for compatility with VSCode styles
this.id = 'theia-app-shell';
this.mainPanel = this.createMainPanel();
this.topPanel = this.createTopPanel();
this.bottomPanel = this.createBottomPanel();
this.leftPanelHandler = this.sidePanelHandlerFactory();
this.leftPanelHandler.create('left', this.options.leftPanel);
this.leftPanelHandler.dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
this.leftPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
this.rightPanelHandler = this.sidePanelHandlerFactory();
this.rightPanelHandler.create('right', this.options.rightPanel);
this.rightPanelHandler.dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
this.rightPanelHandler.dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
this.secondaryWindowHandler.init(this, this.dockPanelRendererFactory);
this.secondaryWindowHandler.onDidAddWidget(([widget, window]) => this.fireDidAddWidget(widget));
this.secondaryWindowHandler.onDidRemoveWidget(([widget, window]) => this.fireDidRemoveWidget(widget));
this.layout = this.createLayout();
this.tracker.currentChanged.connect(this.onCurrentChanged, this);
this.tracker.activeChanged.connect(this.onActiveChanged, this);
}
initSidebarVisibleKeyContext() {
const leftSideBarPanel = this.leftPanelHandler.dockPanel;
const sidebarVisibleKey = this.contextKeyService.createKey('sidebarVisible', leftSideBarPanel.isVisible);
const onAfterShow = leftSideBarPanel['onAfterShow'].bind(leftSideBarPanel);
leftSideBarPanel['onAfterShow'] = (msg) => {
onAfterShow(msg);
sidebarVisibleKey.set(true);
};
const onAfterHide = leftSideBarPanel['onAfterHide'].bind(leftSideBarPanel);
leftSideBarPanel['onAfterHide'] = (msg) => {
onAfterHide(msg);
sidebarVisibleKey.set(false);
};
}
initFocusKeyContexts() {
const sideBarFocus = this.contextKeyService.createKey('sideBarFocus', false);
const panelFocus = this.contextKeyService.createKey('panelFocus', false);
const updateFocusContextKeys = () => {
const area = this.activeWidget && this.getAreaFor(this.activeWidget);
sideBarFocus.set(area === 'left');
panelFocus.set(area === 'main');
};
updateFocusContextKeys();
this.onDidChangeActiveWidget(updateFocusContextKeys);
}
setTopPanelVisibility(preference) {
const hiddenPreferences = ['compact', 'hidden'];
this.topPanel.setHidden(hiddenPreferences.includes(preference));
}
onBeforeAttach(msg) {
document.addEventListener('lm-dragenter', this, true);
document.addEventListener('lm-dragover', this, true);
document.addEventListener('lm-dragleave', this, true);
document.addEventListener('lm-drop', this, true);
}
onAfterDetach(msg) {
document.removeEventListener('lm-dragenter', this, true);
document.removeEventListener('lm-dragover', this, true);
document.removeEventListener('lm-dragleave', this, true);
document.removeEventListener('lm-drop', this, true);
}
handleEvent(event) {
switch (event.type) {
case 'lm-dragenter':
this.onDragEnter(event);
break;
case 'lm-dragover':
this.onDragOver(event);
break;
case 'lm-drop':
this.onDrop(event);
break;
case 'lm-dragleave':
this.onDragLeave(event);
break;
}
}
onDragEnter({ mimeData }) {
if (!this.dragState) {
if (mimeData && mimeData.hasData('application/vnd.lumino.widget-factory')) {
// The drag contains a widget, so we'll track it and expand side panels as needed
this.dragState = {
startTime: performance.now(),
leftExpanded: false,
rightExpanded: false,
bottomExpanded: false
};
}
}
}
onDragOver(event) {
const state = this.dragState;
if (state) {
state.lastDragOver = event;
if (state.leaveTimeout) {
window.clearTimeout(state.leaveTimeout);
state.leaveTimeout = undefined;
}
const { clientX, clientY } = event;
const { offsetLeft, offsetTop, clientWidth, clientHeight } = this.node;
// Don't expand any side panels right after the drag has started
const allowExpansion = performance.now() - state.startTime >= 500;
const expLeft = allowExpansion && clientX >= offsetLeft
&& clientX <= offsetLeft + this.options.leftPanel.expandThreshold;
const expRight = allowExpansion && clientX <= offsetLeft + clientWidth
&& clientX >= offsetLeft + clientWidth - this.options.rightPanel.expandThreshold;
const expBottom = allowExpansion && !expLeft && !expRight && clientY <= offsetTop + clientHeight
&& clientY >= offsetTop + clientHeight - this.options.bottomPanel.expandThreshold;
// eslint-disable-next-line no-null/no-null
if (expLeft && !state.leftExpanded && this.leftPanelHandler.tabBar.currentTitle === null) {
// The mouse cursor is moved close to the left border
this.leftPanelHandler.expand();
this.leftPanelHandler.state.pendingUpdate.then(() => this.dispatchMouseMove());
state.leftExpanded = true;
}
else if (!expLeft && state.leftExpanded) {
// The mouse cursor is moved away from the left border
this.leftPanelHandler.collapse();
state.leftExpanded = false;
}
// eslint-disable-next-line no-null/no-null
if (expRight && !state.rightExpanded && this.rightPanelHandler.tabBar.currentTitle === null) {
// The mouse cursor is moved close to the right border
this.rightPanelHandler.expand();
this.rightPanelHandler.state.pendingUpdate.then(() => this.dispatchMouseMove());
state.rightExpanded = true;
}
else if (!expRight && state.rightExpanded) {
// The mouse cursor is moved away from the right border
this.rightPanelHandler.collapse();
state.rightExpanded = false;
}
if (expBottom && !state.bottomExpanded && this.bottomPanel.isHidden) {
// The mouse cursor is moved close to the bottom border
this.expandBottomPanel();
this.bottomPanelState.pendingUpdate.then(() => this.dispatchMouseMove());
state.bottomExpanded = true;
}
else if (!expBottom && state.bottomExpanded) {
// The mouse cursor is moved away from the bottom border
this.collapseBottomPanel();
state.bottomExpanded = false;
}
}
}
/**
* This method is called after a side panel has been expanded while dragging a widget. It fires
* a `mousemove` event so that the drag overlay markers are updated correctly in all dock panels.
*/
dispatchMouseMove() {
if (this.dragState && this.dragState.lastDragOver) {
const { clientX, clientY } = this.dragState.lastDragOver;
const event = document.createEvent('MouseEvent');
event.initMouseEvent('mousemove', true, true, window, 0, 0, 0,
// eslint-disable-next-line no-null/no-null
clientX, clientY, false, false, false, false, 0, null);
document.dispatchEvent(event);
}
}
onDrop(event) {
const state = this.dragState;
if (state) {
if (state.leaveTimeout) {
window.clearTimeout(state.leaveTimeout);
}
this.dragState = undefined;
window.requestAnimationFrame(() => {
// Clean up the side panel state in the next frame
if (this.leftPanelHandler.dockPanel.isEmpty) {
this.leftPanelHandler.collapse();
}
if (this.rightPanelHandler.dockPanel.isEmpty) {
this.rightPanelHandler.collapse();
}
if (this.bottomPanel.isEmpty) {
this.collapseBottomPanel();
}
});
}
}
onDragLeave(event) {
const state = this.dragState;
if (state) {
state.lastDragOver = undefined;
if (state.leaveTimeout) {
window.clearTimeout(state.leaveTimeout);
}
state.leaveTimeout = window.setTimeout(() => {
this.dragState = undefined;
if (state.leftExpanded || this.leftPanelHandler.dockPanel.isEmpty) {
this.leftPanelHandler.collapse();
}
if (state.rightExpanded || this.rightPanelHandler.dockPanel.isEmpty) {
this.rightPanelHandler.collapse();
}
if (state.bottomExpanded || this.bottomPanel.isEmpty) {
this.collapseBottomPanel();
}
}, 100);
}
}
/**
* Create the dock panel in the main shell area.
*/
createMainPanel() {
const renderer = this.dockPanelRendererFactory();
renderer.tabBarClasses.push(exports.MAIN_BOTTOM_AREA_CLASS);
renderer.tabBarClasses.push(exports.MAIN_AREA_CLASS);
this._mainPanelRenderer = renderer;
const dockPanel = this.dockPanelFactory({
mode: 'multiple-document',
renderer,
spacing: 0
}, area => this.doToggleMaximized(area));
dockPanel.id = theia_dock_panel_1.MAIN_AREA_ID;
dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
const openUri = async (fileUri) => {
try {
const opener = await this.openerService.getOpener(fileUri);
opener.open(fileUri);
}
catch (e) {
this.logger.info(`no opener found for '${fileUri}'`);
}
};
dockPanel.node.addEventListener('drop', event => {
if (event.dataTransfer) {
const uris = this.additionalDraggedUris || ApplicationShell_1.getDraggedEditorUris(event.dataTransfer);
if (uris.length > 0) {
uris.forEach(openUri);
}
else if (event.dataTransfer.files?.length > 0) {
// the files were dragged from the outside the workspace
Array.from(event.dataTransfer.files).forEach(async (file) => {
if (common_1.environment.electron.is()) {
const path = window.electronTheiaCore.getPathForFile(file);
if (path) {
const fileUri = uri_1.default.fromFilePath(path);
openUri(fileUri);
}
}
else {
const fileContent = await file.text();
const fileName = file.name;
const uri = new uri_1.default(`${common_1.UNTITLED_SCHEME}:/${fileName}`);
// Only create a new untitled resource if it doesn't already exist.
// VS Code does the same thing, and there's not really a better solution,
// since we want to keep the original name of the file,
// but also to prevent duplicates of the same file.
if (!this.untitledResourceResolver.has(uri)) {
const untitledResource = await this.untitledResourceResolver.createUntitledResource(fileContent, undefined, new uri_1.default(`${common_1.UNTITLED_SCHEME}:/${fileName}`));
openUri(untitledResource.uri);
}
}
});
}
}
});
dockPanel.node.addEventListener('dblclick', event => {
const el = event.target;
if (el.id === theia_dock_panel_1.MAIN_AREA_ID || el.classList.contains('lm-TabBar-content')) {
this.onDidDoubleClickMainAreaEmitter.fire();
}
});
const handler = (e) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'link';
e.preventDefault();
e.stopPropagation();
}
};
dockPanel.node.addEventListener('dragover', handler);
dockPanel.node.addEventListener('dragenter', handler);
return dockPanel;
}
addAdditionalDraggedEditorUris(uris) {
this.additionalDraggedUris = uris;
}
clearAdditionalDraggedEditorUris() {
this.additionalDraggedUris = undefined;
}
static getDraggedEditorUris(dataTransfer) {
const data = dataTransfer.getData('theia-editor-dnd');
return data ? data.split('\n').map(entry => new uri_1.default(entry)) : [];
}
static setDraggedEditorUris(dataTransfer, uris) {
dataTransfer.setData('theia-editor-dnd', uris.map(uri => uri.toString()).join('\n'));
}
/**
* Create the dock panel in the bottom shell area.
*/
createBottomPanel() {
const renderer = this.dockPanelRendererFactory();
renderer.tabBarClasses.push(exports.MAIN_BOTTOM_AREA_CLASS);
renderer.tabBarClasses.push(exports.BOTTOM_AREA_CLASS);
const dockPanel = this.dockPanelFactory({
mode: 'multiple-document',
renderer,
spacing: 0
}, area => this.doToggleMaximized(area));
dockPanel.id = theia_dock_panel_1.BOTTOM_AREA_ID;
dockPanel.widgetRemoved.connect((sender, widget) => {
if (sender.isEmpty) {
this.collapseBottomPanel();
}
}, this);
dockPanel.node.addEventListener('lm-dragenter', event => {
// Make sure that the main panel hides its overlay when the bottom panel is expanded
this.mainPanel.overlay.hide(0);
});
dockPanel.hide();
dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget));
dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget));
return dockPanel;
}
/**
* Create the top panel, which is used to hold the main menu.
*/
createTopPanel() {
const topPanel = new widgets_1.Panel();
topPanel.id = 'theia-top-panel';
topPanel.hide();
return topPanel;
}
/**
* Create a box layout to assemble the application shell layout.
*/
createBoxLayout(widgets, stretch, options) {
const boxLayout = new widgets_1.BoxLayout(options);
for (let i = 0; i < widgets.length; i++) {
if (stretch !== undefined && i < stretch.length) {
widgets_1.BoxPanel.setStretch(widgets[i], stretch[i]);
}
boxLayout.addWidget(widgets[i]);
}
return boxLayout;
}
/**
* Create a split layout to assemble the application shell layout.
*/
createSplitLayout(widgets, stretch, options) {
let optParam = { renderer: widgets_1.SplitPanel.defaultRenderer, };
if (options) {
optParam = { ...optParam, ...options };
}
const splitLayout = new widgets_1.SplitLayout(optParam);
for (let i = 0; i < widgets.length; i++) {
if (stretch !== undefined && i < stretch.length) {
widgets_1.SplitPanel.setStretch(widgets[i], stretch[i]);
}
splitLayout.addWidget(widgets[i]);
}
return splitLayout;
}
/**
* Assemble the application shell layout. Override this method in order to change the arrangement
* of the main area and the side panels.
*/
createLayout() {
const bottomSplitLayout = this.createSplitLayout([this.mainPanel, this.bottomPanel], [1, 0], { orientation: 'vertical', spacing: 0 });
const panelForBottomArea = new theia_split_panel_1.TheiaSplitPanel({ layout: bottomSplitLayout });
panelForBottomArea.id = 'theia-bottom-split-panel';
const leftRightSplitLayout = this.createSplitLayout([this.leftPanelHandler.container, panelForBottomArea, this.rightPanelHandler.container], [0, 1, 0], { orientation: 'horizontal', spacing: 0 });
const panelForSideAreas = new theia_split_panel_1.TheiaSplitPanel({ layout: leftRightSplitLayout });
panelForSideAreas.id = 'theia-left-right-split-panel';
return this.createBoxLayout([this.topPanel, panelForSideAreas, this.statusBar], [0, 1, 0], { direction: 'top-to-bottom', spacing: 0 });
}
/**
* Create an object that describes the current shell layout. This object may contain references
* to widgets; these need to be transformed before the layout can be serialized.
*/
getLayoutData() {
return {
version: exports.applicationShellLayoutVersion,
mainPanel: this.mainPanel.saveLayout(),
mainPanelPinned: this.getPinnedMainWidgets(),
bottomPanel: {
config: this.bottomPanel.saveLayout(),
pinned: this.getPinnedBottomWidgets(),
size: this.bottomPanel.isVisible ? this.getBottomPanelSize() : this.bottomPanelState.lastPanelSize,
expanded: this.isExpanded('bottom')
},
leftPanel: this.leftPanelHandler.getLayoutData(),
rightPanel: this.rightPanelHandler.getLayoutData(),
activeWidgetId: this.activeWidget ? this.activeWidget.id : undefined
};
}
// Get an array corresponding to main panel widgets' pinned state.
getPinnedMainWidgets() {
const pinned = [];
(0, algorithm_1.toArray)(this.mainPanel.widgets()).forEach((a, i) => {
pinned[i] = a.title.className.includes(widgets_2.PINNED_CLASS);
});
return pinned;
}
// Get an array corresponding to bottom panel widgets' pinned state.
getPinnedBottomWidgets() {
const pinned = [];
(0, algorithm_1.toArray)(this.bottomPanel.widgets()).forEach((a, i) => {
pinned[i] = a.title.className.includes(widgets_2.PINNED_CLASS);
});
return pinned;
}
/**
* Compute the current height of the bottom panel. This implementation assumes that the container
* of the bottom panel is a `SplitPanel`.
*/
getBottomPanelSize() {
const parent = this.bottomPanel.parent;
if (parent instanceof widgets_1.SplitPanel && parent.isVisible) {
const index = parent.widgets.indexOf(this.bottomPanel) - 1;
if (index >= 0) {
const handle = parent.handles[index];
if (!handle.classList.contains('lm-mod-hidden')) {
const parentHeight = parent.node.clientHeight;
return parentHeight - handle.offsetTop;
}
}
}
}
/**
* Determine the default size to apply when the bottom panel is expanded for the first time.
*/
getDefaultBottomPanelSize() {
const parent = this.bottomPanel.parent;
if (parent && parent.isVisible) {
return parent.node.clientHeight * this.options.bottomPanel.initialSizeRatio;
}
}
/**
* Apply a shell layout that has been previously created with `getLayoutData`.
*/
async setLayoutData(layoutData) {
const { mainPanel, mainPanelPinned, bottomPanel, leftPanel, rightPanel, activeWidgetId } = layoutData;
if (leftPanel) {
this.leftPanelHandler.setLayoutData(leftPanel);
this.registerWithFocusTracker(leftPanel);
}
if (rightPanel) {
this.rightPanelHandler.setLayoutData(rightPanel);
this.registerWithFocusTracker(rightPanel);
}
// Proceed with the bottom panel once the side panels are set up
await Promise.all([this.leftPanelHandler.state.pendingUpdate, this.rightPanelHandler.state.pendingUpdate]);
if (bottomPanel) {
if (bottomPanel.config) {
this.bottomPanel.restoreLayout(bottomPanel.config);
this.registerWithFocusTracker(bottomPanel.config.main);
}
if (bottomPanel.size) {
this.bottomPanelState.lastPanelSize = bottomPanel.size;
}
if (bottomPanel.expanded) {
this.expandBottomPanel();
}
else {
this.collapseBottomPanel();
}
const widgets = (0, algorithm_1.toArray)(this.bottomPanel.widgets());
this.bottomPanel.markActiveTabBar(widgets[0]?.title);
if (bottomPanel.pinned && bottomPanel.pinned.length === widgets.length) {
widgets.forEach((a, i) => {
if (bottomPanel.pinned[i]) {
a.title.className += ` ${widgets_2.PINNED_CLASS}`;
a.title.closable = false;
}
});
}
}
// Proceed with the main panel once all others are set up
await this.bottomPanelState.pendingUpdate;
if (mainPanel) {
this.mainPanel.restoreLayout(mainPanel);
this.registerWithFocusTracker(mainPanel.main);
const widgets = (0, algorithm_1.toArray)(this.mainPanel.widgets());
// We don't store information about the last active tabbar
// So we simply mark the first as being active
this.mainPanel.markActiveTabBar(widgets[0]?.title);
if (mainPanelPinned && mainPanelPinned.length === widgets.length) {
widgets.forEach((a, i) => {
if (mainPanelPinned[i]) {
a.title.className += ` ${widgets_2.PINNED_CLASS}`;
a.title.closable = false;
}
});
}
}
if (activeWidgetId) {
this.activateWidget(activeWidgetId);
}
}
/**
* Modify the height of the bottom panel. This implementation assumes that the container of the
* bottom panel is a `SplitPanel`.
*/
setBottomPanelSize(size) {
const enableAnimation = this.applicationStateService.state === 'ready';
const options = {
side: 'bottom',
duration: enableAnimation ? this.options.bottomPanel.expandDuration : 0,
referenceWidget: this.bottomPanel
};
const promise = this.splitPositionHandler.setSidePanelSize(this.bottomPanel, size, options);
const result = new Promise(resolve => {
// Resolve the resulting promise in any case, regardless of whether resizing was successful
promise.then(() => resolve(), () => resolve());
});
this.bottomPanelState.pendingUpdate = this.bottomPanelState.pendingUpdate.then(() => result);
return result;
}
/**
* A promise that is resolved when all currently pending updates are done.
*/
get pendingUpdates() {
return Promise.all([
this.bottomPanelState.pendingUpdate,
this.leftPanelHandler.state.pendingUpdate,
this.rightPanelHandler.state.pendingUpdate
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]);
}
/**
* Track all widgets that are referenced by the given layout data.
*/
registerWithFocusTracker(data) {
if (data) {
if (data.type === 'tab-area') {
for (const widget of data.widgets) {
if (widget) {
this.track(widget);
}
}
}
else if (data.type === 'split-area') {
for (const child of data.children) {
this.registerWithFocusTracker(child);
}
}
else if (data.type === 'sidepanel' && data.items) {
for (const item of data.items) {
if (item.widget) {
this.track(item.widget);
}
}
}
}
}
resolveWidgetArea(widget, options) {
if (this.widgetAreaResolver && !options?.ref) {
const requestedArea = options?.area || 'main';
const resolvedArea = this.widgetAreaResolver.resolveArea(widget.id, requestedArea);
if (resolvedArea && resolvedArea !== requestedArea) {
return { ...options, area: resolvedArea };
}
}
return options;
}
/**
* Add a widget to the application shell. The given widget must have a unique `id` property,
* which will be used as the DOM id.
*
* Widgets are removed from the shell by calling their `close` or `dispose` methods.
*
* Widgets added to the top area are not tracked regarding the _current_ and _active_ states.
*/
async addWidget(widget, options) {
if (!widget.id) {
this.logger.error('Widgets added to the application shell must have a unique id property.');
return;
}
const resolvedOptions = this.resolveWidgetArea(widget, options);
const { area, addOptions } = this.getInsertionOptions(resolvedOptions);
const sidePanelOptions = { rank: options?.rank };
switch (area) {
case 'main':
this.mainPanel.addWidget(widget, addOptions);
break;
case 'top':
this.topPanel.addWidget(widget);
break;
case 'bottom':
this.bottomPanel.addWidget(widget, addOptions);
break;
case 'left':
this.leftPanelHandler.addWidget(widget, sidePanelOptions);
break;
case 'right':
this.rightPanelHandler.addWidget(widget, sidePanelOptions);
break;
case 'secondaryWindow':
const secondaryWindow = (0, secondary_window_handler_1.extractSecondaryWindow)(addOptions.ref);
if (secondaryWindow) {
this.secondaryWindowHandler.addWidgetToSecondaryWindow(widget, secondaryWindow, addOptions);
}
else {
// Fall back to adding widgets to the main area. This is preferred to throwing an error, because toolbar actions on secondary windows/commands
// may e.g. open further editors, e.g. a markdown preview.
this.mainPanel.addWidget(widget, {
...addOptions,
ref: undefined
});
}
break;
default:
throw new Error('Unexpected area: ' + options?.area);
}
if (area !== 'top') {
this.track(widget);
}
}
getInsertionOptions(options) {
let ref = options?.ref;
let area = options?.area || 'main';
if (!ref && (area === 'main' || area === 'bottom')) {
const tabBar = this.getTabBarFor(area);
ref = tabBar && tabBar.currentTitle && tabBar.currentTitle.owner || undefined;
}
// make sure that ref belongs to area
area = ref && this.getAreaFor(ref) || area;
const addOptions = {};
if (ApplicationShell_1.isOpenToSideMode(options?.mode)) {
const areaPanel = area === 'main' ? this.mainPanel : area === 'bottom' ? this.bottomPanel : undefined;
const sideRef = areaPanel && ref && (options?.mode === 'open-to-left' ?
areaPanel.previousTabBarWidget(ref) :
areaPanel.nextTabBarWidget(ref));
if (sideRef) {
addOptions.ref = sideRef;
}
else {
addOptions.ref = ref;
addOptions.mode = options?.mode === 'open-to-left' ? 'split-left' : 'split-right';
}
}
else if (ApplicationShell_1.isReplaceMode(options?.mode)) {
addOptions.ref = options?.ref;
addOptions.closeRef = true;
addOptions.mode = 'tab-after';
}
else {
addOptions.ref = ref;
addOptions.mode = options?.mode;
}
return { area, addOptions };
}
/**
* The widgets contained in the given shell area.
*/
getWidgets(area) {
switch (area) {
case 'main':
return (0, algorithm_1.toArray)(this.mainPanel.widgets());
case 'top':
return (0, algorithm_1.toArray)(this.topPanel.widgets);
case 'bottom':
return (0, algorithm_1.toArray)(this.bottomPanel.widgets());
case 'left':
return (0, algorithm_1.toArray)(this.leftPanelHandler.dockPanel.widgets());
case 'right':
return (0, algorithm_1.toArray)(this.rightPanelHandler.dockPanel.widgets());
case 'secondaryWindow':
return (0, algorithm_1.toArray)(this.secondaryWindowHandler.widgets);
default:
throw new Error('Illegal argument: ' + area);
}
}
/**
* Find the widget that contains the given HTML element. The returned widget may be one
* that is managed by the application shell, or one that is embedded in another widget and
* not directly managed by the shell, or a tab bar.
*/
findWidgetForElement(element) {
let widgetNode = element;
while (widgetNode && !widgetNode.classList.contains('lm-Widget')) {
widgetNode = widgetNode.parentElement;
}
if (widgetNode) {
return this.findWidgetForNode(widgetNode, this);
}
return undefined;
}
findWidgetForNode(widgetNode, widget) {
if (widget.node === widgetNode) {
return widget;
}
let result;
(0, algorithm_1.each)(widget.children(), child => {
result = this.findWidgetForNode(widgetNode, child);
return !result;
});
return result;
}
/**
* Finds the title widget from the tab-bar.
* @param tabBar used for providing an array of titles.
* @returns the selected title widget, else returns the currentTitle or undefined.
*/
findTitle(tabBar, event) {
if (event?.target instanceof HTMLElement) {
const tabNode = event.target;
const titleIndex = Array.from(tabBar.contentNode.getElementsByClassName('lm-TabBar-tab'))
.findIndex(node => node.contains(tabNode));
if (titleIndex !== -1) {
return tabBar.titles[titleIndex];
}
}
return tabBar.currentTitle || undefined;
}
/**
* Finds the tab-bar widget.
* @returns the selected tab-bar, else returns the currentTabBar.
*/
findTabBar(event) {
if (event?.target instanceof HTMLElement) {
const tabBar = this.findWidgetForElement(event.target);
if (tabBar instanceof widgets_1.TabBar) {
return tabBar;
}
}
return this.currentTabBar;
}
/**
* @returns the widget whose title has been targeted by a DOM event on a tabbar, or undefined if none can be found.
*/
findTargetedWidget(event) {
if (event) {
const tab = this.findTabBar(event);
const title = tab && this.findTitle(tab, event);
return title && title.owner;
}
}
/**
* The current widget in the application shell. The current widget is the last widget that
* was active and not yet closed. See the remarks to `activeWidget` on what _active_ means.
*/
get currentWidget() {
return this.tracker.currentWidget || undefined;
}
/**
* The active widget in the application shell. The active widget is the one that has focus
* (either the widget itself or any of its contents).
*
* _Note:_ Focus is taken by a widget through the `onActivateRequest` method. It is up to the
* widget implementation which DOM element will get the focus. The default implementation
* does not take any focus; in that case the widget is never returned by this property.
*/
get activeWidget() {
return this.tracker.activeWidget || undefined;
}
/**
* Returns the last active widget in the given shell area.
*/
getCurrentWidget(area) {
let title;
switch (area) {
case 'main':
title = this.mainPanel.currentTitle;
break;
case 'bottom':
title = this.bottomPanel.currentTitle;
break;
case 'left':
title = this.leftPanelHandler.tabBar.currentTitle;
break;
case 'right':
title = this.rightPanelHandler.tabBar.currentTitle;
break;
case 'secondaryWindow':
// The current widget in a secondary window is not tracked.
return undefined;
default:
throw new Error('Illegal argument: ' + area);
}
return title ? title.owner : undefined;
}
/**
* Handle a change to the current widget.
*/
onCurrentChanged(sender, args) {
this.onDidChangeCurrentWidgetEmitter.fire(args);
}
/**
* Handle a change to the active widget.
*/
onActiveChanged(sender, args) {
this.toDisposeOnActiveChanged.dispose();
const { newValue, oldValue } = args;
if (oldValue) {
let w = oldValue;
while (w) {
// Remove the mark of the previously active widget
w.title.className = w.title.className.replace(' theia-mod-active', '');
w = w.parent;
}
}
if (newValue) {
let w = newValue;
while (w) {
// Mark the tab of the active widget
w.title.className += ' theia-mod-active';
w = w.parent;
}
// Reveal the title of the active widget in its tab bar
const tabBar = this.getTabBarFor(newValue);
if (tabBar instanceof tab_bars_1.ScrollableTabBar) {
const index = tabBar.titles.indexOf(newValue.title);
if (index >= 0) {
tabBar.revealTab(index);
}
}
const widget = this.toTrackedStack(newValue.id).pop();
const panel = this.findPanel(widget);
if (panel) {
// if widget was undefined, we wouldn't have gotten a panel back before
panel.markAsCurrent(widget.title);
}
// activate another widget if an active widget will be closed
const onCloseRequest = newValue['onCloseRequest'];
newValue['onCloseRequest'] = msg => {
const currentTabBar = this.currentTabBar;
if (currentTabBar) {
const recentlyUsedInTabBar = currentTabBar['_previousTitle'];
if (recentlyUsedInTabBar && recentlyUsedInTabBar.owner !== newValue) {
currentTabBar.currentIndex = algorithm_1.ArrayExt.firstIndexOf(currentTabBar.titles, recentlyUsedInTabBar);
if (currentTabBar.currentTitle) {
this.activateWidget(currentTabBar.currentTitle.owner.id);
}
}
else if (!this.activateNextTabInTabBar(currentTabBar)) {
if (!this.activatePreviousTabBar(currentTabBar)) {
this.activateNextTabBar(currentTabBar);
}
}
}
newValue['onCloseRequest'] = onCloseRequest;
newValue['onCloseRequest'](msg);
};
this.toDisposeOnActiveChanged.push(common_1.Disposable.create(() => newValue['onCloseRequest'] = onCloseRequest));
if (previewable_widget_1.PreviewableWidget.is(newValue)) {
newValue.loaded = true;
}
}
this.onDidChangeActiveWidgetEmitter.fire(args);
}
/**
* Track the given widget so it is considered in the `current` and `active` state of the shell.
*/
track(widget) {
if (this.tracker.widgets.indexOf(widget) !== -1) {
return;
}
this.tracker.add(widget);
this.checkActivation(widget);
if (ApplicationShell_1.TrackableWidgetProvider.is(widget)) {
for (const toTrack of widget.getTrackableWidgets()) {
this.track(toTrack);
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
}
}
}
/**
* @returns an array of Widgets, all of which are tracked by the focus tracker
* The first member of the array is the widget whose id is passed in, and the other widgets
* are its tracked parents in ascending order
*/
toTrackedStack(id) {
const tracked = new Map(this.tracker.widgets.map(w => [w.id, w]));
let current = tracked.get(id);
const stack = [];
while (current) {
if (tracked.has(current.id)) {
stack.push(current);
}
current = current.parent || undefined;
}
return stack;
}
/**
* Activate a widget in the application shell. This makes the widget visible and usually
* also assigns focus to it.
*
* _Note:_ Focus is taken by a widget through the `onActivateRequest` method. It is up to the
* widget implementation which DOM element will get the focus. The default implementation
* does not take any focus.
*
* @returns the activated widget if it was found
*/
async activateWidget(id) {
const stack = this.toTrackedStack(id);
let current = stack.pop();
if (current && !this.doActivateWidget(current.id)) {
return undefined;
}
while (current && stack.length) {
const child = stack.pop();
if (ApplicationShell_1.TrackableWidgetProvider.is(current) && current.activateWidget) {
current = current.activateWidget(child.id);
}
else {
child.activate();
current = child;
}
}
if (!current) {
return undefined;
}
return Promise.all([
this.waitForActivation(current.id),
(0, widgets_2.waitForRevealed)(current),
this.pendingUpdates
]).then(() => current, () => undefined);
}
waitForActivation(id) {
if (this.activeWidget && this.activeWidget.id === id) {
return Promise.resolve();
}
const activation = new promise_util_1.Deferred();
const success = this.onDidChangeActiveWidget(() => {
if (this.activeWidget && this.activeWidget.id === id) {
activation.resolve();
}
});
const failure = setTimeout(() => activation.reject(new Error(`Widget with id '${id}' failed to activate.`)), this.activationTimeout + 250);
return activation.promise.finally(() => {
success.dispose();
clearTimeout(failure);
});
}
/**
* Activate top-level area widget.
*/
doActivateWidget(id) {
let widget = (0, algorithm_1.find)(this.mainPanel.widgets(), w => w.id === id);
if (widget) {
this.mainPanel.activateWidget(widget);
}
if (!widget) {
widget = (0, algorithm_1.find)(this.bottomPanel.widgets(), w => w.id === id);
if (widget) {
this.expandBottomPanel();
this.bottomPanel.activateWidget(widget);
}
}
if (!widget) {
widget = this.leftPanelHandler.activate(id);
}
if (!widget) {
widget = this.rightPanelHandler.activate(id);
}
if (widget) {
this.focusWindowIfApplicationFocused();
return widget;
}
return this.secondaryWindowHandler.activateWidget(id);
}
focusWindowIfApplicationFocused() {
// If this application has focus, then on widget activation, activate the window.
// If this application does not have focus, do not routinely steal focus.
if (this.secondaryWindowHandler.getFocusedWindow()) {
this.windowService.focus();
}
}
/**
* Focus is taken by a widget through the `onActivateRequest` method. It is up to the
* widget implementation which DOM element will get the focus. The default implementation
* of Widget does not take any focus. This method can help finding such problems by logging
* a warning in case a widget was explicitly activated, but did not trigger a change of the
* `activeWidget` property.
*/
checkActivation(widget) {
const onActivateRequest = widget['onActivateRequest'].bind(widget);
widget['onActivateRequest'] = (msg) => {
onActivateRequest(msg);
this.assertActivated(widget);
};
return widget;
}
assertActivated(widget) {
this.toDisposeOnActivationCheck.dispose();
const onDispose = () => this.toDisposeOnActivationCheck.dispose();
widget.disposed.connect(onDispose);
this.toDisposeOnActivationCheck.push(common_1.Disposable.create(() => widget.disposed.disconnect(onDispose)));
let start = 0;
const step = () => {
const activeElement = widget.node.ownerDocument.activeElement;
if (activeElement && widget.node.contains(activeElement)) {
return;
}
const now = Date.now();
if (!start) {
start = now;
}
const delta = now - start;
if (delta < this.activationTimeout) {
request = setTimeout(step, 0);
}
else {
this.logger.warn(`Widget was activated, but did not accept focus after ${this.activationTimeout}ms: ${widget.id}`);
}
};
let request = setTimeout(step, 0);
this.toDisposeOnActivationCheck.push(common_1.Disposable.create(() => window.cancelAnimationFrame(request)));
}
/**
* Reveal a widget in the application shell. This makes the widget visible,
* but does not activate it.
*
* @returns the revealed widget if it was found
*/
async revealWidget(id) {
const stack = this.toTrackedStack(id);
let current = stack.pop();
if (current && !this.doRevealWidget(current.id)) {
return undefined;
}
while (current && stack.length) {
const child = stack.pop();
if (ApplicationShell_1.TrackableWidgetProvider.is(current) && current.revealWidget) {
current = current.revealWidget(child.id);
}
else {
current = child;
}
}
if (!current) {
return undefined;
}
await Promise.all([
(0, widgets_2.waitForRevealed)(current),
this.pendingUpdates
]);
return current;
}
/**
* Reveal top-level area widget.
*/
doRevealWidget(id) {
let widget = (0, algorithm_1.find)(this.mainPanel.widgets(), w => w.id === id);
if (!widget) {
widget = (0, algorithm_1.find)(this.bottomPanel.widgets(), w => w.id === id);
if (widget) {
this.expandBottomPanel();
}
}
if (widget) {
const tabBar = this.getTabBarFor(widget);
if (tabBar) {
tabBar.currentTitle = widget.title;
}
}
if (!widget) {
widget = this.leftPanelHandler.expand(id);
}
if (!widget) {
widget = this.rightPanelHandler.expand(id);
}
if (widget) {
this.focusWindowIfApplicationFocused();
return widget;
}
else {
return this.secondaryWindowHandler.revealWidget(id);
}
}
/**
* Expand the named side panel area. This makes sure that the panel is visible, even if there
* are no widgets in it. If the panel is already visible, nothing happens. If the panel is currently
* collapsed (see `collapsePanel`) and it contains widgets, the widgets are revealed that were
* visible before it was collapsed.
*/
expandPanel(area) {
switch (area) {
case 'bottom':
this.expandBottomPanel();
break;
case 'left':
this.leftPanelHandler.expand();
break;
case 'right':
this.rightPanelHandler.expand();
break;
default:
throw new Error('Area cannot be expanded: ' + area);
}
}
/**
* Adjusts the size of the given area in the application shell.
*
* @param size the desired size of the panel in pixels.
* @param area the area to resize.
*/
resize(size, area) {
switch (area) {
case 'bottom':
if (this.bottomPanel.isHidden) {
this.bottomPanelState.lastPanelSize = size;
}
else {
this.setBottomPanelSize(size);
}
break;
case 'left':
this.leftPanelHandler.resize(size);
break;
case 'right':
this.rightPanelHandler.resize(size);
break;
default:
throw new Error('Area cannot be resized: ' + area);
}
}
/**
* Expand the bottom panel. See `expandPanel` regarding the exact behavior.
*/
expandBottomPanel() {
const bottomPanel = this.bottomPanel;
if (bottomPanel.isHidden) {
let relativeSizes;
const parent = bottomPanel.parent;
if (parent instanceof widgets_1.SplitPanel) {
relativeSizes = parent.relativeSizes();
}
bottomPanel.show();
if (relativeSizes && parent instanceof widgets_1.SplitPanel) {
// Make sure that the expansion animation starts at the smallest possible size
parent.setRelativeSizes(relativeSizes);
}
let size;
if (bottomPanel.isEmpty) {
bottomPanel.node.style.minHeight = '0';
}
if (this.bottomPanelState.lastPanelSize) {
size = this.bottomPanelState.lastPanelSize;
}
else if (bottomPanel.isEmpty) {
size = this.options.bottomPanel.emptySize;
}
else {
size = this.getDefaultBottomPanelSize();
}
if (size) {
this.bottomPanelState.expansion = side_panel_handler_1.SidePanel.ExpansionState.expanding;
this.setBottomPanelSize(size).then(() => {
if (this.bottomPanelState.expansion === side_panel_handler_1.SidePanel.ExpansionState.expanding) {
this.bottomPanelState.expansion = side_panel_handler_1.SidePanel.ExpansionState.expanded;
}
});
}
else {
this.bottomPanelState.expansion = side_panel_handler_1.SidePanel.ExpansionState.expanded;
}
}
}
/**
* Collapse the named side panel area. This makes sure that the panel is hidden,
* increasing the space that is available for other shell areas.
*/
collapsePanel(area) {
switch (area) {
case 'bottom':
return this.collapseBottomPanel();
case 'left':
return this.leftPanelHandler.collapse();
case 'right':
return this.rightPanelHandler.collapse();
default:
throw new Error('Area cannot be collapsed: ' + area);
}
}
/**
* Collapse the bottom panel. All contained widgets are hidden, but not closed.
* They can be restored by calling `expandBottomPanel`.
*/
collapseBottomPanel() {
const bottomPanel = this.bottomPanel;
if (bottomPanel.isHidden) {
return Promise.resolve();
}
if (this.bottomPanelState.expansion === side_panel_handler_1.SidePanel.ExpansionState.expanded) {
const size = this.getBottomPanelSize();
if (size) {
this.bottomPanelState.lastPanelSize = size;
}
}
this.bottomPanelState.expansion = side_panel_handler_1.SidePanel.ExpansionState.collapsed;
bottomPanel.hide();
return (0, browser_1.animationFrame)();
}
/**
* Refresh the toggle button for the bottom panel. This implementation creates a status bar entry
* and refers to the command `core.toggle.bottom.panel`.
*/
refreshBottomPanelToggleButton() {
const label = nls_1.nls.localize('theia/core/common/collapseBottomPanel', 'Toggle Bottom Panel');
const element = {
name: label,
text: '$(codicon-window)',
alignment: status_bar_1.StatusBarAlignment.RIGHT,
tooltip: label,
command: 'core.toggle.bottom.panel',
accessibilityInformation: {
label: label,
role: 'button'
},
priority: -1000
};
this.statusBar.setElement(exports.BOTTOM_PANEL_TOGGLE_ID, element);
}
/**
* Check whether the named side panel area is expanded (returns `true`) or collapsed (returns `false`).
*/
isExpanded(area) {
switch (area) {
case 'bottom':
return this.bottomPanelState.expansion === side_panel_handler_1.SidePanel.ExpansionState.expanded;
case 'left':
return this.leftPanelHandler.state.expansion === side_panel_handler_1.SidePanel.ExpansionState.expanded;
case 'right':
return this.rightPanelHandler.state.expansion === side_panel_handler_1.SidePanel.ExpansionState.expanded;
default:
return true;
}
}
/**
* Close all tabs or a selection of tabs in a specific part of the application shell.
*
* @param tabBarOrArea
* Either the name of a shell area or a `TabBar` that is contained in such an area.
* @param filter
* If undefined, all tabs are closed; otherwise only those tabs that match the filter are closed.
*/
async closeTabs(tabBarOrArea, filter) {
const titles = this.getWidgetTitles(tabBarOrArea, filter);
if (titles.length) {
await this.closeMany(titles.map(title => title.owner));
}
}
saveTabs(tabBarOrArea, filter) {
const titles = this.getWidgetTitles(tabBarOrArea, filter);
for (let i = 0; i < titles.length; i++) {
const widget = titles[i].owner;
const saveable = saveable_1.Saveable.get(widget);
saveable?.save();
}
}
/**
* Collects all widget titles for the given tab bar or area and optionally filters them.
*
* @param tabBarOrArea The tab bar or area to retrieve the widget titles for
* @param filter The filter to apply to the result
* @returns The filtered array of widget titles or an empty array
*/
getWidgetTitles(tabBarOrArea, filter) {
const titles = [];
if (tabBarOrArea === 'main') {
this.mainAreaTabBars.forEach(tabbar => titles.push(...(0, algorithm_1.toArray)(tabbar.titles)));
}
else if (tabBarOrArea === 'bottom') {
this.bottomAreaTabBars.forEach(tabbar => titles.push(...(0, algorithm_1.toArray)(tabbar.titles)));
}
else if (tabBarOrArea === 'secondaryWindow') {
titles.push(...this.secondaryWindowHandler.widgets.map(w => w.title));
}
else if (typeof tabBarOrArea === 'string') {
const tabbar = this.getTabBarFor(tabBarOrArea);
if (tabbar) {
titles.push(...(0, algorithm_1.toArray)(tabbar.titles));
}
}
else if (tabBarOrArea) {
titles.push(...(0, algorithm_1.toArray)(tabBarOrArea.titles));
}
return filter ? titles.filter(filter) : titles;
}
/**
* @param targets the widgets to be closed
* @return an array of all the widgets that were actually closed.
*/
async closeMany(targets, options) {
if (options?.save === false || await saveable_1.Saveable.confirmSaveBeforeClose(targets, this.widgets.filter(widget => !targets.includes(widget)))) {
return (await Promise.all(targets.map(target => this.closeWidget(target.id, options)))).filter((widget) => widget !== undefined);
}
return [];
}
/**
* @returns the widget that was closed, if any, `undefined` otherwise.
*
* If your use case requires closing multiple widgets, use {@link ApplicationShell#closeMany} instead. That method handles closing saveable widgets more reliably.
*/
async closeWidget(id, options) {
// TODO handle save for composite widgets, i.e. the preference widget has 2 editors
const stack = this.toTrackedStack(id);
const current = stack.pop();
if (!current) {
return undefined;
}
const saveableOptions = options && { shouldSave: () => options.save };
const pendingClose = saveable_1.SaveableWidget.is(current)
? current.closeWithSaving(saveableOptions)
: (current.close(), (0, widgets_2.waitForClosed)(current));
await Promise.all([
pendingClose,
this.pendingUpdates
]);
return stack[0] || current;
}
/**
* The shell area name of the currently active tab, or undefined.
*/
get currentTabArea() {
const currentWidget = this.currentWidget;
if (currentWidget) {
return this.getAreaFor(currentWidget);
}
}
/**
* Determine the name of the shell area where the given widget resides. The result is
* undefined if the widget does not reside directly in the shell.
*/
getAreaFor(input) {
if (input instanceof widgets_1.TabBar) {
if ((0, algorithm_1.find)(this.mainPanel.tabBars(), tb => tb === input)) {
return 'main';
}
if ((0, algorithm_1.find)(this.bottomPanel.tabBars(), tb => tb === input)) {
return 'bottom';
}
if (this.leftPanelHandler.tabBar === input) {
return 'left';
}
if (this.rightPanelHandler.tabBar === input) {
return 'right';
}
}
const widget = this.toTrackedStack(input.id).pop();
if (!widget) {
return undefined;
}
const title = widget.title;
const mainPanelTabBar = this.mainPanel.findTabBar(title);
if (mainPanelTabBar) {
return 'main';
}
const bottomPanelTabBar = this.bottomPanel.findTabBar(title);
if (bottomPanelTabBar) {
return 'bottom';
}
if (algorithm_1.ArrayExt.firstIndexOf(this.leftPanelHandler.tabBar.titles, title) > -1) {
return 'left';
}
if (algorithm_1.ArrayExt.firstIndexOf(this.rightPanelHandler.tabBar.titles, title) > -1) {
return 'right';
}
if (this.secondaryWindowHandler.widgets.includes(widget)) {
return 'secondaryWindow';
}
return undefined;
}
getAreaPanelFor(input) {
const widget = this.toTrackedStack(input.id).pop();
if (!widget) {
return undefined;
}
return this.findPanel(widget);
}
/**
* Find the shell panel this top-level widget is part of
*/
findPanel(widget) {
if (!widget) {
return undefined;
}
const title = widget.title;
const mainPanelTabBar = this.mainPanel.findTabBar(title);
if (mainPanelTabBar) {
return this.mainPanel;
}
const bottomPanelTabBar = this.bottomPanel.findTabBar(title);
if (bottomPanelTabBar) {
return this.bottomPanel;
}
if (algorithm_1.ArrayExt.firstIndexOf(this.leftPanelHandler.tabBar.titles, title) > -1) {
return this.leftPanelHandler.dockPanel;
}
if (algorithm_1.ArrayExt.firstIndexOf(this.rightPanelHandler.tabBar.titles, title) > -1) {
return this.rightPanelHandler.dockPanel;
}
return undefined;
}
/**
* Return the tab bar that has the currently active widget, or undefined.
*/
get currentTabBar() {
const currentWidget = this.currentWidget;
if (currentWidget) {
return this.getTabBarFor(currentWidget);
}
}
/**
* Return the tab bar in the given shell area, or the tab bar that has the given widget, or undefined.
*/
getTabBarFor(widgetOrArea) {
if (typeof widgetOrArea === 'string') {
switch (widgetOrArea) {
case 'main':
return this.mainPanel.currentTabBar;
case 'bottom':
return this.bottomPanel.currentTabBar;
case 'left':
return this.leftPanelHandler.tabBar;
case 'right':
return this.rightPanelHandler.tabBar;
case 'secondaryWindow':
// there may be multiple secondary windows, so we can't return a single tabbar here
return undefined;
default:
throw new Error('Illegal argument: ' + widgetOrArea);
}
}
const widget = this.toTrackedStack(widgetOrArea.id).pop();
if (!widget) {
return undefined;
}
const widgetTitle = widget.title;
const mainPanelTabBar = this.mainPanel.findTabBar(widgetTitle);
if (mainPanelTabBar) {
return mainPanelTabBar;
}
const bottomPanelTabBar = this.bottomPanel.findTabBar(widgetTitle);
if (bottomPanelTabBar) {
return bottomPanelTabBar;
}
const leftPanelTabBar = this.leftPanelHandler.tabBar;
if (algorithm_1.ArrayExt.firstIndexOf(leftPanelTabBar.titles, widgetTitle) > -1) {
return leftPanelTabBar;
}
const rightPanelTabBar = this.rightPanelHandler.tabBar;
if (algorithm_1.ArrayExt.firstIndexOf(rightPanelTabBar.titles, widgetTitle) > -1) {
return rightPanelTabBar;
}
const secondaryWindowTabBar = this.secondaryWindowHandler.getTabBarFor(widget);
if (secondaryWindowTabBar) {
return secondaryWindowTabBar;
}
return undefined;
}
/**
* The tab bars contained in the main shell area. If there is no widget in the main area, the
* returned array is empty.
*/
get mainAreaTabBars() {
return (0, algorithm_1.toArray)(this.mainPanel.tabBars());
}
/**
* The tab bars contained in the bottom shell area. If there is no widget in the bottom area,
* the returned array is empty.
*/
get bottomAreaTabBars() {
return (0, algorithm_1.toArray)(this.bottomPanel.tabBars());
}
/**
* The tab bars contained in all shell areas.
*/
get allTabBars() {
return [...this.mainAreaTabBars, ...this.bottomAreaTabBars, this.leftPanelHandler.tabBar, this.rightPanelHandler.tabBar];
}
/*
* Activate the next tab in the current tab bar.
*/
activateNextTabInTabBar(current = this.currentTabBar) {
const index = this.nextTabIndexInTabBar(current);
if (!current || index === -1) {
return false;
}
current.currentIndex = index;
if (current.currentTitle) {
this.activateWidget(current.currentTitle.owner.id);
}
return true;
}
nextTabIndexInTabBar(current = this.currentTabBar) {
if (!current || current.titles.length <= 1) {
return -1;
}
const index = current.currentIndex;
if (index === -1) {
return -1;
}
if (index < current.titles.length - 1) {
return index + 1;
}
// last item in tab bar. select the previous one.
if (index === current.titles.length - 1) {
return index - 1;
}
return 0;
}
activateNextTab() {
const current = this.currentTabBar;
if (current) {
const ci = current.currentIndex;
if (ci !== -1) {
if (ci < current.titles.length - 1) {
current.currentIndex += 1;
if (current.currentTitle) {
this.activateWidget(current.currentTitle.owner.id);
}
return true;
}
else if (ci === current.titles.length - 1) {
return this.activateNextTabBar(current);
}
}
}
return false;
}
activateNextTabBar(current = this.currentTabBar) {
const nextBar = this.nextTabBar(current);
if (nextBar) {
nextBar.currentIndex = 0;
if (nextBar.currentTitle) {
this.activateWidget(nextBar.currentTitle.owner.id);
}
return true;
}
return false;
}
/**
* Return the tab bar next to the given tab bar; return the given tab bar if there is no adjacent one.
*/
nextTabBar(current = this.currentTabBar) {
let bars = (0, algorithm_1.toArray)(this.bottomPanel.tabBars());
let len = bars.length;
let ci = algorithm_1.ArrayExt.firstIndexOf(bars, current);
if (ci < 0) {
bars = (0, algorithm_1.toArray)(this.mainPanel.tabBars());
len = bars.length;
ci = algorithm_1.ArrayExt.firstIndexOf(bars, current);
}
if (ci >= 0 && ci < len - 1) {
return bars[ci + 1];
}
else if (ci >= 0 && ci === len - 1) {
return bars[0];
}
else {
return current;
}
}
/*
* Activate the previous tab in the current tab bar.
*/
activatePreviousTabInTabBar(current = this.currentTabBar) {
const index = this.previousTabIndexInTabBar(current);
if (!current || index === -1) {
return false;
}
current.currentIndex = index;
if (current.currentTitle) {
this.activateWidget(current.currentTitle.owner.id);
}
return true;
}
previousTabIndexInTabBar(current = this.currentTabBar) {
if (!current || current.titles.length <= 1) {
return -1;
}
const index = current.currentIndex;
if (index === -1) {
return -1;
}
if (index > 0) {
return index - 1;
}
return current.titles.length - 1;
}
activatePreviousTab() {
const current = this.currentTabBar;
if (current) {
const ci = current.currentIndex;
if (ci !== -1) {
if (ci > 0) {
current.currentIndex -= 1;
if (current.currentTitle) {
this.activateWidget(current.currentTitle.owner.id);
}
return true;
}
else if (ci === 0) {
if (current && current.titles.length > 0) {
current.currentIndex = current.titles.length - 1;
if (current.currentTitle) {
this.activateWidget(current.currentTitle.owner.id);
}
return true;
}
return this.activatePreviousTabBar(current);
}
}
}
return false;
}
activatePreviousTabBar(current = this.currentTabBar) {
const prevBar = this.previousTabBar(current);
if (!prevBar) {
return false;
}
if (!prevBar.currentTitle) {
prevBar.currentIndex = prevBar.titles.length - 1;
}
if (prevBar.currentTitle) {
this.activateWidget(prevBar.currentTitle.owner.id);
}
return true;
}
/**
* Return the tab bar previous to the given tab bar; return the given tab bar if there is no adjacent one.
*/
previousTabBar(current = this.currentTabBar) {
const bars = (0, algorithm_1.toArray)(this.mainPanel.tabBars());
const len = bars.length;
const ci = algorithm_1.ArrayExt.firstIndexOf(bars, current);
if (ci > 0) {
return bars[ci - 1];
}
else if (ci === 0) {
return bars[len - 1];
}
else {
return current;
}
}
/**
* Test whether the current widget is dirty.
*/
canSave() {
return this.saveableService.canSave(this.currentWidget);
}
/**
* Save the current widget if it is dirty.
*/
async save(options) {
await this.saveableService.save(this.currentWidget, options);
}
/**
* Test whether there is a dirty widget.
*/
canSaveAll() {
return this.tracker.widgets.some(widget => this.saveableService.canSave(widget));
}
/**
* Save all dirty widgets.
*/
async saveAll(options) {
for (const widget of this.widgets) {
if (saveable_1.Saveable.isDirty(widget) && this.saveableService.canSaveNotSaveAs(widget)) {
await this.saveableService.save(widget, options);
}
}
}
/**
* Returns a snapshot of all tracked widgets to allow async modifications.
*/
get widgets() {
return [...this.tracker.widgets];
}
getWidgetById(id) {
for (const widget of this.tracker.widgets) {
if (widget.id === id) {
return widget;
}
}
return undefined;
}
canToggleMaximized(widget = this.currentWidget) {
const area = widget && this.getAreaFor(widget);
return area === 'main' || area === 'bottom';
}
toggleMaximized(widget = this.currentWidget) {
const area = widget && this.getAreaPanelFor(widget);
if (area instanceof theia_dock_panel_1.TheiaDockPanel && (area === this.mainPanel || area === this.bottomPanel)) {
this.doToggleMaximized(area);
this.revealWidget(widget.id);
}
}
handleMenuBarVisibility(newValue) {
if (newValue === 'visible') {
const topRect = this.topPanel.node.getBoundingClientRect();
this.maximizedElement.style.top = `${topRect.bottom}px`;
}
else {
this.maximizedElement.style.removeProperty('top');
}
}
doToggleMaximized(area) {
if (this.unmaximize) {
this.unmaximize();
this.unmaximize = undefined;
return;
}
const removedListener = () => {
if (!area.widgets().next().value) {
this.doToggleMaximized(area);
}
};
const parent = area.parent;
const layout = area.parent?.layout;
const sizes = layout.relativeSizes().slice();
const stretch = widgets_1.SplitPanel.getStretch(area);
const index = parent.widgets.indexOf(area);
parent.layout?.removeWidget(area);
// eslint-disable-next-line no-null/no-null
this.maximizedElement.style.display = 'block';
area.addClass(exports.MAXIMIZED_CLASS);
const topRect = this.topPanel.node.getBoundingClientRect();
widgets_2.UnsafeWidgetUtilities.attach(area, this.maximizedElement);
this.maximizedElement.style.top = `${topRect.bottom}px`;
area.fit();
const observer = new ResizeObserver(entries => {
area.fit();
});
observer.observe(this.maximizedElement);
this.unmaximize = () => {
observer.unobserve(this.maximizedElement);
observer.disconnect();
this.maximizedElement.style.display = 'none';
area.removeClass(exports.MAXIMIZED_CLASS);
if (area.isAttached) {
widgets_2.UnsafeWidgetUtilities.detach(area);
}
parent?.insertWidget(index, area);
widgets_1.SplitPanel.setStretch(area, stretch);
layout.setRelativeSizes(sizes);
parent.fit();
this.onDidToggleMaximizedEmitter.fire(area);
area.widgetRemoved.disconnect(removedListener);
};
area.widgetRemoved.connect(removedListener);
this.onDidToggleMaximizedEmitter.fire(area);
}
};
exports.ApplicationShell = ApplicationShell;
tslib_1.__decorate([
(0, inversify_1.inject)(context_key_service_1.ContextKeyService),
tslib_1.__metadata("design:type", Object)
], ApplicationShell.prototype, "contextKeyService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(opener_service_1.OpenerService),
tslib_1.__metadata("design:type", Object)
], ApplicationShell.prototype, "openerService", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(common_1.UntitledResourceResolver),
tslib_1.__metadata("design:type", common_1.UntitledResourceResolver)
], ApplicationShell.prototype, "untitledResourceResolver", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(logger_1.ILogger),
(0, inversify_1.named)('core:ApplicationShell'),
tslib_1.__metadata("design:type", Object)
], ApplicationShell.prototype, "logger", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(theia_dock_panel_1.TheiaDockPanel.Factory),
tslib_1.__metadata("design:type", Function)
], ApplicationShell.prototype, "dockPanelFactory", void 0);
tslib_1.__decorate([
(0, inversify_1.postConstruct)(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], ApplicationShell.prototype, "init", null);
tslib_1.__decorate([
(0, inversify_1.inject)(exports.WidgetAreaResolver),
(0, inversify_1.optional)(),
tslib_1.__metadata("design:type", Object)
], ApplicationShell.prototype, "widgetAreaResolver", void 0);
exports.ApplicationShell = ApplicationShell = ApplicationShell_1 = tslib_1.__decorate([
(0, inversify_1.injectable)(),
tslib_1.__param(0, (0, inversify_1.inject)(exports.DockPanelRendererFactory)),
tslib_1.__param(1, (0, inversify_1.inject)(status_bar_1.StatusBarImpl)),
tslib_1.__param(2, (0, inversify_1.inject)(side_panel_handler_1.SidePanelHandlerFactory)),
tslib_1.__param(3, (0, inversify_1.inject)(split_panels_1.SplitPositionHandler)),
tslib_1.__param(4, (0, inversify_1.inject)(frontend_application_state_1.FrontendApplicationStateService)),
tslib_1.__param(5, (0, inversify_1.inject)(exports.ApplicationShellOptions)),
tslib_1.__param(5, (0, inversify_1.optional)()),
tslib_1.__param(6, (0, inversify_1.inject)(core_preferences_1.CorePreferences)),
tslib_1.__param(7, (0, inversify_1.inject)(saveable_service_1.SaveableService)),
tslib_1.__param(8, (0, inversify_1.inject)(secondary_window_handler_1.SecondaryWindowHandler)),
tslib_1.__param(9, (0, inversify_1.inject)(window_service_1.WindowService)),
tslib_1.__metadata("design:paramtypes", [Function, status_bar_1.StatusBarImpl, Function, split_panels_1.SplitPositionHandler,
frontend_application_state_1.FrontendApplicationStateService, Object, Object, saveable_service_1.SaveableService,
secondary_window_handler_1.SecondaryWindowHandler, Object])
], ApplicationShell);
/**
* The namespace for `ApplicationShell` class statics.
*/
(function (ApplicationShell) {
ApplicationShell.areaLabels = {
main: nls_1.nls.localizeByDefault('Main'),
top: nls_1.nls.localizeByDefault('Top'),
left: nls_1.nls.localizeByDefault('Left'),
right: nls_1.nls.localizeByDefault('Right'),
bottom: nls_1.nls.localizeByDefault('Bottom'),
secondaryWindow: nls_1.nls.localize('theia/shell-area/secondary', 'Secondary Window'),
};
/**
* The _side areas_ are those shell areas that can be collapsed and expanded,
* i.e. `left`, `right`, and `bottom`.
*/
function isSideArea(area) {
return area === 'left' || area === 'right' || area === 'bottom';
}
ApplicationShell.isSideArea = isSideArea;
function isValidArea(area) {
const areas = ['main', 'top', 'left', 'right', 'bottom', 'secondaryWindow'];
return typeof area === 'string' && areas.includes(area);
}
ApplicationShell.isValidArea = isValidArea;
/**
* The default values for application shell options.
*/
ApplicationShell.DEFAULT_OPTIONS = Object.freeze({
bottomPanel: Object.freeze({
emptySize: 140,
expandThreshold: 160,
expandDuration: 0,
initialSizeRatio: 0.382
}),
leftPanel: Object.freeze({
emptySize: 140,
expandThreshold: 140,
expandDuration: 0,
initialSizeRatio: 0.191
}),
rightPanel: Object.freeze({
emptySize: 140,
expandThreshold: 140,
expandDuration: 0,
initialSizeRatio: 0.191
})
});
function isOpenToSideMode(mode) {
return mode === 'open-to-left' || mode === 'open-to-right';
}
ApplicationShell.isOpenToSideMode = isOpenToSideMode;
function isReplaceMode(mode) {
return mode === 'tab-replace';
}
ApplicationShell.isReplaceMode = isReplaceMode;
let TrackableWidgetProvider;
(function (TrackableWidgetProvider) {
function is(widget) {
return (0, common_1.isObject)(widget) && 'getTrackableWidgets' in widget;
}
TrackableWidgetProvider.is = is;
})(TrackableWidgetProvider = ApplicationShell.TrackableWidgetProvider || (ApplicationShell.TrackableWidgetProvider = {}));
})(ApplicationShell || (exports.ApplicationShell = ApplicationShell = {}));
//# sourceMappingURL=application-shell.js.map