editorDropTarget.ts 22.4 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import 'vs/css!./media/editordroptarget';
7
import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver, containsDragType } from 'vs/workbench/browser/dnd';
8
import { addDisposableListener, EventType, EventHelper, isAncestor, toggleClass, addClass, removeClass } from 'vs/base/browser/dom';
9
import { IEditorGroupsAccessor, EDITOR_TITLE_HEIGHT, IEditorGroupView, getActiveTextEditorOptions } from 'vs/workbench/browser/parts/editor/editor';
10 11
import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IThemeService, Themable } from 'vs/platform/theme/common/themeService';
12
import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
B
Benjamin Pasero 已提交
13
import { IEditorIdentifier, EditorInput, EditorOptions } from 'vs/workbench/common/editor';
14
import { isMacintosh, isWeb } from 'vs/base/common/platform';
15
import { GroupDirection, MergeGroupMode, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
16
import { toDisposable } from 'vs/base/common/lifecycle';
17
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
18
import { RunOnceScheduler } from 'vs/base/common/async';
19
import { DataTransfers } from 'vs/base/browser/dnd';
20 21 22 23
import { VSBuffer } from 'vs/base/common/buffer';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
24
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
25 26 27
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
28

29 30 31 32
interface IDropOperation {
	splitDirection?: GroupDirection;
}

33 34
class DropOverlay extends Themable {

35
	private static readonly OVERLAY_ID = 'monaco-workbench-editor-drop-overlay';
36

37 38 39 40
	private static readonly MAX_FILE_UPLOAD_SIZE = 100 * 1024 * 1024; // 100mb

	private container: HTMLElement | undefined;
	private overlay: HTMLElement | undefined;
41

B
Benjamin Pasero 已提交
42 43
	private currentDropOperation: IDropOperation | undefined;
	private _disposed: boolean | undefined;
44

45 46
	private cleanupOverlayScheduler: RunOnceScheduler;

47 48 49
	private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
	private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();

50
	constructor(
51 52
		private accessor: IEditorGroupsAccessor,
		private groupView: IEditorGroupView,
53 54
		@IThemeService themeService: IThemeService,
		@IInstantiationService private instantiationService: IInstantiationService,
55
		@IFileDialogService private readonly fileDialogService: IFileDialogService,
56 57
		@IEditorService private readonly editorService: IEditorService,
		@INotificationService private readonly notificationService: INotificationService
58 59 60
	) {
		super(themeService);

61 62
		this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300));

63 64 65 66
		this.create();
	}

	get disposed(): boolean {
B
Benjamin Pasero 已提交
67
		return !!this._disposed;
68 69 70 71 72 73
	}

	private create(): void {
		const overlayOffsetHeight = this.getOverlayOffsetHeight();

		// Container
74 75 76
		const container = this.container = document.createElement('div');
		container.id = DropOverlay.OVERLAY_ID;
		container.style.top = `${overlayOffsetHeight}px`;
B
Benjamin Pasero 已提交
77 78

		// Parent
79
		this.groupView.element.appendChild(container);
B
Benjamin Pasero 已提交
80 81
		addClass(this.groupView.element, 'dragged-over');
		this._register(toDisposable(() => {
82
			this.groupView.element.removeChild(container);
B
Benjamin Pasero 已提交
83 84
			removeClass(this.groupView.element, 'dragged-over');
		}));
85 86 87 88

		// Overlay
		this.overlay = document.createElement('div');
		addClass(this.overlay, 'editor-group-overlay-indicator');
89
		container.appendChild(this.overlay);
B
Benjamin Pasero 已提交
90 91

		// Overlay Event Handling
92
		this.registerListeners(container);
B
Benjamin Pasero 已提交
93 94 95 96 97 98

		// Styles
		this.updateStyles();
	}

	protected updateStyles(): void {
99
		const overlay = assertIsDefined(this.overlay);
100 101

		// Overlay drop background
102
		overlay.style.backgroundColor = this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) || '';
103 104 105

		// Overlay contrast border (if any)
		const activeContrastBorderColor = this.getColor(activeContrastBorder);
