@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
647 lines • 27.1 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 WITH Classpath-exception-2.0
// *****************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var SidePanelHandler_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SidePanel = exports.SidePanelHandler = exports.SIDE_PANEL_TOOLBAR_CONTEXT_MENU = exports.SidePanelHandlerFactory = exports.LEFT_RIGHT_AREA_CLASS = void 0;
const inversify_1 = require("inversify");
const algorithm_1 = require("@phosphor/algorithm");
const widgets_1 = require("@phosphor/widgets");
const coreutils_1 = require("@phosphor/coreutils");
const dragdrop_1 = require("@phosphor/dragdrop");
const properties_1 = require("@phosphor/properties");
const tab_bars_1 = require("./tab-bars");
const sidebar_menu_widget_1 = require("./sidebar-menu-widget");
const split_panels_1 = require("./split-panels");
const browser_1 = require("../browser");
const frontend_application_state_1 = require("../frontend-application-state");
const theia_dock_panel_1 = require("./theia-dock-panel");
const side_panel_toolbar_1 = require("./side-panel-toolbar");
const tab_bar_toolbar_1 = require("./tab-bar-toolbar");
const disposable_1 = require("../../common/disposable");
const context_menu_renderer_1 = require("../context-menu-renderer");
const widgets_2 = require("../widgets");
/** The class name added to the left and right area panels. */
exports.LEFT_RIGHT_AREA_CLASS = 'theia-app-sides';
/** The class name added to collapsed side panels. */
const COLLAPSED_CLASS = 'theia-mod-collapsed';
exports.SidePanelHandlerFactory = Symbol('SidePanelHandlerFactory');
exports.SIDE_PANEL_TOOLBAR_CONTEXT_MENU = ['SIDE_PANEL_TOOLBAR_CONTEXT_MENU'];
/**
* A class which manages a dock panel and a related side bar. This is used for the left and right
* panel of the application shell.
*/
let SidePanelHandler = SidePanelHandler_1 = class SidePanelHandler {
constructor() {
/**
* The current state of the side panel.
*/
this.state = {
empty: true,
expansion: SidePanel.ExpansionState.collapsed,
pendingUpdate: Promise.resolve()
};
// should be a property to preserve fn identity
this.updateToolbarTitle = () => {
const currentTitle = this.tabBar && this.tabBar.currentTitle;
this.toolBar.toolbarTitle = currentTitle || undefined;
};
this.toDisposeOnCurrentTabChanged = new disposable_1.DisposableCollection();
}
/**
* Create the side bar and dock panel widgets.
*/
create(side, options) {
this.side = side;
this.options = options;
this.topMenu = this.createSidebarTopMenu();
this.tabBar = this.createSideBar();
this.bottomMenu = this.createSidebarBottomMenu();
this.toolBar = this.createToolbar();
this.dockPanel = this.createSidePanel();
this.container = this.createContainer();
this.refresh();
}
createSideBar() {
const side = this.side;
const tabBarRenderer = this.tabBarRendererFactory();
const sideBar = new tab_bars_1.SideTabBar({
// Tab bar options
orientation: side === 'left' || side === 'right' ? 'vertical' : 'horizontal',
insertBehavior: 'none',
removeBehavior: 'select-previous-tab',
allowDeselect: false,
tabsMovable: true,
renderer: tabBarRenderer,
// Scroll bar options
handlers: ['drag-thumb', 'keyboard', 'wheel', 'touch'],
useBothWheelAxes: true,
scrollYMarginOffset: 8,
suppressScrollX: true
});
tabBarRenderer.tabBar = sideBar;
sideBar.disposed.connect(() => tabBarRenderer.dispose());
tabBarRenderer.contextMenuPath = tab_bars_1.SHELL_TABBAR_CONTEXT_MENU;
sideBar.addClass('theia-app-' + side);
sideBar.addClass(exports.LEFT_RIGHT_AREA_CLASS);
sideBar.tabAdded.connect((sender, { title }) => {
const widget = title.owner;
if (!(0, algorithm_1.some)(this.dockPanel.widgets(), w => w === widget)) {
this.dockPanel.addWidget(widget);
}
}, this);
sideBar.tabActivateRequested.connect((sender, { title }) => title.owner.activate());
sideBar.tabCloseRequested.connect((sender, { title }) => title.owner.close());
sideBar.collapseRequested.connect(() => this.collapse(), this);
sideBar.currentChanged.connect(this.onCurrentTabChanged, this);
sideBar.tabDetachRequested.connect(this.onTabDetachRequested, this);
return sideBar;
}
createSidePanel() {
const sidePanel = this.dockPanelFactory({
mode: 'single-document'
});
sidePanel.id = 'theia-' + this.side + '-side-panel';
sidePanel.addClass('theia-side-panel');
sidePanel.widgetActivated.connect((sender, widget) => {
this.tabBar.currentTitle = widget.title;
}, this);
sidePanel.widgetAdded.connect(this.onWidgetAdded, this);
sidePanel.widgetRemoved.connect(this.onWidgetRemoved, this);
return sidePanel;
}
createToolbar() {
const toolbar = new side_panel_toolbar_1.SidePanelToolbar(this.tabBarToolBarRegistry, this.tabBarToolBarFactory, this.side);
toolbar.onContextMenu(e => this.showContextMenu(e));
return toolbar;
}
createSidebarTopMenu() {
return this.createSidebarMenu(this.sidebarTopWidgetFactory);
}
createSidebarBottomMenu() {
return this.createSidebarMenu(this.sidebarBottomWidgetFactory);
}
createSidebarMenu(factory) {
const menu = factory();
menu.addClass('theia-sidebar-menu');
return menu;
}
showContextMenu(e) {
const title = this.tabBar.currentTitle;
if (!title) {
return;
}
e.stopPropagation();
e.preventDefault();
this.contextMenuRenderer.render({
args: [title.owner],
menuPath: exports.SIDE_PANEL_TOOLBAR_CONTEXT_MENU,
anchor: e
});
}
createContainer() {
const contentBox = new widgets_1.BoxLayout({ direction: 'top-to-bottom', spacing: 0 });
widgets_1.BoxPanel.setStretch(this.toolBar, 0);
contentBox.addWidget(this.toolBar);
widgets_1.BoxPanel.setStretch(this.dockPanel, 1);
contentBox.addWidget(this.dockPanel);
const contentPanel = new widgets_1.BoxPanel({ layout: contentBox });
const side = this.side;
let direction;
switch (side) {
case 'left':
direction = 'left-to-right';
break;
case 'right':
direction = 'right-to-left';
break;
default:
throw new Error('Illegal argument: ' + side);
}
const containerLayout = new widgets_1.BoxLayout({ direction, spacing: 0 });
const sidebarContainerLayout = new widgets_1.PanelLayout();
const sidebarContainer = new widgets_1.Panel({ layout: sidebarContainerLayout });
sidebarContainer.addClass('theia-app-sidebar-container');
sidebarContainerLayout.addWidget(this.topMenu);
sidebarContainerLayout.addWidget(this.tabBar);
sidebarContainerLayout.addWidget(this.bottomMenu);
widgets_1.BoxPanel.setStretch(sidebarContainer, 0);
widgets_1.BoxPanel.setStretch(contentPanel, 1);
containerLayout.addWidget(sidebarContainer);
containerLayout.addWidget(contentPanel);
const boxPanel = new widgets_1.BoxPanel({ layout: containerLayout });
boxPanel.id = 'theia-' + side + '-content-panel';
return boxPanel;
}
/**
* Create an object that describes the current side panel layout. This object may contain references
* to widgets; these need to be transformed before the layout can be serialized.
*/
getLayoutData() {
const currentTitle = this.tabBar.currentTitle;
const items = (0, algorithm_1.toArray)((0, algorithm_1.map)(this.tabBar.titles, title => ({
widget: title.owner,
rank: SidePanelHandler_1.rankProperty.get(title.owner),
expanded: title === currentTitle,
pinned: title.className.includes(widgets_2.PINNED_CLASS)
})));
// eslint-disable-next-line no-null/no-null
const size = currentTitle !== null ? this.getPanelSize() : this.state.lastPanelSize;
return { type: 'sidepanel', items, size };
}
/**
* Apply a side panel layout that has been previously created with `getLayoutData`.
*/
setLayoutData(layoutData) {
// eslint-disable-next-line no-null/no-null
this.tabBar.currentTitle = null;
let currentTitle;
if (layoutData.items) {
for (const { widget, rank, expanded, pinned } of layoutData.items) {
if (widget) {
if (rank) {
SidePanelHandler_1.rankProperty.set(widget, rank);
}
if (expanded) {
currentTitle = widget.title;
}
if (pinned) {
widget.title.className += ` ${widgets_2.PINNED_CLASS}`;
widget.title.closable = false;
}
// Add the widgets directly to the tab bar in the same order as they are stored
this.tabBar.addTab(widget.title);
}
}
}
if (layoutData.size) {
this.state.lastPanelSize = layoutData.size;
}
// If the layout data contains an expanded item, update the currentTitle property
// This implies a refresh through the `currentChanged` signal
if (currentTitle) {
this.tabBar.currentTitle = currentTitle;
}
else {
this.refresh();
}
}
/**
* Activate a widget residing in the side panel by ID.
*
* @returns the activated widget if it was found
*/
activate(id) {
const widget = this.expand(id);
if (widget) {
widget.activate();
}
return widget;
}
/**
* Expand a widget residing in the side panel by ID. If no ID is given and the panel is
* currently collapsed, the last active tab of this side panel is expanded. If no tab
* was expanded previously, the first one is taken.
*
* @returns the expanded widget if it was found
*/
expand(id) {
if (id) {
const widget = (0, algorithm_1.find)(this.dockPanel.widgets(), w => w.id === id);
if (widget) {
this.tabBar.currentTitle = widget.title;
}
return widget;
}
else if (this.tabBar.currentTitle) {
return this.tabBar.currentTitle.owner;
}
else if (this.tabBar.titles.length > 0) {
let index = this.state.lastActiveTabIndex;
if (!index) {
index = 0;
}
else if (index >= this.tabBar.titles.length) {
index = this.tabBar.titles.length - 1;
}
const title = this.tabBar.titles[index];
this.tabBar.currentTitle = title;
return title.owner;
}
else {
// Reveal the tab bar and dock panel even if there is no widget
// The next call to `refreshVisibility` will collapse them again
this.state.expansion = SidePanel.ExpansionState.expanding;
let relativeSizes;
const parent = this.container.parent;
if (parent instanceof widgets_1.SplitPanel) {
relativeSizes = parent.relativeSizes();
}
this.container.removeClass(COLLAPSED_CLASS);
this.container.show();
this.tabBar.show();
this.dockPanel.node.style.minWidth = '0';
this.dockPanel.show();
if (relativeSizes && parent instanceof widgets_1.SplitPanel) {
// Make sure that the expansion animation starts at zero size
parent.setRelativeSizes(relativeSizes);
}
this.setPanelSize(this.options.emptySize).then(() => {
if (this.state.expansion === SidePanel.ExpansionState.expanding) {
this.state.expansion = SidePanel.ExpansionState.expanded;
}
});
}
}
/**
* Collapse the sidebar so no items are expanded.
*/
collapse() {
if (this.tabBar.currentTitle) {
// eslint-disable-next-line no-null/no-null
this.tabBar.currentTitle = null;
}
else {
this.refresh();
}
return (0, browser_1.animationFrame)();
}
/**
* Add a widget and its title to the dock panel and side bar.
*
* If the widget is already added, it will be moved.
*/
addWidget(widget, options) {
if (options.rank) {
SidePanelHandler_1.rankProperty.set(widget, options.rank);
}
this.dockPanel.addWidget(widget);
}
/**
* Add a menu to the sidebar top.
*
* If the menu is already added, it will be ignored.
*/
addTopMenu(menu) {
this.topMenu.addMenu(menu);
}
/**
* Remove a menu from the sidebar top.
*
* @param menuId id of the menu to remove
*/
removeTopMenu(menuId) {
this.topMenu.removeMenu(menuId);
}
/**
* Add a menu to the sidebar bottom.
*
* If the menu is already added, it will be ignored.
*/
addBottomMenu(menu) {
this.bottomMenu.addMenu(menu);
}
/**
* Remove a menu from the sidebar bottom.
*
* @param menuId id of the menu to remove
*/
removeBottomMenu(menuId) {
this.bottomMenu.removeMenu(menuId);
}
/**
* Refresh the visibility of the side bar and dock panel.
*/
refresh() {
const container = this.container;
const parent = container.parent;
const tabBar = this.tabBar;
const dockPanel = this.dockPanel;
const isEmpty = tabBar.titles.length === 0;
const currentTitle = tabBar.currentTitle;
// eslint-disable-next-line no-null/no-null
const hideDockPanel = currentTitle === null;
this.updateSashState(this.container, hideDockPanel);
let relativeSizes;
if (hideDockPanel) {
container.addClass(COLLAPSED_CLASS);
if (this.state.expansion === SidePanel.ExpansionState.expanded && !this.state.empty) {
// Update the lastPanelSize property
const size = this.getPanelSize();
if (size) {
this.state.lastPanelSize = size;
}
}
this.state.expansion = SidePanel.ExpansionState.collapsed;
}
else {
container.removeClass(COLLAPSED_CLASS);
let size;
if (this.state.expansion !== SidePanel.ExpansionState.expanded) {
if (this.state.lastPanelSize) {
size = this.state.lastPanelSize;
}
else {
size = this.getDefaultPanelSize();
}
}
if (size) {
// Restore the panel size to the last known size or the default size
this.state.expansion = SidePanel.ExpansionState.expanding;
if (parent instanceof widgets_1.SplitPanel) {
relativeSizes = parent.relativeSizes();
}
this.setPanelSize(size).then(() => {
if (this.state.expansion === SidePanel.ExpansionState.expanding) {
this.state.expansion = SidePanel.ExpansionState.expanded;
}
});
}
else {
this.state.expansion = SidePanel.ExpansionState.expanded;
}
}
container.setHidden(isEmpty && hideDockPanel);
tabBar.setHidden(isEmpty);
dockPanel.setHidden(hideDockPanel);
this.state.empty = isEmpty;
if (currentTitle) {
dockPanel.selectWidget(currentTitle.owner);
}
if (relativeSizes && parent instanceof widgets_1.SplitPanel) {
// Make sure that the expansion animation starts at the smallest possible size
parent.setRelativeSizes(relativeSizes);
}
}
/**
* Sets the size of the side panel.
*
* @param size the desired size (width) of the panel in pixels.
*/
resize(size) {
if (this.dockPanel.isHidden) {
this.state.lastPanelSize = size;
}
else {
this.setPanelSize(size);
}
}
/**
* Compute the current width of the panel. This implementation assumes that the parent of
* the panel container is a `SplitPanel`.
*/
getPanelSize() {
const parent = this.container.parent;
if (parent instanceof widgets_1.SplitPanel && parent.isVisible) {
const index = parent.widgets.indexOf(this.container);
if (this.side === 'left') {
const handle = parent.handles[index];
if (!handle.classList.contains('p-mod-hidden')) {
return handle.offsetLeft;
}
}
else if (this.side === 'right') {
const handle = parent.handles[index - 1];
if (!handle.classList.contains('p-mod-hidden')) {
const parentWidth = parent.node.clientWidth;
return parentWidth - handle.offsetLeft;
}
}
}
}
/**
* Determine the default size to apply when the panel is expanded for the first time.
*/
getDefaultPanelSize() {
const parent = this.container.parent;
if (parent && parent.isVisible) {
return parent.node.clientWidth * this.options.initialSizeRatio;
}
}
/**
* Modify the width of the panel. This implementation assumes that the parent of the panel
* container is a `SplitPanel`.
*/
setPanelSize(size) {
const enableAnimation = this.applicationStateService.state === 'ready';
const options = {
side: this.side,
duration: enableAnimation ? this.options.expandDuration : 0,
referenceWidget: this.dockPanel
};
const promise = this.splitPositionHandler.setSidePanelSize(this.container, 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.state.pendingUpdate = this.state.pendingUpdate.then(() => result);
return result;
}
/**
* Handle a `currentChanged` signal from the sidebar. The side panel is refreshed so it displays
* the new selected widget.
*/
onCurrentTabChanged(sender, { currentTitle, currentIndex }) {
this.toDisposeOnCurrentTabChanged.dispose();
if (currentTitle) {
this.updateToolbarTitle();
currentTitle.changed.connect(this.updateToolbarTitle);
this.toDisposeOnCurrentTabChanged.push(disposable_1.Disposable.create(() => currentTitle.changed.disconnect(this.updateToolbarTitle)));
}
if (currentIndex >= 0) {
this.state.lastActiveTabIndex = currentIndex;
sender.revealTab(currentIndex);
}
this.refresh();
}
/**
* Handle a `tabDetachRequested` signal from the sidebar. A drag is started so the widget can be
* moved to another application shell area.
*/
onTabDetachRequested(sender, { title, tab, clientX, clientY }) {
// Release the tab bar's hold on the mouse
sender.releaseMouse();
// Clone the selected tab and use that as drag image
const clonedTab = tab.cloneNode(true);
clonedTab.style.width = '';
clonedTab.style.height = '';
const label = clonedTab.getElementsByClassName('p-TabBar-tabLabel')[0];
label.style.width = '';
label.style.height = '';
// Create and start a drag to move the selected tab to another panel
const mimeData = new coreutils_1.MimeData();
mimeData.setData('application/vnd.phosphor.widget-factory', () => title.owner);
const drag = new dragdrop_1.Drag({
mimeData,
dragImage: clonedTab,
proposedAction: 'move',
supportedActions: 'move',
});
tab.classList.add('p-mod-hidden');
drag.start(clientX, clientY).then(() => {
// The promise is resolved when the drag has ended
tab.classList.remove('p-mod-hidden');
});
}
/*
* Handle the `widgetAdded` signal from the dock panel. The widget's title is inserted into the
* tab bar according to the `rankProperty` value that may be attached to the widget.
*/
onWidgetAdded(sender, widget) {
const titles = this.tabBar.titles;
if (!(0, algorithm_1.find)(titles, t => t.owner === widget)) {
const rank = SidePanelHandler_1.rankProperty.get(widget);
let index = titles.length;
if (rank !== undefined) {
for (let i = index - 1; i >= 0; i--) {
const r = SidePanelHandler_1.rankProperty.get(titles[i].owner);
if (r !== undefined && r > rank) {
index = i;
}
}
}
this.tabBar.insertTab(index, widget.title);
this.refresh();
}
}
/*
* Handle the `widgetRemoved` signal from the dock panel. The widget's title is also removed
* from the tab bar.
*/
onWidgetRemoved(sender, widget) {
this.tabBar.removeTab(widget.title);
this.refresh();
}
updateSashState(sidePanelElement, sidePanelCollapsed) {
if (sidePanelElement) {
// Hide the sash when the left/right side panel is collapsed
if (sidePanelElement.id === 'theia-left-content-panel' && sidePanelElement.node.nextElementSibling) {
sidePanelElement.node.nextElementSibling.classList.toggle('sash-hidden', sidePanelCollapsed);
}
else if (sidePanelElement.id === 'theia-right-content-panel' && sidePanelElement.node.previousElementSibling) {
sidePanelElement.node.previousElementSibling.classList.toggle('sash-hidden', sidePanelCollapsed);
}
}
}
};
/**
* A property that can be attached to widgets in order to determine the insertion index
* of their title in the tab bar.
*/
SidePanelHandler.rankProperty = new properties_1.AttachedProperty({
name: 'sidePanelRank',
create: () => undefined
});
__decorate([
(0, inversify_1.inject)(tab_bar_toolbar_1.TabBarToolbarRegistry),
__metadata("design:type", tab_bar_toolbar_1.TabBarToolbarRegistry)
], SidePanelHandler.prototype, "tabBarToolBarRegistry", void 0);
__decorate([
(0, inversify_1.inject)(tab_bar_toolbar_1.TabBarToolbarFactory),
__metadata("design:type", Function)
], SidePanelHandler.prototype, "tabBarToolBarFactory", void 0);
__decorate([
(0, inversify_1.inject)(tab_bars_1.TabBarRendererFactory),
__metadata("design:type", Function)
], SidePanelHandler.prototype, "tabBarRendererFactory", void 0);
__decorate([
(0, inversify_1.inject)(sidebar_menu_widget_1.SidebarTopMenuWidgetFactory),
__metadata("design:type", Function)
], SidePanelHandler.prototype, "sidebarTopWidgetFactory", void 0);
__decorate([
(0, inversify_1.inject)(sidebar_menu_widget_1.SidebarBottomMenuWidgetFactory),
__metadata("design:type", Function)
], SidePanelHandler.prototype, "sidebarBottomWidgetFactory", void 0);
__decorate([
(0, inversify_1.inject)(split_panels_1.SplitPositionHandler),
__metadata("design:type", split_panels_1.SplitPositionHandler)
], SidePanelHandler.prototype, "splitPositionHandler", void 0);
__decorate([
(0, inversify_1.inject)(frontend_application_state_1.FrontendApplicationStateService),
__metadata("design:type", frontend_application_state_1.FrontendApplicationStateService)
], SidePanelHandler.prototype, "applicationStateService", void 0);
__decorate([
(0, inversify_1.inject)(theia_dock_panel_1.TheiaDockPanel.Factory),
__metadata("design:type", Function)
], SidePanelHandler.prototype, "dockPanelFactory", void 0);
__decorate([
(0, inversify_1.inject)(context_menu_renderer_1.ContextMenuRenderer),
__metadata("design:type", context_menu_renderer_1.ContextMenuRenderer)
], SidePanelHandler.prototype, "contextMenuRenderer", void 0);
SidePanelHandler = SidePanelHandler_1 = __decorate([
(0, inversify_1.injectable)()
], SidePanelHandler);
exports.SidePanelHandler = SidePanelHandler;
var SidePanel;
(function (SidePanel) {
let ExpansionState;
(function (ExpansionState) {
ExpansionState["collapsed"] = "collapsed";
ExpansionState["expanding"] = "expanding";
ExpansionState["expanded"] = "expanded";
ExpansionState["collapsing"] = "collapsing";
})(ExpansionState = SidePanel.ExpansionState || (SidePanel.ExpansionState = {}));
})(SidePanel = exports.SidePanel || (exports.SidePanel = {}));
//# sourceMappingURL=side-panel-handler.js.map