Path: blob/main/src/vs/workbench/browser/parts/views/viewPaneContainer.ts
5292 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<MementoType extends object = object> extends Component<MementoType> 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;422423if (this.viewDescriptorService.canMoveViews()) {424this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, {425onDragEnter: (e) => {426bounds = getOverlayBounds();427if (overlay?.disposed) {428overlay = undefined;429}430431if (!overlay && inBounds(bounds, e.eventData)) {432const dropData = e.dragAndDropData.getData();433if (dropData.type === 'view') {434435const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);436const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);437438if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {439return;440}441442overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);443}444445if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {446const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;447const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;448449if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {450overlay = new ViewPaneDropOverlay(parent, undefined, bounds, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);451}452}453}454},455onDragOver: (e) => {456if (overlay?.disposed) {457overlay = undefined;458}459460if (overlay && !inBounds(bounds, e.eventData)) {461overlay.dispose();462overlay = undefined;463}464465if (inBounds(bounds, e.eventData)) {466toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);467}468},469onDragLeave: (e) => {470overlay?.dispose();471overlay = undefined;472},473onDrop: (e) => {474if (overlay) {475const dropData = e.dragAndDropData.getData();476const viewsToMove: IViewDescriptor[] = [];477478if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) {479const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;480const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;481if (!allViews.some(v => !v.canMoveView)) {482viewsToMove.push(...allViews);483}484} else if (dropData.type === 'view') {485const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);486const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);487if (oldViewContainer !== this.viewContainer && viewDescriptor?.canMoveView) {488this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewContainer, undefined, 'dnd');489}490}491492const paneCount = this.panes.length;493494if (viewsToMove.length > 0) {495this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');496}497498if (paneCount > 0) {499for (const view of viewsToMove) {500const paneToMove = this.panes.find(p => p.id === view.id);501if (paneToMove) {502this.movePane(paneToMove, this.panes[this.panes.length - 1]);503}504}505}506}507508overlay?.dispose();509overlay = undefined;510}511}));512}513514this._register(this.onDidSashChange(() => this.saveViewSizes()));515this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(added => this.onDidAddViewDescriptors(added)));516this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(removed => this.onDidRemoveViewDescriptors(removed)));517const addedViews: IAddedViewDescriptorRef[] = this.viewContainerModel.visibleViewDescriptors.map((viewDescriptor, index) => {518const size = this.viewContainerModel.getSize(viewDescriptor.id);519const collapsed = this.viewContainerModel.isCollapsed(viewDescriptor.id);520return ({ viewDescriptor, index, size, collapsed });521});522if (addedViews.length) {523this.onDidAddViewDescriptors(addedViews);524}525526// 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 #29609527this.extensionService.whenInstalledExtensionsRegistered().then(() => {528this.areExtensionsReady = true;529if (this.panes.length) {530this.updateTitleArea();531this.updateViewHeaders();532}533this._register(this.configurationService.onDidChangeConfiguration(e => {534if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) {535this.updateViewHeaders();536}537}));538});539540this._register(this.viewContainerModel.onDidChangeActiveViewDescriptors(() => this._onTitleAreaUpdate.fire()));541}542543getTitle(): string {544const containerTitle = this.viewContainerModel.title;545546if (this.isViewMergedWithContainer()) {547const singleViewPaneContainerTitle = this.paneItems[0].pane.singleViewPaneContainerTitle;548if (singleViewPaneContainerTitle) {549return singleViewPaneContainerTitle;550}551552const paneItemTitle = this.paneItems[0].pane.title;553if (containerTitle === paneItemTitle) {554return paneItemTitle;555}556557return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle;558}559560return containerTitle;561}562563private showContextMenu(event: StandardMouseEvent): void {564for (const paneItem of this.paneItems) {565// Do not show context menu if target is coming from inside pane views566if (isAncestor(event.target, paneItem.pane.element)) {567return;568}569}570571event.stopPropagation();572event.preventDefault();573574this.contextMenuService.showContextMenu({575getAnchor: () => event,576getActions: () => this.menuActions?.getContextMenuActions() ?? []577});578}579580getActionsContext(): unknown {581if (this.isViewMergedWithContainer()) {582return this.panes[0].getActionsContext();583}584return undefined;585}586587getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {588if (this.isViewMergedWithContainer()) {589return this.paneItems[0].pane.createActionViewItem(action, options);590}591return createActionViewItem(this.instantiationService, action, options);592}593594focus(): void {595let paneToFocus: ViewPane | undefined = undefined;596if (this.lastFocusedPane) {597paneToFocus = this.lastFocusedPane;598} else if (this.paneItems.length > 0) {599for (const { pane } of this.paneItems) {600if (pane.isExpanded()) {601paneToFocus = pane;602break;603}604}605}606if (paneToFocus) {607paneToFocus.focus();608}609}610611private get orientation(): Orientation {612switch (this.viewDescriptorService.getViewContainerLocation(this.viewContainer)) {613case ViewContainerLocation.Sidebar:614case ViewContainerLocation.AuxiliaryBar:615return Orientation.VERTICAL;616case ViewContainerLocation.Panel: {617return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL;618}619}620621return Orientation.VERTICAL;622}623624layout(dimension: Dimension): void {625if (this.paneview) {626if (this.paneview.orientation !== this.orientation) {627this.paneview.flipOrientation(dimension.height, dimension.width);628}629630this.paneview.layout(dimension.height, dimension.width);631}632633this.dimension = dimension;634if (this.didLayout) {635this.saveViewSizes();636} else {637this.didLayout = true;638this.restoreViewSizes();639}640}641642setBoundarySashes(sashes: IBoundarySashes): void {643this._boundarySashes = sashes;644this.paneview?.setBoundarySashes(sashes);645}646647getOptimalWidth(): number {648const additionalMargin = 16;649const optimalWidth = Math.max(...this.panes.map(view => view.getOptimalWidth() || 0));650return optimalWidth + additionalMargin;651}652653addPanes(panes: { pane: ViewPane; size: number; index?: number; disposable: IDisposable }[]): void {654const wasMerged = this.isViewMergedWithContainer();655656for (const { pane, size, index, disposable } of panes) {657this.addPane(pane, size, disposable, index);658}659660this.updateViewHeaders();661if (this.isViewMergedWithContainer() !== wasMerged) {662this.updateTitleArea();663}664665this._onDidAddViews.fire(panes.map(({ pane }) => pane));666}667668setVisible(visible: boolean): void {669if (this.visible !== !!visible) {670this.visible = visible;671672this._onDidChangeVisibility.fire(visible);673}674675this.panes.filter(view => view.isVisible() !== visible)676.map((view) => view.setVisible(visible));677}678679isVisible(): boolean {680return this.visible;681}682683protected updateTitleArea(): void {684this._onTitleAreaUpdate.fire();685}686687protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane {688return this.instantiationService.createInstance(viewDescriptor.ctorDescriptor.ctor, ...(viewDescriptor.ctorDescriptor.staticArguments || []), options);689}690691getView(id: string): ViewPane | undefined {692return this.panes.filter(view => view.id === id)[0];693}694695private saveViewSizes(): void {696// Save size only when the layout has happened697if (this.didLayout) {698this.viewContainerModel.setSizes(this.panes.map(view => ({ id: view.id, size: this.getPaneSize(view) })));699}700}701702private restoreViewSizes(): void {703// Restore sizes only when the layout has happened704if (this.didLayout) {705let initialSizes;706for (let i = 0; i < this.viewContainerModel.visibleViewDescriptors.length; i++) {707const pane = this.panes[i];708const viewDescriptor = this.viewContainerModel.visibleViewDescriptors[i];709const size = this.viewContainerModel.getSize(viewDescriptor.id);710711if (typeof size === 'number') {712this.resizePane(pane, size);713} else {714initialSizes = initialSizes ? initialSizes : this.computeInitialSizes();715this.resizePane(pane, initialSizes.get(pane.id) || 200);716}717}718}719}720721private computeInitialSizes(): Map<string, number> {722const sizes: Map<string, number> = new Map<string, number>();723if (this.dimension) {724const totalWeight = this.viewContainerModel.visibleViewDescriptors.reduce((totalWeight, { weight }) => totalWeight + (weight || 20), 0);725for (const viewDescriptor of this.viewContainerModel.visibleViewDescriptors) {726if (this.orientation === Orientation.VERTICAL) {727sizes.set(viewDescriptor.id, this.dimension.height * (viewDescriptor.weight || 20) / totalWeight);728} else {729sizes.set(viewDescriptor.id, this.dimension.width * (viewDescriptor.weight || 20) / totalWeight);730}731}732}733return sizes;734}735736protected override saveState(): void {737this.panes.forEach((view) => view.saveState());738this.storageService.store(this.visibleViewsStorageId, this.length, StorageScope.WORKSPACE, StorageTarget.MACHINE);739}740741private onContextMenu(event: StandardMouseEvent, viewPane: ViewPane): void {742event.stopPropagation();743event.preventDefault();744745const actions: IAction[] = viewPane.menuActions.getContextMenuActions();746747this.contextMenuService.showContextMenu({748getAnchor: () => event,749getActions: () => actions750});751}752753openView(id: string, focus?: boolean): IView | undefined {754let view = this.getView(id);755if (!view) {756this.toggleViewVisibility(id);757}758view = this.getView(id);759if (view) {760view.setExpanded(true);761if (focus) {762view.focus();763}764}765return view;766}767768protected onDidAddViewDescriptors(added: IAddedViewDescriptorRef[]): ViewPane[] {769const panesToAdd: { pane: ViewPane; size: number; index: number; disposable: IDisposable }[] = [];770771for (const { viewDescriptor, collapsed, index, size } of added) {772const pane = this.createView(viewDescriptor,773{774id: viewDescriptor.id,775title: viewDescriptor.name.value,776fromExtensionId: (viewDescriptor as Partial<ICustomViewDescriptor>).extensionId,777expanded: !collapsed,778singleViewPaneContainerTitle: viewDescriptor.singleViewPaneContainerTitle,779});780781try {782pane.render();783} catch (error) {784this.logService.error(`Fail to render view ${viewDescriptor.id}`, error);785continue;786}787if (pane.draggableElement) {788const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => {789e.stopPropagation();790e.preventDefault();791this.onContextMenu(new StandardMouseEvent(getWindow(pane.draggableElement), e), pane);792});793794const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => {795this.viewContainerModel.setCollapsed(viewDescriptor.id, collapsed);796});797798panesToAdd.push({ pane, size: size || pane.minimumSize, index, disposable: combinedDisposable(contextMenuDisposable, collapseDisposable) });799}800}801802this.addPanes(panesToAdd);803this.restoreViewSizes();804805const panes: ViewPane[] = [];806for (const { pane } of panesToAdd) {807pane.setVisible(this.isVisible());808panes.push(pane);809}810return panes;811}812813private onDidRemoveViewDescriptors(removed: IViewDescriptorRef[]): void {814removed = removed.sort((a, b) => b.index - a.index);815const panesToRemove: ViewPane[] = [];816for (const { index } of removed) {817const paneItem = this.paneItems[index];818if (paneItem) {819panesToRemove.push(this.paneItems[index].pane);820}821}822823if (panesToRemove.length) {824this.removePanes(panesToRemove);825826for (const pane of panesToRemove) {827pane.setVisible(false);828}829}830}831832toggleViewVisibility(viewId: string): void {833// Check if view is active834if (this.viewContainerModel.activeViewDescriptors.some(viewDescriptor => viewDescriptor.id === viewId)) {835const visible = !this.viewContainerModel.isVisible(viewId);836this.viewContainerModel.setVisible(viewId, visible);837}838}839840private addPane(pane: ViewPane, size: number, disposable: IDisposable, index = this.paneItems.length - 1): void {841const onDidFocus = pane.onDidFocus(() => {842this._onDidFocusView.fire(pane);843this.lastFocusedPane = pane;844});845const onDidBlur = pane.onDidBlur(() => this._onDidBlurView.fire(pane));846const onDidChangeTitleArea = pane.onDidChangeTitleArea(() => {847if (this.isViewMergedWithContainer()) {848this.updateTitleArea();849}850});851852const onDidChangeVisibility = pane.onDidChangeBodyVisibility(() => this._onDidChangeViewVisibility.fire(pane));853const onDidChange = pane.onDidChange(() => {854if (pane === this.lastFocusedPane && !pane.isExpanded()) {855this.lastFocusedPane = undefined;856}857});858859const isPanel = this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel;860pane.style({861headerForeground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_FOREGROUND : SIDE_BAR_SECTION_HEADER_FOREGROUND),862headerBackground: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BACKGROUND : SIDE_BAR_SECTION_HEADER_BACKGROUND),863headerBorder: asCssVariable(isPanel ? PANEL_SECTION_HEADER_BORDER : SIDE_BAR_SECTION_HEADER_BORDER),864dropBackground: asCssVariable(isPanel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND),865leftBorder: isPanel ? asCssVariable(PANEL_SECTION_BORDER) : undefined866});867868const store = new DisposableStore();869store.add(disposable);870store.add(combinedDisposable(pane, onDidFocus, onDidBlur, onDidChangeTitleArea, onDidChange, onDidChangeVisibility));871const paneItem: IViewPaneItem = { pane, disposable: store };872873this.paneItems.splice(index, 0, paneItem);874assertReturnsDefined(this.paneview).addPane(pane, size, index);875876let overlay: ViewPaneDropOverlay | undefined;877878if (this.viewDescriptorService.canMoveViews()) {879880if (pane.draggableElement) {881store.add(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, () => { return { type: 'view', id: pane.id }; }, {}));882}883884store.add(CompositeDragAndDropObserver.INSTANCE.registerTarget(pane.dropTargetElement, {885onDragEnter: (e) => {886if (!overlay) {887const dropData = e.dragAndDropData.getData();888if (dropData.type === 'view' && dropData.id !== pane.id) {889890const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);891const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);892893if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView || this.viewContainer.rejectAddedViews)) {894return;895}896897overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);898}899900if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {901const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;902const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;903904if (!viewsToMove.some(v => !v.canMoveView) && viewsToMove.length > 0) {905overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService);906}907}908}909},910onDragOver: (e) => {911toggleDropEffect(e.eventData.dataTransfer, 'move', overlay !== undefined);912},913onDragLeave: (e) => {914overlay?.dispose();915overlay = undefined;916},917onDrop: (e) => {918if (overlay) {919const dropData = e.dragAndDropData.getData();920const viewsToMove: IViewDescriptor[] = [];921let anchorView: IViewDescriptor | undefined;922923if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) {924const container = this.viewDescriptorService.getViewContainerById(dropData.id)!;925const allViews = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors;926927if (allViews.length > 0 && !allViews.some(v => !v.canMoveView)) {928viewsToMove.push(...allViews);929anchorView = allViews[0];930}931} else if (dropData.type === 'view') {932const oldViewContainer = this.viewDescriptorService.getViewContainerByViewId(dropData.id);933const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(dropData.id);934if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView && !this.viewContainer.rejectAddedViews) {935viewsToMove.push(viewDescriptor);936}937938if (viewDescriptor) {939anchorView = viewDescriptor;940}941}942943if (viewsToMove) {944this.viewDescriptorService.moveViewsToContainer(viewsToMove, this.viewContainer, undefined, 'dnd');945}946947if (anchorView) {948if (overlay.currentDropOperation === DropDirection.DOWN ||949overlay.currentDropOperation === DropDirection.RIGHT) {950951const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);952let toIndex = this.panes.findIndex(p => p.id === pane.id);953954if (fromIndex >= 0 && toIndex >= 0) {955if (fromIndex > toIndex) {956toIndex++;957}958959if (toIndex < this.panes.length && toIndex !== fromIndex) {960this.movePane(this.panes[fromIndex], this.panes[toIndex]);961}962}963}964965if (overlay.currentDropOperation === DropDirection.UP ||966overlay.currentDropOperation === DropDirection.LEFT) {967const fromIndex = this.panes.findIndex(p => p.id === anchorView!.id);968let toIndex = this.panes.findIndex(p => p.id === pane.id);969970if (fromIndex >= 0 && toIndex >= 0) {971if (fromIndex < toIndex) {972toIndex--;973}974975if (toIndex >= 0 && toIndex !== fromIndex) {976this.movePane(this.panes[fromIndex], this.panes[toIndex]);977}978}979}980981if (viewsToMove.length > 1) {982viewsToMove.slice(1).forEach(view => {983let toIndex = this.panes.findIndex(p => p.id === anchorView!.id);984const fromIndex = this.panes.findIndex(p => p.id === view.id);985if (fromIndex >= 0 && toIndex >= 0) {986if (fromIndex > toIndex) {987toIndex++;988}989990if (toIndex < this.panes.length && toIndex !== fromIndex) {991this.movePane(this.panes[fromIndex], this.panes[toIndex]);992anchorView = view;993}994}995});996}997}998}9991000overlay?.dispose();1001overlay = undefined;1002}1003}));1004}1005}10061007removePanes(panes: ViewPane[]): void {1008const wasMerged = this.isViewMergedWithContainer();10091010panes.forEach(pane => this.removePane(pane));10111012this.updateViewHeaders();1013if (wasMerged !== this.isViewMergedWithContainer()) {1014this.updateTitleArea();1015}10161017this._onDidRemoveViews.fire(panes);1018}10191020private removePane(pane: ViewPane): void {1021const index = this.paneItems.findIndex(i => i.pane === pane);10221023if (index === -1) {1024return;1025}10261027if (this.lastFocusedPane === pane) {1028this.lastFocusedPane = undefined;1029}10301031assertReturnsDefined(this.paneview).removePane(pane);1032const [paneItem] = this.paneItems.splice(index, 1);1033paneItem.disposable.dispose();10341035}10361037movePane(from: ViewPane, to: ViewPane): void {1038const fromIndex = this.paneItems.findIndex(item => item.pane === from);1039const toIndex = this.paneItems.findIndex(item => item.pane === to);10401041const fromViewDescriptor = this.viewContainerModel.visibleViewDescriptors[fromIndex];1042const toViewDescriptor = this.viewContainerModel.visibleViewDescriptors[toIndex];10431044if (fromIndex < 0 || fromIndex >= this.paneItems.length) {1045return;1046}10471048if (toIndex < 0 || toIndex >= this.paneItems.length) {1049return;1050}10511052const [paneItem] = this.paneItems.splice(fromIndex, 1);1053this.paneItems.splice(toIndex, 0, paneItem);10541055assertReturnsDefined(this.paneview).movePane(from, to);10561057this.viewContainerModel.move(fromViewDescriptor.id, toViewDescriptor.id);10581059this.updateTitleArea();1060}10611062resizePane(pane: ViewPane, size: number): void {1063assertReturnsDefined(this.paneview).resizePane(pane, size);1064}10651066getPaneSize(pane: ViewPane): number {1067return assertReturnsDefined(this.paneview).getPaneSize(pane);1068}10691070private updateViewHeaders(): void {1071if (this.isViewMergedWithContainer()) {1072if (this.paneItems[0].pane.isExpanded()) {1073this.lastMergedCollapsedPane = undefined;1074} else {1075this.lastMergedCollapsedPane = this.paneItems[0].pane;1076this.paneItems[0].pane.setExpanded(true);1077}1078this.paneItems[0].pane.headerVisible = false;1079this.paneItems[0].pane.collapsible = true;1080} else {1081if (this.paneItems.length === 1) {1082this.paneItems[0].pane.headerVisible = true;1083if (this.paneItems[0].pane === this.lastMergedCollapsedPane) {1084this.paneItems[0].pane.setExpanded(false);1085}1086this.paneItems[0].pane.collapsible = false;1087} else {1088this.paneItems.forEach(i => {1089i.pane.headerVisible = true;1090i.pane.collapsible = true;1091if (i.pane === this.lastMergedCollapsedPane) {1092i.pane.setExpanded(false);1093}1094});1095}1096this.lastMergedCollapsedPane = undefined;1097}1098}10991100isViewMergedWithContainer(): boolean {1101if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) {1102return false;1103}1104if (!this.areExtensionsReady) {1105if (this.visibleViewsCountFromCache === undefined) {1106return this.paneItems[0].pane.isExpanded();1107}1108// Check in cache so that view do not jump. See #296091109return this.visibleViewsCountFromCache === 1;1110}1111return true;1112}11131114private onDidScrollPane() {1115for (const pane of this.panes) {1116pane.onDidScrollRoot();1117}1118}11191120private onDidSashReset(index: number) {1121let firstPane = undefined;1122let secondPane = undefined;11231124// Deal with collapsed views: to be clever, we split the space taken by the nearest uncollapsed views1125for (let i = index; i >= 0; i--) {1126if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {1127firstPane = this.paneItems[i].pane;1128break;1129}1130}11311132for (let i = index + 1; i < this.paneItems.length; i++) {1133if (this.paneItems[i].pane?.isVisible() && this.paneItems[i]?.pane.isExpanded()) {1134secondPane = this.paneItems[i].pane;1135break;1136}1137}11381139if (firstPane && secondPane) {1140const firstPaneSize = this.getPaneSize(firstPane);1141const secondPaneSize = this.getPaneSize(secondPane);11421143// Avoid rounding errors and be consistent when resizing1144// The first pane always get half rounded up and the second is half rounded down1145const newFirstPaneSize = Math.ceil((firstPaneSize + secondPaneSize) / 2);1146const newSecondPaneSize = Math.floor((firstPaneSize + secondPaneSize) / 2);11471148// Shrink the larger pane first, then grow the smaller pane1149// This prevents interfering with other view sizes1150if (firstPaneSize > secondPaneSize) {1151this.resizePane(firstPane, newFirstPaneSize);1152this.resizePane(secondPane, newSecondPaneSize);1153} else {1154this.resizePane(secondPane, newSecondPaneSize);1155this.resizePane(firstPane, newFirstPaneSize);1156}1157}1158}11591160override dispose(): void {1161super.dispose();1162this.paneItems.forEach(i => i.disposable.dispose());1163if (this.paneview) {1164this.paneview.dispose();1165}1166}1167}11681169export abstract class ViewPaneContainerAction<T extends IViewPaneContainer> extends Action2 {1170override readonly desc: Readonly<IAction2Options> & { viewPaneContainerId: string };1171constructor(desc: Readonly<IAction2Options> & { viewPaneContainerId: string }) {1172super(desc);1173this.desc = desc;1174}11751176run(accessor: ServicesAccessor, ...args: unknown[]): unknown {1177const viewPaneContainer = accessor.get(IViewsService).getActiveViewPaneContainerWithId(this.desc.viewPaneContainerId);1178if (viewPaneContainer) {1179return this.runInViewPaneContainer(accessor, <T>viewPaneContainer, ...args);1180}1181return undefined;1182}11831184abstract runInViewPaneContainer(accessor: ServicesAccessor, viewPaneContainer: T, ...args: unknown[]): unknown;1185}11861187class MoveViewPosition extends Action2 {1188constructor(desc: Readonly<IAction2Options>, private readonly offset: number) {1189super(desc);1190}11911192async run(accessor: ServicesAccessor): Promise<void> {1193const viewDescriptorService = accessor.get(IViewDescriptorService);1194const contextKeyService = accessor.get(IContextKeyService);11951196const viewId = FocusedViewContext.getValue(contextKeyService);1197if (viewId === undefined) {1198return;1199}12001201const viewContainer = viewDescriptorService.getViewContainerByViewId(viewId)!;1202const model = viewDescriptorService.getViewContainerModel(viewContainer);12031204const viewDescriptor = model.visibleViewDescriptors.find(vd => vd.id === viewId)!;1205const currentIndex = model.visibleViewDescriptors.indexOf(viewDescriptor);1206if (currentIndex + this.offset < 0 || currentIndex + this.offset >= model.visibleViewDescriptors.length) {1207return;1208}12091210const newPosition = model.visibleViewDescriptors[currentIndex + this.offset];12111212model.move(viewDescriptor.id, newPosition.id);1213}1214}12151216registerAction2(1217class MoveViewUp extends MoveViewPosition {1218constructor() {1219super({1220id: 'views.moveViewUp',1221title: nls.localize('viewMoveUp', "Move View Up"),1222keybinding: {1223primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.UpArrow),1224weight: KeybindingWeight.WorkbenchContrib + 1,1225when: FocusedViewContext.notEqualsTo('')1226}1227}, -1);1228}1229}1230);12311232registerAction2(1233class MoveViewLeft extends MoveViewPosition {1234constructor() {1235super({1236id: 'views.moveViewLeft',1237title: nls.localize('viewMoveLeft', "Move View Left"),1238keybinding: {1239primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.LeftArrow),1240weight: KeybindingWeight.WorkbenchContrib + 1,1241when: FocusedViewContext.notEqualsTo('')1242}1243}, -1);1244}1245}1246);12471248registerAction2(1249class MoveViewDown extends MoveViewPosition {1250constructor() {1251super({1252id: 'views.moveViewDown',1253title: nls.localize('viewMoveDown', "Move View Down"),1254keybinding: {1255primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.DownArrow),1256weight: KeybindingWeight.WorkbenchContrib + 1,1257when: FocusedViewContext.notEqualsTo('')1258}1259}, 1);1260}1261}1262);12631264registerAction2(1265class MoveViewRight extends MoveViewPosition {1266constructor() {1267super({1268id: 'views.moveViewRight',1269title: nls.localize('viewMoveRight', "Move View Right"),1270keybinding: {1271primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KeyK, KeyCode.RightArrow),1272weight: KeybindingWeight.WorkbenchContrib + 1,1273when: FocusedViewContext.notEqualsTo('')1274}1275}, 1);1276}1277}1278);127912801281registerAction2(class MoveViews extends Action2 {1282constructor() {1283super({1284id: 'vscode.moveViews',1285title: nls.localize('viewsMove', "Move Views"),1286});1287}12881289async run(accessor: ServicesAccessor, options: { viewIds: string[]; destinationId: string }): Promise<void> {1290if (!Array.isArray(options?.viewIds) || typeof options?.destinationId !== 'string') {1291return Promise.reject('Invalid arguments');1292}12931294const viewDescriptorService = accessor.get(IViewDescriptorService);12951296const destination = viewDescriptorService.getViewContainerById(options.destinationId);1297if (!destination) {1298return;1299}13001301// FYI, don't use `moveViewsToContainer` in 1 shot, because it expects all views to have the same current location1302for (const viewId of options.viewIds) {1303const viewDescriptor = viewDescriptorService.getViewDescriptorById(viewId);1304if (viewDescriptor?.canMoveView) {1305viewDescriptorService.moveViewsToContainer([viewDescriptor], destination, ViewVisibilityState.Default, this.desc.id);1306}1307}13081309await accessor.get(IViewsService).openViewContainer(destination.id, true);1310}1311});131213131314