106 107 108 109
		overlay.style.outlineColor = activeContrastBorderColor || '';
		overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : '';
		overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : '';
		overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : '';
110 111
	}

112 113
	private registerListeners(container: HTMLElement): void {
		this._register(new DragAndDropObserver(container, {
R
Rob Lourens 已提交
114
			onDragEnter: e => undefined,
115
			onDragOver: e => {
116 117
				const isDraggingGroup = this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype);
				const isDraggingEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype);
118 119 120

				// Update the dropEffect to "copy" if there is no local data to be dragged because
				// in that case we can only copy the data into and not move it from its source
121
				if (!isDraggingEditor && !isDraggingGroup && e.dataTransfer) {
122 123
					e.dataTransfer.dropEffect = 'copy';
				}
124

125
				// Find out if operation is valid
126 127 128 129 130 131 132 133 134 135
				let isCopy = true;
				if (isDraggingGroup) {
					isCopy = this.isCopyOperation(e);
				} else if (isDraggingEditor) {
					const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
					if (Array.isArray(data)) {
						isCopy = this.isCopyOperation(e, data[0].identifier);
					}
				}

136 137 138 139 140 141 142 143 144 145
				if (!isCopy) {
					const sourceGroupView = this.findSourceGroupView();
					if (sourceGroupView === this.groupView) {
						if (isDraggingGroup || (isDraggingEditor && sourceGroupView.count < 2)) {
							this.hideOverlay();
							return; // do not allow to drop group/editor on itself if this results in an empty group
						}
					}
				}

146
				// Position overlay
B
Benjamin Pasero 已提交
147
				this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup);
148 149 150 151 152

				// Make sure to stop any running cleanup scheduler to remove the overlay
				if (this.cleanupOverlayScheduler.isScheduled()) {
					this.cleanupOverlayScheduler.cancel();
				}
153
			},
154

155 156
			onDragLeave: e => this.dispose(),
			onDragEnd: e => this.dispose(),
157

158 159
			onDrop: e => {
				EventHelper.stop(e, true);
160

161 162
				// Dispose overlay
				this.dispose();
163

164 165 166 167
				// Handle drop if we have a valid operation
				if (this.currentDropOperation) {
					this.handleDrop(e, this.currentDropOperation.splitDirection);
				}
168
			}
169 170
		}));

171
		this._register(addDisposableListener(container, EventType.MOUSE_OVER, () => {
172 173 174 175 176 177 178
			// 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.
179 180 181
			if (!this.cleanupOverlayScheduler.isScheduled()) {
				this.cleanupOverlayScheduler.schedule();
			}
182 183 184
		}));
	}

185
	private findSourceGroupView(): IEditorGroupView | undefined {
186 187 188

		// Check for group transfer
		if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) {
189 190 191 192
			const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype);
			if (Array.isArray(data)) {
				return this.accessor.getGroup(data[0].identifier);
			}
193 194 195 196
		}

		// Check for editor transfer
		else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
197 198 199 200
			const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
			if (Array.isArray(data)) {
				return this.accessor.getGroup(data[0].identifier.groupId);
			}
201 202
		}

R
Rob Lourens 已提交
203
		return undefined;
