Skip to content

Commit e984f91

Browse files
authored
Aux window - allow to drag tabs/groups out to open in windows (#197809)
1 parent e8ce9b3 commit e984f91

File tree

22 files changed

+298
-213
lines changed

22 files changed

+298
-213
lines changed

src/vs/base/browser/dom.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,10 +2031,12 @@ export function getCookieValue(name: string): string | undefined {
20312031
}
20322032

20332033
export interface IDragAndDropObserverCallbacks {
2034-
readonly onDragEnter: (e: DragEvent) => void;
2035-
readonly onDragLeave: (e: DragEvent) => void;
2036-
readonly onDrop: (e: DragEvent) => void;
2037-
readonly onDragEnd: (e: DragEvent) => void;
2034+
readonly onDragEnter?: (e: DragEvent) => void;
2035+
readonly onDragLeave?: (e: DragEvent) => void;
2036+
readonly onDrop?: (e: DragEvent) => void;
2037+
readonly onDragEnd?: (e: DragEvent) => void;
2038+
readonly onDragStart?: (e: DragEvent) => void;
2039+
readonly onDrag?: (e: DragEvent) => void;
20382040
readonly onDragOver?: (e: DragEvent, dragDuration: number) => void;
20392041
}
20402042

@@ -2056,11 +2058,23 @@ export class DragAndDropObserver extends Disposable {
20562058
}
20572059

20582060
private registerListeners(): void {
2061+
if (this.callbacks.onDragStart) {
2062+
this._register(addDisposableListener(this.element, EventType.DRAG_START, (e: DragEvent) => {
2063+
this.callbacks.onDragStart?.(e);
2064+
}));
2065+
}
2066+
2067+
if (this.callbacks.onDrag) {
2068+
this._register(addDisposableListener(this.element, EventType.DRAG, (e: DragEvent) => {
2069+
this.callbacks.onDrag?.(e);
2070+
}));
2071+
}
2072+
20592073
this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => {
20602074
this.counter++;
20612075
this.dragStartTime = e.timeStamp;
20622076

2063-
this.callbacks.onDragEnter(e);
2077+
this.callbacks.onDragEnter?.(e);
20642078
}));
20652079

20662080
this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => {
@@ -2075,22 +2089,22 @@ export class DragAndDropObserver extends Disposable {
20752089
if (this.counter === 0) {
20762090
this.dragStartTime = 0;
20772091

2078-
this.callbacks.onDragLeave(e);
2092+
this.callbacks.onDragLeave?.(e);
20792093
}
20802094
}));
20812095

20822096
this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => {
20832097
this.counter = 0;
20842098
this.dragStartTime = 0;
20852099

2086-
this.callbacks.onDragEnd(e);
2100+
this.callbacks.onDragEnd?.(e);
20872101
}));
20882102

20892103
this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => {
20902104
this.counter = 0;
20912105
this.dragStartTime = 0;
20922106

2093-
this.callbacks.onDrop(e);
2107+
this.callbacks.onDrop?.(e);
20942108
}));
20952109
}
20962110
}

src/vs/editor/browser/widget/codeEditorWidget.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
363363
};
364364

