Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/dnd.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { DataTransfers, IDragAndDropData } from '../../base/browser/dnd.js';
7
import { DragAndDropObserver, EventType, addDisposableListener, onDidRegisterWindow } from '../../base/browser/dom.js';
8
import { DragMouseEvent } from '../../base/browser/mouseEvent.js';
9
import { IListDragAndDrop } from '../../base/browser/ui/list/list.js';
10
import { ElementsDragAndDropData, ListViewTargetSector } from '../../base/browser/ui/list/listView.js';
11
import { ITreeDragOverReaction } from '../../base/browser/ui/tree/tree.js';
12
import { coalesce } from '../../base/common/arrays.js';
13
import { UriList, VSDataTransfer } from '../../base/common/dataTransfer.js';
14
import { Emitter, Event } from '../../base/common/event.js';
15
import { Disposable, DisposableStore, IDisposable, markAsSingleton } from '../../base/common/lifecycle.js';
16
import { stringify } from '../../base/common/marshalling.js';
17
import { Mimes } from '../../base/common/mime.js';
18
import { FileAccess, Schemas } from '../../base/common/network.js';
19
import { isWindows } from '../../base/common/platform.js';
20
import { basename, isEqual } from '../../base/common/resources.js';
21
import { URI } from '../../base/common/uri.js';
22
import { CodeDataTransfers, Extensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput, IResourceStat, LocalSelectionTransfer, createDraggedEditorInputFromRawResourcesData, extractEditorsAndFilesDropData } from '../../platform/dnd/browser/dnd.js';
23
import { IFileService } from '../../platform/files/common/files.js';
24
import { IInstantiationService, ServicesAccessor } from '../../platform/instantiation/common/instantiation.js';
25
import { ILabelService } from '../../platform/label/common/label.js';
26
import { extractSelection, withSelection } from '../../platform/opener/common/opener.js';
27
import { Registry } from '../../platform/registry/common/platform.js';
28
import { IWindowOpenable } from '../../platform/window/common/window.js';
29
import { IWorkspaceContextService, hasWorkspaceFileExtension, isTemporaryWorkspace } from '../../platform/workspace/common/workspace.js';
30
import { IWorkspaceFolderCreationData, IWorkspacesService } from '../../platform/workspaces/common/workspaces.js';
31
import { EditorResourceAccessor, GroupIdentifier, IEditorIdentifier, isEditorIdentifier, isResourceDiffEditorInput, isResourceMergeEditorInput, isResourceSideBySideEditorInput } from '../common/editor.js';
32
import { IEditorGroup } from '../services/editor/common/editorGroupsService.js';
33
import { IEditorService } from '../services/editor/common/editorService.js';
34
import { IHostService } from '../services/host/browser/host.js';
35
import { ITextFileService } from '../services/textfile/common/textfiles.js';
36
import { IWorkspaceEditingService } from '../services/workspaces/common/workspaceEditing.js';
37
import { IEditorOptions } from '../../platform/editor/common/editor.js';
38
import { mainWindow } from '../../base/browser/window.js';
39
import { BroadcastDataChannel } from '../../base/browser/broadcast.js';
40
41
//#region Editor / Resources DND
42
43
export class DraggedEditorIdentifier {
44
45
constructor(readonly identifier: IEditorIdentifier) { }
46
}
47
48
export class DraggedEditorGroupIdentifier {
49
50
constructor(readonly identifier: GroupIdentifier) { }
51
}
52
53
export async function extractTreeDropData(dataTransfer: VSDataTransfer): Promise<Array<IDraggedResourceEditorInput>> {
54
const editors: IDraggedResourceEditorInput[] = [];
55
const resourcesKey = Mimes.uriList.toLowerCase();
56
57
// Data Transfer: Resources
58
if (dataTransfer.has(resourcesKey)) {
59
try {
60
const asString = await dataTransfer.get(resourcesKey)?.asString();
61
const rawResourcesData = JSON.stringify(UriList.parse(asString ?? ''));
62
editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData));
63
} catch (error) {
64
// Invalid transfer
65
}
66
}
67
68
return editors;
69
}
70
71
export interface IResourcesDropHandlerOptions {
72
73
/**
74
* Whether we probe for the dropped resource to be a workspace
75
* (i.e. code-workspace file or even a folder), allowing to
76
* open it as workspace instead of opening as editor.
77
*/
78
readonly allowWorkspaceOpen: boolean;
79
}
80
81
/**
82
* Shared function across some components to handle drag & drop of resources.
83
* E.g. of folders and workspace files to open them in the window instead of
84
* the editor or to handle dirty editors being dropped between instances of Code.
85
*/
86
export class ResourcesDropHandler {
87
88
constructor(
89
private readonly options: IResourcesDropHandlerOptions,
90
@IFileService private readonly fileService: IFileService,
91
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
92
@IEditorService private readonly editorService: IEditorService,
93
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
94
@IHostService private readonly hostService: IHostService,
95
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
96
@IInstantiationService private readonly instantiationService: IInstantiationService,
97
) {
98
}
99
100
async handleDrop(event: DragEvent, targetWindow: Window, resolveTargetGroup?: () => IEditorGroup | undefined, afterDrop?: (targetGroup: IEditorGroup | undefined) => void, options?: IEditorOptions): Promise<void> {
101
const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, event));
102
if (!editors.length) {
103
return;
104
}
105
106
// Make the window active to handle the drop properly within
107
await this.hostService.focus(targetWindow);
108
109
// Check for registered drop handlers
110
const dndRegistry = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution);
111
for (const { resource } of editors) {
112
if (resource) {
113
const handled = await this.instantiationService.invokeFunction(accessor => dndRegistry.handleResourceDrop(resource, accessor));
114
if (handled) {
115
return;
116
}
117
}
118
}
119
120
// Check for workspace file / folder being dropped if we are allowed to do so
121
if (this.options.allowWorkspaceOpen) {
122
const localFilesAllowedToOpenAsWorkspace = coalesce(editors.filter(editor => editor.allowWorkspaceOpen && editor.resource?.scheme === Schemas.file).map(editor => editor.resource));
123
if (localFilesAllowedToOpenAsWorkspace.length > 0) {
124
const isWorkspaceOpening = await this.handleWorkspaceDrop(localFilesAllowedToOpenAsWorkspace);
125
if (isWorkspaceOpening) {
126
return; // return early if the drop operation resulted in this window changing to a workspace
127
}
128
}
129
}
130
131
// Add external ones to recently open list unless dropped resource is a workspace
132
const externalLocalFiles = coalesce(editors.filter(editor => editor.isExternal && editor.resource?.scheme === Schemas.file).map(editor => editor.resource));
133
if (externalLocalFiles.length) {
134
this.workspacesService.addRecentlyOpened(externalLocalFiles.map(resource => ({ fileUri: resource })));
135
}
136
137
// Open in Editor
138
const targetGroup = resolveTargetGroup?.();
139
await this.editorService.openEditors(editors.map(editor => ({
140
...editor,
141
resource: editor.resource,
142
options: {
143
...editor.options,
144
...options,
145
pinned: true
146
}
147
})), targetGroup, { validateTrust: true });
148
149
// Finish with provided function
150
afterDrop?.(targetGroup);
151
}
152
153
private async handleWorkspaceDrop(resources: URI[]): Promise<boolean> {
154
const toOpen: IWindowOpenable[] = [];
155
const folderURIs: IWorkspaceFolderCreationData[] = [];
156
157
await Promise.all(resources.map(async resource => {
158
159
// Check for Workspace
160
if (hasWorkspaceFileExtension(resource)) {
161
toOpen.push({ workspaceUri: resource });
162
163
return;
164
}
165
166
// Check for Folder
167
try {
168
const stat = await this.fileService.stat(resource);
169
if (stat.isDirectory) {
170
toOpen.push({ folderUri: stat.resource });
171
folderURIs.push({ uri: stat.resource });
172
}
173
} catch (error) {
174
// Ignore error
175
}
176
}));
177
178
// Return early if no external resource is a folder or workspace
179
if (toOpen.length === 0) {
180
return false;
181
}
182
183
// Open in separate windows if we drop workspaces or just one folder
184
if (toOpen.length > folderURIs.length || folderURIs.length === 1) {
185
await this.hostService.openWindow(toOpen);
186
}
187
188
// Add to workspace if we are in a temporary workspace
189
else if (isTemporaryWorkspace(this.contextService.getWorkspace())) {
190
await this.workspaceEditingService.addFolders(folderURIs);
191
}
192
193
// Finally, enter untitled workspace when dropping >1 folders
194
else {
195
await this.workspaceEditingService.createAndEnterWorkspace(folderURIs);
196
}
197
198
return true;
199
}
200
}
201
202
export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent, options?: { disableStandardTransfer: boolean }): void;
203
export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent, options?: { disableStandardTransfer: boolean }): void;
204
export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent, options?: { disableStandardTransfer: boolean }): void;
205
export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEditors: Array<URI | IResourceStat | IEditorIdentifier>, event: DragMouseEvent | DragEvent, options?: { disableStandardTransfer: boolean }): void {
206
if (resourcesOrEditors.length === 0 || !event.dataTransfer) {
207
return;
208
}
209
210
const textFileService = accessor.get(ITextFileService);
211
const editorService = accessor.get(IEditorService);
212
const fileService = accessor.get(IFileService);
213
const labelService = accessor.get(ILabelService);
214
215
// Extract resources from URIs or Editors that
216
// can be handled by the file service
217
const resources = coalesce(resourcesOrEditors.map((resourceOrEditor): IResourceStat | undefined => {
218
if (URI.isUri(resourceOrEditor)) {
219
return { resource: resourceOrEditor };
220
}
221
222
if (isEditorIdentifier(resourceOrEditor)) {
223
if (URI.isUri(resourceOrEditor.editor.resource)) {
224
return { resource: resourceOrEditor.editor.resource };
225
}
226
227
return undefined; // editor without resource
228
}
229
230
return {
231
resource: resourceOrEditor.selection ? withSelection(resourceOrEditor.resource, resourceOrEditor.selection) : resourceOrEditor.resource,
232
isDirectory: resourceOrEditor.isDirectory,
233
selection: resourceOrEditor.selection,
234
};
235
}));
236
237
const fileSystemResources = resources.filter(({ resource }) => fileService.hasProvider(resource));
238
if (!options?.disableStandardTransfer) {
239
240
// Text: allows to paste into text-capable areas
241
const lineDelimiter = isWindows ? '\r\n' : '\n';
242
event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter));
243
244
// Download URL: enables support to drag a tab as file to desktop
245
// Requirements:
246
// - Chrome/Edge only
247
// - only a single file is supported
248
// - only file:/ resources are supported
249
const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory);
250
if (firstFile) {
251
const firstFileUri = FileAccess.uriToFileUri(firstFile.resource); // enforce `file:` URIs
252
if (firstFileUri.scheme === Schemas.file) {
253
event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [Mimes.binary, basename(firstFile.resource), firstFileUri.toString()].join(':'));
254
}
255
}
256
}
257
258
// Resource URLs: allows to drop multiple file resources to a target in VS Code
259
const files = fileSystemResources.filter(({ isDirectory }) => !isDirectory);
260
if (files.length) {
261
event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(files.map(({ resource }) => resource.toString())));
262
}
263
264
// Contributions
265
const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll();
266
for (const contribution of contributions) {
267
contribution.setData(resources, event);
268
}
269
270
// Editors: enables cross window DND of editors
271
// into the editor area while presering UI state
272
const draggedEditors: IDraggedResourceEditorInput[] = [];
273
274
for (const resourceOrEditor of resourcesOrEditors) {
275
276
// Extract resource editor from provided object or URI
277
let editor: IDraggedResourceEditorInput | undefined = undefined;
278
if (isEditorIdentifier(resourceOrEditor)) {
279
const untypedEditor = resourceOrEditor.editor.toUntyped({ preserveViewState: resourceOrEditor.groupId });
280
if (untypedEditor) {
281
editor = { ...untypedEditor, resource: EditorResourceAccessor.getCanonicalUri(untypedEditor) };
282
}
283
} else if (URI.isUri(resourceOrEditor)) {
284
const { selection, uri } = extractSelection(resourceOrEditor);
285
editor = { resource: uri, options: selection ? { selection } : undefined };
286
} else if (!resourceOrEditor.isDirectory) {
287
editor = {
288
resource: resourceOrEditor.resource,
289
options: {
290
selection: resourceOrEditor.selection,
291
}
292
};
293
}
294
295
if (!editor) {
296
continue; // skip over editors that cannot be transferred via dnd
297
}
298
299
// Fill in some properties if they are not there already by accessing
300
// some well known things from the text file universe.
301
// This is not ideal for custom editors, but those have a chance to
302
// provide everything from the `toUntyped` method.
303
{
304
const resource = editor.resource;
305
if (resource) {
306
const textFileModel = textFileService.files.get(resource);
307
if (textFileModel) {
308
309
// language
310
if (typeof editor.languageId !== 'string') {
311
editor.languageId = textFileModel.getLanguageId();
312
}
313
314
// encoding
315
if (typeof editor.encoding !== 'string') {
316
editor.encoding = textFileModel.getEncoding();
317
}
318
319
// contents (only if dirty and not too large)
320
if (typeof editor.contents !== 'string' && textFileModel.isDirty() && !textFileModel.textEditorModel.isTooLargeForHeapOperation()) {
321
editor.contents = textFileModel.textEditorModel.getValue();
322
}
323
}
324
325
// viewState
326
if (!editor.options?.viewState) {
327
editor.options = {
328
...editor.options,
329
viewState: (() => {
330
for (const visibleEditorPane of editorService.visibleEditorPanes) {
331
if (isEqual(visibleEditorPane.input.resource, resource)) {
332
const viewState = visibleEditorPane.getViewState();
333
if (viewState) {
334
return viewState;
335
}
336
}
337
}
338
339
return undefined;
340
})()
341
};
342
}
343
}
344
}
345
346
// Add as dragged editor
347
draggedEditors.push(editor);
348
}
349
350
if (draggedEditors.length) {
351
event.dataTransfer.setData(CodeDataTransfers.EDITORS, stringify(draggedEditors));
352
}
353
354
// Add a URI list entry
355
const draggedDirectories: URI[] = fileSystemResources.filter(({ isDirectory }) => isDirectory).map(({ resource }) => resource);
356
if (draggedEditors.length || draggedDirectories.length) {
357
const uriListEntries: URI[] = [...draggedDirectories];
358
for (const editor of draggedEditors) {
359
if (editor.resource) {
360
uriListEntries.push(editor.options?.selection ? withSelection(editor.resource, editor.options.selection) : editor.resource);
361
} else if (isResourceDiffEditorInput(editor)) {
362
if (editor.modified.resource) {
363
uriListEntries.push(editor.modified.resource);
364
}
365
} else if (isResourceSideBySideEditorInput(editor)) {
366
if (editor.primary.resource) {
367
uriListEntries.push(editor.primary.resource);
368
}
369
} else if (isResourceMergeEditorInput(editor)) {
370
uriListEntries.push(editor.result.resource);
371
}
372
}
373
374
// Due to https://bugs.chromium.org/p/chromium/issues/detail?id=239745, we can only set
375
// a single uri for the real `text/uri-list` type. Otherwise all uris end up joined together
376
// However we write the full uri-list to an internal type so that other parts of VS Code
377
// can use the full list.
378
if (!options?.disableStandardTransfer) {
379
event.dataTransfer.setData(Mimes.uriList, UriList.create(uriListEntries.slice(0, 1)));
380
}
381
event.dataTransfer.setData(DataTransfers.INTERNAL_URI_LIST, UriList.create(uriListEntries));
382
}
383
}
384
385
//#endregion
386
387
//#region Composites DND
388
389
export type Before2D = {
390
readonly verticallyBefore: boolean;
391
readonly horizontallyBefore: boolean;
392
};
393
394
export interface ICompositeDragAndDrop {
395
drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent, before?: Before2D): void;
396
onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean;
397
onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean;
398
}
399
400
export interface ICompositeDragAndDropObserverCallbacks {
401
onDragEnter?: (e: IDraggedCompositeData) => void;
402
onDragLeave?: (e: IDraggedCompositeData) => void;
403
onDrop?: (e: IDraggedCompositeData) => void;
404
onDragOver?: (e: IDraggedCompositeData) => void;
405
onDragStart?: (e: IDraggedCompositeData) => void;
406
onDragEnd?: (e: IDraggedCompositeData) => void;
407
}
408
409
export class CompositeDragAndDropData implements IDragAndDropData {
410
411
constructor(private type: 'view' | 'composite', private id: string) { }
412
413
update(dataTransfer: DataTransfer): void {
414
// no-op
415
}
416
417
getData(): {
418
type: 'view' | 'composite';
419
id: string;
420
} {
421
return { type: this.type, id: this.id };
422
}
423
}
424
425
export interface IDraggedCompositeData {
426
readonly eventData: DragEvent;
427
readonly dragAndDropData: CompositeDragAndDropData;
428
}
429
430
export class DraggedCompositeIdentifier {
431
432
constructor(private compositeId: string) { }
433
434
get id(): string {
435
return this.compositeId;
436
}
437
}
438
439
export class DraggedViewIdentifier {
440
441
constructor(private viewId: string) { }
442
443
get id(): string {
444
return this.viewId;
445
}
446
}
447
448
export type ViewType = 'composite' | 'view';
449
450
export class CompositeDragAndDropObserver extends Disposable {
451
452
private static instance: CompositeDragAndDropObserver | undefined;
453
454
static get INSTANCE(): CompositeDragAndDropObserver {
455
if (!CompositeDragAndDropObserver.instance) {
456
CompositeDragAndDropObserver.instance = new CompositeDragAndDropObserver();
457
markAsSingleton(CompositeDragAndDropObserver.instance);
458
}
459
460
return CompositeDragAndDropObserver.instance;
461
}
462
463
private readonly transferData = LocalSelectionTransfer.getInstance<DraggedCompositeIdentifier | DraggedViewIdentifier>();
464
465
private readonly onDragStart = this._register(new Emitter<IDraggedCompositeData>());
466
private readonly onDragEnd = this._register(new Emitter<IDraggedCompositeData>());
467
468
private constructor() {
469
super();
470
471
this._register(this.onDragEnd.event(e => {
472
const id = e.dragAndDropData.getData().id;
473
const type = e.dragAndDropData.getData().type;
474
const data = this.readDragData(type);
475
if (data?.getData().id === id) {
476
this.transferData.clearData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
477
}
478
}));
479
}
480
481
private readDragData(type: ViewType): CompositeDragAndDropData | undefined {
482
if (this.transferData.hasData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype)) {
483
const data = this.transferData.getData(type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
484
if (data?.[0]) {
485
return new CompositeDragAndDropData(type, data[0].id);
486
}
487
}
488
489
return undefined;
490
}
491
492
private writeDragData(id: string, type: ViewType): void {
493
this.transferData.setData([type === 'view' ? new DraggedViewIdentifier(id) : new DraggedCompositeIdentifier(id)], type === 'view' ? DraggedViewIdentifier.prototype : DraggedCompositeIdentifier.prototype);
494
}
495
496
registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable {
497
const disposableStore = new DisposableStore();
498
disposableStore.add(new DragAndDropObserver(element, {
499
onDragEnter: e => {
500
e.preventDefault();
501
502
if (callbacks.onDragEnter) {
503
const data = this.readDragData('composite') || this.readDragData('view');
504
if (data) {
505
callbacks.onDragEnter({ eventData: e, dragAndDropData: data });
506
}
507
}
508
},
509
onDragLeave: e => {
510
const data = this.readDragData('composite') || this.readDragData('view');
511
if (callbacks.onDragLeave && data) {
512
callbacks.onDragLeave({ eventData: e, dragAndDropData: data });
513
}
514
},
515
onDrop: e => {
516
if (callbacks.onDrop) {
517
const data = this.readDragData('composite') || this.readDragData('view');
518
if (!data) {
519
return;
520
}
521
522
callbacks.onDrop({ eventData: e, dragAndDropData: data });
523
524
// Fire drag event in case drop handler destroys the dragged element
525
this.onDragEnd.fire({ eventData: e, dragAndDropData: data });
526
}
527
},
528
onDragOver: e => {
529
e.preventDefault();
530
531
if (callbacks.onDragOver) {
532
const data = this.readDragData('composite') || this.readDragData('view');
533
if (!data) {
534
return;
535
}
536
537
callbacks.onDragOver({ eventData: e, dragAndDropData: data });
538
}
539
}
540
}));
541
542
if (callbacks.onDragStart) {
543
this.onDragStart.event(e => {
544
callbacks.onDragStart!(e);
545
}, this, disposableStore);
546
}
547
548
if (callbacks.onDragEnd) {
549
this.onDragEnd.event(e => {
550
callbacks.onDragEnd!(e);
551
}, this, disposableStore);
552
}
553
554
return this._register(disposableStore);
555
}
556
557
registerDraggable(element: HTMLElement, draggedItemProvider: () => { type: ViewType; id: string }, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable {
558
element.draggable = true;
559
560
const disposableStore = new DisposableStore();
561
562
disposableStore.add(new DragAndDropObserver(element, {
563
onDragStart: e => {
564
const { id, type } = draggedItemProvider();
565
this.writeDragData(id, type);
566
567
e.dataTransfer?.setDragImage(element, 0, 0);
568
569
this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! });
570
},
571
onDragEnd: e => {
572
const { type } = draggedItemProvider();
573
const data = this.readDragData(type);
574
if (!data) {
575
return;
576
}
577
578
this.onDragEnd.fire({ eventData: e, dragAndDropData: data });
579
},
580
onDragEnter: e => {
581
if (callbacks.onDragEnter) {
582
const data = this.readDragData('composite') || this.readDragData('view');
583
if (!data) {
584
return;
585
}
586
587
if (data) {
588
callbacks.onDragEnter({ eventData: e, dragAndDropData: data });
589
}
590
}
591
},
592
onDragLeave: e => {
593
const data = this.readDragData('composite') || this.readDragData('view');
594
if (!data) {
595
return;
596
}
597
598
callbacks.onDragLeave?.({ eventData: e, dragAndDropData: data });
599
},
600
onDrop: e => {
601
if (callbacks.onDrop) {
602
const data = this.readDragData('composite') || this.readDragData('view');
603
if (!data) {
604
return;
605
}
606
607
callbacks.onDrop({ eventData: e, dragAndDropData: data });
608
609
// Fire drag event in case drop handler destroys the dragged element
610
this.onDragEnd.fire({ eventData: e, dragAndDropData: data });
611
}
612
},
613
onDragOver: e => {
614
if (callbacks.onDragOver) {
615
const data = this.readDragData('composite') || this.readDragData('view');
616
if (!data) {
617
return;
618
}
619
620
callbacks.onDragOver({ eventData: e, dragAndDropData: data });
621
}
622
}
623
}));
624
625
if (callbacks.onDragStart) {
626
this.onDragStart.event(e => {
627
callbacks.onDragStart!(e);
628
}, this, disposableStore);
629
}
630
631
if (callbacks.onDragEnd) {
632
this.onDragEnd.event(e => {
633
callbacks.onDragEnd!(e);
634
}, this, disposableStore);
635
}
636
637
return this._register(disposableStore);
638
}
639
}
640
641
export function toggleDropEffect(dataTransfer: DataTransfer | null, dropEffect: 'none' | 'copy' | 'link' | 'move', shouldHaveIt: boolean) {
642
if (!dataTransfer) {
643
return;
644
}
645
646
dataTransfer.dropEffect = shouldHaveIt ? dropEffect : 'none';
647
}
648
649
export class ResourceListDnDHandler<T> implements IListDragAndDrop<T> {
650
constructor(
651
private readonly toResource: (e: T) => URI | null,
652
@IInstantiationService private readonly instantiationService: IInstantiationService
653
) { }
654
655
getDragURI(element: T): string | null {
656
const resource = this.toResource(element);
657
return resource ? resource.toString() : null;
658
}
659
660
getDragLabel(elements: T[]): string | undefined {
661
const resources = coalesce(elements.map(this.toResource));
662
return resources.length === 1 ? basename(resources[0]) : resources.length > 1 ? String(resources.length) : undefined;
663
}
664
665
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
666
const resources: URI[] = [];
667
const elements = (data as ElementsDragAndDropData<T>).elements;
668
for (const element of elements) {
669
const resource = this.toResource(element);
670
if (resource) {
671
resources.push(resource);
672
}
673
}
674
this.onWillDragElements(elements, originalEvent);
675
if (resources.length) {
676
// Apply some datatransfer types to allow for dragging the element outside of the application
677
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
678
}
679
}
680
681
protected onWillDragElements(elements: readonly T[], originalEvent: DragEvent): void {
682
// noop
683
}
684
685
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
686
return false;
687
}
688
689
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void { }
690
691
dispose(): void { }
692
}
693
694
//#endregion
695
696
class GlobalWindowDraggedOverTracker extends Disposable {
697
698
private static readonly CHANNEL_NAME = 'monaco-workbench-global-dragged-over';
699
700
private readonly broadcaster = this._register(new BroadcastDataChannel<boolean>(GlobalWindowDraggedOverTracker.CHANNEL_NAME));
701
702
constructor() {
703
super();
704
705
this.registerListeners();
706
}
707
708
private registerListeners(): void {
709
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
710
disposables.add(addDisposableListener(window, EventType.DRAG_OVER, () => this.markDraggedOver(false), true));
711
disposables.add(addDisposableListener(window, EventType.DRAG_LEAVE, () => this.clearDraggedOver(false), true));
712
}, { window: mainWindow, disposables: this._store }));
713
714
this._register(this.broadcaster.onDidReceiveData(data => {
715
if (data === true) {
716
this.markDraggedOver(true);
717
} else {
718
this.clearDraggedOver(true);
719
}
720
}));
721
}
722
723
private draggedOver = false;
724
get isDraggedOver(): boolean { return this.draggedOver; }
725
726
private markDraggedOver(fromBroadcast: boolean): void {
727
if (this.draggedOver === true) {
728
return; // alrady marked
729
}
730
731
this.draggedOver = true;
732
733
if (!fromBroadcast) {
734
this.broadcaster.postData(true);
735
}
736
}
737
738
private clearDraggedOver(fromBroadcast: boolean): void {
739
if (this.draggedOver === false) {
740
return; // alrady cleared
741
}
742
743
this.draggedOver = false;
744
745
if (!fromBroadcast) {
746
this.broadcaster.postData(false);
747
}
748
}
749
}
750
751
const globalDraggedOverTracker = new GlobalWindowDraggedOverTracker();
752
753
/**
754
* Returns whether the workbench is currently dragged over in any of
755
* the opened windows (main windows and auxiliary windows).
756
*/
757
export function isWindowDraggedOver(): boolean {
758
return globalDraggedOverTracker.isDraggedOver;
759
}
760
761