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

Support for dragging between list items #201786

Merged
merged 6 commits into from
Jan 4, 2024
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
19 changes: 16 additions & 3 deletions src/vs/base/browser/ui/list/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { GestureEvent } from 'vs/base/browser/touch';
import { ListViewTargetSector } from 'vs/base/browser/ui/list/listView';
import { IDisposable } from 'vs/base/common/lifecycle';

export interface IListVirtualDelegate<T> {
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface IListDragEvent<T> {
readonly browserEvent: DragEvent;
readonly element: T | undefined;
readonly index: number | undefined;
readonly sector: ListViewTargetSector | undefined;
}

export interface IListContextMenuEvent<T> {
Expand Down Expand Up @@ -84,11 +86,22 @@ export interface IKeyboardNavigationDelegate {
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
}

export const enum ListDragOverEffect {
export const enum ListDragOverEffectType {
Copy,
Move
}

export const enum ListDragOverEffectPosition {
Over = 'drop-target',
Before = 'drop-target-before',
After = 'drop-target-after'
}

export interface ListDragOverEffect {
type: ListDragOverEffectType;
position?: ListDragOverEffectPosition;
}

export interface IListDragOverReaction {
accept: boolean;
effect?: ListDragOverEffect;
Expand All @@ -108,9 +121,9 @@ export interface IListDragAndDrop<T> extends IDisposable {
getDragURI(element: T): string | null;
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined;
onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void;
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
onDragLeave?(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void;
onDragEnd?(originalEvent: DragEvent): void;
}

Expand Down
53 changes: 41 additions & 12 deletions src/vs/base/browser/ui/list/listView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/
import { IRange, Range } from 'vs/base/common/range';
import { INewScrollDimensions, Scrollable, ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
import { ISpliceable } from 'vs/base/common/sequence';
import { IListDragAndDrop, IListDragEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
import { IListDragAndDrop, IListDragEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListDragOverEffectPosition, ListDragOverEffectType } from 'vs/base/browser/ui/list/list';
import { RangeMap, shift } from 'vs/base/browser/ui/list/rangeMap';
import { IRow, RowCache } from 'vs/base/browser/ui/list/rowCache';
import { IObservableValue } from 'vs/base/common/observableValue';
Expand Down Expand Up @@ -48,6 +48,14 @@ export interface IListViewDragAndDrop<T> extends IListDragAndDrop<T> {
getDragElements(element: T): T[];
}

export const enum ListViewTargetSector {
// drop position relative to the top of the item
TOP = 0, // [0%-25%)
CENTER_TOP = 1, // [25%-50%)
CENTER_BOTTOM = 2, // [50%-75%)
BOTTOM = 3 // [75%-100%)
}

export interface IListViewAccessibilityProvider<T> {
getSetSize?(element: T, index: number, listLength: number): number;
getPosInSet?(element: T, index: number): number;
Expand Down Expand Up @@ -309,6 +317,7 @@ export class ListView<T> implements IListView<T> {
private canDrop: boolean = false;
private currentDragData: IDragAndDropData | undefined;
private currentDragFeedback: number[] | undefined;
private currentDragFeedbackPosition: ListDragOverEffectPosition | undefined;
private currentDragFeedbackDisposable: IDisposable = Disposable.None;
private onDragLeaveTimeout: IDisposable = Disposable.None;

Expand Down Expand Up @@ -1083,7 +1092,8 @@ export class ListView<T> implements IListView<T> {
const index = this.getItemIndexFromEventTarget(browserEvent.target || null);
const item = typeof index === 'undefined' ? undefined : this.items[index];
const element = item && item.element;
return { browserEvent, index, element };
const sector = this.getTargetSector(browserEvent, index);
return { browserEvent, index, element, sector };
}

private onScroll(e: ScrollEvent): void {
Expand Down Expand Up @@ -1184,7 +1194,7 @@ export class ListView<T> implements IListView<T> {
}
}

const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.browserEvent);
const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.sector, event.browserEvent);
this.canDrop = typeof result === 'boolean' ? result : result.accept;

if (!this.canDrop) {
Expand All @@ -1193,7 +1203,7 @@ export class ListView<T> implements IListView<T> {
return false;
}

event.browserEvent.dataTransfer.dropEffect = (typeof result !== 'boolean' && result.effect === ListDragOverEffect.Copy) ? 'copy' : 'move';
event.browserEvent.dataTransfer.dropEffect = (typeof result !== 'boolean' && result.effect?.type === ListDragOverEffectType.Copy) ? 'copy' : 'move';

let feedback: number[];

Expand All @@ -1211,34 +1221,43 @@ export class ListView<T> implements IListView<T> {
feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort((a, b) => a - b);
feedback = feedback[0] === -1 ? [-1] : feedback;

if (equalsDragFeedback(this.currentDragFeedback, feedback)) {
const dragOverEffctPosition = typeof result !== 'boolean' && result.effect && result.effect.position ? result.effect.position : ListDragOverEffectPosition.Over;

if (equalsDragFeedback(this.currentDragFeedback, feedback) && this.currentDragFeedbackPosition === dragOverEffctPosition) {
return true;
}

this.currentDragFeedback = feedback;
this.currentDragFeedbackPosition = dragOverEffctPosition;
this.currentDragFeedbackDisposable.dispose();

if (feedback[0] === -1) { // entire list feedback
this.domNode.classList.add('drop-target');
this.rowsContainer.classList.add('drop-target');
console.log('entire list feedback', dragOverEffctPosition);
this.domNode.classList.add(dragOverEffctPosition);
this.rowsContainer.classList.add(dragOverEffctPosition);
this.currentDragFeedbackDisposable = toDisposable(() => {
this.domNode.classList.remove('drop-target');
this.rowsContainer.classList.remove('drop-target');
this.domNode.classList.remove(dragOverEffctPosition);
this.rowsContainer.classList.remove(dragOverEffctPosition);
});
} else {

if (feedback.length > 1 && dragOverEffctPosition !== ListDragOverEffectPosition.Over) {
throw new Error('Can\'t use multiple feedbacks with position different than \'over\'');
}

for (const index of feedback) {
const item = this.items[index]!;
item.dropTarget = true;

item.row?.domNode.classList.add('drop-target');
item.row?.domNode.classList.add(dragOverEffctPosition);
}

this.currentDragFeedbackDisposable = toDisposable(() => {
for (const index of feedback) {
const item = this.items[index]!;
item.dropTarget = false;

item.row?.domNode.classList.remove('drop-target');
item.row?.domNode.classList.remove(dragOverEffctPosition);
}
});
}
Expand Down Expand Up @@ -1272,7 +1291,7 @@ export class ListView<T> implements IListView<T> {

event.browserEvent.preventDefault();
dragData.update(event.browserEvent.dataTransfer);
this.dnd.drop(dragData, event.element, event.index, event.browserEvent);
this.dnd.drop(dragData, event.element, event.index, event.sector, event.browserEvent);
}

private onDragEnd(event: DragEvent): void {
Expand All @@ -1288,6 +1307,7 @@ export class ListView<T> implements IListView<T> {

private clearDragOverFeedback(): void {
this.currentDragFeedback = undefined;
this.currentDragFeedbackPosition = undefined;
this.currentDragFeedbackDisposable.dispose();
this.currentDragFeedbackDisposable = Disposable.None;
}
Expand Down Expand Up @@ -1337,6 +1357,15 @@ export class ListView<T> implements IListView<T> {

// Util
benibenj marked this conversation as resolved.
Show resolved Hide resolved

private getTargetSector(browserEvent: DragEvent, targetIndex: number | undefined): ListViewTargetSector | undefined {
if (targetIndex === undefined) {
return undefined;
}
benibenj marked this conversation as resolved.
Show resolved Hide resolved

const relativePosition = browserEvent.offsetY / this.items[targetIndex].size;
return Math.floor(relativePosition / 0.25);
}

private getItemIndexFromEventTarget(target: EventTarget | null): number | undefined {
const scrollableElement = this.scrollableElement.getDomNode();
let element: HTMLElement | null = target as (HTMLElement | null);
Expand Down
36 changes: 27 additions & 9 deletions src/vs/base/browser/ui/list/listWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ISpliceable } from 'vs/base/common/sequence';
import { isNumber } from 'vs/base/common/types';
import 'vs/css!./list';
import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError } from './list';
import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListView } from './listView';
import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';

interface ITraitChangeEvent {
Expand Down Expand Up @@ -965,14 +965,30 @@ export class DefaultStyleController implements IStyleController {
content.push(`.monaco-list${suffix} .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
}

if (styles.listDropBackground) {
if (styles.listDropOverBackground) {
content.push(`
.monaco-list${suffix}.drop-target,
.monaco-list${suffix} .monaco-list-rows.drop-target,
.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
.monaco-list${suffix} .monaco-list-row.drop-target { background-color: ${styles.listDropOverBackground} !important; color: inherit !important; }
`);
}

if (styles.listDropBetweenBackground) {
content.push(`
.monaco-list${suffix} .monaco-list-rows.drop-target-before .monaco-list-row:first-child::before,
.monaco-list${suffix} .monaco-list-row.drop-target-after + .monaco-list-row::before,
.monaco-list${suffix} .monaco-list-row.drop-target-before::before {
content: ""; position: absolute; top: 0px; left: 0px; width: 100%; height: 1px;
background-color: ${styles.listDropBetweenBackground};
}`);
content.push(`
.monaco-list${suffix} .monaco-list-rows.drop-target-after .monaco-list-row:last-child::after,
.monaco-list${suffix} .monaco-list-row:last-child.drop-target-after::after {
content: ""; position: absolute; bottom: 0px; left: 0px; width: 100%; height: 1px;
background-color: ${styles.listDropBetweenBackground};
}`);
}

if (styles.tableColumnsBorder) {
content.push(`
.monaco-table > .monaco-split-view2,
Expand Down Expand Up @@ -1059,7 +1075,8 @@ export interface IListStyles {
listInactiveFocusBackground: string | undefined;
listHoverBackground: string | undefined;
listHoverForeground: string | undefined;
listDropBackground: string | undefined;
listDropOverBackground: string | undefined;
listDropBetweenBackground: string | undefined;
listFocusOutline: string | undefined;
listInactiveFocusOutline: string | undefined;
listSelectionOutline: string | undefined;
Expand All @@ -1081,7 +1098,8 @@ export const unthemedListStyles: IListStyles = {
listInactiveSelectionBackground: '#3F3F46',
listInactiveSelectionIconForeground: '#FFFFFF',
listHoverBackground: '#2A2D2E',
listDropBackground: '#383B3D',
listDropOverBackground: '#383B3D',
listDropBetweenBackground: '#EEEEEE',
treeIndentGuidesStroke: '#a9a9a9',
treeInactiveIndentGuidesStroke: Color.fromHex('#a9a9a9').transparent(0.4).toString(),
tableColumnsBorder: Color.fromHex('#cccccc').transparent(0.2).toString(),
Expand Down Expand Up @@ -1293,8 +1311,8 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
this.dnd.onDragStart?.(data, originalEvent);
}

onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent);
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction {
return this.dnd.onDragOver(data, targetElement, targetIndex, targetSector, originalEvent);
}

onDragLeave(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
Expand All @@ -1305,8 +1323,8 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
this.dnd.onDragEnd?.(originalEvent);
}

drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
this.dnd.drop(data, targetElement, targetIndex, originalEvent);
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {
this.dnd.drop(data, targetElement, targetIndex, targetSector, originalEvent);
}

dispose(): void {
Expand Down
12 changes: 6 additions & 6 deletions src/vs/base/browser/ui/tree/abstractTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
import { IInputBoxStyles, IMessage, MessageType, unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { ElementsDragAndDropData, ListViewTargetSector } from 'vs/base/browser/ui/list/listView';
import { IListOptions, IListStyles, isActionItem, isButton, isInputElement, isMonacoCustomToggle, isMonacoEditor, isStickyScrollElement, List, MouseController, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget';
import { IToggleStyles, Toggle, unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle';
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
Expand Down Expand Up @@ -81,8 +81,8 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
this.dnd.onDragStart?.(asTreeDragAndDropData(data), originalEvent);
}

onDragOver(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent, raw = true): boolean | IListDragOverReaction {
const result = this.dnd.onDragOver(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
onDragOver(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent, raw = true): boolean | IListDragOverReaction {
const result = this.dnd.onDragOver(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
const didChangeAutoExpandNode = this.autoExpandNode !== targetNode;

if (didChangeAutoExpandNode) {
Expand Down Expand Up @@ -124,7 +124,7 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
const parentNode = model.getNode(parentRef);
const parentIndex = parentRef && model.getListIndex(parentRef);

return this.onDragOver(data, parentNode, parentIndex, originalEvent, false);
return this.onDragOver(data, parentNode, parentIndex, targetSector, originalEvent, false);
}

const model = this.modelProvider();
Expand All @@ -135,11 +135,11 @@ class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<
return { ...result, feedback: range(start, start + length) };
}

drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {
this.autoExpandDisposable.dispose();
this.autoExpandNode = undefined;

this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
}

onDragEnd(originalEvent: DragEvent): void {
Expand Down
10 changes: 5 additions & 5 deletions src/vs/base/browser/ui/tree/asyncDataTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { IDragAndDropData } from 'vs/base/browser/dnd';
import { IIdentityProvider, IListDragAndDrop, IListDragOverReaction, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { ElementsDragAndDropData, ListViewTargetSector } from 'vs/base/browser/ui/list/listView';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType } from 'vs/base/browser/ui/tree/abstractTree';
import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
Expand Down Expand Up @@ -199,12 +199,12 @@ class AsyncDataTreeNodeListDragAndDrop<TInput, T> implements IListDragAndDrop<IA
this.dnd.onDragStart?.(asAsyncDataTreeDragAndDropData(data), originalEvent);
}

onDragOver(data: IDragAndDropData, targetNode: IAsyncDataTreeNode<TInput, T> | undefined, targetIndex: number | undefined, originalEvent: DragEvent, raw = true): boolean | IListDragOverReaction {
return this.dnd.onDragOver(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, originalEvent);
onDragOver(data: IDragAndDropData, targetNode: IAsyncDataTreeNode<TInput, T> | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent, raw = true): boolean | IListDragOverReaction {
return this.dnd.onDragOver(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, targetSector, originalEvent);
}

drop(data: IDragAndDropData, targetNode: IAsyncDataTreeNode<TInput, T> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, originalEvent);
drop(data: IDragAndDropData, targetNode: IAsyncDataTreeNode<TInput, T> | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void {
this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element as T, targetIndex, targetSector, originalEvent);
}

onDragEnd(originalEvent: DragEvent): void {
Expand Down
Loading
Loading