Path: blob/main/src/vs/workbench/browser/parts/views/viewPaneContainer.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $, addDisposableListener, Dimension, DragAndDropObserver, EventType, getWindow, isAncestor } from '../../../../base/browser/dom.js';6import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';7import { EventType as TouchEventType, Gesture } from '../../../../base/browser/touch.js';8import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';9import { IBoundarySashes, Orientation } from '../../../../base/browser/ui/sash/sash.js';10import { IPaneViewOptions, PaneView } from '../../../../base/browser/ui/splitview/paneview.js';11import { IAction } from '../../../../base/common/actions.js';12import { RunOnceScheduler } from '../../../../base/common/async.js';13import { Emitter, Event } from '../../../../base/common/event.js';14import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';15import { combinedDisposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';16import { assertReturnsDefined } from '../../../../base/common/types.js';17import './media/paneviewlet.css';18import * as nls from '../../../../nls.js';19import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';20import { Action2, IAction2Options, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';21import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';22import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';23import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';24import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';25import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';26import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';27import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';28import { activeContrastBorder, asCssVariable } from '../../../../platform/theme/common/colorRegistry.js';29import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';30import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';31import { CompositeDragAndDropObserver, toggleDropEffect } from '../../dnd.js';32import { ViewPane } from './viewPane.js';33import { IViewletViewOptions } from './viewsViewlet.js';34import { Component } from '../../../common/component.js';35import { PANEL_SECTION_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_HEADER_FOREGROUND, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, SIDE_BAR_SECTION_HEADER_FOREGROUND } from '../../../common/theme.js';36import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerModel, IViewDescriptor, IViewDescriptorRef, IViewDescriptorService, IViewPaneContainer, ViewContainer, ViewContainerLocation, ViewVisibilityState } from '../../../common/views.js';37import { IViewsService } from '../../../services/views/common/viewsService.js';38import { FocusedViewContext } from '../../../common/contextkeys.js';39import { IExtensionService } from '../../../services/extensions/common/extensions.js';40import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from '../../../services/layout/browser/layoutService.js';41import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';42import { ILogService } from '../../../../platform/log/common/log.js';43import { ViewContainerMenuActions } from './viewMenuActions.js';4445export const ViewsSubMenu = new MenuId('Views');46MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {47submenu: ViewsSubMenu,48title: nls.localize('views', "Views"),49order: 1,50} satisfies ISubmenuItem);5152export interface IViewPaneContainerOptions extends IPaneViewOptions {53mergeViewWithContainerWhenSingleView: boolean;54}5556interface IViewPaneItem {57pane: ViewPane;58disposable: IDisposable;59}6061const enum DropDirection {62UP,63DOWN,64LEFT,65RIGHT66}6768type BoundingRect = { top: number; left: number; bottom: number; right: number };6970class ViewPaneDropOverlay extends Themable {7172private static readonly OVERLAY_ID = 'monaco-pane-drop-overlay';7374private container!: HTMLElement;75private overlay!: HTMLElement;7677private _currentDropOperation: DropDirection | undefined;7879// private currentDropOperation: IDropOperation | undefined;80private _disposed: boolean | undefined;8182private cleanupOverlayScheduler: RunOnceScheduler;8384get currentDropOperation(): DropDirection | undefined {85return this._currentDropOperation;86}8788constructor(89private paneElement: HTMLElement,90private orientation: Orientation | undefined,91private bounds: BoundingRect | undefined,92protected location: ViewContainerLocation,93themeService: IThemeService,94) {95super(themeService);96this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300));9798this.create();99}100101get disposed(): boolean {102return !!this._disposed;103}104105private create(): void {106107// Container108this.container = $('div', { id: ViewPaneDropOverlay.OVERLAY_ID });109this.container.style.top = '0px';110111// Parent112this.paneElement.appendChild(this.container);113this.paneElement.classList.add('dragged-over');114this._register(toDisposable(() => {115this.container.remove();116this.paneElement.classList.remove('dragged-over');117}));118119// Overlay120this.overlay = $('.pane-overlay-indicator');121this.container.appendChild(this.overlay);122123// Overlay Event Handling124this.registerListeners();125126// Styles127this.updateStyles();128}129130override updateStyles(): void {131132// Overlay drop background133this.overlay.style.backgroundColor = this.getColor(this.location === ViewContainerLocation.Panel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND) || '';134135// Overlay contrast border (if any)136const activeContrastBorderColor = this.getColor(activeContrastBorder);137this.overlay.style.outlineColor = activeContrastBorderColor || '';138this.overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : '';139this.overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : '';140this.overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : '';141142this.overlay.style.borderColor = activeContrastBorderColor || '';143this.overlay.style.borderStyle = 'solid';144this.overlay.style.borderWidth = '0px';145}146147private registerListeners(): void {148this._register(new DragAndDropObserver(this.container, {149onDragOver: e => {150151// Position overlay152this.positionOverlay(e.offsetX, e.offsetY);153154// Make sure to stop any running cleanup scheduler to remove the overlay155if (this.cleanupOverlayScheduler.isScheduled()) {156this.cleanupOverlayScheduler.cancel();157}158},159160onDragLeave: e => this.dispose(),161onDragEnd: e => this.dispose(),162163onDrop: e => {164// Dispose overlay165this.dispose();166}167}));168169this._register(addDisposableListener(this.container, EventType.MOUSE_OVER, () => {170// Under some circumstances we have seen reports where the drop overlay is not being171// cleaned up and as such the editor area remains under the overlay so that you cannot172// type into the editor anymore. This seems related to using VMs and DND via host and173// guest OS, though some users also saw it without VMs.174// To protect against this issue we always destroy the overlay as soon as we detect a175// mouse event over it. The delay is used to guarantee we are not interfering with the176// actual DROP event that can also trigger a mouse over event.177if (!this.cleanupOverlayScheduler.isScheduled()) {178this.cleanupOverlayScheduler.schedule();179}180}));181}182183private positionOverlay(mousePosX: number, mousePosY: number): void {184const paneWidth = this.paneElement.clientWidth;185const paneHeight = this.paneElement.clientHeight;186187const splitWidthThreshold = paneWidth / 2;188const splitHeightThreshold = paneHeight / 2;189190let dropDirection: DropDirection | undefined;191192if (this.orientation === Orientation.VERTICAL) {193if (mousePosY < splitHeightThreshold) {194dropDirection = DropDirection.UP;195} else if (mousePosY >= splitHeightThreshold) {196dropDirection = DropDirection.DOWN;197}198} else if (this.orientation === Orientation.HORIZONTAL) {199if (mousePosX < splitWidthThreshold) {200dropDirection = DropDirection.LEFT;201} else if (mousePosX >= splitWidthThreshold) {202dropDirection = DropDirection.RIGHT;203}204}205206// Draw overlay based on split direction207switch (dropDirection) {208case DropDirection.UP:209this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' });210break;211case DropDirection.DOWN:212this.doPositionOverlay({ bottom: '0', left: '0', width: '100%', height: '50%' });213break;214case DropDirection.LEFT:215this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' });216break;217case DropDirection.RIGHT:218this.doPositionOverlay({ top: '0', right: '0', width: '50%', height: '100%' });219break;220default: {221// const top = this.bounds?.top || 0;222// const left = this.bounds?.bottom || 0;223224let top = '0';225let left = '0';226let width = '100%';227let height = '100%';228if (this.bounds) {229const boundingRect = this.container.getBoundingClientRect();230top = `${this.bounds.top - boundingRect.top}px`;231left = `${this.bounds.left - boundingRect.left}px`;232height = `${this.bounds.bottom - this.bounds.top}px`;233width = `${this.bounds.right - this.bounds.left}px`;234}235236this.doPositionOverlay({ top, left, width, height });237}238}239240if ((this.orientation === Orientation.VERTICAL && paneHeight <= 25) ||241(this.orientation === Orientation.HORIZONTAL && paneWidth <= 25)) {242this.doUpdateOverlayBorder(dropDirection);243} else {244this.doUpdateOverlayBorder(undefined);245}246247// Make sure the overlay is visible now248this.overlay.style.opacity = '1';249250// Enable transition after a timeout to prevent initial animation251setTimeout(() => this.overlay.classList.add('overlay-move-transition'), 0);252253// Remember as current split direction254this._currentDropOperation = dropDirection;255}256257private doUpdateOverlayBorder(direction: DropDirection | undefined): void {258this.overlay.style.borderTopWidth = direction === DropDirection.UP ? '2px' : '0px';259this.overlay.style.borderLeftWidth = direction === DropDirection.LEFT ? '2px' : '0px';260this.overlay.style.borderBottomWidth = direction === DropDirection.DOWN ? '2px' : '0px';261this.overlay.style.borderRightWidth = direction === DropDirection.RIGHT ? '2px' : '0px';262}263264private doPositionOverlay(options: { top?: string; bottom?: string; left?: string; right?: string; width: string; height: string }): void {265266// Container267this.container.style.height = '100%';268269// Overlay270this.overlay.style.top = options.top || '';271this.overlay.style.left = options.left || '';272this.overlay.style.bottom = options.bottom || '';273this.overlay.style.right = options.right || '';274this.overlay.style.width = options.width;275this.overlay.style.height = options.height;276}277278279contains(element: HTMLElement): boolean {280return element === this.container || element === this.overlay;281}282283override dispose(): void {284super.dispose();285286this._disposed = true;287}288}289290export class ViewPaneContainer extends Component implements IViewPaneContainer {291292readonly viewContainer: ViewContainer;293private lastFocusedPane: ViewPane | undefined;294private lastMergedCollapsedPane: ViewPane | undefined;295private paneItems: IViewPaneItem[] = [];296private paneview?: PaneView;297298private visible: boolean = false;299300private areExtensionsReady: boolean = false;301302private didLayout = false;303private dimension: Dimension | undefined;304private _boundarySashes: IBoundarySashes | undefined;305306private readonly visibleViewsCountFromCache: number | undefined;307private readonly visibleViewsStorageId: string;308protected readonly viewContainerModel: IViewContainerModel;309310private readonly _onTitleAreaUpdate: Emitter<void> = this._register(new Emitter<void>());311readonly onTitleAreaUpdate: Event<void> = this._onTitleAreaUpdate.event;312313private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());314readonly onDidChangeVisibility = this._onDidChangeVisibility.event;315316private readonly _onDidAddViews = this._register(new Emitter<IView[]>());317readonly onDidAddViews = this._onDidAddViews.event;318319private readonly _onDidRemoveViews = this._register(new Emitter<IView[]>());320readonly onDidRemoveViews = this._onDidRemoveViews.event;321322private readonly _onDidChangeViewVisibility = this._register(new Emitter<IView>());323readonly onDidChangeViewVisibility = this._onDidChangeViewVisibility.event;324325private readonly _onDidFocusView = this._register(new Emitter<IView>());326readonly onDidFocusView = this._onDidFocusView.event;327328private readonly _onDidBlurView = this._register(new Emitter<IView>());329readonly onDidBlurView = this._onDidBlurView.event;330331get onDidSashChange(): Event<number> {332return assertReturnsDefined(this.paneview).onDidSashChange;333}334335get panes(): ViewPane[] {336return this.paneItems.map(i => i.pane);337}338339get views(): IView[] {340return this.panes;341}342343get length(): number {344return this.paneItems.length;345}346347private _menuActions?: ViewContainerMenuActions;348get menuActions(): ViewContainerMenuActions | undefined {349return this._menuActions;350}351352constructor(353id: string,354private options: IViewPaneContainerOptions,355@IInstantiationService protected instantiationService: IInstantiationService,356@IConfigurationService protected configurationService: IConfigurationService,357@IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService,358@IContextMenuService protected contextMenuService: IContextMenuService,359@ITelemetryService protected telemetryService: ITelemetryService,360@IExtensionService protected extensionService: IExtensionService,361@IThemeService themeService: IThemeService,362@IStorageService protected storageService: IStorageService,363@IWorkspaceContextService protected contextService: IWorkspaceContextService,364@IViewDescriptorService protected viewDescriptorService: IViewDescriptorService,365@ILogService protected readonly logService: ILogService,366) {367368super(id, themeService, storageService);369370const container = this.viewDescriptorService.getViewContainerById(id);371if (!container) {372throw new Error('Could not find container');373}374375376this.viewContainer = container;377this.visibleViewsStorageId = `${id}.numberOfVisibleViews`;378this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined);379this.viewContainerModel = this.viewDescriptorService.getViewContainerModel(container);380}381382create(parent: HTMLElement): void {383const options = this.options as IPaneViewOptions;384options.orientation = this.orientation;385this.paneview = this._register(new PaneView(parent, this.options));386387if (this._boundarySashes) {388this.paneview.setBoundarySashes(this._boundarySashes);389}390391this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane)));392this._register(this.paneview.onDidScroll(_ => this.onDidScrollPane()));393this._register(this.paneview.onDidSashReset((index) => this.onDidSashReset(index)));394this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));395this._register(Gesture.addTarget(parent));396this._register(addDisposableListener(parent, TouchEventType.Contextmenu, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));397398this._menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.paneview.element, this.viewContainer));399this._register(this._menuActions.onDidChange(() => this.updateTitleArea()));400401let overlay: ViewPaneDropOverlay | undefined;402const getOverlayBounds: () => BoundingRect = () => {403const fullSize = parent.getBoundingClientRect();404const lastPane = this.panes[this.panes.length - 1].element.getBoundingClientRect();405const top = this.orientation === Orientation.VERTICAL ? lastPane.bottom : fullSize.top;406const left = this.orientation === Orientation.HORIZONTAL ? lastPane.right : fullSize.left;407408return {409top,410bottom: fullSize.bottom,411left,412right: fullSize.right,413};414};415416const inBounds = (bounds: BoundingRect, pos: { x: number; y: number }) => {417return pos.x >= bounds.left && pos.x <= bounds.right && pos.y >= bounds.top && pos.y <= bounds.bottom;418};419420421let bounds: BoundingRect;422423this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, {424onDragEnter: (e) => {425bounds = getOverlayBounds();426if (overlay && overlay.disposed) {427overlay = undefined;428}429430if (!overlay && inBounds(bounds, e.eventData)) {431const dropData = e.dragAndDropData.getData();432if (dropData.type === 'view') {433434const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);435const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);436437if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {438return;439}440441overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);442}443444if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {445const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;446const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;447448if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {449overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);450}451}452}453},454onDragOver: (e) => {455if (overlay && overlay.disposed) {456overlay = undefined;457}458459if (overlay && !inBounds(bounds, e.eventData)) {460overlay.dispose();461overlay = undefined;462}463464if (inBounds(bounds, e.eventData)) {465toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);466}467},468onDragLeave: (e) => {469overlay?.dispose();470overlay = undefined;471},472onDrop: (e) => {473if (overlay) {474const dropData = e.dragAndDropData.getData();475const viewsToMove: IViewDescriptor[] = [];476477if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {478const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;479const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;480if (!allViews.some(v => !v.canMoveView)) {481viewsToMove.push(...allViews);482}483} else if (dropData.type === 'view') {484const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);485const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);486if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView) {487this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewContainer, undefined, 'dnd');488}489}490491const paneCount = this.panes.length;492493if (viewsToMove.length > 0) {494this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');495}496497if (paneCount > 0) {498for (const view of viewsToMove) {499const paneToMove = this.panes.find(p => p.id === view.id);500if (paneToMove) {501this.movePane(paneToMove, this.panes[this.panes.length - 1]);502}503}504}505}506507overlay?.dispose();508overlay = undefined;509}510}));511512this._register(this.onDidSashChange(() => this.saveViewSizes()));513this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(added => this.onDidAddViewDescriptors(added)));514this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(removed => this.onDidRemoveViewDescriptors(removed)));515const addedViews: IAddedViewDescriptorRef[] = this.viewContainerModel.visibleViewDescriptors.map((viewDescriptor, index) => {516const size = this.viewContainerModel.getSize(viewDescriptor.id);517const collapsed = this.viewContainerModel.isCollapsed(viewDescriptor.id);518return ({ viewDescriptor, index, size, collapsed });519});520if (addedViews.length) {521this.onDidAddViewDescriptors(addedViews);522}523524// Update headers after and title contributed views after available, since we read from cache in the beginning to know if the viewlet has single view or not. Ref #29609525this.extensionService.whenInstalledExtensionsRegistered().then(() => {526this.areExtensionsReady = true;527if (this.panes.length) {528this.updateTitleArea();529this.updateViewHeaders();530}531this._register(this.configurationService.onDidChangeConfiguration(e => {532if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) {533this.updateViewHeaders();534}535}));536});537538this._register(this.viewContainerModel.onDidChangeActiveViewDescriptors(() => this._onTitleAreaUpdate.fire()));539}540541getTitle(): string {542const containerTitle = this.viewContainerModel.title;543544if (this.isViewMergedWithContainer()) {545const singleViewPaneContainerTitle = this.paneItems[0].pane.singleViewPaneContainerTitle;546if (singleViewPaneContainerTitle) {547return singleViewPaneContainerTitle;548}549550const paneItemTitle = this.paneItems[0].pane.title;551if (containerTitle === paneItemTitle) {552return paneItemTitle;553}554555return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle;556}557558return containerTitle;559}560561private showContextMenu(event: StandardMouseEvent): void {562for (const paneItem of this.paneItems) {563// Do not show context menu if target is coming from inside pane views564if (isAncestor(event.target, paneItem.pane.element)) {565return;566}567}568569event.stopPropagation();570event.preventDefault();571572this.contextMenuService.showContextMenu({573getAnchor: () => event,574getActions: () => this.menuActions?.getContextMenuActions() ?? []575});576}577578getActionsContext(): unknown {579if (this.isViewMergedWithContainer()) {580return this.panes[0].getActionsContext();581}582return undefined;583}584585getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {586if (this.isViewMergedWithContainer()) {587return this.paneItems[0].pane.createActionViewItem(action, options);588}589return createActionViewItem(this.instantiationService, action, options);590}591592focus(): void {593let paneToFocus: ViewPane | undefined = undefined;594if (this.lastFocusedPane) {595paneToFocus = this.lastFocusedPane;596} else if (this.paneItems.length > 0) {597for (const { pane } of this.paneItems) {598if (pane.isExpanded()) {599paneToFocus = pane;600break;601}602}603}604if (paneToFocus) {605paneToFocus.focus();606}607}608609private get orientation(): Orientation {610switch (this.viewDescriptorService.getViewContainerLocation(this.viewContainer)) {611case ViewContainerLocation.Sidebar:612case ViewContainerLocation.AuxiliaryBar:613return Orientation.VERTICAL;614case ViewContainerLocation.Panel: {615return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL;616}617}618619return Orientation.VERTICAL;620}621622layout(dimension: Dimension): void {623if (this.paneview) {624if (this.paneview.orientation !== this.orientation) {625this.paneview.flipOrientation(dimension.height, dimension.width);626}627628this.paneview.layout(dimension.height, dimension.width);629}630631this.dimension = dimension;632if (this.didLayout) {633this.saveViewSizes();634} else {635this.didLayout = true;636this.restoreViewSizes();637}638}639640setBoundarySashes(sashes: IBoundarySashes): void {641this._boundarySashes = sashes;642this.paneview?.setBoundarySashes(sashes);643}644645getOptimalWidth(): number {646const additionalMargin = 16;647const optimalWidth = Math.max(...this.panes.map(view => view.getOptimalWidth() || 0));648return optimalWidth + additionalMargin;649}650651addPanes(panes: { pane: ViewPane; size: number; index?: number; disposable: IDisposable }[]): void {652const wasMerged = this.isViewMergedWithContainer();653654for (const { pane, size, index, disposable } of panes) {655this.addPane(pane, size, disposable, index);656}657658this.updateViewHeaders();659if (this.isViewMergedWithContainer() !== wasMerged) {660this.updateTitleArea();661}662663this._onDidAddViews.fire(panes.map(({ pane }) => pane));664}665666setVisible(visible: boolean): void {667if (this.visible !== !!visible) {668this.visible = visible;669670this._onDidChangeVisibility.fire(visible);671}672673this.panes.filter(view => view.isVisible() !== visible)674.map((view) => view.setVisible(visible));675}676677isVisible(): boolean {678return this.visible;679}680681protected updateTitleArea(): void {682this._onTitleAreaUpdate.fire();683}684685protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane {686return (this.instantiationService as any).createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.staticArguments || []), options) as ViewPane;687}688689getView(id: string): ViewPane | undefined {690return this.panes.filter(view => view.id === id)[0];691}692693private saveViewSizes(): void {694// Save size only when the layout has happened695if (this.didLayout) {696this.viewContainerModel.setSizes(this.panes.map(view => ({ id: view.id, size: this.getPaneSize(view) })));697}698}699700private restoreViewSizes(): void {701// Restore sizes only when the layout has happened702if (this.didLayout) {703let initialSizes;704for (let i = 0; i < this.viewContainerModel.visibleViewDescriptors.length; i++) {705const pane = this.panes[i];706const viewDescriptor = this.viewContainerModel.visibleViewDescriptors[i];707const size = this.viewContainerModel.getSize(viewDescriptor.id);708709if (typeof size === 'number') {710this.resizePane(pane, size);711} else {712initialSizes = initialSizes ? initialSizes : this.computeInitialSizes();713this.resizePane(pane, initialSizes.get(pane.id) || 200);714}715}716}717}718719private computeInitialSizes(): Map<string, number> {720const sizes: Map<string, number> = new Map<string, number>();721if (this.dimension) {722const totalWeight = this.viewContainerModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0);723for (const viewDescriptor of this.viewContainerModel.visibleViewDescriptors) {724if (this.orientation === Orientation.VERTICAL) {725sizes.set(viewDescriptor.id, this.dimension.height * (viewDescriptor.weight || 20) / totalWeight);726} else {727sizes.set(viewDescriptor.id, this.dimension.width * (viewDescriptor.weight || 20) / totalWeight);728}729}730}731return sizes;732}733734protected override saveState(): void {735this.panes.forEach((view) => view.saveState());736this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.MACHINE);737}738739private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void {740event.stopPropagation();741event.preventDefault();742743const actions: IAction[] = viewPane.menuActions.getContextMenuActions();744745this.contextMenuService.showContextMenu({746getAnchor: () => event,747getActions: () => actions748});749}750751openView(id: string, focus?: boolean): IView | undefined {752let view = this.getView(id);753if (!view) {754this.toggleViewVisibility(id);755}756view = this.getView(id);757if (view) {758view.setExpanded(true);759if (focus) {760view.focus();761}762}763return view;764}765766protected onDidAddViewDescriptors(added: IAddedViewDescriptorRef[]): ViewPane[] {767const panesToAdd: { pane: ViewPane; size: number; index: number; disposable: IDisposable }[] = [];768769for (const { viewDescriptor, collapsed, index, size } of added) {770const pane = this.createView(viewDescriptor,771{772id: viewDescriptor.id,773title: viewDescriptor.name.value,774fromExtensionId: (viewDescriptor as Partial<ICustomViewDescriptor>).extensionId,775expanded: !collapsed,776singleViewPaneContainerTitle: viewDescriptor.singleViewPaneContainerTitle,777});778779try {780pane.render();781} catch (error) {782this.logService.error(`Fail to render view ${viewDescriptor.id}`, error);783continue;784}785if (pane.draggableElement) {786const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => {787e.stopPropagation();788e.preventDefault();789this.onContextMenu(new StandardMouseEvent(getWindow(pane.draggableElement), e), pane);790});791792const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => {793this.viewContainerModel.setCollapsed(viewDescriptor.id, collapsed);794});795796panesToAdd.push({ pane, size: size || pane.minimumSize, index, disposable: combinedDisposable(contextMenuDisposable, collapseDisposable) });797}798}799800this.addPanes(panesToAdd);801this.restoreViewSizes();802803const panes: ViewPane[] = [];804for (const { pane } of panesToAdd) {805pane.setVisible(this.isVisible());806panes.push(pane);807}808return panes;809}810811private onDidRemoveViewDescriptors(removed: IViewDescriptorRef[]): void {812removed = removed.sort((a, b) => b.index - a.index);813const panesToRemove: ViewPane[] = [];814for (const { index } of removed) {815const paneItem = this.paneItems[index];816if (paneItem) {817panesToRemove.push(this.paneItems[index].pane);818}819}820821if (panesToRemove.length) {822this.removePanes(panesToRemove);823824for (const pane of panesToRemove) {825pane.setVisible(false);826}827}828}829830toggleViewVisibility(viewId: string): void {831// Check if view is active832if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) {833const visible = !this.viewContainerModel.isVisible(viewId);834this.viewContainerModel.setVisible(viewId, visible);835}836}837838private addPane(pane: ViewPane, size: number, disposable: IDisposable, index = this.paneItems.length - 1): void {839const onDidFocus = pane.onDidFocus(() => {840this._onDidFocusView.fire(pane);841this.lastFocusedPane = pane;842});843const onDidBlur = pane.onDidBlur(() => this._onDidBlurView.fire(pane));844const onDidChangeTitleArea = pane.onDidChangeTitleArea(() => {845if (this.isViewMergedWithContainer()) {846this.updateTitleArea();847}848});849850const onDidChangeVisibility = pane.onDidChangeBodyVisibility(() => this._onDidChangeViewVisibility.fire(pane));851const onDidChange = pane.onDidChange(() => {852if (pane === this.lastFocusedPane && !pane.isExpanded()) {853this.lastFocusedPane = undefined;854}855});856857const isPanel = this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel;858pane.style({859headerForeground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_FOREGROUND : SIDE_BAR_SECTION_HEADER_FOREGROUND),860headerBackground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BACKGROUND : SIDE_BAR_SECTION_HEADER_BACKGROUND),861headerBorder: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BORDER : SIDE_BAR_SECTION_HEADER_BORDER),862dropBackground: asCssVariable(isPanel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND),863leftBorder: isPanel ? asCssVariable(PANEL_SECTION_BORDER) : undefined864});865866const store = new DisposableStore();867store.add(disposable);868store.add(combinedDisposable(pane, onDidFocus, onDidBlur, onDidChangeTitleArea, onDidChange, onDidChangeVisibility));869const paneItem: IViewPaneItem = { pane, disposable: store };870871this.paneItems.splice(index, 0, paneItem);872assertReturnsDefined(this.paneview).addPane(pane, size, index);873874let overlay: ViewPaneDropOverlay | undefined;875876if (pane.draggableElement) {877store.add(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, () => { return { type: 'view', id: pane.id }; }, {}));878}879880store.add(CompositeDragAndDropObserver.INSTANCE.registerTarget(pane.dropTargetElement, {881onDragEnter: (e) => {882if (!overlay) {883const dropData = e.dragAndDropData.getData();884if (dropData.type === 'view' && dropData.id !== pane.id) {885886const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);887const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);888889if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {890return;891}892893overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);894}895896if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {897const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;898const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;899900if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {901overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);902}903}904}905},906onDragOver: (e) => {907toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);908},909onDragLeave: (e) => {910overlay?.dispose();911overlay = undefined;912},913onDrop: (e) => {914if (overlay) {915const dropData = e.dragAndDropData.getData();916const viewsToMove: IViewDescriptor[] = [];917let anchorView: IViewDescriptor | undefined;918919if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {920const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;921const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;922923if (allViews.length > 0 && !allViews.some(v => !v.canMoveView)) {924viewsToMove.push(...allViews);925anchorView = allViews[0];926}927} else if (dropData.type === 'view') {928const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);929const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);930if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView && !this.viewContainer.rejectAddedViews) {931viewsToMove.push(viewDescriptor);932}933934if (viewDescriptor) {935anchorView = viewDescriptor;936}937}938939if (viewsToMove) {940this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');941}942943if (anchorView) {944if (overlay.currentDropOperation === DropDirection.DOWN ||945overlay.currentDropOperation === DropDirection.RIGHT) {946947const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);948let toIndex = this.panes.findIndex(p => p.id === pane.id);949950if (fromIndex >= 0 && toIndex >= 0) {951if (fromIndex > toIndex) {952toIndex++;953}954955if (toIndex < this.panes.length && toIndex !== fromIndex) {956this.movePane(this.panes[fromIndex], this.panes[toIndex]);957}958}959}960961if (overlay.currentDropOperation === DropDirection.UP ||962overlay.currentDropOperation === DropDirection.LEFT) {963const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);964let toIndex = this.panes.findIndex(p => p.id === pane.id);965966if (fromIndex >= 0 && toIndex >= 0) {967if (fromIndex < toIndex) {968toIndex--;969}970971if (toIndex >= 0 && toIndex !== fromIndex) {972this.movePane(this.panes[fromIndex], this.panes[toIndex]);973}974}975}976977if (viewsToMove.length > 1) {978viewsToMove.slice(1).forEach(view => {979let toIndex = this.panes.findIndex(p => p.id === anchorView!.id);980const fromIndex = this.panes.findIndex(p => p.id === view.id);981if (fromIndex >= 0 && toIndex >= 0) {982if (fromIndex > toIndex) {983toIndex++;984}985986if (toIndex < this.panes.length && toIndex !== fromIndex) {987this.movePane(this.panes[fromIndex], this.panes[toIndex]);988anchorView = view;989}990}991});992}993}994}995996overlay?.dispose();997overlay = undefined;998}999}));1000}10011002removePanes(panes: ViewPane[]): void {1003const wasMerged = this.isViewMergedWithContainer();10041005panes.forEach(pane => this.removePane(pane));10061007this.updateViewHeaders();1008if (wasMerged !== this.isViewMergedWithContainer()) {1009this.updateTitleArea();1010}10111012this._onDidRemoveViews.fire(panes);1013}10141015private removePane(pane: ViewPane): void {1016const index = this.paneItems.findIndex(i => i.pane === pane);10171018if (index === -1) {1019return;1020}10211022if (this.lastFocusedPane === pane) {1023this.lastFocusedPane = undefined;1024}10251026assertReturnsDefined(this.paneview).removePane(pane);1027const [paneItem] = this.paneItems.splice(index, 1);1028paneItem.disposable.dispose();10291030}10311032movePane(from: ViewPane, to: ViewPane): void {1033const fromIndex = this.paneItems.findIndex(item => item.pane === from);1034const toIndex = this.paneItems.findIndex(item => item.pane === to);10351036const fromViewDescriptor = this.viewContainerModel.visibleViewDescriptors[fromIndex];1037const toViewDescriptor = this.viewContainerModel.visibleViewDescriptors[toIndex];10381039if (fromIndex < 0 || fromIndex >= this.paneItems.length) {1040return;1041}10421043if (toIndex < 0 || toIndex >= this.paneItems.length) {1044return;1045}10461047const [paneItem] = this.paneItems.splice(fromIndex, 1);1048this.paneItems.splice(toIndex, 0, paneItem);10491050assertReturnsDefined(this.paneview).movePane(from, to);10511052this.viewContainerModel.move(fromViewDescriptor.id, toViewDescriptor.id);10531054this.updateTitleArea();1055}10561057resizePane(pane: ViewPane, size: number): void {1058assertReturnsDefined(this.paneview).resizePane(pane, size);1059}10601061getPaneSize(pane: ViewPane): number {1062return assertReturnsDefined(this.paneview).getPaneSize(pane);1063}10641065private updateViewHeaders(): void {1066if (this.isViewMergedWithContainer()) {1067if (this.paneItems[0].pane.isExpanded()) {1068this.lastMergedCollapsedPane = undefined;1069} else {1070this.lastMergedCollapsedPane = this.paneItems[0].pane;1071this.paneItems[0].pane.setExpanded(true);1072}1073this.paneItems[0].pane.headerVisible = false;1074this.paneItems[0].pane.collapsible = true;1075} else {1076if (this.paneItems.length === 1) {1077this.paneItems[0].pane.headerVisible = true;1078if (this.paneItems[0].pane === this.lastMergedCollapsedPane) {1079this.paneItems[0].pane.setExpanded(false);1080}1081this.paneItems[0].pane.collapsible = false;1082} else {1083this.paneItems.forEach(i => {1084i.pane.headerVisible = true;1085i.pane.collapsible = true;1086if (i.pane === this.lastMergedCollapsedPane) {1087i.pane.setExpanded(false);1088}1089});1090}1091this.lastMergedCollapsedPane = undefined;1092}1093}10941095isViewMergedWithContainer(): boolean {1096if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) {1097return false;1098}1099if (!this.areExtensionsReady) {1100if (this.visibleViewsCountFromCache === undefined) {1101return this.paneItems[0].pane.isExpanded();1102}1103// Check in cache so that view do not jump. See #296091104return this.visibleViewsCountFromCache === 1;1105}1106return true;1107}11081109private onDidScrollPane() {1110for (const pane of this.panes) {1111pane.onDidScrollRoot();1112}1113}11141115private onDidSashReset(index: number) {1116let firstPane = undefined;1117let secondPane = undefined;11181119// Deal with collapsed views: to be clever, we split the space taken by the nearest uncollapsed views1120for (let i = index; i >= 0; i--) {1121if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {1122firstPane = this.paneItems[i].pane;1123break;1124}1125}11261127for (let i = index + 1; i < this.paneItems.length; i++) {1128if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {1129secondPane = this.paneItems[i].pane;1130break;1131}1132}11331134if (firstPane && secondPane) {1135const firstPaneSize = this.getPaneSize(firstPane);1136const secondPaneSize = this.getPaneSize(secondPane);11371138// Avoid rounding errors and be consistent when resizing1139// The first pane always get half rounded up and the second is half rounded down1140const newFirstPaneSize = Math.ceil((firstPaneSize + secondPaneSize) / 2);1141const newSecondPaneSize = Math.floor((firstPaneSize + secondPaneSize) / 2);11421143// Shrink the larger pane first, then grow the smaller pane1144// This prevents interfering with other view sizes1145if (firstPaneSize > secondPaneSize) {1146this.resizePane(firstPane, newFirstPaneSize);1147this.resizePane(secondPane, newSecondPaneSize);1148} else {1149this.resizePane(secondPane, newSecondPaneSize);1150this.resizePane(firstPane, newFirstPaneSize);1151}1152}1153}11541155override dispose(): void {1156super.dispose();1157this.paneItems.forEach(i => i.disposable.dispose());1158if (this.paneview) {1159this.paneview.dispose();1160}1161}1162}11631164export abstract class ViewPaneContainerAction<T extends IViewPaneContainer> extends Action2 {1165override readonly desc: Readonly<IAction2Options> & { viewPaneContainerId: string };1166constructor(desc: Readonly<IAction2Options> & { viewPaneContainerId: string }) {1167super(desc);1168this.desc = desc;1169}11701171run(accessor: ServicesAccessor, ...args: any[]): unknown {1172const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId);1173if (viewPaneContainer) {1174return this.runInViewPaneContainer(accessor, <T>viewPaneContainer, ...args);1175}1176return undefined;1177}11781179abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: any[]): unknown;1180}11811182class MoveViewPosition extends Action2 {1183constructor(desc: Readonly<IAction2Options>, private readonly offset: number) {1184super(desc);1185}11861187async run(accessor: ServicesAccessor): Promise<void> {1188const viewDescriptorService = accessor.get(IViewDescriptorService);1189const contextKeyService = accessor.get(IContextKeyService);11901191const viewId = FocusedViewContext.getValue(contextKeyService);1192if (viewId === undefined) {1193return;1194}11951196const viewContainer = viewDescriptorService.getViewContainerByViewId(viewId)!;1197const model = viewDescriptorService.getViewContainerModel(viewContainer);11981199const viewDescriptor = model.visibleViewDescriptors.find(vd => vd.id === viewId)!;1200const currentIndex = model.visibleViewDescriptors.indexOf(viewDescriptor);1201if (currentIndex + this.offset < 0 || currentIndex + this.offset >= model.visibleViewDescriptors.length) {1202return;1203}12041205const newPosition = model.visibleViewDescriptors[currentIndex + this.offset];12061207model.move(viewDescriptor.id, newPosition.id);1208}1209}12101211registerAction2(1212class MoveViewUp extends MoveViewPosition {1213constructor() {1214super({1215id: 'views.moveViewUp',1216title: nls.localize('viewMoveUp', "Move View Up"),1217keybinding: {1218primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.UpArrow),1219weight: KeybindingWeight.WorkbenchContrib + 1,1220when: FocusedViewContext.notEqualsTo('')1221}1222}, -1);1223}1224}1225);12261227registerAction2(1228class MoveViewLeft extends MoveViewPosition {1229constructor() {1230super({1231id: 'views.moveViewLeft',1232title: nls.localize('viewMoveLeft', "Move View Left"),1233keybinding: {1234primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.LeftArrow),1235weight: KeybindingWeight.WorkbenchContrib + 1,1236when: FocusedViewContext.notEqualsTo('')1237}1238}, -1);1239}1240}1241);12421243registerAction2(1244class MoveViewDown extends MoveViewPosition {1245constructor() {1246super({1247id: 'views.moveViewDown',1248title: nls.localize('viewMoveDown', "Move View Down"),1249keybinding: {1250primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.DownArrow),1251weight: KeybindingWeight.WorkbenchContrib + 1,1252when: FocusedViewContext.notEqualsTo('')1253}1254}, 1);1255}1256}1257);12581259registerAction2(1260class MoveViewRight extends MoveViewPosition {1261constructor() {1262super({1263id: 'views.moveViewRight',1264title: nls.localize('viewMoveRight', "Move View Right"),1265keybinding: {1266primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.RightArrow),1267weight: KeybindingWeight.WorkbenchContrib + 1,1268when: FocusedViewContext.notEqualsTo('')1269}1270}, 1);1271}1272}1273);127412751276registerAction2(class MoveViews extends Action2 {1277constructor() {1278super({1279id: 'vscode.moveViews',1280title: nls.localize('viewsMove', "Move Views"),1281});1282}12831284async run(accessor: ServicesAccessor, options: { viewIds: string[]; destinationId: string }): Promise<void> {1285if (!Array.isArray(options?.viewIds) || typeof options?.destinationId !== 'string') {1286return Promise.reject('Invalid arguments');1287}12881289const viewDescriptorService = accessor.get(IViewDescriptorService);12901291const destination = viewDescriptorService.getViewContainerById(options.destinationId);1292if (!destination) {1293return;1294}12951296// FYI, don't use `moveViewsToContainer` in 1 shot, because it expects all views to have the same current location1297for (const viewId of options.viewIds) {1298const viewDescriptor = viewDescriptorService.getViewDescriptorById(viewId);1299if (viewDescriptor?.canMoveView) {1300viewDescriptorService.moveViewsToContainer([viewDescriptor], destination, ViewVisibilityState.Default, this.desc.id);1301}1302}13031304await accessor.get(IViewsService).openViewContainer(destination.id, true);1305}1306});130713081309