diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a0688d37e01..b945b1f4bed 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -881,6 +881,28 @@ declare module 'vscode' { } //#endregion + //#region Custom Tree View Drag and Drop https://github.com/microsoft/vscode/issues/32592 + export interface TreeViewOptions { + /** + * * Whether the tree supports drag and drop. + */ + canDragAndDrop?: boolean; + } + + export interface TreeDataProvider { + /** + * Optional method to reparent an `element`. + * + * **NOTE:** This method should be implemented if the tree supports drag and drop. + * + * @param elements The selected elements that will be reparented. + * @param targetElement The new parent of the elements. + */ + setParent?(elements: T[], targetElement: T): Thenable; + } + + //#endregion + //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 export interface TaskPresentationOptions { /** diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index b70ff475447..219fa248b0c 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -31,7 +31,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): Promise { + async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean }): Promise { this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options); this.extensionService.whenInstalledExtensionsRegistered().then(() => { @@ -43,6 +43,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie // Set all other properties first! viewer.showCollapseAllAction = !!options.showCollapseAll; viewer.canSelectMany = !!options.canSelectMany; + viewer.canDragAndDrop = !!options.canDragAndDrop; viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); @@ -183,6 +184,10 @@ class TreeViewDataProvider implements ITreeViewDataProvider { })); } + setParent(treeItem: ITreeItem[], targetTreeItem: ITreeItem): Promise { + return this._proxy.$setParent(this.treeViewId, treeItem.map(item => item.handle), targetTreeItem.handle); + } + getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] { const itemsToRefresh: ITreeItem[] = []; if (itemsToRefreshByHandle) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 749267dfc3a..6d0ca153d3a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -289,7 +289,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean; }): Promise; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean; }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem; }): Promise; $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise; $setMessage(treeViewId: string, message: string): void; @@ -1229,6 +1229,7 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise; + $setParent(treeViewId: string, treeItemHandle: string[], newParentTreeItemHandle: string): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index bf2801705cf..eb94d4f861b 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -84,7 +84,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { if (!options || !options.treeDataProvider) { throw new Error('Options with treeDataProvider is mandatory'); } - const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany }); + const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, canDragAndDrop: !!options.canDragAndDrop }); const treeView = this.createExtHostTreeView(viewId, options, extension); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, @@ -127,6 +127,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } + $setParent(treeViewId: string, treeItemHandles: string[], newParentItemHandle: string): Promise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); + } + return treeView.setParent(treeItemHandles, newParentItemHandle); + } + async $hasResolve(treeViewId: string): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { @@ -369,6 +377,15 @@ class ExtHostTreeView extends Disposable { } } + setParent(treeItemHandleOrNodes: TreeItemHandle[], newParentHandleOrNode: TreeItemHandle): Promise { + const elements = treeItemHandleOrNodes.map(item => this.getExtensionElement(item)).filter(element => !isUndefinedOrNull(element)); + const newParentElement = this.getExtensionElement(newParentHandleOrNode); + if (this.dataProvider.setParent && elements && newParentElement) { + return asPromise(() => this.dataProvider.setParent!(elements, newParentElement)); + } + return Promise.resolve(undefined); + } + get hasResolve(): boolean { return !!this.dataProvider.resolveTreeItem; } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 075cf857ad1..e3dbbad6383 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -38,7 +38,9 @@ import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMat import { isString } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; +import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { IDragAndDropData } from 'vs/base/browser/dnd'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; @@ -87,6 +89,7 @@ export class TreeViewPane extends ViewPane { if (options.titleDescription !== this.treeView.description) { this.updateTitleDescription(this.treeView.description); } + this.updateTreeVisibility(); } @@ -156,6 +159,7 @@ export class TreeView extends Disposable implements ITreeView { private treeContainer!: HTMLElement; private _messageValue: string | undefined; private _canSelectMany: boolean = false; + private _canDragAndDrop = false; private messageElement!: HTMLDivElement; private tree: Tree | undefined; private treeLabels: ResourceLabels | undefined; @@ -277,6 +281,12 @@ export class TreeView extends Disposable implements ITreeView { } return children; } + + async setParent(nodes: ITreeItem[], parentNode: ITreeItem): Promise { + if (dataProvider.setParent) { + await dataProvider.setParent(nodes, parentNode); + } + } }; if (this._dataProvider.onDidChangeEmpty) { this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); @@ -329,6 +339,14 @@ export class TreeView extends Disposable implements ITreeView { this._canSelectMany = canSelectMany; } + get canDragAndDrop(): boolean { + return this._canDragAndDrop; + } + + set canDragAndDrop(canDragAndDrop: boolean) { + this._canDragAndDrop = canDragAndDrop; + } + get hasIconForParentNode(): boolean { return this._hasIconForParentNode; } @@ -503,6 +521,7 @@ export class TreeView extends Disposable implements ITreeView { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, multipleSelectionSupport: this.canSelectMany, + dnd: this.canDragAndDrop ? this.instantiationService.createInstance(CustomTreeViewDragAndDrop, dataSource) : undefined, overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } @@ -795,6 +814,18 @@ class TreeDataSource implements IAsyncDataSource { } return result; } + + async setParent(elements: ITreeItem[], newParentElement: ITreeItem): Promise { + if (this.treeView.dataProvider && this.treeView.dataProvider.setParent) { + try { + await this.withProgress(this.treeView.dataProvider.setParent(elements, newParentElement)); + } catch (e) { + if (!(e.message).startsWith('Bad progress location:')) { + throw e; + } + } + } + } } // todo@jrieken,sandy make this proper and contributable from extensions @@ -1183,3 +1214,32 @@ export class CustomTreeView extends TreeView { } } } + +export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { + constructor(private dataSource: TreeDataSource, @ILabelService private readonly labelService: ILabelService) { } + + onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true }; + } + + getDragURI(element: ITreeItem): string | null { + return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle; + } + + getDragLabel?(elements: ITreeItem[]): string | undefined { + if (elements.length > 1) { + return String(elements.length); + } + const element = elements[0]; + return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined); + } + + async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise { + if (data instanceof ElementsDragAndDropData) { + const elements = data.elements; + if (targetNode) { + await this.dataSource.setParent(elements, targetNode); + } + } + } +} diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index e7b8d1d6145..484af52fd8f 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -634,6 +634,8 @@ export interface ITreeView extends IDisposable { canSelectMany: boolean; + canDragAndDrop: boolean; + message?: string; title: string; @@ -810,7 +812,7 @@ export interface ITreeViewDataProvider { readonly isTreeEmpty?: boolean; onDidChangeEmpty?: Event; getChildren(element?: ITreeItem): Promise; - + setParent?(elements: ITreeItem[], newParent: ITreeItem): Promise; } export interface IEditableData { diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts index 502e756ea61..8a8f2514c9b 100644 --- a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -70,7 +70,7 @@ suite('MainThreadHostTreeView', function () { } drain(): any { return null; } }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); - mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); + mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, canDragAndDrop: false }); await testExtensionService.whenInstalledExtensionsRegistered(); });