204 205 206
	}

	private handleDrop(event: DragEvent, splitDirection?: GroupDirection): void {
207 208

		// Determine target group
209
		const ensureTargetGroup = () => {
210
			let targetGroup: IEditorGroupView;
211
			if (typeof splitDirection === 'number') {
B
Benjamin Pasero 已提交
212
				targetGroup = this.accessor.addGroup(this.groupView, splitDirection);
213 214 215
			} else {
				targetGroup = this.groupView;
			}
216

217 218 219
			return targetGroup;
		};

220
		// Check for group transfer
221
		if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) {
222 223 224 225 226 227 228 229 230 231
			const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype);
			if (Array.isArray(data)) {
				const draggedEditorGroup = data[0].identifier;

				// Return if the drop is a no-op
				const sourceGroup = this.accessor.getGroup(draggedEditorGroup);
				if (sourceGroup) {
					if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) {
						return;
					}
232

233 234 235 236 237 238 239 240 241
					// Split to new group
					let targetGroup: IEditorGroupView | undefined;
					if (typeof splitDirection === 'number') {
						if (this.isCopyOperation(event)) {
							targetGroup = this.accessor.copyGroup(sourceGroup, this.groupView, splitDirection);
						} else {
							targetGroup = this.accessor.moveGroup(sourceGroup, this.groupView, splitDirection);
						}
					}
242

243 244 245 246 247 248 249
					// Merge into existing group
					else {
						if (this.isCopyOperation(event)) {
							targetGroup = this.accessor.mergeGroup(sourceGroup, this.groupView, { mode: MergeGroupMode.COPY_EDITORS });
						} else {
							targetGroup = this.accessor.mergeGroup(sourceGroup, this.groupView);
						}
B
Benjamin Pasero 已提交
250
					}
251

252 253
					if (targetGroup) {
						this.accessor.activateGroup(targetGroup);
B
Benjamin Pasero 已提交
254 255 256
					}
				}

257
				this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype);
258 259 260
			}
		}

261
		// Check for editor transfer
262
		else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) {
263 264 265 266 267 268 269 270 271 272 273
			const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype);
			if (Array.isArray(data)) {
				const draggedEditor = data[0].identifier;
				const targetGroup = ensureTargetGroup();

				// Return if the drop is a no-op
				const sourceGroup = this.accessor.getGroup(draggedEditor.groupId);
				if (sourceGroup) {
					if (sourceGroup === targetGroup) {
						return;
					}
274

275
					// Open in target group
276 277 278 279
					const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({
						pinned: true,										// always pin dropped editor
						sticky: sourceGroup.isSticky(draggedEditor.editor)	// preserve sticky state
					}));
280 281
					const copyEditor = this.isCopyOperation(event, draggedEditor);
					targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR);
282

283 284
					// Ensure target has focus
					targetGroup.focus();
B
Benjamin Pasero 已提交
285

286 287 288 289
					// Close in source group unless we copy
					if (!copyEditor) {
						sourceGroup.closeEditor(draggedEditor.editor);
					}
B
Benjamin Pasero 已提交
290
				}
291

292 293
				this.editorTransfer.clearData(DraggedEditorIdentifier.prototype);
			}
294
		}
295

296 297 298 299 300 301 302 303 304
		// Web: check for file transfer
		else if (isWeb && containsDragType(event, DataTransfers.FILES)) {
			let targetGroup: IEditorGroupView | undefined = undefined;

			const files = event.dataTransfer?.files;
			if (files) {
				for (let i = 0; i < files.length; i++) {
					const file = files.item(i);
					if (file) {
305 306 307 308 309 310 311 312

						// Skip for very large files because this operation is unbuffered
						if (file.size > DropOverlay.MAX_FILE_UPLOAD_SIZE) {
							this.notificationService.warn(localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again."));
							continue;
						}

						// Read file fully and open as untitled editor
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
						const reader = new FileReader();
						reader.readAsArrayBuffer(file);
						reader.onload = async event => {
							const name = file.name;
							if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {

								// Try to come up with a good file path for the untitled
								// editor by asking the file dialog service for the default
								let proposedFilePath: URI | undefined = undefined;
								const defaultFilePath = this.fileDialogService.defaultFilePath();
								if (defaultFilePath) {
									proposedFilePath = joinPath(defaultFilePath, name);
								}

								// Open as untitled file with the provided contents
328
								const untitledEditor = this.editorService.createEditorInput({
329 330 331 332
									resource: proposedFilePath,
									forceUntitled: true,
									contents: VSBuffer.wrap(new Uint8Array(event.target.result)).toString()
								});
333 334 335 336 337 338 339 340 341 342 343 344 345

								if (!targetGroup) {
									targetGroup = ensureTargetGroup();
								}

								await targetGroup.openEditor(untitledEditor);
							}
						};
					}
				}
			}
		}

346 347 348
		// Check for URI transfer
		else {
			const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true /* open workspace instead of file if dropped */ });
