Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drag and drop controller #123542

Merged
merged 3 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,24 +894,18 @@ declare module 'vscode' {

//#region Custom Tree View Drag and Drop https://github.com/microsoft/vscode/issues/32592
export interface TreeViewOptions<T> {
/**
* * Whether the tree supports drag and drop.
*/
canDragAndDrop?: boolean;
dragAndDropController?: DragAndDropController<T>;
}

export interface TreeDataProvider<T> {
export interface DragAndDropController<T> extends Disposable {
/**
* Optional method to reparent an `element`.
* Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed.
*
* **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.
* @param source
* @param target
*/
setParent?(elements: T[], targetElement: T): Thenable<void>;
onDrop(source: T[], target: T): Thenable<void>;
}

//#endregion

//#region Task presentation group: https://github.com/microsoft/vscode/issues/47265
Expand Down
19 changes: 13 additions & 6 deletions src/vs/workbench/api/browser/mainThreadTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Disposable } from 'vs/base/common/lifecycle';
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem } from 'vs/workbench/common/views';
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { distinct } from 'vs/base/common/arrays';
import { INotificationService } from 'vs/platform/notification/common/notification';
Expand Down Expand Up @@ -37,13 +37,14 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService);
this._dataProviders.set(treeViewId, dataProvider);
const dndController = options.canDragAndDrop ? new TreeViewDragAndDropController(treeViewId, this._proxy) : undefined;
const viewer = this.getTreeView(treeViewId);
if (viewer) {
// Order is important here. The internal tree isn't created until the dataProvider is set.
// Set all other properties first!
viewer.showCollapseAllAction = !!options.showCollapseAll;
viewer.canSelectMany = !!options.canSelectMany;
viewer.canDragAndDrop = !!options.canDragAndDrop;
viewer.dragAndDropController = dndController;
viewer.dataProvider = dataProvider;
this.registerListeners(treeViewId, viewer);
this._proxy.$setVisible(treeViewId, viewer.visible);
Expand Down Expand Up @@ -162,6 +163,16 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie

type TreeItemHandle = string;

class TreeViewDragAndDropController implements ITreeViewDragAndDropController {

constructor(private readonly treeViewId: string,
private readonly _proxy: ExtHostTreeViewsShape) { }

onDrop(treeItem: ITreeItem[], targetTreeItem: ITreeItem): Promise<void> {
return this._proxy.$onDrop(this.treeViewId, treeItem.map(item => item.handle), targetTreeItem.handle);
}
}

class TreeViewDataProvider implements ITreeViewDataProvider {

private readonly itemsMap: Map<TreeItemHandle, ITreeItem> = new Map<TreeItemHandle, ITreeItem>();
Expand All @@ -184,10 +195,6 @@ class TreeViewDataProvider implements ITreeViewDataProvider {
}));
}

setParent(treeItem: ITreeItem[], targetTreeItem: ITreeItem): Promise<void> {
return this._proxy.$setParent(this.treeViewId, treeItem.map(item => item.handle), targetTreeItem.handle);
}

getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] {
const itemsToRefresh: ITreeItem[] = [];
if (itemsToRefreshByHandle) {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,7 +1217,7 @@ export interface ExtHostDocumentsAndEditorsShape {

export interface ExtHostTreeViewsShape {
$getChildren(treeViewId: string, treeItemHandle?: string): Promise<ITreeItem[]>;
$setParent(treeViewId: string, treeItemHandle: string[], newParentTreeItemHandle: string): Promise<void>;
$onDrop(treeViewId: string, treeItemHandle: string[], newParentTreeItemHandle: string): Promise<void>;
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void;
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
$setVisible(treeViewId: string, visible: boolean): void;
Expand Down
17 changes: 10 additions & 7 deletions src/vs/workbench/api/common/extHostTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ 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, canDragAndDrop: !!options.canDragAndDrop });
const canDragAndDrop = options.dragAndDropController !== undefined;
const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, canDragAndDrop: canDragAndDrop });
const treeView = this.createExtHostTreeView(viewId, options, extension);
return {
get onDidCollapseElement() { return treeView.onDidCollapseElement; },
Expand Down Expand Up @@ -127,12 +128,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return treeView.getChildren(treeItemHandle);
}

$setParent(treeViewId: string, treeItemHandles: string[], newParentItemHandle: string): Promise<void> {
$onDrop(treeViewId: string, treeItemHandles: string[], newParentItemHandle: string): Promise<void> {
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);
return treeView.onDrop(treeItemHandles, newParentItemHandle);
}

