diff --git a/src/vs/base/browser/ui/splitview/paneview.css b/src/vs/base/browser/ui/splitview/paneview.css index c1397d6c62c5d86661a0c1f32d88ee5b00bb81af..adcbb195e5990f66e26ca971f10b8806720216e9 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -99,3 +99,26 @@ .monaco-pane-view.animated.horizontal .split-view-view { transition-property: width; } + +#monaco-workbench-pane-drop-overlay { + position: absolute; + z-index: 10000; + width: 100%; + height: 100%; + left: 0; + box-sizing: border-box; +} + +#monaco-workbench-pane-drop-overlay > .pane-overlay-indicator { + position: absolute; + width: 100%; + height: 100%; + min-height: 22px; + + pointer-events: none; /* very important to not take events away from the parent */ + transition: opacity 150ms ease-out; +} + +#monaco-workbench-pane-drop-overlay > .pane-overlay-indicator.overlay-move-transition { + transition: top 70ms ease-out, left 70ms ease-out, width 70ms ease-out, height 70ms ease-out, opacity 150ms ease-out; +} diff --git a/src/vs/base/parts/composite/browser/compositeDnd.ts b/src/vs/base/parts/composite/browser/compositeDnd.ts deleted file mode 100644 index 6091c8a10d3a1938802361a776c39de4efbfaf81..0000000000000000000000000000000000000000 --- a/src/vs/base/parts/composite/browser/compositeDnd.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDragAndDropData } from 'vs/base/browser/dnd'; - -export class CompositeDragAndDropData implements IDragAndDropData { - constructor(private type: 'view' | 'composite', private id: string) { } - update(dataTransfer: DataTransfer): void { - // no-op - } - getData(): { - type: 'view' | 'composite'; - id: string; - } { - return { type: this.type, id: this.id }; - } -} - -export interface ICompositeDragAndDrop { - drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): void; - onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; - onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; -} diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 5dfa195cfd36554df0791c669a9b682ccb25842c..4119fac3bb22f98bb223ccbd079b27a444e1b7d4 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { Schemas } from 'vs/base/common/network'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; -import { DataTransfers } from 'vs/base/browser/dnd'; +import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { MIME_BINARY } from 'vs/base/common/mime'; @@ -21,7 +21,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorIdentifier, GroupIdentifier } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType, asDomUri } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; @@ -29,6 +29,7 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { isStandalone } from 'vs/base/browser/browser'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { Emitter } from 'vs/base/common/event'; export interface IDraggedResource { resource: URI; @@ -507,3 +508,219 @@ export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]) return false; } + +export interface ICompositeDragAndDrop { + drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent, before?: boolean): void; + onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; + onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; +} + +export interface ICompositeDragAndDropObserverCallbacks { + onDragEnter?: (e: IDraggedCompositeData) => void; + onDragLeave?: (e: IDraggedCompositeData) => void; + onDrop?: (e: IDraggedCompositeData) => void; + onDragOver?: (e: IDraggedCompositeData) => void; + onDragStart?: (e: IDraggedCompositeData) => void; + onDragEnd?: (e: IDraggedCompositeData) => void; +} + +export class CompositeDragAndDropData implements IDragAndDropData { + constructor(private type: 'view' | 'composite', private id: string) { } + update(dataTransfer: DataTransfer): void { + // no-op + } + getData(): { + type: 'view' | 'composite'; + id: string; + } { + return { type: this.type, id: this.id }; + } +} + +export interface IDraggedCompositeData { + eventData: DragEvent; + dragAndDropData: CompositeDragAndDropData; +} + +export class DraggedCompositeIdentifier { + constructor(private _compositeId: string) { } + + get id(): string { + return this._compositeId; + } +} + +export class DraggedViewIdentifier { + constructor(private _viewId: string) { } + + get id(): string { + return this._viewId; + } +} + +export type ViewType = 'composite' | 'view'; + +export class CompositeDragAndDropObserver extends Disposable { + private transferData: LocalSelectionTransfer; + private _onDragStart = this._register(new Emitter()); + private _onDragEnd = this._register(new Emitter()); + private static _instance: CompositeDragAndDropObserver | undefined; + static get INSTANCE(): CompositeDragAndDropObserver { + if (!CompositeDragAndDropObserver._instance) { + CompositeDragAndDropObserver._instance = new CompositeDragAndDropObserver(); + } + return CompositeDragAndDropObserver._instance; + } + private constructor() { + super(); + this.transferData = LocalSelectionTransfer.getInstance(); + } + private readDragData(type: ViewType): CompositeDragAndDropData | undefined { + if (this.transferData.hasData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype)) { + const data = this.transferData.getData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); + if (data && data[0]) { + return new CompositeDragAndDropData(type, data[0].id); + } + } + return undefined; + } + private writeDragData(id: string, type: ViewType): void { + this.transferData.setData([type === 'view' ? new DraggedViewIdentifier(id) : new DraggedCompositeIdentifier(id)], type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); + } + registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { + const disposableStore = new DisposableStore(); + disposableStore.add(new DragAndDropObserver(element, { + onDragEnd: e => { + // no-op + }, + onDragEnter: e => { + e.preventDefault(); + if (callbacks.onDragEnter) { + const data = this.readDragData('composite') || this.readDragData('view'); + if (data) { + callbacks.onDragEnter({ eventData: e, dragAndDropData: data! }); + } + } + }, + onDragLeave: e => { + const data = this.readDragData('composite') || this.readDragData('view'); + if (callbacks.onDragLeave && data) { + callbacks.onDragLeave({ eventData: e, dragAndDropData: data! }); + } + }, + onDrop: e => { + if (callbacks.onDrop) { + const data = this.readDragData('composite') || this.readDragData('view'); + if (!data) { + return; + } + + callbacks.onDrop({ eventData: e, dragAndDropData: data! }); + + // Fire drag event in case drop handler destroys the dragged element + this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + } + }, + onDragOver: e => { + e.preventDefault(); + if (callbacks.onDragOver) { + const data = this.readDragData('composite') || this.readDragData('view'); + if (!data) { + return; + } + + callbacks.onDragOver({ eventData: e, dragAndDropData: data! }); + } + } + })); + if (callbacks.onDragStart) { + this._onDragStart.event(e => { + callbacks.onDragStart!(e); + }, this, disposableStore); + } + if (callbacks.onDragEnd) { + this._onDragEnd.event(e => { + callbacks.onDragEnd!(e); + }); + } + return this._register(disposableStore); + } + registerDraggable(element: HTMLElement, type: ViewType, id: string, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable { + element.draggable = true; + const disposableStore = new DisposableStore(); + disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => { + this.writeDragData(id, type); + this._onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); + })); + disposableStore.add(new DragAndDropObserver(element, { + onDragEnd: e => { + const data = this.readDragData(type); + if (data && data.getData().id === id) { + this.transferData.clearData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype); + } + + if (!data) { + return; + } + + this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + }, + onDragEnter: e => { + if (callbacks.onDragEnter) { + const data = this.readDragData('composite') || this.readDragData('view'); + if (!data) { + return; + } + + if (data) { + callbacks.onDragEnter({ eventData: e, dragAndDropData: data! }); + } + } + }, + onDragLeave: e => { + const data = this.readDragData('composite') || this.readDragData('view'); + if (!data) { + return; + } + + if (callbacks.onDragLeave) { + callbacks.onDragLeave({ eventData: e, dragAndDropData: data! }); + } + }, + onDrop: e => { + if (callbacks.onDrop) { + const data = this.readDragData('composite') || this.readDragData('view'); + + if (!data) { + return; + } + callbacks.onDrop({ eventData: e, dragAndDropData: data! }); + + // Fire drag event in case drop handler destroys the dragged element + this._onDragEnd.fire({ eventData: e, dragAndDropData: data! }); + } + }, + onDragOver: e => { + if (callbacks.onDragOver) { + const data = this.readDragData('composite') || this.readDragData('view'); + if (!data) { + return; + } + + callbacks.onDragOver({ eventData: e, dragAndDropData: data! }); + } + } + })); + if (callbacks.onDragStart) { + this._onDragStart.event(e => { + callbacks.onDragStart!(e); + }, this, disposableStore); + } + if (callbacks.onDragEnd) { + this._onDragEnd.event(e => { + callbacks.onDragEnd!(e); + }); + } + return this._register(disposableStore); + } +} diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 5b9517e3429323c86d2bae9bec35ca00b4228095..3cdeaa2aeade81419b4ccdd0f1af0fbaf7e07627 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { Dimension, addClass, removeNode } from 'vs/base/browser/dom'; @@ -39,6 +39,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; interface ICachedViewlet { id: string; @@ -130,7 +131,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { hidePart: () => this.layoutService.setSideBarHidden(true), dndHandler: new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Sidebar, (id: string, focus?: boolean) => this.viewletService.openViewlet(id, focus), - (from: string, to: string) => this.compositeBar.move(from, to), + (from: string, to: string, before?: boolean) => this.compositeBar.move(from, to, before), () => this.getPinnedViewletIds() ), compositeSize: 50, @@ -298,6 +299,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; + const overlay = document.createElement('div'); + addClass(overlay, 'drag-overlay'); + parent.appendChild(overlay); + this.content = document.createElement('div'); addClass(this.content, 'content'); parent.appendChild(this.content); @@ -317,6 +322,23 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.createGlobalActivityActionBar(globalActivities); + CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { + onDragStart: e => { + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + }, + onDragEnd: e => { + overlay.style.opacity = ''; + }, + onDragEnter: e => { + overlay.style.opacity = ''; + }, + onDragLeave: e => { + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + } + }); + return this.content; } @@ -337,6 +359,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : ''; container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : ''; container.style.borderLeftColor = !isPositionLeft ? borderColor : ''; + // container.style.outlineColor = this.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND) ?? ''; } private getActivitybarItemColors(theme: IColorTheme): ICompositeBarColors { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 5d5cf2b498a75a5b06586cee593c28abe7421a6f..aff76872a069aee3b0a276837acd5ce95cfe15c6 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -9,6 +9,26 @@ margin-bottom: 4px; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.top::before { + content: ''; + width: 48px; + height: 2px; + display: block; + background-color: var(--insert-border-color); + margin-top: -3px; + margin-bottom: 1px; +} + +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.bottom::after { + content: ''; + width: 48px; + height: 2px; + display: block; + background-color: var(--insert-border-color); + margin-top: 1px; + margin-bottom: -3px; +} + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-label { position: relative; z-index: 1; diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 6e3ff5bf0f429fb5d4462c8ed9b5458696ea6d13..3b355971ce7d39a83466b0cee20ce2f4683eb6d8 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -7,6 +7,17 @@ width: 48px; } +.monaco-workbench .part > .drag-overlay { + transition-property: opacity; + transition-duration: .2s; + width: 100%; + height: 100%; + position: absolute; + opacity: 0; + top: 0; + pointer-events: none; +} + .monaco-workbench .activitybar > .content { height: 100%; display: flex; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 51c974473a437574b59cdf6e72f84a568a039004..c9c6b518b7a8b1fdc33626aeccb99a284fd13ad4 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -11,21 +11,19 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ActionBar, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CompositeActionViewItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionViewItem, ActivityAction, ICompositeBar, ICompositeBarColors, DraggedCompositeIdentifier } from 'vs/workbench/browser/parts/compositeBarActions'; +import { CompositeActionViewItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionViewItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositeBarActions'; import { Dimension, $, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { LocalSelectionTransfer, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { Emitter } from 'vs/base/common/event'; -import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; -import { ICompositeDragAndDrop, CompositeDragAndDropData } from 'vs/base/parts/composite/browser/compositeDnd'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IComposite } from 'vs/workbench/common/composite'; +import { CompositeDragAndDropData, CompositeDragAndDropObserver, IDraggedCompositeData, ICompositeDragAndDrop } from 'vs/workbench/browser/dnd'; export interface ICompositeBarItem { id: string; @@ -41,30 +39,39 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { private viewDescriptorService: IViewDescriptorService, private targetContainerLocation: ViewContainerLocation, private openComposite: (id: string, focus?: boolean) => Promise, - private moveComposite: (from: string, to: string) => void, + private moveComposite: (from: string, to: string, before?: boolean) => void, private getVisibleCompositeIds: () => string[] ) { } - drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): void { + drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent, before?: boolean): void { const dragData = data.getData(); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); if (dragData.type === 'composite') { const currentContainer = viewContainerRegistry.get(dragData.id)!; const currentLocation = viewContainerRegistry.getViewContainerLocation(currentContainer); + + // Inserting a composite between composites if (targetCompositeId) { - if (currentLocation !== this.targetContainerLocation && this.targetContainerLocation !== ViewContainerLocation.Panel) { - const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer && !destinationContainer.rejectAddedViews) { - const viewsToMove = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView); - this.viewDescriptorService.moveViewsToContainer(viewsToMove, destinationContainer); - this.openComposite(targetCompositeId, true).then(composite => { + // ... on the same composite bar + if (currentLocation === this.targetContainerLocation) { + this.moveComposite(dragData.id, targetCompositeId, before); + } + // ... on a different composite bar + else { + const viewsToMove = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView); + if (viewsToMove.length === 1) { + this.viewDescriptorService.moveViewToLocation(viewsToMove[0], this.targetContainerLocation); + + const newContainer = this.viewDescriptorService.getViewContainer(viewsToMove[0].id)!; + + this.moveComposite(newContainer.id, targetCompositeId, before); + + this.openComposite(newContainer.id, true).then(composite => { if (composite && viewsToMove.length === 1) { composite.openView(viewsToMove[0].id, true); } }); } - } else { - this.moveComposite(dragData.id, targetCompositeId); } } else { const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer).allViewDescriptors; @@ -76,38 +83,24 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } if (dragData.type === 'view') { - const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dragData.id); - if (viewDescriptor && viewDescriptor.canMoveView) { - if (targetCompositeId) { - const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer && !destinationContainer.rejectAddedViews) { - if (this.targetContainerLocation === ViewContainerLocation.Sidebar || this.targetContainerLocation === ViewContainerLocation.Panel) { - this.viewDescriptorService.moveViewsToContainer([viewDescriptor], destinationContainer); - this.openComposite(targetCompositeId, true).then(composite => { - if (composite) { - composite.openView(viewDescriptor.id, true); - } - }); - } else { - this.viewDescriptorService.moveViewToLocation(viewDescriptor, this.targetContainerLocation); - this.moveComposite(this.viewDescriptorService.getViewContainer(viewDescriptor.id)!.id, targetCompositeId); - } - } - } else { - this.viewDescriptorService.moveViewToLocation(viewDescriptor, this.targetContainerLocation); - const newCompositeId = this.viewDescriptorService.getViewContainer(dragData.id)!.id; - const visibleItems = this.getVisibleCompositeIds(); - const targetId = visibleItems.length ? visibleItems[visibleItems.length - 1] : undefined; - if (targetId && targetId !== newCompositeId) { - this.moveComposite(newCompositeId, targetId); - } + if (targetCompositeId) { + const viewToMove = this.viewDescriptorService.getViewDescriptor(dragData.id)!; + + if (viewToMove && viewToMove.canMoveView) { + this.viewDescriptorService.moveViewToLocation(viewToMove, this.targetContainerLocation); + + const newContainer = this.viewDescriptorService.getViewContainer(viewToMove.id)!; + + this.moveComposite(newContainer.id, targetCompositeId, before); - this.openComposite(newCompositeId, true).then(composite => { + this.openComposite(newContainer.id, true).then(composite => { if (composite) { - composite.openView(viewDescriptor.id, true); + composite.openView(viewToMove.id, true); } }); } + } else { + } } } @@ -129,41 +122,21 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { const currentContainer = viewContainerRegistry.get(dragData.id)!; const currentLocation = viewContainerRegistry.getViewContainerLocation(currentContainer); - // ... to the same location + // ... to the same composite location if (currentLocation === this.targetContainerLocation) { return true; } - // ... across view containers but without a destination composite - if (!targetCompositeId) { - const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors; - if (draggedViews.some(vd => !vd.canMoveView)) { - return false; - } - - if (draggedViews.length !== 1) { - return false; - } - - const defaultLocation = viewContainerRegistry.getViewContainerLocation(this.viewDescriptorService.getDefaultContainer(draggedViews[0].id)!); - if (this.targetContainerLocation === ViewContainerLocation.Sidebar && this.targetContainerLocation !== defaultLocation) { - return false; - } - - return true; - } - - // ... from panel to the sidebar - if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { - const destinationContainer = viewContainerRegistry.get(targetCompositeId); - return !!destinationContainer && - !destinationContainer.rejectAddedViews && - this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.some(vd => vd.canMoveView); - } - // ... from sidebar to the panel - else { + // ... to another composite location + const draggedViews = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors; + if (draggedViews.length !== 1) { return false; } + + // ... single view + const defaultContainer = this.viewDescriptorService.getDefaultContainer(draggedViews[0].id); + const canMoveToDefault = !!defaultContainer && this.viewDescriptorService.getViewContainerLocation(defaultContainer) === this.targetContainerLocation; + return !!draggedViews[0].canMoveView && (!!draggedViews[0].containerIcon || canMoveToDefault || this.targetContainerLocation === ViewContainerLocation.Panel); } else { // Dragging an individual view const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dragData.id); @@ -174,13 +147,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... to create a view container - if (!targetCompositeId) { - return this.targetContainerLocation === ViewContainerLocation.Panel; - } - - // ... into a destination - const destinationContainer = viewContainerRegistry.get(targetCompositeId); - return !!destinationContainer && !destinationContainer.rejectAddedViews; + return this.targetContainerLocation === ViewContainerLocation.Panel || !!viewDescriptor.containerIcon; } } } @@ -215,8 +182,6 @@ export class CompositeBar extends Widget implements ICompositeBar { private visibleComposites: string[]; private compositeSizeInBar: Map; - private compositeTransfer: LocalSelectionTransfer; - private readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -232,7 +197,6 @@ export class CompositeBar extends Widget implements ICompositeBar { this.model = new CompositeBarModel(items, options); this.visibleComposites = []; this.compositeSizeInBar = new Map(); - this.compositeTransfer = LocalSelectionTransfer.getInstance(); this.computeSizes(this.model.visibleItems); } @@ -279,99 +243,21 @@ export class CompositeBar extends Widget implements ICompositeBar { this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); // Allow to drop at the end to move composites to the end - this._register(new DragAndDropObserver(excessDiv, { - onDragOver: (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; - } - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; - } - } - } + this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(excessDiv, { + onDragEnter: (e: IDraggedCompositeData) => { + const pinnedItems = this.getPinnedComposites(); + const validDropTarget = this.options.dndHandler.onDragEnter(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData); + this.updateFromDragging(excessDiv, validDropTarget); }, - onDragEnter: (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - - // Check if drop is allowed - const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); - this.updateFromDragging(excessDiv, validDropTarget); - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - - // Check if drop is allowed - const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('view', draggedViewId), undefined, e); - this.updateFromDragging(excessDiv, validDropTarget); - } - } - }, - - onDragLeave: (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || - this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - this.updateFromDragging(excessDiv, false); - } - }, - onDragEnd: (e: DragEvent) => { - // no-op, will not be called - }, - onDrop: (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); - - this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); - this.updateFromDragging(excessDiv, false); - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); - - this.options.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), undefined, e); - this.updateFromDragging(excessDiv, false); - } - } + onDragLeave: (e: IDraggedCompositeData) => { + this.updateFromDragging(excessDiv, false); }, + onDrop: (e: IDraggedCompositeData) => { + const pinnedItems = this.getPinnedComposites(); + this.options.dndHandler.drop(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData, false); + this.updateFromDragging(excessDiv, false); + } })); return actionBarDiv; @@ -519,10 +405,34 @@ export class CompositeBar extends Widget implements ICompositeBar { return item?.pinned; } - move(compositeId: string, toCompositeId: string): void { - if (this.model.move(compositeId, toCompositeId)) { - // timeout helps to prevent artifacts from showing up - setTimeout(() => this.updateCompositeSwitcher(), 0); + move(compositeId: string, toCompositeId: string, before?: boolean): void { + + if (before !== undefined) { + const fromIndex = this.model.items.findIndex(c => c.id === compositeId); + let toIndex = this.model.items.findIndex(c => c.id === toCompositeId); + + if (fromIndex >= 0 && toIndex >= 0) { + if (!before && fromIndex > toIndex) { + toIndex++; + } + + if (before && fromIndex < toIndex) { + toIndex--; + } + + if (toIndex < this.model.items.length && toIndex >= 0 && toIndex !== fromIndex) { + if (this.model.move(this.model.items[fromIndex].id, this.model.items[toIndex].id)) { + // timeout helps to prevent artifacts from showing up + setTimeout(() => this.updateCompositeSwitcher(), 0); + } + } + } + + } else { + if (this.model.move(compositeId, toCompositeId)) { + // timeout helps to prevent artifacts from showing up + setTimeout(() => this.updateCompositeSwitcher(), 0); + } } } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index bfd1ca055363884ce2ddc1d3457cc7e42f62c53e..53c6766a71a1d59bb84fbf3d3d99943b73a4870e 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -18,10 +18,8 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IActivity } from 'vs/workbench/common/activity'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Emitter } from 'vs/base/common/event'; -import { DragAndDropObserver, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { LocalSelectionTransfer, DraggedCompositeIdentifier, DraggedViewIdentifier, CompositeDragAndDropObserver, ICompositeDragAndDrop } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; -import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { ICompositeDragAndDrop, CompositeDragAndDropData } from 'vs/base/parts/composite/browser/compositeDnd'; export interface ICompositeActivity { badge: IBadge; @@ -167,11 +165,15 @@ export class ActivityActionViewItem extends BaseActionViewItem { // Apply foreground color to activity bar items provided with codicons this.label.style.color = foreground ? foreground.toString() : ''; } + + const dragColor = colors.activeBackgroundColor || colors.activeForegroundColor; + this.container.style.setProperty('--insert-border-color', dragColor ? dragColor.toString() : ''); } else { const foreground = this._action.checked ? colors.activeForegroundColor : colors.inactiveForegroundColor; const borderBottomColor = this._action.checked ? colors.activeBorderBottomColor : null; this.label.style.color = foreground ? foreground.toString() : ''; this.label.style.borderBottomColor = borderBottomColor ? borderBottomColor.toString() : ''; + this.container.style.setProperty('--insert-border-color', colors.activeForegroundColor ? colors.activeForegroundColor.toString() : ''); } } @@ -445,14 +447,6 @@ class ManageExtensionAction extends Action { } } -export class DraggedCompositeIdentifier { - constructor(private _compositeId: string) { } - - get id(): string { - return this._compositeId; - } -} - export class CompositeActionViewItem extends ActivityActionViewItem { private static manageExtensionAction: ManageExtensionAction; @@ -522,105 +516,38 @@ export class CompositeActionViewItem extends ActivityActionViewItem { this.showContextMenu(container); })); + let insertDropBefore: boolean | undefined = undefined; // Allow to drag - this._register(dom.addDisposableListener(this.container, dom.EventType.DRAG_START, (e: DragEvent) => { - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = 'move'; - } - - // Registe as dragged to local transfer - this.compositeTransfer.setData([new DraggedCompositeIdentifier(this.activity.id)], DraggedCompositeIdentifier.prototype); - - // Trigger the action even on drag start to prevent clicks from failing that started a drag - if (!this.getAction().checked) { - this.getAction().run(); - } - })); - - this._register(new DragAndDropObserver(this.container, { - onDragEnter: e => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data) && data[0].id !== this.activity.id) { - const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', data[0].id), this.activity.id, e); - this.updateFromDragging(container, validDropTarget); - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data) && data[0].id !== this.activity.id) { - const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('view', data[0].id), this.activity.id, e); - this.updateFromDragging(container, validDropTarget); - } - } - }, - + this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(this.container, 'composite', this.activity.id, { onDragOver: e => { - dom.EventHelper.stop(e, true); - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - if (draggedCompositeId !== this.activity.id) { - if (e.dataTransfer && !this.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), this.activity.id, e)) { - e.dataTransfer.dropEffect = 'none'; - } - } - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - if (e.dataTransfer && !this.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), this.activity.id, e)) { - e.dataTransfer.dropEffect = 'none'; - } - } - } + const isValidMove = e.dragAndDropData.getData().id !== this.activity.id && this.dndHandler.onDragOver(e.dragAndDropData, this.activity.id, e.eventData); + insertDropBefore = this.updateFromDragging(container, isValidMove, e.eventData); }, onDragLeave: e => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || - this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - this.updateFromDragging(container, false); - } + insertDropBefore = this.updateFromDragging(container, false, e.eventData); }, onDragEnd: e => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - this.updateFromDragging(container, false); - - this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); - } + insertDropBefore = this.updateFromDragging(container, false, e.eventData); }, onDrop: e => { - dom.EventHelper.stop(e, true); - - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - if (draggedCompositeId !== this.activity.id) { - this.updateFromDragging(container, false); - this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); - - this.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), this.activity.id, e); - } - } + this.dndHandler.drop(e.dragAndDropData, this.activity.id, e.eventData, !!insertDropBefore); + insertDropBefore = this.updateFromDragging(container, false, e.eventData); + }, + onDragStart: e => { + if (e.dragAndDropData.getData().id !== this.activity.id) { + return; } - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - this.updateFromDragging(container, false); - this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); + if (e.eventData.dataTransfer) { + e.eventData.dataTransfer.effectAllowed = 'move'; + } - this.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), this.activity.id, e); - } + // Trigger the action even on drag start to prevent clicks from failing that started a drag + if (!this.getAction().checked) { + this.getAction().run(); } } })); @@ -637,11 +564,42 @@ export class CompositeActionViewItem extends ActivityActionViewItem { this.updateStyles(); } - private updateFromDragging(element: HTMLElement, isDragging: boolean): void { - const theme = this.themeService.getColorTheme(); - const dragBackground = this.options.colors(theme).dragAndDropBackground; + private updateFromDragging(element: HTMLElement, showFeedback: boolean, event: DragEvent): boolean | undefined { + const rect = element.getBoundingClientRect(); + const posX = event.clientX; + const posY = event.clientY; + const height = rect.bottom - rect.top; + const width = rect.right - rect.left; + + const forceTop = posY <= rect.top + height * 0.4; + const forceBottom = posY > rect.bottom - height * 0.4; + const preferTop = posY <= rect.top + height * 0.5; + + const forceLeft = posX <= rect.left + width * 0.4; + const forceRight = posX > rect.right - width * 0.4; + const preferLeft = posX <= rect.left + width * 0.5; + + const classes = element.classList; + const lastClasses = { + vertical: classes.contains('top') ? 'top' : (classes.contains('bottom') ? 'bottom' : undefined), + horizontal: classes.contains('left') ? 'left' : (classes.contains('right') ? 'right' : undefined) + }; + + const top = forceTop || (preferTop && !lastClasses.vertical) || (!forceBottom && lastClasses.vertical === 'top'); + const bottom = forceBottom || (!preferTop && !lastClasses.vertical) || (!forceTop && lastClasses.vertical === 'bottom'); + const left = forceLeft || (preferLeft && !lastClasses.horizontal) || (!forceRight && lastClasses.horizontal === 'left'); + const right = forceRight || (!preferLeft && !lastClasses.horizontal) || (!forceLeft && lastClasses.horizontal === 'right'); + + dom.toggleClass(element, 'top', showFeedback && top); + dom.toggleClass(element, 'bottom', showFeedback && bottom); + dom.toggleClass(element, 'left', showFeedback && left); + dom.toggleClass(element, 'right', showFeedback && right); + + if (!showFeedback) { + return undefined; + } - element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : ''; + return top || left; } private showContextMenu(container: HTMLElement): void { diff --git a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css index b961f0defd713f57fd86498ecdaab59488f06873..1c11b159030ca60023326f800ab2404a826469ad 100644 --- a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css +++ b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css @@ -22,8 +22,9 @@ opacity: 0; /* hidden initially */ transition: opacity 150ms ease-out; + /* color: red; */ } #monaco-workbench-editor-drop-overlay > .editor-group-overlay-indicator.overlay-move-transition { transition: top 70ms ease-out, left 70ms ease-out, width 70ms ease-out, height 70ms ease-out, opacity 150ms ease-out; -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index b4f6d8b08bb65040e96dddb3facb3dcbad8fd8f9..fb3ab764c941f88cc83ef26af8e7e891415f8307 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -85,6 +85,26 @@ display: flex; } +.monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.left::before { + content: ''; + width: 2px; + height: 35px; + display: block; + background-color: var(--insert-border-color); + margin-left: -11px; + margin-right: 9px; +} + +.monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item.right::after { + content: ''; + width: 2px; + height: 35px; + display: block; + background-color: var(--insert-border-color); + margin-right: -11px; + margin-left: 9px; +} + .monaco-workbench .part.panel > .composite.title> .panel-switcher-container > .monaco-action-bar .action-item .action-label{ margin-right: 0; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 131c81613e3413a03d096af9e8a93a964d9f3213..ac8210e0c644c59d285c9f1d25c267d265c813b3 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -20,13 +20,13 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER } from 'vs/workbench/common/theme'; +import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension, trackFocus } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addClass } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -37,6 +37,7 @@ import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExte import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; interface ICachedPanel { id: string; @@ -145,7 +146,7 @@ export class PanelPart extends CompositePart implements IPanelService { hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Panel, (id: string, focus?: boolean) => this.openPanel(id, focus) as Promise, - (from: string, to: string) => this.compositeBar.move(from, to), + (from: string, to: string, before?: boolean) => this.compositeBar.move(from, to, before), () => this.getPinnedPanels().map(p => p.id) ), compositeSize: 0, @@ -341,6 +342,31 @@ export class PanelPart extends CompositePart implements IPanelService { super.create(parent); + const overlay = document.createElement('div'); + addClass(overlay, 'drag-overlay'); + parent.appendChild(overlay); + + CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { + onDragStart: e => { + // this.element.style.outline = `1px solid`; + // this.element.style.outlineOffset = '-1px'; + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + }, + onDragEnd: e => { + // this.element.style.outline = ''; + overlay.style.opacity = ''; + }, + onDragEnter: e => { + overlay.style.opacity = ''; + }, + onDragLeave: e => { + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + } + }); + + const focusTracker = this._register(trackFocus(parent)); this._register(focusTracker.onDidFocus(() => this.panelFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.panelFocusContextKey.set(false))); diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 76e0dcce5196cdf632d336426ef8828d2b97a016..51c20b30732b5018b98c414f27948ef7338eea5b 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -23,9 +23,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER } from 'vs/workbench/common/theme'; +import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, trackFocus, addClass } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; @@ -33,9 +33,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { assertIsDefined } from 'vs/base/common/types'; -import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { DraggedCompositeIdentifier } from 'vs/workbench/browser/parts/compositeBarActions'; +import { LocalSelectionTransfer, CompositeDragAndDropObserver, DraggedViewIdentifier, DraggedCompositeIdentifier } from 'vs/workbench/browser/dnd'; export class SidebarPart extends CompositePart implements IViewletService { @@ -154,6 +152,28 @@ export class SidebarPart extends CompositePart implements IViewletServi super.create(parent); + const overlay = document.createElement('div'); + addClass(overlay, 'drag-overlay'); + parent.appendChild(overlay); + + CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { + onDragStart: e => { + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + }, + onDragEnd: e => { + // this.element.style.outline = ''; + overlay.style.opacity = ''; + }, + onDragEnter: e => { + overlay.style.opacity = ''; + }, + onDragLeave: e => { + overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; + overlay.style.opacity = '.8'; + } + }); + const focusTracker = this._register(trackFocus(parent)); this._register(focusTracker.onDidFocus(() => this.sideBarFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.sideBarFocusContextKey.set(false))); @@ -209,6 +229,7 @@ export class SidebarPart extends CompositePart implements IViewletServi container.style.borderLeftWidth = borderColor && !isPositionLeft ? '1px' : ''; container.style.borderLeftStyle = borderColor && !isPositionLeft ? 'solid' : ''; container.style.borderLeftColor = !isPositionLeft ? borderColor || '' : ''; + container.style.outlineColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? ''; } layout(width: number, height: number): void { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 8cc4285aa63f0986f1121c97a16d9d9446598c9d..6264a15277d45543df865162e4d0484a2d85211b 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,9 +6,9 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; -import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; @@ -20,8 +20,8 @@ import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane, DefaultPaneDndController } from 'vs/base/browser/ui/splitview/paneview'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { PaneView, IPaneViewOptions, IPaneOptions, Pane } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -42,11 +42,12 @@ import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Button } from 'vs/base/browser/ui/button/button'; import { Link } from 'vs/platform/opener/browser/link'; -import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { CompositeDragAndDropObserver, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { CompositeProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; +import { RunOnceScheduler } from 'vs/base/common/async'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -61,14 +62,6 @@ export interface IViewPaneOptions extends IPaneOptions { titleMenuId?: MenuId; } -export class DraggedViewIdentifier { - constructor(private _viewId: string) { } - - get id(): string { - return this._viewId; - } -} - type WelcomeActionClassification = { viewId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; uri: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -508,6 +501,210 @@ interface IViewPaneItem { disposable: IDisposable; } +const enum DropDirection { + UP, + DOWN, + LEFT, + RIGHT +} + +class ViewPaneDropOverlay extends Themable { + + private static readonly OVERLAY_ID = 'monaco-workbench-pane-drop-overlay'; + + private container!: HTMLElement; + private overlay!: HTMLElement; + + private _currentDropOperation: DropDirection | undefined; + + // private currentDropOperation: IDropOperation | undefined; + private _disposed: boolean | undefined; + + private cleanupOverlayScheduler: RunOnceScheduler; + + get currentDropOperation(): DropDirection | undefined { + return this._currentDropOperation; + } + + constructor( + private paneElement: HTMLElement, + private orientation: Orientation, + protected themeService: IThemeService + ) { + super(themeService); + this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); + + this.create(); + } + + get disposed(): boolean { + return !!this._disposed; + } + + private create(): void { + // Container + this.container = document.createElement('div'); + this.container.id = ViewPaneDropOverlay.OVERLAY_ID; + + // Parent + this.paneElement.appendChild(this.container); + addClass(this.paneElement, 'dragged-over'); + this._register(toDisposable(() => { + this.paneElement.removeChild(this.container); + removeClass(this.paneElement, 'dragged-over'); + })); + + // Overlay + this.overlay = document.createElement('div'); + addClass(this.overlay, 'pane-overlay-indicator'); + this.container.appendChild(this.overlay); + + // Overlay Event Handling + this.registerListeners(); + + // Styles + this.updateStyles(); + } + + protected updateStyles(): void { + + // Overlay drop background + this.overlay.style.backgroundColor = this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) || ''; + + // Overlay contrast border (if any) + const activeContrastBorderColor = this.getColor(activeContrastBorder); + this.overlay.style.outlineColor = activeContrastBorderColor || ''; + this.overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; + this.overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; + this.overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; + + this.overlay.style.borderColor = activeContrastBorderColor || ''; + this.overlay.style.borderStyle = 'solid' || ''; + } + + private registerListeners(): void { + this._register(new DragAndDropObserver(this.container, { + onDragEnter: e => undefined, + onDragOver: e => { + + // Position overlay + this.positionOverlay(e.offsetX, e.offsetY); + + // Make sure to stop any running cleanup scheduler to remove the overlay + if (this.cleanupOverlayScheduler.isScheduled()) { + this.cleanupOverlayScheduler.cancel(); + } + }, + + onDragLeave: e => this.dispose(), + onDragEnd: e => this.dispose(), + + onDrop: e => { + // Dispose overlay + this.dispose(); + } + })); + + this._register(addDisposableListener(this.container, EventType.MOUSE_OVER, () => { + // Under some circumstances we have seen reports where the drop overlay is not being + // cleaned up and as such the editor area remains under the overlay so that you cannot + // type into the editor anymore. This seems related to using VMs and DND via host and + // guest OS, though some users also saw it without VMs. + // To protect against this issue we always destroy the overlay as soon as we detect a + // mouse event over it. The delay is used to guarantee we are not interfering with the + // actual DROP event that can also trigger a mouse over event. + if (!this.cleanupOverlayScheduler.isScheduled()) { + this.cleanupOverlayScheduler.schedule(); + } + })); + } + + private positionOverlay(mousePosX: number, mousePosY: number): void { + const paneWidth = this.paneElement.clientWidth; + const paneHeight = this.paneElement.clientHeight; + + const splitWidthThreshold = paneWidth / 2; + const splitHeightThreshold = paneHeight / 2; + + let dropDirection: DropDirection | undefined; + + if (this.orientation === Orientation.VERTICAL) { + if (mousePosY < splitHeightThreshold) { + dropDirection = DropDirection.UP; + } else if (mousePosY >= splitHeightThreshold) { + dropDirection = DropDirection.DOWN; + } + } else { + if (mousePosX < splitWidthThreshold) { + dropDirection = DropDirection.LEFT; + } else if (mousePosX >= splitWidthThreshold) { + dropDirection = DropDirection.RIGHT; + } + } + + // Draw overlay based on split direction + switch (dropDirection) { + case DropDirection.UP: + this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' }); + break; + case DropDirection.DOWN: + this.doPositionOverlay({ bottom: '0', left: '0', width: '100%', height: '50%' }); + break; + case DropDirection.LEFT: + this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' }); + break; + case DropDirection.RIGHT: + this.doPositionOverlay({ top: '0', right: '0', width: '50%', height: '100%' }); + break; + default: + this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); + } + + this.doUpdateOverlayBorder(dropDirection); + + // Make sure the overlay is visible now + this.overlay.style.opacity = '1'; + + // Enable transition after a timeout to prevent initial animation + setTimeout(() => addClass(this.overlay, 'overlay-move-transition'), 0); + + // Remember as current split direction + this._currentDropOperation = dropDirection; + } + + private doUpdateOverlayBorder(direction: DropDirection | undefined): void { + this.overlay.style.borderTopWidth = direction === DropDirection.UP ? '2px' : '0px'; + this.overlay.style.borderLeftWidth = direction === DropDirection.LEFT ? '2px' : '0px'; + this.overlay.style.borderBottomWidth = direction === DropDirection.DOWN ? '2px' : '0px'; + this.overlay.style.borderRightWidth = direction === DropDirection.RIGHT ? '2px' : '0px'; + } + + private doPositionOverlay(options: { top?: string, bottom?: string, left?: string, right?: string, width: string, height: string }): void { + + // Container + this.container.style.height = '100%'; + + // Overlay + this.overlay.style.top = options.top || ''; + this.overlay.style.left = options.left || ''; + this.overlay.style.bottom = options.bottom || ''; + this.overlay.style.right = options.right || ''; + this.overlay.style.width = options.width; + this.overlay.style.height = options.height; + } + + + contains(element: HTMLElement): boolean { + return element === this.container || element === this.overlay; + } + + dispose(): void { + super.dispose(); + + this._disposed = true; + } +} + export class ViewPaneContainer extends Component implements IViewPaneContainer { readonly viewContainer: ViewContainer; @@ -515,8 +712,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private paneItems: IViewPaneItem[] = []; private paneview?: PaneView; - private static viewTransfer = LocalSelectionTransfer.getInstance(); - private visible: boolean = false; private areExtensionsReady: boolean = false; @@ -583,10 +778,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { throw new Error('Could not find container'); } - // Use default pane dnd controller if not specified - if (!this.options.dnd) { - this.options.dnd = new DefaultPaneDndController(); - } this.viewContainer = container; this.visibleViewsStorageId = `${id}.numberOfVisibleViews`; @@ -949,19 +1140,104 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.paneItems.splice(index, 0, paneItem); assertIsDefined(this.paneview).addPane(pane, size, index); - this._register(addDisposableListener(pane.draggableElement, EventType.DRAG_START, (e: DragEvent) => { - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = 'move'; - } + let overlay: ViewPaneDropOverlay | undefined; - // Register as dragged to local transfer - ViewPaneContainer.viewTransfer.setData([new DraggedViewIdentifier(pane.id)], DraggedViewIdentifier.prototype); - })); + this._register(CompositeDragAndDropObserver.INSTANCE.registerDraggable(pane.draggableElement, 'view', pane.id, {})); + + this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(pane.dropTargetElement, { + onDragEnter: (e) => { + if (!overlay) { + const dropData = e.dragAndDropData.getData(); + if (dropData.type === 'view' && dropData.id !== pane.id) { + + const oldViewContainer = this.viewDescriptorService.getViewContainer(dropData.id); + const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dropData.id); + + if (oldViewContainer !== this.viewContainer && (!viewDescriptor || !viewDescriptor.canMoveView)) { + return; + } + + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.options.orientation ?? Orientation.VERTICAL, this.themeService); + } + + if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + + const container = viewContainerRegistry.get(dropData.id)!; + const viewsToMove = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + + if (viewsToMove.length === 1 && viewsToMove[0].canMoveView) { + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.options.orientation ?? Orientation.VERTICAL, this.themeService); + } + } + } + }, + onDragLeave: (e) => { + overlay?.dispose(); + overlay = undefined; + }, + onDrop: (e) => { + if (overlay) { + const dropData = e.dragAndDropData.getData(); + + if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + + const container = viewContainerRegistry.get(dropData.id)!; + const viewsToMove = this.viewDescriptorService.getViewDescriptors(container).allViewDescriptors; + + if (viewsToMove.length === 1 && viewsToMove[0].canMoveView) { + dropData.type = 'view'; + dropData.id = viewsToMove[0].id; + } + } + + if (dropData.type === 'view') { + + const oldViewContainer = this.viewDescriptorService.getViewContainer(dropData.id); + const viewDescriptor = this.viewDescriptorService.getViewDescriptor(dropData.id); + if (oldViewContainer !== this.viewContainer && viewDescriptor && viewDescriptor.canMoveView) { + this.viewDescriptorService.moveViewsToContainer([viewDescriptor], this.viewContainer); + } + + if (overlay.currentDropOperation === DropDirection.DOWN || + overlay.currentDropOperation === DropDirection.RIGHT) { + + const fromIndex = this.panes.findIndex(p => p.id === dropData.id); + let toIndex = this.panes.findIndex(p => p.id === pane.id); + + if (fromIndex >= 0 && toIndex >= 0) { + if (fromIndex > toIndex) { + toIndex++; + } + + if (toIndex < this.panes.length && toIndex !== fromIndex) { + this.movePane(this.panes[fromIndex], this.panes[toIndex]); + } + } + } + + if (overlay.currentDropOperation === DropDirection.UP || + overlay.currentDropOperation === DropDirection.LEFT) { + const fromIndex = this.panes.findIndex(p => p.id === dropData.id); + let toIndex = this.panes.findIndex(p => p.id === pane.id); + + if (fromIndex >= 0 && toIndex >= 0) { + if (fromIndex < toIndex) { + toIndex--; + } + + if (toIndex >= 0 && toIndex !== fromIndex) { + this.movePane(this.panes[fromIndex], this.panes[toIndex]); + } + } + } + } + } - this._register(addDisposableListener(pane.draggableElement, EventType.DRAG_END, (e: DragEvent) => { - if (ViewPaneContainer.viewTransfer.hasData(DraggedViewIdentifier.prototype)) { - ViewPaneContainer.viewTransfer.clearData(DraggedViewIdentifier.prototype); + overlay?.dispose(); + overlay = undefined; } })); } diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 9a8b2d91a6aa9fae19a40421ada5c416e46b86d2..cabfa6c8365985efba5123af534ad56b5d584b85 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -445,11 +445,6 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor } moveViewToLocation(view: IViewDescriptor, location: ViewContainerLocation): void { - const previousContainer = this.getViewContainer(view.id); - if (previousContainer && this.getViewContainerLocation(previousContainer) === location) { - return; - } - let container = this.getDefaultContainer(view.id)!; if (this.getViewContainerLocation(container) !== location) { container = this.registerViewContainerForSingleView(view, location);