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