B
Benjamin Pasero 已提交
349 350 351 352 353
			dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => {
				if (targetGroup) {
					targetGroup.focus();
				}
			});
354 355 356
		}
	}

357
	private isCopyOperation(e: DragEvent, draggedEditor?: IEditorIdentifier): boolean {
B
Benjamin Pasero 已提交
358
		if (draggedEditor?.editor instanceof EditorInput && !draggedEditor.editor.supportsSplitEditor()) {
B
Benjamin Pasero 已提交
359
			return false;
360 361 362 363 364
		}

		return (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh);
	}

B
Benjamin Pasero 已提交
365 366 367
	private positionOverlay(mousePosX: number, mousePosY: number, isDraggingGroup: boolean): void {
		const preferSplitVertically = this.accessor.partOptions.openSideBySideDirection === 'right';

B
Benjamin Pasero 已提交
368 369
		const editorControlWidth = this.groupView.element.clientWidth;
		const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight();
370

B
Benjamin Pasero 已提交
371 372 373 374 375 376 377 378 379 380 381 382 383 384
		let edgeWidthThresholdFactor: number;
		if (isDraggingGroup) {
			edgeWidthThresholdFactor = preferSplitVertically ? 0.3 : 0.1; // give larger threshold when dragging group depending on preferred split direction
		} else {
			edgeWidthThresholdFactor = 0.1; // 10% threshold to split if dragging editors
		}

		let edgeHeightThresholdFactor: number;
		if (isDraggingGroup) {
			edgeHeightThresholdFactor = preferSplitVertically ? 0.1 : 0.3; // give larger threshold when dragging group depending on preferred split direction
		} else {
			edgeHeightThresholdFactor = 0.1; // 10% threshold to split if dragging editors
		}

B
Benjamin Pasero 已提交
385 386
		const edgeWidthThreshold = editorControlWidth * edgeWidthThresholdFactor;
		const edgeHeightThreshold = editorControlHeight * edgeHeightThresholdFactor;
387

B
Benjamin Pasero 已提交
388 389 390 391 392 393 394 395 396 397 398
		const splitWidthThreshold = editorControlWidth / 3;		// offer to split left/right at 33%
		const splitHeightThreshold = editorControlHeight / 3;	// offer to split up/down at 33%

		// Enable to debug the drop threshold square
		// let child = this.overlay.children.item(0) as HTMLElement || this.overlay.appendChild(document.createElement('div'));
		// child.style.backgroundColor = 'red';
		// child.style.position = 'absolute';
		// child.style.width = (groupViewWidth - (2 * edgeWidthThreshold)) + 'px';
		// child.style.height = (groupViewHeight - (2 * edgeHeightThreshold)) + 'px';
		// child.style.left = edgeWidthThreshold + 'px';
		// child.style.top = edgeHeightThreshold + 'px';
399

400
		// No split if mouse is above certain threshold in the center of the view
401
		let splitDirection: GroupDirection | undefined;
402
		if (
B
Benjamin Pasero 已提交
403 404
			mousePosX > edgeWidthThreshold && mousePosX < editorControlWidth - edgeWidthThreshold &&
			mousePosY > edgeHeightThreshold && mousePosY < editorControlHeight - edgeHeightThreshold
405
		) {
R
Rob Lourens 已提交
406
			splitDirection = undefined;
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
		}

		// Offer to split otherwise
		else {

			// User prefers to split vertically: offer a larger hitzone
			// for this direction like so:
			// ----------------------------------------------
			// |		|		SPLIT UP		|			|
			// | SPLIT 	|-----------------------|	SPLIT	|
			// |		|		  MERGE			|			|
			// | LEFT	|-----------------------|	RIGHT	|
			// |		|		SPLIT DOWN		|			|
			// ----------------------------------------------
			if (preferSplitVertically) {
				if (mousePosX < splitWidthThreshold) {
					splitDirection = GroupDirection.LEFT;
				} else if (mousePosX > splitWidthThreshold * 2) {
					splitDirection = GroupDirection.RIGHT;
B
Benjamin Pasero 已提交
426
				} else if (mousePosY < editorControlHeight / 2) {
427
					splitDirection = GroupDirection.UP;
428
				} else {
429 430
					splitDirection = GroupDirection.DOWN;
				}
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
			}

			// User prefers to split horizontally: offer a larger hitzone
			// for this direction like so:
			// ----------------------------------------------
			// |				SPLIT UP					|
			// |--------------------------------------------|
			// |  SPLIT LEFT  |	   MERGE	|  SPLIT RIGHT  |
			// |--------------------------------------------|
			// |				SPLIT DOWN					|
			// ----------------------------------------------
			else {
				if (mousePosY < splitHeightThreshold) {
					splitDirection = GroupDirection.UP;
				} else if (mousePosY > splitHeightThreshold * 2) {
					splitDirection = GroupDirection.DOWN;
B
Benjamin Pasero 已提交
447
				} else if (mousePosX < editorControlWidth / 2) {
448
					splitDirection = GroupDirection.LEFT;
449
				} else {
450 451
					splitDirection = GroupDirection.RIGHT;
				}
452
			}
453 454
		}

455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
		// Draw overlay based on split direction
		switch (splitDirection) {
			case GroupDirection.UP:
				this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '50%' });
				break;
			case GroupDirection.DOWN:
				this.doPositionOverlay({ top: '50%', left: '0', width: '100%', height: '50%' });
				break;
			case GroupDirection.LEFT:
				this.doPositionOverlay({ top: '0', left: '0', width: '50%', height: '100%' });
				break;
			case GroupDirection.RIGHT:
				this.doPositionOverlay({ top: '0', left: '50%', width: '50%', height: '100%' });
				break;
			default:
				this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' });