365365
this._register(new dom.DragAndDropObserver(this._domElement, {
366-
onDragEnter: () => undefined,
367366
onDragOver: e => {
368367
if (!isDropIntoEnabled()) {
369368
return;

src/vs/editor/common/config/editorOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4911,7 +4911,7 @@ class EditorDropIntoEditor extends BaseEditorOption<EditorOption.dropIntoEditor,
49114911
'editor.dropIntoEditor.enabled': {
49124912
type: 'boolean',
49134913
default: defaults.enabled,
4914-
markdownDescription: nls.localize('dropIntoEditor.enabled', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."),
4914+
markdownDescription: nls.localize('dropIntoEditor.enabled', "Controls whether you can drag and drop a file into a text editor by holding down `Shift`-key (instead of opening the file in an editor)."),
49154915
},
49164916
'editor.dropIntoEditor.showDropSelector': {
49174917
type: 'string',

src/vs/workbench/browser/dnd.ts

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd';
7-
import { DragAndDropObserver, EventType, addDisposableListener } from 'vs/base/browser/dom';
7+
import { DragAndDropObserver, EventType, addDisposableListener, onDidRegisterWindow } from 'vs/base/browser/dom';
88
import { DragMouseEvent } from 'vs/base/browser/mouseEvent';
99
import { IListDragAndDrop } from 'vs/base/browser/ui/list/list';
1010
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
1111
import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree';
1212
import { coalesce } from 'vs/base/common/arrays';
1313
import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
14-
import { Emitter } from 'vs/base/common/event';
14+
import { Emitter, Event } from 'vs/base/common/event';
1515
import { Disposable, DisposableStore, IDisposable, markAsSingleton } from 'vs/base/common/lifecycle';
1616
import { stringify } from 'vs/base/common/marshalling';
1717
import { Mimes } from 'vs/base/common/mime';
@@ -35,6 +35,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
3535
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
3636
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
3737
import { IEditorOptions } from 'vs/platform/editor/common/editor';
38+
import { mainWindow } from 'vs/base/browser/window';
39+
import { BroadcastDataChannel } from 'vs/base/browser/broadcast';
3840

3941
//#region Editor / Resources DND
4042

@@ -48,7 +50,6 @@ export class DraggedEditorGroupIdentifier {
4850
constructor(readonly identifier: GroupIdentifier) { }
4951
}
5052

51-
5253
export async function extractTreeDropData(dataTransfer: VSDataTransfer): Promise<Array<IDraggedResourceEditorInput>> {
5354
const editors: IDraggedResourceEditorInput[] = [];
5455
const resourcesKey = Mimes.uriList.toLowerCase();
@@ -187,10 +188,10 @@ export class ResourcesDropHandler {
187188
}
188189
}
189190

190-
export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent): void;
191-
export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent): void;
192-
export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent): void;
193-
export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEditors: Array<URI | IResourceStat | IEditorIdentifier>, event: DragMouseEvent | DragEvent): void {
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 {
194195
if (resourcesOrEditors.length === 0 || !event.dataTransfer) {
195196
return;
196197
}
@@ -217,22 +218,25 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito
217218

218219
return resourceOrEditor;
219220
}));
220-
const fileSystemResources = resources.filter(({ resource }) => fileService.hasProvider(resource));
221221

222-
// Text: allows to paste into text-capable areas
223-
const lineDelimiter = isWindows ? '\r\n' : '\n';
224-
event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter));
225-
226-
// Download URL: enables support to drag a tab as file to desktop
227-
// Requirements:
228-
// - Chrome/Edge only
229-
// - only a single file is supported
230-
// - only file:/ resources are supported
231-
const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory);
232-
if (firstFile) {
233-
const firstFileUri = FileAccess.uriToFileUri(firstFile.resource); // enforce `file:` URIs
234-
if (firstFileUri.scheme === Schemas.file) {
235-
event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [Mimes.binary, basename(firstFile.resource), firstFileUri.toString()].join(':'));
222+
const fileSystemResources = resources.filter(({ resource }) => fileService.hasProvider(resource));
223+
if (!options?.disableStandardTransfer) {
224+
225+
// Text: allows to paste into text-capable areas
226+
const lineDelimiter = isWindows ? '\r\n' : '\n';
227+
event.dataTransfer.setData(DataTransfers.TEXT, fileSystemResources.map(({ resource }) => labelService.getUriLabel(resource, { noPrefix: true })).join(lineDelimiter));
228+
229+
// Download URL: enables support to drag a tab as file to desktop
230+
// Requirements:
231+
// - Chrome/Edge only
232+
// - only a single file is supported
233+
// - only file:/ resources are supported
234+
const firstFile = fileSystemResources.find(({ isDirectory }) => !isDirectory);
235+
if (firstFile) {
236+
const firstFileUri = FileAccess.uriToFileUri(firstFile.resource); // enforce `file:` URIs
237+
if (firstFileUri.scheme === Schemas.file) {
238+
event.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [Mimes.binary, basename(firstFile.resource), firstFileUri.toString()].join(':'));
239+
}
236240
}
237241
}
238242