async $hasResolve(treeViewId: string): Promise<boolean> {
Expand Down Expand Up @@ -204,6 +205,7 @@ class ExtHostTreeView<T> extends Disposable {
private static readonly ID_HANDLE_PREFIX = '1';

private readonly dataProvider: vscode.TreeDataProvider<T>;
private readonly dndController: vscode.DragAndDropController<T> | undefined;

private roots: TreeNode[] | null = null;
private elements: Map<TreeItemHandle, T> = new Map<TreeItemHandle, T>();
Expand Down Expand Up @@ -250,6 +252,7 @@ class ExtHostTreeView<T> extends Disposable {
}
}
this.dataProvider = options.treeDataProvider;
this.dndController = options.dragAndDropController;
if (this.dataProvider.onDidChangeTreeData) {
this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element })));
}
Expand Down Expand Up @@ -377,11 +380,11 @@ class ExtHostTreeView<T> extends Disposable {
}
}

setParent(treeItemHandleOrNodes: TreeItemHandle[], newParentHandleOrNode: TreeItemHandle): Promise<void> {
onDrop(treeItemHandleOrNodes: TreeItemHandle[], targetHandleOrNode: TreeItemHandle): Promise<void> {
const elements = <T[]>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));
const target = this.getExtensionElement(targetHandleOrNode);
if (elements && target) {
return asPromise(() => this.dndController?.onDrop(elements, target));
}
return Promise.resolve(undefined);
}
Expand Down
42 changes: 11 additions & 31 deletions src/vs/workbench/browser/parts/views/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views';
import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
Expand Down Expand Up @@ -159,7 +159,6 @@ 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;
Expand Down Expand Up @@ -241,6 +240,13 @@ export class TreeView extends Disposable implements ITreeView {
get viewLocation(): ViewContainerLocation {
return this.viewDescriptorService.getViewLocationById(this.id)!;
}
private _dragAndDropController: ITreeViewDragAndDropController | undefined;
get dragAndDropController(): ITreeViewDragAndDropController | undefined {
return this._dragAndDropController;
}
set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) {
this._dragAndDropController = dnd;
}

private _dataProvider: ITreeViewDataProvider | undefined;
get dataProvider(): ITreeViewDataProvider | undefined {
Expand Down Expand Up @@ -281,12 +287,6 @@ export class TreeView extends Disposable implements ITreeView {
}
return children;
}

async setParent(nodes: ITreeItem[], parentNode: ITreeItem): Promise<void> {
if (dataProvider.setParent) {
await dataProvider.setParent(nodes, parentNode);
}
}
};
if (this._dataProvider.onDidChangeEmpty) {
this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire()));
Expand Down Expand Up @@ -339,14 +339,6 @@ 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;
}
Expand Down Expand Up @@ -521,7 +513,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,
dnd: this.dragAndDropController ? this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.dragAndDropController) : undefined,
overrideStyles: {
listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND
}
Expand Down Expand Up @@ -814,18 +806,6 @@ class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {
}
return result;
}

async setParent(elements: ITreeItem[], newParentElement: ITreeItem): Promise<void> {
if (this.treeView.dataProvider && this.treeView.dataProvider.setParent) {
try {
await this.withProgress(this.treeView.dataProvider.setParent(elements, newParentElement));
} catch (e) {
if (!(<string>e.message).startsWith('Bad progress location:')) {
throw e;
}
}
}
}
}

// todo@jrieken,sandy make this proper and contributable from extensions
Expand Down Expand Up @@ -1216,7 +1196,7 @@ export class CustomTreeView extends TreeView {
}

export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {
constructor(private dataSource: TreeDataSource, @ILabelService private readonly labelService: ILabelService) { }
constructor(private dndController: ITreeViewDragAndDropController, @ILabelService private readonly labelService: ILabelService) { }

onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true };
Expand All @@ -1238,7 +1218,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {
if (data instanceof ElementsDragAndDropData) {
const elements = data.elements;
if (targetNode) {
await this.dataSource.setParent(elements, targetNode);
await this.dndController.onDrop(elements, targetNode);
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/vs/workbench/common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,12 @@ export interface ITreeView extends IDisposable {

dataProvider: ITreeViewDataProvider | undefined;

dragAndDropController?: ITreeViewDragAndDropController;

showCollapseAllAction: boolean;

canSelectMany: boolean;

canDragAndDrop: boolean;

message?: string;

title: string;
Expand Down Expand Up @@ -812,7 +812,10 @@ export interface ITreeViewDataProvider {
readonly isTreeEmpty?: boolean;
onDidChangeEmpty?: Event<void>;
getChildren(element?: ITreeItem): Promise<ITreeItem[]>;
setParent?(elements: ITreeItem[], newParent: ITreeItem): Promise<void>;
}

export interface ITreeViewDragAndDropController {
onDrop(elements: ITreeItem[], target: ITreeItem): Promise<void>;
}

export interface IEditableData {
Expand Down