UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

647 lines • 27.1 kB
"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