diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index e4d2f368a0004..61efd72cbebbd 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -32,12 +32,22 @@ import { isWindows } from 'vs/base/common/platform'; import { coalesce } from 'vs/base/common/arrays'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService'; +import { IEditorIdentifier } from 'vs/workbench/common/editor'; export interface IDraggedResource { resource: URI; isExternal: boolean; } +export class DraggedEditorIdentifier { + constructor(private _identifier: IEditorIdentifier) { + } + + public get identifier(): IEditorIdentifier { + return this._identifier; + } +} + export interface IDraggedEditor extends IDraggedResource { backupResource?: URI; viewState?: IEditorViewState; @@ -110,13 +120,23 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): (IDragge return resources; } +export interface IResourcesDropHandlerOptions { + + /** + * Wether to open the actual workspace when a workspace configuration file is dropped + * or wether to open the configuration file within the editor as normal file. + */ + allowWorkspaceOpen: boolean; +} + /** - * Shared function across some editor components to handle drag & drop of external resources. E.g. of folders and workspace files + * Shared function across some components to handle drag & drop of resources. E.g. of folders and workspace files * to open them in the window instead of the editor or to handle dirty editors being dropped between instances of Code. */ -export class EditorAreaDropHandler { +export class ResourcesDropHandler { constructor( + private options: IResourcesDropHandlerOptions, @IFileService private fileService: IFileService, @IWindowsService private windowsService: IWindowsService, @IWindowService private windowService: IWindowService, @@ -177,8 +197,8 @@ export class EditorAreaDropHandler { return TPromise.join(resourcesWithBackups.map(resourceWithBackup => this.handleDirtyEditorDrop(resourceWithBackup))).then(() => false); } - // Check for workspace file being dropped - if (resources.some(r => r.isExternal)) { + // Check for workspace file being dropped if we are allowed to do so + if (this.options.allowWorkspaceOpen && resources.some(r => r.isExternal)) { return this.handleWorkspaceFileDrop(resources); } @@ -377,4 +397,47 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: }); event.dataTransfer.setData(CodeDataTransfers.EDITORS, JSON.stringify(draggedEditors)); +} + +/** + * A singleton to store transfer data during drag & drop operations that are only valid within the application. + */ +export class LocalSelectionTransfer { + + private static readonly INSTANCE = new LocalSelectionTransfer(); + + private data: T[]; + private proto: T; + + private constructor() { + // protect against external instantiation + } + + public static getInstance(): LocalSelectionTransfer { + return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer; + } + + public hasData(proto: T): boolean { + return proto && proto === this.proto; + } + + public clearData(): void { + this.proto = void 0; + this.data = void 0; + } + + public getData(proto: T): T[] { + if (this.hasData(proto)) { + return this.data; + } + + return void 0; + } + + public setData(data: T[], proto: T): void { + if (proto) { + this.data = data; + this.proto = proto; + } + } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index c984e61fda328..710368690e38d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -26,7 +26,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; -import { TitleControl, ITitleAreaControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { ITitleAreaControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { NoTabsTitleControl } from 'vs/workbench/browser/parts/editor/noTabsTitleControl'; import { IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup, EditorOptions, TextEditorOptions, IEditorIdentifier } from 'vs/workbench/common/editor'; import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService'; @@ -35,7 +35,7 @@ import { editorBackground, contrastBorder, activeContrastBorder } from 'vs/platf import { Themable, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_GROUP_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme'; import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { EditorAreaDropHandler } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, LocalSelectionTransfer, DraggedEditorIdentifier } from 'vs/workbench/browser/dnd'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export enum Rochade { @@ -137,6 +137,8 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro private onStacksChangeScheduler: RunOnceScheduler; private stacksChangedBuffer: IStacksModelChangeEvent[]; + private transfer = LocalSelectionTransfer.getInstance(); + constructor( parent: Builder, groupOrientation: GroupOrientation, @@ -1074,8 +1076,8 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro const freeGroup = (stacks.groups.length === 1) ? Position.TWO : Position.THREE; // Check for transfer from title control - const draggedEditor = TitleControl.getDraggedEditor(); - if (draggedEditor) { + if ($this.transfer.hasData(DraggedEditorIdentifier.prototype)) { + const draggedEditor = $this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier; const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); // Copy editor to new location @@ -1114,7 +1116,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro // Check for URI transfer else { - const dropHandler = $this.instantiationService.createInstance(EditorAreaDropHandler); + const dropHandler = $this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); dropHandler.handleDrop(e, () => { if (splitEditor && splitTo !== freeGroup) { groupService.moveGroup(freeGroup, splitTo); @@ -1129,7 +1131,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro const target = e.target; const overlayIsSplit = typeof overlay.getProperty(splitToPropertyKey) === 'number'; const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh); - const draggedEditor = TitleControl.getDraggedEditor(); + const draggedEditor = $this.transfer.hasData(DraggedEditorIdentifier.prototype) ? $this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0; const overlaySize = $this.layoutVertically ? target.clientWidth : target.clientHeight; const splitThreshold = overlayIsSplit ? overlaySize / 5 : overlaySize / 10; @@ -1234,7 +1236,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro // update the dropEffect, otherwise it would look like a "move" operation. but only if we are // not dragging a tab actually because there we support both moving as well as copying - if (!TabsTitleControl.getDraggedEditor()) { + if (!$this.transfer.hasData(DraggedEditorIdentifier.prototype)) { e.dataTransfer.dropEffect = 'copy'; } @@ -1275,7 +1277,7 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro // Drag enter let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470 this.toUnbind.push(DOM.addDisposableListener(node, DOM.EventType.DRAG_ENTER, (e: DragEvent) => { - if (!TitleControl.getDraggedEditor()) { + if (!$this.transfer.hasData(DraggedEditorIdentifier.prototype)) { // we used to check for the dragged resources here (via dnd.extractResources()) but this // seems to be not possible on Linux and Windows where during DRAG_ENTER the resources // are always undefined up until they are dropped when dragged from the tree. The workaround diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 629e6049ceaa5..41dadd0e7fdb7 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -41,7 +41,7 @@ import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { Dimension } from 'vs/base/browser/builder'; import { scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; -import { EditorAreaDropHandler, fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, fillResourceDataTransfers, LocalSelectionTransfer, DraggedEditorIdentifier } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; interface IEditorInputLabel { @@ -63,6 +63,7 @@ export class TabsTitleControl extends TitleControl { private blockRevealActiveTab: boolean; private dimension: Dimension; private layoutScheduled: IDisposable; + private transfer = LocalSelectionTransfer.getInstance(); constructor( @IContextMenuService contextMenuService: IContextMenuService, @@ -166,7 +167,7 @@ export class TabsTitleControl extends TitleControl { // Drag over this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => { - const draggedEditor = TabsTitleControl.getDraggedEditor(); + const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0; // update the dropEffect, otherwise it would look like a "move" operation. but only if we are // not dragging a tab actually because there we support both moving as well as copying @@ -743,7 +744,8 @@ export class TabsTitleControl extends TitleControl { disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => { const { group, editor } = this.getGroupPositionAndEditor(index); - this.onEditorDragStart({ editor, group }); + this.transfer.setData([new DraggedEditorIdentifier({ editor, group })], DraggedEditorIdentifier.prototype); + e.dataTransfer.effectAllowed = 'copyMove'; // Apply some datatransfer types to allow for dragging the element outside of the application @@ -770,7 +772,7 @@ export class TabsTitleControl extends TitleControl { // Find out if the currently dragged editor is this tab and in that // case we do not want to show any drop feedback let draggedEditorIsTab = false; - const draggedEditor = TabsTitleControl.getDraggedEditor(); + const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0; if (draggedEditor) { const { group, editor } = this.getGroupPositionAndEditor(index); if (draggedEditor.editor === editor && draggedEditor.group === group) { @@ -800,7 +802,7 @@ export class TabsTitleControl extends TitleControl { DOM.removeClass(tab, 'dragged-over'); this.updateDropFeedback(tab, false, index); - this.onEditorDragEnd(); + this.transfer.clearData(); })); // Drop @@ -836,7 +838,7 @@ export class TabsTitleControl extends TitleControl { DOM.removeClass(this.tabsContainer, 'scroll'); // Local DND - const draggedEditor = TabsTitleControl.getDraggedEditor(); + const draggedEditor = this.transfer.hasData(DraggedEditorIdentifier.prototype) ? this.transfer.getData(DraggedEditorIdentifier.prototype)[0].identifier : void 0; if (draggedEditor) { // Move editor to target position and index @@ -849,12 +851,12 @@ export class TabsTitleControl extends TitleControl { this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError); } - this.onEditorDragEnd(); + this.transfer.clearData(); } // External DND else { - const dropHandler = this.instantiationService.createInstance(EditorAreaDropHandler); + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); dropHandler.handleDrop(e, () => this.editorGroupService.focusGroup(targetPosition), targetPosition, targetIndex); } } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index a4dba3d6963e1..2f2028aa133fe 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -60,8 +60,6 @@ export interface ITitleAreaControl { export abstract class TitleControl extends Themable implements ITitleAreaControl { - private static draggedEditor: IEditorIdentifier; - protected stacks: IEditorStacksModel; protected context: IEditorGroup; @@ -115,22 +113,10 @@ export abstract class TitleControl extends Themable implements ITitleAreaControl this.registerListeners(); } - public static getDraggedEditor(): IEditorIdentifier { - return TitleControl.draggedEditor; - } - public setDragged(dragged: boolean): void { this.dragged = dragged; } - protected onEditorDragStart(editor: IEditorIdentifier): void { - TitleControl.draggedEditor = editor; - } - - protected onEditorDragEnd(): void { - TitleControl.draggedEditor = void 0; - } - private registerListeners(): void { this.toUnbind.push(this.stacks.onModelChanged(e => this.onStacksChanged(e))); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 059994a4835ec..cbe5ed138f308 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -40,7 +40,7 @@ import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { OpenEditorsGroupContext, DirtyEditorContext } from 'vs/workbench/parts/files/electron-browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { fillResourceDataTransfers } from 'vs/workbench/browser/dnd'; +import { fillResourceDataTransfers, ResourcesDropHandler, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; const $ = dom.$; @@ -448,6 +448,8 @@ class OpenEditorsDelegate implements IDelegate { class EditorGroupRenderer implements IRenderer { static readonly ID = 'editorgroup'; + private transfer = LocalSelectionTransfer.getInstance(); + constructor( private keybindingService: IKeybindingService, private instantiationService: IInstantiationService, @@ -472,21 +474,24 @@ class EditorGroupRenderer implements IRenderer { - if (OpenEditorRenderer.DRAGGED_OPEN_EDITORS) { - dom.addClass(container, 'focused'); - } + dom.addClass(container, 'focused'); })); editorGroupTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_LEAVE, (e: DragEvent) => { dom.removeClass(container, 'focused'); })); - editorGroupTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DROP, () => { + editorGroupTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DROP, e => { dom.removeClass(container, 'focused'); - if (OpenEditorRenderer.DRAGGED_OPEN_EDITORS) { - const model = this.editorGroupService.getStacksModel(); - const positionOfTargetGroup = model.positionOfGroup(editorGroupTemplate.editorGroup); - OpenEditorRenderer.DRAGGED_OPEN_EDITORS.forEach(oe => + + const model = this.editorGroupService.getStacksModel(); + const positionOfTargetGroup = model.positionOfGroup(editorGroupTemplate.editorGroup); + + if (this.transfer.hasData(OpenEditor.prototype)) { + this.transfer.getData(OpenEditor.prototype).forEach(oe => this.editorGroupService.moveEditor(oe.editor, model.positionOfGroup(oe.group), positionOfTargetGroup, { preserveFocus: true })); this.editorGroupService.activateGroup(positionOfTargetGroup); + } else { + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); + dropHandler.handleDrop(e, () => this.editorGroupService.activateGroup(positionOfTargetGroup), positionOfTargetGroup); } })); @@ -507,7 +512,8 @@ class EditorGroupRenderer implements IRenderer { static readonly ID = 'openeditor'; - public static DRAGGED_OPEN_EDITORS: OpenEditor[]; + + private transfer = LocalSelectionTransfer.getInstance(); constructor( private getSelectedElements: () => (OpenEditor | IEditorGroup)[], @@ -548,34 +554,35 @@ class OpenEditorRenderer implements IRenderer document.body.removeChild(dragImage), 0); const dragged = this.getSelectedElements().filter(e => e instanceof OpenEditor && !!e.getResource()); - OpenEditorRenderer.DRAGGED_OPEN_EDITORS = dragged; + this.transfer.setData(dragged, OpenEditor.prototype); if (editorTemplate.openEditor && editorTemplate.openEditor.editor) { this.instantiationService.invokeFunction(fillResourceDataTransfers, dragged.map(d => d.getResource()), e); } })); editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_OVER, () => { - if (OpenEditorRenderer.DRAGGED_OPEN_EDITORS) { - dom.addClass(container, 'focused'); - } + dom.addClass(container, 'focused'); })); editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_LEAVE, () => { dom.removeClass(container, 'focused'); })); editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DROP, (e: DragEvent) => { dom.removeClass(container, 'focused'); - if (OpenEditorRenderer.DRAGGED_OPEN_EDITORS) { - const model = this.editorGroupService.getStacksModel(); - const positionOfTargetGroup = model.positionOfGroup(editorTemplate.openEditor.group); - const index = editorTemplate.openEditor.group.indexOf(editorTemplate.openEditor.editor); + const model = this.editorGroupService.getStacksModel(); + const positionOfTargetGroup = model.positionOfGroup(editorTemplate.openEditor.group); + const index = editorTemplate.openEditor.group.indexOf(editorTemplate.openEditor.editor); - OpenEditorRenderer.DRAGGED_OPEN_EDITORS.forEach(oe => + if (this.transfer.hasData(OpenEditor.prototype)) { + this.transfer.getData(OpenEditor.prototype).forEach(oe => this.editorGroupService.moveEditor(oe.editor, model.positionOfGroup(oe.group), positionOfTargetGroup, { index, preserveFocus: true })); this.editorGroupService.activateGroup(positionOfTargetGroup); + } else { + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); + dropHandler.handleDrop(e, () => this.editorGroupService.activateGroup(positionOfTargetGroup), positionOfTargetGroup, index); } })); editorTemplate.toDispose.push(dom.addDisposableListener(container, dom.EventType.DRAG_END, () => { - OpenEditorRenderer.DRAGGED_OPEN_EDITORS = undefined; + this.transfer.clearData(); })); return editorTemplate;