471 472 473
		}

		// Make sure the overlay is visible now
474 475
		const overlay = assertIsDefined(this.overlay);
		overlay.style.opacity = '1';
476 477

		// Enable transition after a timeout to prevent initial animation
478
		setTimeout(() => addClass(overlay, 'overlay-move-transition'), 0);
479 480

		// Remember as current split direction
481
		this.currentDropOperation = { splitDirection };
482 483
	}

B
Benjamin Pasero 已提交
484
	private doPositionOverlay(options: { top: string, left: string, width: string, height: string }): void {
485
		const [container, overlay] = assertAllDefined(this.container, this.overlay);
B
Benjamin Pasero 已提交
486

B
Benjamin Pasero 已提交
487
		// Container
B
Benjamin Pasero 已提交
488 489
		const offsetHeight = this.getOverlayOffsetHeight();
		if (offsetHeight) {
490
			container.style.height = `calc(100% - ${offsetHeight}px)`;
B
Benjamin Pasero 已提交
491
		} else {
492
			container.style.height = '100%';
B
Benjamin Pasero 已提交
493
		}
B
Benjamin Pasero 已提交
494 495

		// Overlay
496 497 498 499
		overlay.style.top = options.top;
		overlay.style.left = options.left;
		overlay.style.width = options.width;
		overlay.style.height = options.height;
B
Benjamin Pasero 已提交
500 501
	}

502
	private getOverlayOffsetHeight(): number {
503
		if (!this.groupView.isEmpty && this.accessor.partOptions.showTabs) {
504 505 506 507 508 509
			return EDITOR_TITLE_HEIGHT; // show overlay below title if group shows tabs
		}

		return 0;
	}

510
	private hideOverlay(): void {
511
		const overlay = assertIsDefined(this.overlay);
512 513 514

		// Reset overlay
		this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' });
515 516
		overlay.style.opacity = '0';
		removeClass(overlay, 'overlay-move-transition');
517 518

		// Reset current operation
R
Rob Lourens 已提交
519
		this.currentDropOperation = undefined;
520 521
	}

522
	contains(element: HTMLElement): boolean {
B
Benjamin Pasero 已提交
523
		return element === this.container || element === this.overlay;
524 525 526 527 528 529 530 531 532
	}

	dispose(): void {
		super.dispose();

		this._disposed = true;
	}
}

