Skip to content

Commit 7b50be2

Browse files
committed
tree: dnd bubble behavior
1 parent cd6769c commit 7b50be2

7 files changed

Lines changed: 113 additions & 34 deletions

File tree

src/vs/base/browser/ui/list/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const ListDragOverReactions = {
8787

8888
export interface IListDragAndDrop<T> {
8989
getDragURI(element: T): string | null;
90-
getDragLabel?(elements: T[]): string;
90+
getDragLabel?(elements: T[]): string | undefined;
9191
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void;
9292
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
9393
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;

src/vs/base/browser/ui/list/listView.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,11 +611,13 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
611611
event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([uri]));
612612

613613
if (event.dataTransfer.setDragImage) {
614-
let label: string;
614+
let label: string | undefined;
615615

616616
if (this.dnd.getDragLabel) {
617617
label = this.dnd.getDragLabel(elements);
618-
} else {
618+
}
619+
620+
if (typeof label === 'undefined') {
619621
label = String(elements.length);
620622
}
621623

@@ -721,6 +723,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
721723
}
722724

723725
private onDragLeave(): void {
726+
this.onDragLeaveTimeout.dispose();
724727
this.onDragLeaveTimeout = DOM.timeout(() => this.clearDragOverFeedback(), 100);
725728
}
726729

@@ -750,6 +753,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
750753
private clearDragOverFeedback(): void {
751754
this.currentDragFeedback = undefined;
752755
this.currentDragFeedbackDisposable.dispose();
756+
this.currentDragFeedbackDisposable = Disposable.None;
753757
}
754758

755759
// DND scroll top animation

src/vs/base/browser/ui/list/listWidget.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ import { KeyCode } from 'vs/base/common/keyCodes';
1616
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1717
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
1818
import { domEvent } from 'vs/base/browser/event';
19-
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop } from './list';
20-
import { ListView, IListViewOptions } from './listView';
19+
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction } from './list';
20+
import { ListView, IListViewOptions, IListViewDragAndDrop } from './listView';
2121
import { Color } from 'vs/base/common/color';
2222
import { mixin } from 'vs/base/common/objects';
2323
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
2424
import { ISpliceable } from 'vs/base/common/sequence';
2525
import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
2626
import { clamp } from 'vs/base/common/numbers';
2727
import { matchesPrefix } from 'vs/base/common/filters';
28+
import { IDragAndDropData } from 'vs/base/browser/dnd';
2829

2930
interface ITraitChangeEvent {
3031
indexes: number[];
@@ -940,6 +941,37 @@ class AccessibiltyRenderer<T> implements IListRenderer<T, HTMLElement> {
940941
}
941942
}
942943

