UNPKG

@theia/core

Version:

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

1,009 lines • 63.2 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-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.SideTabBar = exports.ToolbarAwareTabBar = exports.ScrollableTabBar = exports.TabBarRenderer = exports.TabBarRendererFactory = exports.SHELL_TABBAR_CONTEXT_SPLIT = exports.SHELL_TABBAR_CONTEXT_PIN = exports.SHELL_TABBAR_CONTEXT_COPY = exports.SHELL_TABBAR_CONTEXT_CLOSE = exports.SHELL_TABBAR_CONTEXT_MENU = void 0; const perfect_scrollbar_1 = require("perfect-scrollbar"); const widgets_1 = require("@lumino/widgets"); const virtualdom_1 = require("@lumino/virtualdom"); const common_1 = require("../../common"); const signaling_1 = require("@lumino/signaling"); const algorithm_1 = require("@lumino/algorithm"); const domutils_1 = require("@lumino/domutils"); const theia_dock_panel_1 = require("./theia-dock-panel"); const widget_decoration_1 = require("../widget-decoration"); const navigatable_types_1 = require("../navigatable-types"); const widget_1 = require("../widgets/widget"); const client_1 = require("react-dom/client"); const select_component_1 = require("../widgets/select-component"); const react_1 = require("react"); const previewable_widget_1 = require("../widgets/previewable-widget"); const enhanced_preview_widget_1 = require("../widgets/enhanced-preview-widget"); const browser_1 = require("../browser"); /** The class name added to hidden content nodes, which are required to render vertical side bars. */ const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content'; /** Menu path for tab bars used throughout the application shell. */ exports.SHELL_TABBAR_CONTEXT_MENU = ['shell-tabbar-context-menu']; exports.SHELL_TABBAR_CONTEXT_CLOSE = [...exports.SHELL_TABBAR_CONTEXT_MENU, '0_close']; exports.SHELL_TABBAR_CONTEXT_COPY = [...exports.SHELL_TABBAR_CONTEXT_MENU, '1_copy']; // Kept here in anticipation of tab pinning behavior implemented in tab-bars.ts exports.SHELL_TABBAR_CONTEXT_PIN = [...exports.SHELL_TABBAR_CONTEXT_MENU, '4_pin']; exports.SHELL_TABBAR_CONTEXT_SPLIT = [...exports.SHELL_TABBAR_CONTEXT_MENU, '5_split']; exports.TabBarRendererFactory = Symbol('TabBarRendererFactory'); /** * A tab bar renderer that offers a context menu. In addition, this renderer is able to * set an explicit position and size on the icon and label of each tab in a side bar. * This is necessary because the elements of side bar tabs are rotated using the CSS * `transform` property, disrupting the browser's ability to arrange those elements * automatically. */ class TabBarRenderer extends widgets_1.TabBar.Renderer { // TODO refactor shell, rendered should only receive props with event handlers // events should be handled by clients, like ApplicationShell // right now it is mess: (1) client logic belong to renderer, (2) cyclic dependencies between renderers and clients constructor(contextMenuRenderer, decoratorService, iconThemeService, selectionService, commandService, corePreferences, hoverService, contextKeyService) { super(); this.contextMenuRenderer = contextMenuRenderer; this.decoratorService = decoratorService; this.iconThemeService = iconThemeService; this.selectionService = selectionService; this.commandService = commandService; this.corePreferences = corePreferences; this.hoverService = hoverService; this.contextKeyService = contextKeyService; this.toDispose = new common_1.DisposableCollection(); this.toDisposeOnTabBar = new common_1.DisposableCollection(); this.decorations = new Map(); this.renderEnhancedPreview = (title) => { const hoverBox = document.createElement('div'); hoverBox.classList.add('theia-horizontal-tabBar-hover-div'); const labelElement = document.createElement('p'); labelElement.classList.add('theia-horizontal-tabBar-hover-title'); labelElement.textContent = title.label; hoverBox.append(labelElement); const widget = title.owner; if (enhanced_preview_widget_1.EnhancedPreviewWidget.is(widget)) { const enhancedPreviewNode = widget.getEnhancedPreviewNode(); if (enhancedPreviewNode) { hoverBox.appendChild(enhancedPreviewNode); } } else if (title.caption) { const captionElement = document.createElement('p'); captionElement.classList.add('theia-horizontal-tabBar-hover-caption'); captionElement.textContent = title.caption; hoverBox.appendChild(captionElement); } return hoverBox; }; this.handleMouseEnterEvent = (event) => { var _a; if (this.tabBar && this.hoverService && event.currentTarget instanceof HTMLElement) { const id = event.currentTarget.id; const title = this.tabBar.titles.find(t => this.createTabId(t) === id); if (title) { if (this.tabBar.orientation === 'horizontal') { this.hoverService.requestHover({ content: this.renderEnhancedPreview(title), target: event.currentTarget, position: 'bottom', cssClasses: ['extended-tab-preview'], visualPreview: ((_a = this.corePreferences) === null || _a === void 0 ? void 0 : _a['window.tabbar.enhancedPreview']) === 'visual' ? width => this.renderVisualPreview(width, title) : undefined }); } else if (title.caption) { this.hoverService.requestHover({ content: title.caption, target: event.currentTarget, position: 'right' }); } } } }; this.handleContextMenuEvent = (event) => { var _a, _b; if (this.contextMenuRenderer && this.contextMenuPath && event.currentTarget instanceof HTMLElement) { event.stopPropagation(); event.preventDefault(); let widget = undefined; if (this.tabBar) { const titleIndex = Array.from(this.tabBar.contentNode.getElementsByClassName('lm-TabBar-tab')) .findIndex(node => node.contains(event.currentTarget)); if (titleIndex !== -1) { widget = this.tabBar.titles[titleIndex].owner; } } const oldSelection = (_a = this.selectionService) === null || _a === void 0 ? void 0 : _a.selection; if (widget && this.selectionService) { this.selectionService.selection = navigatable_types_1.NavigatableWidget.is(widget) ? { uri: widget.getResourceUri() } : widget; } const contextKeyServiceOverlay = (_b = this.contextKeyService) === null || _b === void 0 ? void 0 : _b.createOverlay([['isTerminalTab', widget && 'terminalId' in widget]]); this.contextMenuRenderer.render({ menuPath: this.contextMenuPath, anchor: event, args: [event], context: event.currentTarget, contextKeyService: contextKeyServiceOverlay, // We'd like to wait until the command triggered by the context menu has been run, but this should let it get through the preamble, at least. onHide: () => setTimeout(() => { if (this.selectionService) { this.selectionService.selection = oldSelection; } }) }); } }; this.handleCloseClickEvent = (event) => { if (this.tabBar && event.currentTarget instanceof HTMLElement) { const id = event.currentTarget.parentElement.id; const title = this.tabBar.titles.find(t => this.createTabId(t) === id); if ((title === null || title === void 0 ? void 0 : title.closable) === false && (title === null || title === void 0 ? void 0 : title.className.includes(widget_1.PINNED_CLASS)) && this.commandService) { this.commandService.executeCommand('workbench.action.unpinEditor', event); } } }; this.handleDblClickEvent = (event) => { var _a; if (!((_a = this.corePreferences) === null || _a === void 0 ? void 0 : _a.get('workbench.tab.maximize'))) { return; } if (this.tabBar && event.currentTarget instanceof HTMLElement) { const id = event.currentTarget.id; const title = this.tabBar.titles.find(t => this.createTabId(t) === id); const area = title === null || title === void 0 ? void 0 : title.owner.parent; if (area instanceof theia_dock_panel_1.TheiaDockPanel && (area.id === theia_dock_panel_1.BOTTOM_AREA_ID || area.id === theia_dock_panel_1.MAIN_AREA_ID)) { area.toggleMaximized(); } } }; if (this.decoratorService) { this.toDispose.push(common_1.Disposable.create(() => this.resetDecorations())); this.toDispose.push(this.decoratorService.onDidChangeDecorations(() => this.resetDecorations())); } if (this.iconThemeService) { this.toDispose.push(this.iconThemeService.onDidChangeCurrent(() => { if (this._tabBar) { this._tabBar.update(); } })); } } dispose() { this.toDispose.dispose(); } /** * A reference to the tab bar is required in order to activate it when a context menu * is requested. */ set tabBar(tabBar) { if (this.toDispose.disposed) { throw new Error('disposed'); } if (this._tabBar === tabBar) { return; } this.toDisposeOnTabBar.dispose(); this.toDispose.push(this.toDisposeOnTabBar); this._tabBar = tabBar; if (tabBar) { const listener = (_, { title }) => this.resetDecorations(title); tabBar.tabCloseRequested.connect(listener); this.toDisposeOnTabBar.push(common_1.Disposable.create(() => tabBar.tabCloseRequested.disconnect(listener))); } this.resetDecorations(); } get tabBar() { return this._tabBar; } /** * Render tabs with the default DOM structure, but additionally register a context menu listener. * @param {SideBarRenderData} data Data used to render the tab. * @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel. * @param {boolean} isPartOfHiddenTabBar An optional check which determines if the tab is in the hidden horizontal tab bar. * @returns {VirtualElement} The virtual element of the rendered tab. */ renderTab(data, isInSidePanel, isPartOfHiddenTabBar) { var _a; const title = data.title; const id = this.createTabId(title, isPartOfHiddenTabBar); const key = this.createTabKey(data); const style = this.createTabStyle(data); const className = this.createTabClass(data); const dataset = this.createTabDataset(data); const closeIconTitle = data.title.className.includes(widget_1.PINNED_CLASS) ? common_1.nls.localizeByDefault('Unpin') : common_1.nls.localizeByDefault('Close'); const hover = this.tabBar && (this.tabBar.orientation === 'horizontal' && ((_a = this.corePreferences) === null || _a === void 0 ? void 0 : _a['window.tabbar.enhancedPreview']) === 'classic') ? { title: title.caption } : { onmouseenter: this.handleMouseEnterEvent }; return virtualdom_1.h.li({ ...hover, key, className, id, style, dataset, oncontextmenu: this.handleContextMenuEvent, ondblclick: this.handleDblClickEvent, onauxclick: (e) => { // If user closes the tab using mouse wheel, nothing should be pasted to an active editor e.preventDefault(); } }, virtualdom_1.h.div({ className: 'theia-tab-icon-label' }, this.renderIcon(data, isInSidePanel), this.renderLabel(data, isInSidePanel), this.renderTailDecorations(data, isInSidePanel), this.renderBadge(data, isInSidePanel), this.renderLock(data, isInSidePanel)), virtualdom_1.h.div({ className: 'lm-TabBar-tabCloseIcon action-label', title: closeIconTitle, onclick: this.handleCloseClickEvent })); } createTabClass(data) { var _a; let tabClass = super.createTabClass(data); if (!((_a = data.visible) !== null && _a !== void 0 ? _a : true)) { tabClass += ' lm-mod-invisible'; } return tabClass; } /** * Generate ID for an entry in the tab bar * @param {Title<Widget>} title Title of the widget controlled by this tab bar * @param {boolean} isPartOfHiddenTabBar Tells us if this entry is part of the hidden horizontal tab bar. * If yes, add a suffix to differentiate it's ID from the entry in the visible tab bar * @returns {string} DOM element ID */ createTabId(title, isPartOfHiddenTabBar = false) { return 'shell-tab-' + title.owner.id + (isPartOfHiddenTabBar ? '-hidden' : ''); } /** * If size information is available for the label and icon, set an explicit height on the tab. * The height value also considers padding, which should be derived from CSS settings. */ createTabStyle(data) { const zIndex = `${data.zIndex}`; const labelSize = data.labelSize; const iconSize = data.iconSize; let height; let width; if (labelSize || iconSize) { const labelHeight = labelSize ? (this.tabBar && this.tabBar.orientation === 'horizontal' ? labelSize.height : labelSize.width) : 0; const iconHeight = iconSize ? iconSize.height : 0; let paddingTop = data.paddingTop || 0; if (labelHeight > 0 && iconHeight > 0) { // Leave some extra space between icon and label paddingTop = paddingTop * 1.5; } const paddingBottom = data.paddingBottom || 0; height = `${labelHeight + iconHeight + paddingTop + paddingBottom}px`; } if (data.tabWidth) { width = `${data.tabWidth}px`; } else { width = ''; } return { zIndex, height, minWidth: width, maxWidth: width }; } /** * If size information is available for the label, set it as inline style. * Tab padding and icon size are also considered in the `top` position. * @param {SideBarRenderData} data Data used to render the tab. * @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel. * @returns {VirtualElement} The virtual element of the rendered label. */ renderLabel(data, isInSidePanel) { const labelSize = data.labelSize; const iconSize = data.iconSize; let width; let height; let top; if (labelSize) { width = `${labelSize.width}px`; height = `${labelSize.height}px`; } if (data.paddingTop || iconSize) { const iconHeight = iconSize ? iconSize.height : 0; let paddingTop = data.paddingTop || 0; if (iconHeight > 0) { // Leave some extra space between icon and label paddingTop = paddingTop * 1.5; } top = `${paddingTop + iconHeight}px`; } const style = { width, height, top }; // No need to check for duplicate labels if the tab is rendered in the side panel (title is not displayed), // or if there are less than two files in the tab bar. if (isInSidePanel || (this.tabBar && this.tabBar.titles.length < 2)) { return virtualdom_1.h.div({ className: 'lm-TabBar-tabLabel', style }, data.title.label); } const originalToDisplayedMap = this.findDuplicateLabels([...this.tabBar.titles]); const labelDetails = originalToDisplayedMap.get(data.title.caption); if (labelDetails) { return virtualdom_1.h.div({ className: 'lm-TabBar-tabLabelWrapper' }, virtualdom_1.h.div({ className: 'lm-TabBar-tabLabel', style }, data.title.label), virtualdom_1.h.div({ className: 'lm-TabBar-tabLabelDetails', style }, labelDetails)); } return virtualdom_1.h.div({ className: 'lm-TabBar-tabLabel', style }, data.title.label); } renderTailDecorations(renderData, isInSidePanel) { var _a; if (!((_a = this.corePreferences) === null || _a === void 0 ? void 0 : _a.get('workbench.editor.decorations.badges'))) { return []; } const tailDecorations = common_1.ArrayUtils.coalesce(this.getDecorationData(renderData.title, 'tailDecorations')).flat(); if (tailDecorations === undefined || tailDecorations.length === 0) { return []; } let dotDecoration; const otherDecorations = []; tailDecorations.reverse().forEach(decoration => { const partial = decoration; if (widget_decoration_1.WidgetDecoration.TailDecoration.isDotDecoration(partial)) { dotDecoration || (dotDecoration = partial); } else if (partial.data || partial.icon || partial.iconClass) { otherDecorations.push(partial); } }); const decorationsToRender = dotDecoration ? [dotDecoration, ...otherDecorations] : otherDecorations; return decorationsToRender.map((decoration, index) => { const { tooltip, data, fontData, color, icon, iconClass } = decoration; const iconToRender = icon !== null && icon !== void 0 ? icon : iconClass; const className = ['lm-TabBar-tail', 'flex'].join(' '); const style = fontData ? fontData : color ? { color } : undefined; const content = (data ? data : iconToRender ? virtualdom_1.h.span({ className: this.getIconClass(iconToRender, iconToRender === 'circle' ? [widget_decoration_1.WidgetDecoration.Styles.DECORATOR_SIZE_CLASS] : []) }) : '') + (index !== decorationsToRender.length - 1 ? ',' : ''); return virtualdom_1.h.span({ key: ('tailDecoration_' + index), className, style, title: tooltip !== null && tooltip !== void 0 ? tooltip : content }, content); }); } renderBadge(data, isInSidePanel) { const totalBadge = this.getDecorationData(data.title, 'badge').reduce((sum, badge) => sum + badge, 0); if (!totalBadge) { return virtualdom_1.h.div({}); } const limitedBadge = totalBadge >= 100 ? '99+' : totalBadge; return isInSidePanel ? virtualdom_1.h.div({ className: 'theia-badge-decorator-sidebar' }, `${limitedBadge}`) : virtualdom_1.h.div({ className: 'theia-badge-decorator-horizontal' }, `${limitedBadge}`); } renderLock(data, isInSidePanel) { return !isInSidePanel && data.title.className.includes(widget_1.LOCKED_CLASS) ? virtualdom_1.h.div({ className: 'lm-TabBar-tabLock' }) : virtualdom_1.h.div({}); } resetDecorations(title) { if (title) { this.decorations.delete(title); } else { this.decorations.clear(); } if (this.tabBar) { this.tabBar.update(); } } /** * Get all available decorations of a given tab. * @param {string} title The widget title. */ getDecorations(title) { if (this.tabBar && this.decoratorService) { const owner = title.owner; if (!owner.resetTabBarDecorations) { owner.resetTabBarDecorations = () => this.decorations.delete(title); title.owner.disposed.connect(owner.resetTabBarDecorations); } const decorations = this.decorations.get(title) || this.decoratorService.getDecorations(title); this.decorations.set(title, decorations); return decorations; } return []; } /** * Get the decoration data given the tab URI and the decoration data type. * @param {string} title The title. * @param {K} key The type of the decoration data. */ getDecorationData(title, key) { return this.getDecorations(title).filter(data => data[key] !== undefined).map(data => data[key]); } /** * Get the class of an icon. * @param {string | string[]} iconName The name of the icon. * @param {string[]} additionalClasses Additional classes of the icon. */ getIconClass(iconName, additionalClasses = []) { const iconClass = (typeof iconName === 'string') ? ['a', 'fa', `fa-${iconName}`] : ['a'].concat(iconName); return iconClass.concat(additionalClasses).join(' '); } /** * Find duplicate labels from the currently opened tabs in the tab bar. * Return the appropriate partial paths that can distinguish the identical labels. * * E.g., a/p/index.ts => a/..., b/p/index.ts => b/... * * To prevent excessively long path displayed, show at maximum three levels from the end by default. * @param {Title<Widget>[]} titles Array of titles in the current tab bar. * @returns {Map<string, string>} A map from each tab's original path to its displayed partial path. */ findDuplicateLabels(titles) { // Filter from all tabs to group them by the distinct label (file name). // E.g., 'foo.js' => {0 (index) => 'a/b/foo.js', '2 => a/c/foo.js' }, // 'bar.js' => {1 => 'a/d/bar.js', ...} const labelGroups = new Map(); titles.forEach((title, index) => { if (!labelGroups.has(title.label)) { labelGroups.set(title.label, new Map()); } labelGroups.get(title.label).set(index, title.caption); }); const originalToDisplayedMap = new Map(); // Parse each group of editors with the same label. labelGroups.forEach(labelGroup => { // Filter to get groups that have duplicates. if (labelGroup.size > 1) { const paths = []; let maxPathLength = 0; labelGroup.forEach((pathStr, index) => { const steps = pathStr.split('/'); maxPathLength = Math.max(maxPathLength, steps.length); paths[index] = (steps.slice(0, steps.length - 1)); // By default, show at maximum three levels from the end. let defaultDisplayedPath = steps.slice(-4, -1).join('/'); if (steps.length > 4) { defaultDisplayedPath = '.../' + defaultDisplayedPath; } originalToDisplayedMap.set(pathStr, defaultDisplayedPath); }); // Iterate through the steps of the path from the left to find the step that can distinguish it. // E.g., ['root', 'foo', 'c'], ['root', 'bar', 'd'] => 'foo', 'bar' let i = 0; while (i < maxPathLength - 1) { // Store indexes of all paths that have the identical element in each step. const stepOccurrences = new Map(); // Compare the current step of all paths paths.forEach((path, index) => { const step = path[i]; if (path.length > 0) { if (i > path.length - 1) { paths[index] = []; } else if (!stepOccurrences.has(step)) { stepOccurrences.set(step, [index]); } else { stepOccurrences.get(step).push(index); } } }); // Set the displayed path for each tab. stepOccurrences.forEach((indexArr, displayedPath) => { if (indexArr.length === 1) { const originalPath = labelGroup.get(indexArr[0]); if (originalPath) { const originalElements = originalPath.split('/'); const displayedElements = displayedPath.split('/'); if (originalElements.slice(-2)[0] !== displayedElements.slice(-1)[0]) { displayedPath += '/...'; } if (originalElements[0] !== displayedElements[0]) { displayedPath = '.../' + displayedPath; } originalToDisplayedMap.set(originalPath, displayedPath); paths[indexArr[0]] = []; } } }); i++; } } }); return originalToDisplayedMap; } /** * If size information is available for the icon, set it as inline style. Tab padding * is also considered in the `top` position. * @param {SideBarRenderData} data Data used to render the tab icon. * @param {boolean} isInSidePanel An optional check which determines if the tab is in the side-panel. */ renderIcon(data, isInSidePanel) { if (!isInSidePanel && this.iconThemeService && this.iconThemeService.current === 'none') { return virtualdom_1.h.div(); } let top; if (data.paddingTop) { top = `${data.paddingTop || 0}px`; } const style = { top }; const baseClassName = this.createIconClass(data); const overlayIcons = []; const decorationData = this.getDecorationData(data.title, 'iconOverlay'); // Check if the tab has decoration markers to be rendered on top. if (decorationData.length > 0) { const baseIcon = virtualdom_1.h.div({ className: baseClassName, style }, data.title.iconLabel); const wrapperClassName = widget_decoration_1.WidgetDecoration.Styles.ICON_WRAPPER_CLASS; const decoratorSizeClassName = isInSidePanel ? widget_decoration_1.WidgetDecoration.Styles.DECORATOR_SIDEBAR_SIZE_CLASS : widget_decoration_1.WidgetDecoration.Styles.DECORATOR_SIZE_CLASS; decorationData .filter(common_1.notEmpty) .map(overlay => [overlay.position, overlay]) .forEach(([position, overlay]) => { const iconAdditionalClasses = [decoratorSizeClassName, widget_decoration_1.WidgetDecoration.IconOverlayPosition.getStyle(position, isInSidePanel)]; const overlayIconStyle = (color) => { if (color === undefined) { return {}; } return { color }; }; // Parse the optional background (if it exists) of the overlay icon. if (overlay.background) { const backgroundIconClassName = this.getIconClass(overlay.background.shape, iconAdditionalClasses); overlayIcons.push(virtualdom_1.h.div({ key: data.title.label + '-background', className: backgroundIconClassName, style: overlayIconStyle(overlay.background.color) })); } // Parse the overlay icon. const overlayIcon = overlay.icon || overlay.iconClass; const overlayIconClassName = this.getIconClass(overlayIcon, iconAdditionalClasses); overlayIcons.push(virtualdom_1.h.span({ key: data.title.label, className: overlayIconClassName, style: overlayIconStyle(overlay.color) })); }); return virtualdom_1.h.div({ className: wrapperClassName, style }, [baseIcon, ...overlayIcons]); } return virtualdom_1.h.div({ className: baseClassName, style }, data.title.iconLabel); } renderVisualPreview(desiredWidth, title) { var _a; const widget = title.owner; // Check that the widget is not currently shown, is a PreviewableWidget and it was already loaded before if (this.tabBar && this.tabBar.currentTitle !== title && previewable_widget_1.PreviewableWidget.isPreviewable(widget)) { const html = document.getElementById(widget.id); if (html) { const previewNode = widget.getPreviewNode(); if (previewNode) { const clonedNode = previewNode.cloneNode(true); const visualPreviewDiv = document.createElement('div'); visualPreviewDiv.classList.add('enhanced-preview-container'); // Add the clonedNode and get it from the children to have a HTMLElement instead of a Node visualPreviewDiv.append(clonedNode); const visualPreview = visualPreviewDiv.children.item(visualPreviewDiv.children.length - 1); if (visualPreview instanceof HTMLElement) { visualPreview.classList.remove('lm-mod-hidden'); visualPreview.classList.add('enhanced-preview'); visualPreview.id = `preview:${widget.id}`; // Use the current visible editor as a fallback if not available const height = visualPreview.style.height === '' ? this.tabBar.currentTitle.owner.node.offsetHeight : parseFloat(visualPreview.style.height); const width = visualPreview.style.width === '' ? this.tabBar.currentTitle.owner.node.offsetWidth : parseFloat(visualPreview.style.width); const desiredRatio = 9 / 16; const desiredHeight = desiredWidth * desiredRatio; const ratio = height / width; visualPreviewDiv.style.width = `${desiredWidth}px`; visualPreviewDiv.style.height = `${desiredHeight}px`; // If the view is wider than the desiredRatio scale the width and crop the height. If the view is longer its the other way around. const scale = ratio < desiredRatio ? (desiredHeight / height) : (desiredWidth / width); visualPreview.style.transform = `scale(${scale},${scale})`; visualPreview.style.removeProperty('top'); visualPreview.style.removeProperty('left'); // Copy canvases (They are cloned empty) const originalCanvases = html.getElementsByTagName('canvas'); const previewCanvases = visualPreview.getElementsByTagName('canvas'); // If this is not given, something went wrong during the cloning if (originalCanvases.length === previewCanvases.length) { for (let i = 0; i < originalCanvases.length; i++) { (_a = previewCanvases[i].getContext('2d')) === null || _a === void 0 ? void 0 : _a.drawImage(originalCanvases[i], 0, 0); } } return visualPreviewDiv; } } } } return undefined; } } exports.TabBarRenderer = TabBarRenderer; /** * A specialized tab bar for the main and bottom areas. */ class ScrollableTabBar extends widgets_1.TabBar { constructor(options, scrollbarOptions, dynamicTabOptions) { super(options); this.scrollbarOptions = scrollbarOptions; this.isMouseOver = false; this.needsRecompute = false; this.tabSize = 0; this.toDispose = new common_1.DisposableCollection(); this._dynamicTabOptions = dynamicTabOptions; this.topRow = document.createElement('div'); this.topRow.classList.add('theia-tabBar-tab-row'); this.node.appendChild(this.topRow); const contentNode = this.contentNode; if (!contentNode) { throw new Error('tab bar does not have the content node.'); } this.node.removeChild(contentNode); this.contentContainer = document.createElement('div'); this.contentContainer.classList.add(ScrollableTabBar.Styles.TAB_BAR_CONTENT_CONTAINER); this.contentContainer.appendChild(contentNode); this.topRow.appendChild(this.contentContainer); this.openTabsContainer = document.createElement('div'); this.openTabsContainer.classList.add('theia-tabBar-open-tabs'); this.openTabsRoot = (0, client_1.createRoot)(this.openTabsContainer); this.topRow.appendChild(this.openTabsContainer); } set dynamicTabOptions(options) { this._dynamicTabOptions = options; this.updateTabs(); } get dynamicTabOptions() { return this._dynamicTabOptions; } dispose() { if (this.isDisposed) { return; } super.dispose(); this.toDispose.dispose(); } onBeforeAttach(msg) { this.contentNode.addEventListener('pointerdown', this); this.contentNode.addEventListener('dblclick', this); this.contentNode.addEventListener('keydown', this); } onAfterDetach(msg) { this.contentNode.removeEventListener('pointerdown', this); this.contentNode.removeEventListener('dblclick', this); this.contentNode.removeEventListener('keydown', this); this.doReleaseMouse(); } doReleaseMouse() { this._releaseMouse(); } onAfterAttach(msg) { this.node.addEventListener('mouseenter', () => { this.isMouseOver = true; }); this.node.addEventListener('mouseleave', () => { this.isMouseOver = false; if (this.needsRecompute) { this.updateTabs(); } }); super.onAfterAttach(msg); this.scrollBar = new perfect_scrollbar_1.default(this.contentContainer, this.scrollbarOptions); } onBeforeDetach(msg) { var _a; super.onBeforeDetach(msg); (_a = this.scrollBar) === null || _a === void 0 ? void 0 : _a.destroy(); } onUpdateRequest(msg) { this.updateTabs(); } updateTabs() { const content = []; if (this.dynamicTabOptions) { this.openTabsRoot.render((0, react_1.createElement)(select_component_1.SelectComponent, { options: this.titles, onChange: (option, index) => { this.currentIndex = index; }, alignment: 'right' })); if (this.isMouseOver) { this.needsRecompute = true; } else { this.needsRecompute = false; if (this.orientation === 'horizontal') { let availableWidth = this.contentNode.clientWidth; let effectiveWidth = availableWidth; if (!this.openTabsContainer.classList.contains('lm-mod-hidden')) { availableWidth += this.openTabsContainer.getBoundingClientRect().width; } if (this.dynamicTabOptions.minimumTabSize * this.titles.length <= availableWidth) { effectiveWidth += this.openTabsContainer.getBoundingClientRect().width; this.openTabsContainer.classList.add('lm-mod-hidden'); } else { this.openTabsContainer.classList.remove('lm-mod-hidden'); } this.tabSize = Math.max(Math.min(effectiveWidth / this.titles.length, this.dynamicTabOptions.defaultTabSize), this.dynamicTabOptions.minimumTabSize); } } this.node.classList.add('dynamic-tabs'); } else { this.openTabsContainer.classList.add('lm-mod-hidden'); this.node.classList.remove('dynamic-tabs'); } for (let i = 0, n = this.titles.length; i < n; ++i) { const title = this.titles[i]; const current = title === this.currentTitle; const zIndex = current ? n : n - i - 1; const renderData = { title: title, current: current, zIndex: zIndex }; if (this.dynamicTabOptions && this.orientation === 'horizontal') { renderData.tabWidth = this.tabSize; } content[i] = this.renderer.renderTab(renderData); } virtualdom_1.VirtualDOM.render(content, this.contentNode); if (this.scrollBar) { if (!(this.dynamicTabOptions && this.isMouseOver)) { this.scrollBar.update(); } } } onResize(msg) { super.onResize(msg); if (this.dynamicTabOptions) { this.updateTabs(); } if (this.scrollBar) { if (this.currentIndex >= 0) { this.revealTab(this.currentIndex); } this.scrollBar.update(); } } /** * Reveal the tab with the given index by moving the scroll bar if necessary. */ revealTab(index) { if (this.pendingReveal) { // A reveal has already been scheduled return this.pendingReveal; } const result = new Promise((resolve, reject) => { // The tab might not have been created yet, so wait until the next frame window.requestAnimationFrame(() => { const tab = this.contentNode.children[index]; if (tab && this.isVisible) { const parent = this.contentNode; if (this.orientation === 'horizontal') { const scroll = parent.scrollLeft; const left = tab.offsetLeft; if (scroll > left) { parent.scrollLeft = left; } else { const right = left + tab.clientWidth - parent.clientWidth; if (scroll < right && tab.clientWidth < parent.clientWidth) { parent.scrollLeft = right; } } } else { const scroll = parent.scrollTop; const top = tab.offsetTop; if (scroll > top) { parent.scrollTop = top; } else { const bottom = top + tab.clientHeight - parent.clientHeight; if (scroll < bottom && tab.clientHeight < parent.clientHeight) { parent.scrollTop = bottom; } } } } if (this.pendingReveal === result) { this.pendingReveal = undefined; } resolve(); }); }); this.pendingReveal = result; return result; } } exports.ScrollableTabBar = ScrollableTabBar; (function (ScrollableTabBar) { let Styles; (function (Styles) { Styles.TAB_BAR_CONTENT_CONTAINER = 'lm-TabBar-content-container'; })(Styles = ScrollableTabBar.Styles || (ScrollableTabBar.Styles = {})); })(ScrollableTabBar || (exports.ScrollableTabBar = ScrollableTabBar = {})); /** * Specialized scrollable tab-bar which comes with toolbar support. * Instead of the following DOM structure. * * +-------------------------+ * |[TAB_0][TAB_1][TAB_2][TAB| * +-------------Scrollable--+ * * There is a dedicated HTML element for toolbar which does **not** contained in the scrollable element. * * +-------------------------+-----------------+ * |[TAB_0][TAB_1][TAB_2][TAB| Toolbar | * +-------------Scrollable--+-Non-Scrollable-+ * */ class ToolbarAwareTabBar extends ScrollableTabBar { constructor(tabBarToolbarRegistry, tabBarToolbarFactory, breadcrumbsRendererFactory, options, scrollbarOptions, dynamicTabOptions) { super(options, scrollbarOptions, dynamicTabOptions); this.tabBarToolbarRegistry = tabBarToolbarRegistry; this.tabBarToolbarFactory = tabBarToolbarFactory; this.breadcrumbsRendererFactory = breadcrumbsRendererFactory; this.breadcrumbsRenderer = this.breadcrumbsRendererFactory(); this.breadcrumbsContainer = document.createElement('div'); this.breadcrumbsContainer.classList.add('theia-tabBar-breadcrumb-row'); this.breadcrumbsContainer.appendChild(this.breadcrumbsRenderer.host); this.node.appendChild(this.breadcrumbsContainer); this.toolbar = this.tabBarToolbarFactory(); this.toDispose.push(this.tabBarToolbarRegistry.onDidChange(() => this.update())); this.toDispose.push(this.breadcrumbsRenderer); if (!this.breadcrumbsRenderer.active) { this.breadcrumbsContainer.style.setProperty('display', 'none'); } else { this.node.classList.add('theia-tabBar-multirow'); } this.toDispose.push(this.breadcrumbsRenderer.onDidChangeActiveState(active => { if (active) { this.breadcrumbsContainer.style.removeProperty('display'); this.node.classList.add('theia-tabBar-multirow'); } else { this.breadcrumbsContainer.style.setProperty('display', 'none'); this.node.classList.remove('theia-tabBar-multirow'); } if (this.dockPanel) { this.dockPanel.fit(); } })); const handler = () => this.updateBreadcrumbs(); this.currentChanged.connect(handler); this.toDispose.push(common_1.Disposable.create(() => this.currentChanged.disconnect(handler))); } setDockPanel(panel) { this.dockPanel = panel; } async updateBreadcrumbs() { var _a; const current = (_a = this.currentTitle) === null || _a === void 0 ? void 0 : _a.owner; const uri = navigatable_types_1.NavigatableWidget.is(current) ? current.getResourceUri() : undefined; await this.breadcrumbsRenderer.refresh(uri); } onAfterAttach(msg) { if (this.toolbar) { if (this.toolbar.isAttached) { widgets_1.Widget.detach(this.toolbar); } widgets_1.Widget.attach(this.toolbar, this.topRow); if (this.breadcrumbsContainer) { this.node.appendChild(this.breadcrumbsContainer); } this.updateBreadcrumbs(); } super.onAfterAttach(msg); } onBeforeDetach(msg) { if (this.toolbar && this.toolbar.isAttached) { this.toolbar.dispose(); } super.onBeforeDetach(msg); } onUpdateRequest(msg) { super.onUpdateRequest(msg); this.updateToolbar(); } updateToolbar() { var _a, _b; if (!this.toolbar) { return; } const widget = (_b = (_a = this.currentTitle) === null || _a === void 0 ? void 0 : _a.owner) !== null && _b !== void 0 ? _b : undefined; this.toolbar.updateTarget(widget); this.updateTabs(); } handleEvent(event) { if (event instanceof MouseEvent) { if ((0, browser_1.isContextMenuEvent)(event)) { // Let this bubble up to handle the context menu return; } if (this.toolbar && this.toolbar.shouldHandleMouseEvent(event) || this.isOver(event, this.openTabsContainer)) { // if the mouse event is over the toolbar part don't handle it. return; } } super.handleEvent(event); } isOver(event, element) { return element && event.target instanceof Element && element.contains(event.target); } } exports.ToolbarAwareTabBar = ToolbarAwareTabBar; /** * A specialized tab bar for side areas. */ class SideTabBar extends ScrollableTabBar { constructor(options) { super(options); /** * Emitted when a tab is added to the tab bar. */ this.tabAdded = new signaling_1.Signal(this); /** * Side panels can be collapsed by clicking on the currently selected tab. This signal is * emitted when the mouse is released on the selected tab without initiating a drag. */ this.collapseRequested = new signaling_1.Signal(this); /** * Emitted when the set of overflowing/hidden tabs changes. */ this.tabsOverflowChanged = new signaling_1.Signal(this); this.toCancelViewContainerDND = new common_1.DisposableCollection(); this.cancelViewContainerDND = () => { this.toCancelViewContainerDND.dispose(); }; /** * Handles `viewContainerPart` drag enter. */ this.onDragEnter = (event) => { this.cancelViewContainerDND(); if (event.mimeData.getData('application/vnd.lumino.view-container-factory')) { event.preventDefault(); event.stopPropagation(); } }; /** * Handle `viewContainerPart` drag over, * Defines the appropriate `dropAction` and opens the tab on which the mouse stands on for more than 800 ms. */ this.onDragOver = (event) => { const factory = event.mimeData.getData('application/vnd.lumino.view-container-factory'); const widget = factory && factory(); if (!widget) { event.dropAction = 'none'; return; } event.preventDefault(); event.stopPropagation(); if (!this.toCancelViewContainerDND.disposed) { event.dropAction = event.proposedAction; return; } const { target, clientX, clientY } = event; if (target instanceof HTMLElement) { if (widget.options.disableDraggingToOtherContainers || widget.viewContainer.disableDNDBetweenContainers) { event.dropAction = 'none'; target.classList.add('theia-cursor-no-drop'); this.toCancelViewContainerDND.push(common_1.Disposable.create(() => { target.classList.remove('theia-cursor-no-drop'); })); } else { event.dropAction = event.proposedAction; } const { top, bottom, left, right, height } = target.getBoundingClientRect(); const mouseOnTop = (clientY - top) < (height / 2); const dropTargetClass = `drop-target-${mouseOnTop ? 'top' : 'bottom'}`; const tabs = this.contentNode.children; const targetTab = algorithm_1.ArrayExt.findFirstValue(tabs, t => domutils_1.ElementExt.hitTest(t, clientX, clientY)); if (!targetTab) { return; } targetTab.classList.add(dropTargetClass); this.toCancelViewContainerDND.push(common_1.Disposable.create(() => { if (targetTab) { targetTab.classList.remove(dropTargetClass); } })); const openTabTimer = setTimeout(() => { const title = this.titles.find(t => this.renderer.createTabId(t) === targetTab.id); if (title) { const mouseStillOnTab = clientX >= left && clientX <= right && clientY >= top && clientY <= bottom; if (mouseStillOnTab) { this.currentTitle = title; } } }, 800); this.toCancelViewContainerDND.push(common_1.Disposable.create(() => { clearTimeout(openTabTimer); })); } }; // Create the hidden content node (see `hiddenContentNode` fo