533 534 535 536 537 538
export interface IEditorDropTargetDelegate {

	/**
	 * A helper to figure out if the drop target contains the provided group.
	 */
	containsGroup?(groupView: IEditorGroupView): boolean;
539 540
}

541
export class EditorDropTarget extends Themable {
542

543
	private _overlay?: DropOverlay;
544

B
Benjamin Pasero 已提交
545
	private counter = 0;
546

547 548 549
	private readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();
	private readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();

550
	constructor(
551
		private accessor: IEditorGroupsAccessor,
552
		private container: HTMLElement,
553
		private readonly delegate: IEditorDropTargetDelegate,
554
		@IThemeService themeService: IThemeService,
555
		@IInstantiationService private readonly instantiationService: IInstantiationService
556 557 558 559 560 561
	) {
		super(themeService);

		this.registerListeners();
	}

562
	private get overlay(): DropOverlay | undefined {
563 564 565 566
		if (this._overlay && !this._overlay.disposed) {
			return this._overlay;
		}

R
Rob Lourens 已提交
567
		return undefined;
568 569 570 571
	}

	private registerListeners(): void {
		this._register(addDisposableListener(this.container, EventType.DRAG_ENTER, e => this.onDragEnter(e)));
572
		this._register(addDisposableListener(this.container, EventType.DRAG_LEAVE, () => this.onDragLeave()));
573 574 575 576
		[this.container, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => this.onDragEnd())));
	}

	private onDragEnter(event: DragEvent): void {
577 578 579
		this.counter++;

		// Validate transfer
580 581 582
		if (
			!this.editorTransfer.hasData(DraggedEditorIdentifier.prototype) &&
			!this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype) &&
583
			event.dataTransfer && !event.dataTransfer.types.length // see https://github.com/Microsoft/vscode/issues/25789
584 585 586
		) {
			event.dataTransfer.dropEffect = 'none';
			return; // unsupported transfer
587 588 589 590 591 592 593 594 595 596
		}

		// Signal DND start
		this.updateContainer(true);

		const target = event.target as HTMLElement;
		if (target) {

			// Somehow we managed to move the mouse quickly out of the current overlay, so destroy it
			if (this.overlay && !this.overlay.contains(target)) {
B
Benjamin Pasero 已提交
597
				this.disposeOverlay();
598 599 600 601
			}

			// Create overlay over target
			if (!this.overlay) {
602
				const targetGroupView = this.findTargetGroupView(target);
603
				if (targetGroupView) {
604
					this._overlay = this.instantiationService.createInstance(DropOverlay, this.accessor, targetGroupView);
605
				}
606 607 608 609 610
			}
		}
	}

	private onDragLeave(): void {
B
Benjamin Pasero 已提交
611
		this.counter--;
612

B
Benjamin Pasero 已提交
613
		if (this.counter === 0) {
614 615 616 617 618
			this.updateContainer(false);
		}
	}

	private onDragEnd(): void {
B
Benjamin Pasero 已提交
619
		this.counter = 0;
620 621

		this.updateContainer(false);
B
Benjamin Pasero 已提交
622
		this.disposeOverlay();
623 624
	}

625
	private findTargetGroupView(child: HTMLElement): IEditorGroupView | undefined {
626
		const groups = this.accessor.groups;
627 628

		return groups.find(groupView => isAncestor(child, groupView.element) || this.delegate.containsGroup?.(groupView));
629 630 631 632 633 634 635 636 637
	}

	private updateContainer(isDraggedOver: boolean): void {
		toggleClass(this.container, 'dragged-over', isDraggedOver);
	}

	dispose(): void {
		super.dispose();

B
Benjamin Pasero 已提交
638 639 640 641
		this.disposeOverlay();
	}

	private disposeOverlay(): void {
642 643
		if (this.overlay) {
			this.overlay.dispose();
R
Rob Lourens 已提交
644
			this._overlay = undefined;
645 646
		}
	}
I
isidor 已提交
647
}