@@ -467,9 +471,6 @@ export class CompositeDragAndDropObserver extends Disposable {
467471
registerTarget(element: HTMLElement, callbacks: ICompositeDragAndDropObserverCallbacks): IDisposable {
468472
const disposableStore = new DisposableStore();
469473
disposableStore.add(new DragAndDropObserver(element, {
470-
onDragEnd: e => {
471-
// no-op
472-
},
473474
onDragEnter: e => {
474475
e.preventDefault();
475476

@@ -533,16 +534,15 @@ export class CompositeDragAndDropObserver extends Disposable {
533534

534535
const disposableStore = new DisposableStore();
535536

536-
disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => {
537-
const { id, type } = draggedItemProvider();
538-
this.writeDragData(id, type);
539-
540-
e.dataTransfer?.setDragImage(element, 0, 0);
537+
disposableStore.add(new DragAndDropObserver(element, {
538+
onDragStart: e => {
539+
const { id, type } = draggedItemProvider();
540+
this.writeDragData(id, type);
541541

542-
this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! });
543-
}));
542+
e.dataTransfer?.setDragImage(element, 0, 0);
544543

545-
disposableStore.add(new DragAndDropObserver(element, {
544+
this.onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! });
545+
},
546546
onDragEnd: e => {
547547
const { type } = draggedItemProvider();
548548
const data = this.readDragData(type);
@@ -661,3 +661,68 @@ export class ResourceListDnDHandler<T> implements IListDragAndDrop<T> {
661661
}
662662

663663
//#endregion
664+
665+
class GlobalWindowDraggedOverTracker extends Disposable {
666+
667+
private static readonly CHANNEL_NAME = 'monaco-workbench-global-dragged-over';
668+
669+
private readonly broadcaster = this._register(new BroadcastDataChannel<boolean>(GlobalWindowDraggedOverTracker.CHANNEL_NAME));
670+
671+
constructor() {
672+
super();
673+
674+
this.registerListeners();
675+
}
676+
677+
private registerListeners(): void {
678+
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
679+
disposables.add(addDisposableListener(window, EventType.DRAG_OVER, () => this.markDraggedOver(false), true));
680+
disposables.add(addDisposableListener(window, EventType.DRAG_LEAVE, () => this.clearDraggedOver(false), true));
681+
}, { window: mainWindow, disposables: this._store }));
682+
683+
this._register(this.broadcaster.onDidReceiveData(data => {
684+
if (data === true) {
685+
this.markDraggedOver(true);
686+
} else {
687+
this.clearDraggedOver(true);
688+
}
689+
}));
690+
}
691+
692+
private draggedOver = false;
693+
get isDraggedOver(): boolean { return this.draggedOver; }
694+
695+
private markDraggedOver(fromBroadcast: boolean): void {
696+
if (this.draggedOver === true) {
697+
return; // alrady marked
698+
}
699+
700+
this.draggedOver = true;
701+
702+
if (!fromBroadcast) {
703+
this.broadcaster.postData(true);
704+
}
705+
}
706+
707+
private clearDraggedOver(fromBroadcast: boolean): void {
708+
if (this.draggedOver === false) {
709+
return; // alrady cleared
710+
}
711+
712+
this.draggedOver = false;
713+
714+
if (!fromBroadcast) {
715+
this.broadcaster.postData(false);
716+
}
717+
}
718+
}
719+
720+
const globalDraggedOverTracker = new GlobalWindowDraggedOverTracker();
721+
722+
/**
723+
* Returns whether the workbench is currently dragged over in any of
724+
* the opened windows (main windows and auxiliary windows).
725+
*/
726+
export function isWindowDraggedOver(): boolean {
727+
return globalDraggedOverTracker.isDraggedOver;
728+
}

src/vs/workbench/browser/layout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
230230
// main window
231231
return this.mainContainerOffset;
232232
} else {
233-
// TODO@bpasero auxiliary window: no support for custom title bar or banner yet
233+
// auxiliary window: no support for custom title bar or banner yet
234234
return { top: 0, quickPickTop: 0 };
235235
}
236236
}

src/vs/workbench/browser/parts/editor/editor.contribution.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
6666
import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler';
6767
import { DynamicEditorConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration';
6868
import { EditorActionsDefaultAction, EditorActionsTitleBarAction, HideEditorActionsAction, HideEditorTabsAction, ShowMultipleEditorTabsAction, ShowSingleEditorTabAction } from 'vs/workbench/browser/actions/layoutActions';
69-
import product from 'vs/platform/product/common/product';
7069
import { ICommandAction } from 'vs/platform/action/common/action';
7170

7271
//#region Editor Registrations
@@ -293,11 +292,7 @@ registerAction2(QuickAccessLeastRecentlyUsedEditorAction);
293292
registerAction2(QuickAccessPreviousRecentlyUsedEditorInGroupAction);
294293
registerAction2(QuickAccessLeastRecentlyUsedEditorInGroupAction);
295294
registerAction2(QuickAccessPreviousEditorFromHistoryAction);
296-
297-
if (product.quality !== 'stable') {
298-
// TODO@bpasero revisit
299-
registerAction2(ExperimentalMoveEditorIntoNewWindowAction);
300-
}
295+
registerAction2(ExperimentalMoveEditorIntoNewWindowAction);
301296

302297
const quickAccessNavigateNextInEditorPickerId = 'workbench.action.quickOpenNavigateNextInEditorPicker';
303298
KeybindingsRegistry.registerCommandAndKeybindingRule({

src/vs/workbench/browser/parts/editor/editor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { isObject } from 'vs/base/common/types';
1717
import { IEditorOptions } from 'vs/platform/editor/common/editor';
1818
import { IWindowsConfiguration } from 'vs/platform/window/common/window';
1919
import { BooleanVerifier, EnumVerifier, NumberVerifier, ObjectVerifier, SetVerifier, verifyObject } from 'vs/base/common/verifier';
20+
import product from 'vs/platform/product/common/product';
2021

2122
export interface IEditorPartCreationOptions {
2223
readonly restorePreviousState: boolean;
@@ -49,6 +50,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = {
4950
labelFormat: 'default',
5051
splitSizing: 'auto',
5152
splitOnDragAndDrop: true,
53+
dragToOpenWindow: product.quality !== 'stable',
5254
centeredLayoutFixedWidth: false,
5355
doubleClickTabToToggleEditorGroupSizes: 'expand',
5456
editorActionsLocation: 'default',
@@ -131,6 +133,7 @@ function validateEditorPartOptions(options: IEditorPartOptions): IEditorPartOpti
131133
'mouseBackForwardToNavigate': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['mouseBackForwardToNavigate']),
132134
'restoreViewState': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['restoreViewState']),
133135
'splitOnDragAndDrop': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['splitOnDragAndDrop']),
136+
'dragToOpenWindow': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['dragToOpenWindow']),
134137
'centeredLayoutFixedWidth': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['centeredLayoutFixedWidth']),
135138
'hasIcons': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['hasIcons']),
136139

src/vs/workbench/browser/parts/editor/editorDropTarget.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ class DropOverlay extends Themable {
144144

145145
private registerListeners(container: HTMLElement): void {
146146
this._register(new DragAndDropObserver(container, {
147-
onDragEnter: e => undefined,
148147
onDragOver: e => {
149148
if (this.enableDropIntoEditor && isDragIntoEditorEvent(e)) {
150149
this.dispose();

src/vs/workbench/browser/parts/editor/editorPanes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export class EditorPanes extends Disposable {
131131
try {
132132

133133
// Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition
134-
// TODO@bpasero revisit this once all editors can support aux windows
135134
if (getWindow(this.editorPanesParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) {
136135
return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in floating windows yet."), [
137136
toAction({

src/vs/workbench/browser/parts/editor/editorPart.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,11 +1406,11 @@ export class AuxiliaryEditorPart extends EditorPart implements IAuxiliaryEditorP
14061406
}
14071407

14081408
protected override saveState(): void {
1409-
return; // TODO@bpasero support auxiliary editor state
1409+
return; // TODO support auxiliary editor state
14101410
}
14111411

14121412
async close(): Promise<void> {
1413-
// TODO@bpasero this needs full support for closing all editors, handling vetos and showing dialogs
1413+
// TODO this needs full support for closing all editors, handling vetos and showing dialogs
14141414
this._onDidClose.fire();
14151415
}
14161416
}

src/vs/workbench/browser/parts/editor/editorParts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class EditorParts extends Disposable implements IEditorGroupsService, IEd
4040

4141
//#region Auxiliary Editor Parts
4242

43-
async createAuxiliaryEditorPart(options?: { position?: IRectangle }): Promise<IAuxiliaryEditorPart> {
43+
async createAuxiliaryEditorPart(options?: { bounds?: Partial<IRectangle> }): Promise<IAuxiliaryEditorPart> {
4444
const disposables = new DisposableStore();
4545

4646
const auxiliaryWindow = disposables.add(await this.auxiliaryWindowService.open(options));

0 commit comments

Comments
 (0)