944+
class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
945+
946+
constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }
947+
948+
getDragElements(element: T): T[] {
949+
const selection = this.list.getSelectedElements();
950+
const elements = selection.indexOf(element) > -1 ? selection : [element];
951+
return elements;
952+
}
953+
954+
getDragURI(element: T): string | null {
955+
return this.dnd.getDragURI(element);
956+
}
957+
958+
getDragLabel?(elements: T[]): string | undefined {
959+
return this.dnd.getDragLabel && this.dnd.getDragLabel(elements);
960+
}
961+
962+
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
963+
this.dnd.onDragStart(data, originalEvent);
964+
}
965+
966+
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
967+
return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent);
968+
}
969+
970+
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
971+
this.dnd.drop(data, targetElement, targetIndex, originalEvent);
972+
}
973+
}
974+
943975
export class List<T> implements ISpliceable<T>, IDisposable {
944976

945977
private static InstanceCount = 0;
@@ -1051,14 +1083,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
10511083

10521084
const viewOptions: IListViewOptions<T> = {
10531085
...options,
1054-
dnd: options.dnd && {
1055-
...options.dnd,
1056-
getDragElements: element => {
1057-
const selection = this.getSelectedElements();
1058-
const elements = selection.indexOf(element) > -1 ? selection : [element];
1059-
return elements;
1060-
}
1061-
}
1086+
dnd: options.dnd && new ListViewDragAndDrop(this, options.dnd)
10621087
};
10631088

10641089
this.view = new ListView(container, virtualDelegate, renderers, viewOptions);

src/vs/base/browser/ui/tree/abstractTree.ts

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,79 @@
66
import 'vs/css!./media/tree';
77
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
88
import { IListOptions, List, IListStyles } from 'vs/base/browser/ui/list/listWidget';
9-
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
9+
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
1010
import { append, $, toggleClass } from 'vs/base/browser/dom';
1111
import { Event, Relay } from 'vs/base/common/event';
1212
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1313
import { KeyCode } from 'vs/base/common/keyCodes';
14-
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree';
14+
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
1515
import { ISpliceable } from 'vs/base/common/sequence';
16+
import { IDragAndDropData } from 'vs/base/browser/dnd';
17+
import { range } from 'vs/base/common/arrays';
1618

17-
function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
19+
class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<ITreeNode<T, TFilterData>> {
20+
21+
constructor(private modelProvider: () => ITreeModel<T, TFilterData, TRef>, private dnd: ITreeDragAndDrop<T>) { }
22+
23+
getDragURI(node: ITreeNode<T, TFilterData>): string | null {
24+
return this.dnd.getDragURI(node.element);
25+
}
26+
27+
getDragLabel(nodes: ITreeNode<T, TFilterData>[]): string | undefined {
28+
return this.dnd.getDragLabel && this.dnd.getDragLabel(nodes.map(node => node.element));
29+
}
30+
31+
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
32+
this.dnd.onDragStart(data, originalEvent);
33+
}
34+
35+
onDragOver(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent, force = false): boolean | IListDragOverReaction {
36+
const result = this.dnd.onDragOver(data, targetNode && targetNode.element, targetIndex, originalEvent);
37+
38+
if (typeof targetNode === 'undefined') {
39+
return result;
40+
}
41+
42+
if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined') {
43+
if (force) {
44+
const accept = typeof result === 'boolean' ? result : result.accept;
45+
const effect = typeof result === 'boolean' ? undefined : result.effect;
46+
return { accept, effect, feedback: [targetIndex!] };
47+
}
48+
49+
return result;
50+
}
51+
52+
if (result.bubble === TreeDragOverBubble.Up) {
53+
const parentNode = targetNode.parent;
54+
const model = this.modelProvider();
55+
const parentIndex = parentNode && model.getListIndex(model.getNodeLocation(parentNode));
56+
57+
return this.onDragOver(data, parentNode, parentIndex, originalEvent, true);
58+
}
59+
60+
const model = this.modelProvider();
61+
const ref = model.getNodeLocation(targetNode);
62+
const start = model.getListIndex(ref);
63+
const length = model.getListRenderCount(ref);
64+
65+
return { ...result, feedback: range(start, start + length) };
66+
}
67+
68+
drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
69+
this.dnd.drop(data, targetNode && targetNode.element, targetIndex, originalEvent);
70+
}
71+
}
72+
73+
function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
1874
return options && {
1975
...options,
2076
identityProvider: options.identityProvider && {
2177
getId(el) {
2278
return options.identityProvider!.getId(el.element);
2379
}
2480
},
25-
dnd: options.dnd && {
26-
getDragURI(node) {
27-
return options.dnd!.getDragURI(node.element);
28-
},
29-
getDragLabel: options.dnd!.getDragLabel && ((nodes) => {
30-
return options.dnd!.getDragLabel!(nodes.map(node => node.element));
31-
}),
32-
onDragStart(data, originalEvent) {
33-
return options.dnd!.onDragStart(data, originalEvent);
34-
},
35-
onDragOver(data, targetNode, targetIndex, originalEvent) {
36-
return options.dnd!.onDragOver(data, targetNode && targetNode.element, targetIndex, originalEvent);
37-
},
38-
drop(data, targetNode, targetIndex, originalEvent) {
39-
return options.dnd!.drop(data, targetNode && targetNode.element, targetIndex, originalEvent);
40-
}
41-
},
81+
dnd: options.dnd && new TreeNodeListDragAndDrop(modelProvider, options.dnd),
4282
multipleSelectionController: options.multipleSelectionController && {
4383
isSelectionSingleChangeEvent(e) {
4484
return options.multipleSelectionController!.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
@@ -239,7 +279,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
239279
const treeRenderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event));
240280
this.disposables.push(...treeRenderers);
241281

242-
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(options));
282+
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(() => this.model, options));
243283

244284
this.model = this.createModel(this.view, options);
245285
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;

src/vs/base/browser/ui/tree/indexTreeModel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
127127
return this.getTreeNodeWithListIndex(location).listIndex;
128128
}
129129

130+
getListRenderCount(location: number[]): number {
131+
return this.getTreeNode(location).renderNodeCount;
132+
}
133+
130134
isCollapsible(location: number[]): boolean {
131135
return this.getTreeNode(location).collapsible;
132136
}

src/vs/base/browser/ui/tree/objectTreeModel.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
123123
return this.model.getListIndex(location);
124124
}
125125

126+
getListRenderCount(element: T): number {
127+
const location = this.getElementLocation(element);
128+
return this.model.getListRenderCount(location);
129+
}
130+
126131
isCollapsible(element: T): boolean {
127132
const location = this.getElementLocation(element);
128133
return this.model.isCollapsible(location);

src/vs/base/browser/ui/tree/tree.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
101101
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
102102

103103
getListIndex(location: TRef): number;
104+
getListRenderCount(location: TRef): number;
104105
getNode(location?: TRef): ITreeNode<T, any>;
105106
getNodeLocation(node: ITreeNode<T, any>): TRef;
106107
getParentNodeLocation(location: TRef): TRef;

0 commit comments

Comments
 (0)