From 3dc05e36e15a9b9b910fb41d8dd9bf86544e46d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 10 Mar 2021 16:40:02 +0100 Subject: [PATCH] list: anchor trait fixes #118044 --- src/vs/base/browser/ui/list/listPaging.ts | 8 ++++ src/vs/base/browser/ui/list/listWidget.ts | 48 +++++++++++++------ src/vs/base/browser/ui/table/tableWidget.ts | 8 ++++ src/vs/base/browser/ui/tree/abstractTree.ts | 47 +++++++++++++++++- src/vs/base/browser/ui/tree/asyncDataTree.ts | 9 ++++ .../workbench/browser/actions/listCommands.ts | 1 + 6 files changed, 105 insertions(+), 16 deletions(-) diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 8a9e1f60dff8a..eca039244343c 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -226,6 +226,14 @@ export class PagedList implements IThemable, IDisposable { this.list.scrollLeft = scrollLeft; } + setAnchor(index: number | undefined): void { + this.list.setAnchor(index); + } + + getAnchor(): number | undefined { + return this.list.getAnchor(); + } + setFocus(indexes: number[]): void { this.list.setFocus(indexes); } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index fc1e911678862..41cfef5cd14e3 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./list'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; -import { range, binarySearch } from 'vs/base/common/arrays'; +import { range, binarySearch, firstOrDefault } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; import * as platform from 'vs/base/common/platform'; import { Gesture } from 'vs/base/browser/touch'; @@ -618,27 +618,25 @@ export class MouseController implements IDisposable { return; } - let reference = this.list.getFocus()[0]; - const selection = this.list.getSelection(); - reference = reference === undefined ? selection[0] : reference; - const focus = e.index; if (typeof focus === 'undefined') { this.list.setFocus([], e.browserEvent); this.list.setSelection([], e.browserEvent); + this.list.setAnchor(undefined); return; } if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) { - return this.changeSelection(e, reference); + return this.changeSelection(e); } if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) { - return this.changeSelection(e, reference); + return this.changeSelection(e); } this.list.setFocus([focus], e.browserEvent); + this.list.setAnchor(focus); if (!isMouseRightClick(e.browserEvent)) { this.list.setSelection([focus], e.browserEvent); @@ -660,15 +658,16 @@ export class MouseController implements IDisposable { this.list.setSelection(focus, e.browserEvent); } - private changeSelection(e: IListMouseEvent | IListTouchEvent, reference: number | undefined): void { + private changeSelection(e: IListMouseEvent | IListTouchEvent): void { const focus = e.index!; + const anchor = this.list.getAnchor(); - if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) { - const min = Math.min(reference, focus); - const max = Math.max(reference, focus); + if (this.isSelectionRangeChangeEvent(e) && typeof anchor === 'number') { + const min = Math.min(anchor, focus); + const max = Math.max(anchor, focus); const rangeSelection = range(min, max + 1); const selection = this.list.getSelection(); - const contiguousRange = getContiguousRangeContaining(disjunction(selection, [reference]), reference); + const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor); if (contiguousRange.length === 0) { return; @@ -676,12 +675,14 @@ export class MouseController implements IDisposable { const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange)); this.list.setSelection(newSelection, e.browserEvent); + this.list.setFocus([focus], e.browserEvent); } else if (this.isSelectionSingleChangeEvent(e)) { const selection = this.list.getSelection(); const newSelection = selection.filter(i => i !== focus); this.list.setFocus([focus]); + this.list.setAnchor(focus); if (selection.length === newSelection.length) { this.list.setSelection([...newSelection, focus], e.browserEvent); @@ -1131,8 +1132,9 @@ export interface IListOptionsUpdate extends IListViewOptionsUpdate { export class List implements ISpliceable, IThemable, IDisposable { - private focus: Trait; + private focus = new Trait('focused'); private selection: Trait; + private anchor = new Trait('anchor'); private eventBufferer = new EventBufferer(); protected view: ListView; private spliceable: ISpliceable; @@ -1224,7 +1226,6 @@ export class List implements ISpliceable, IThemable, IDisposable { ) { const role = this._options.accessibilityProvider && this._options.accessibilityProvider.getWidgetRole ? this._options.accessibilityProvider?.getWidgetRole() : 'list'; this.selection = new SelectionTrait(role !== 'listbox'); - this.focus = new Trait('focused'); mixin(_options, defaultStyles, false); @@ -1260,11 +1261,13 @@ export class List implements ISpliceable, IThemable, IDisposable { this.spliceable = new CombinedSpliceable([ new TraitSpliceable(this.focus, this.view, _options.identityProvider), new TraitSpliceable(this.selection, this.view, _options.identityProvider), + new TraitSpliceable(this.anchor, this.view, _options.identityProvider), this.view ]); this.disposables.add(this.focus); this.disposables.add(this.selection); + this.disposables.add(this.anchor); this.disposables.add(this.view); this.disposables.add(this._onDidDispose); @@ -1437,6 +1440,23 @@ export class List implements ISpliceable, IThemable, IDisposable { return this.getSelection().map(i => this.view.element(i)); } + setAnchor(index: number | undefined): void { + if (typeof index === 'undefined') { + this.anchor.set([]); + return; + } + + if (index < 0 || index >= this.length) { + throw new ListError(this.user, `Invalid index ${index}`); + } + + this.anchor.set([index]); + } + + getAnchor(): number | undefined { + return firstOrDefault(this.anchor.get(), undefined); + } + setFocus(indexes: number[], browserEvent?: UIEvent): void { for (const index of indexes) { if (index < 0 || index >= this.length) { diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index 718b8592083fe..033474eb7cfc0 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -273,6 +273,14 @@ export class Table implements ISpliceable, IThemable, IDisposable { this.list.domFocus(); } + setAnchor(index: number | undefined): void { + this.list.setAnchor(index); + } + + getAnchor(): number | undefined { + return this.list.getAnchor(); + } + getSelectedElements(): TRow[] { return this.list.getSelectedElements(); } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index ac22c886cf1e8..4d91a275c0bf9 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd'; -import { range, equals, distinctES6 } from 'vs/base/common/arrays'; +import { range, equals, distinctES6, firstOrDefault } from 'vs/base/common/arrays'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { domEvent } from 'vs/base/browser/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; @@ -1168,6 +1168,7 @@ class TreeNodeList extends List> renderers: IListRenderer[], private focusTrait: Trait, private selectionTrait: Trait, + private anchorTrait: Trait, options: ITreeNodeListOptions ) { super(user, container, virtualDelegate, renderers, options); @@ -1186,6 +1187,7 @@ class TreeNodeList extends List> const additionalFocus: number[] = []; const additionalSelection: number[] = []; + let anchor: number | undefined; elements.forEach((node, index) => { if (this.focusTrait.has(node)) { @@ -1195,6 +1197,10 @@ class TreeNodeList extends List> if (this.selectionTrait.has(node)) { additionalSelection.push(start + index); } + + if (this.anchorTrait.has(node)) { + anchor = start + index; + } }); if (additionalFocus.length > 0) { @@ -1204,6 +1210,10 @@ class TreeNodeList extends List> if (additionalSelection.length > 0) { super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection])); } + + if (typeof anchor === 'number') { + super.setAnchor(anchor); + } } setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void { @@ -1221,6 +1231,18 @@ class TreeNodeList extends List> this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent); } } + + setAnchor(index: number | undefined, fromAPI = false): void { + super.setAnchor(index); + + if (!fromAPI) { + if (typeof index === 'undefined') { + this.anchorTrait.set([]); + } else { + this.anchorTrait.set([this.element(index)]); + } + } + } } export abstract class AbstractTree implements IDisposable { @@ -1230,6 +1252,7 @@ export abstract class AbstractTree implements IDisposable protected model: ITreeModel; private focus: Trait; private selection: Trait; + private anchor: Trait; private eventBufferer = new EventBufferer(); private typeFilterController?: TypeFilterController; private focusNavigationFilter: ((node: ITreeNode) => boolean) | undefined; @@ -1298,7 +1321,8 @@ export abstract class AbstractTree implements IDisposable this.focus = new Trait(_options.identityProvider); this.selection = new Trait(_options.identityProvider); - this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this }); + this.anchor = new Trait(_options.identityProvider); + this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, this.anchor, { ...asListOptions(() => this.model, _options), tree: this }); this.model = this.createModel(user, this.view, _options); onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; @@ -1552,6 +1576,25 @@ export abstract class AbstractTree implements IDisposable this.model.refilter(); } + setAnchor(element: TRef | undefined): void { + if (typeof element === 'undefined') { + return this.view.setAnchor(undefined); + } + + const node = this.model.getNode(element); + this.anchor.set([node]); + + const index = this.model.getListIndex(element); + + if (index > -1) { + this.view.setAnchor(index, true); + } + } + + getAnchor(): T | undefined { + return firstOrDefault(this.anchor.get(), undefined); + } + setSelection(elements: TRef[], browserEvent?: UIEvent): void { const nodes = elements.map(e => this.model.getNode(e)); this.selection.set(nodes, browserEvent); diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 171212f89c752..d9bb4c2e8b6b1 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -628,6 +628,15 @@ export class AsyncDataTree implements IDisposable this.tree.refilter(); } + setAnchor(element: T | undefined): void { + this.tree.setAnchor(typeof element === 'undefined' ? undefined : this.getDataNode(element)); + } + + getAnchor(): T | undefined { + const node = this.tree.getAnchor(); + return node?.element as T; + } + setSelection(elements: T[], browserEvent?: UIEvent): void { const nodes = elements.map(e => this.getDataNode(e)); this.tree.setSelection(nodes, browserEvent); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 63ed71e27d7dc..fdb4c8880f432 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -586,6 +586,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const fakeKeyboardEvent = new KeyboardEvent('keydown'); widget.setSelection([], fakeKeyboardEvent); widget.setFocus([], fakeKeyboardEvent); + widget.setAnchor(undefined); } });