Skip to content

Commit 31feeba

Browse files
committed
true tree focus and selection traits
fixes microsoft#67220
1 parent 2d1da69 commit 31feeba

4 files changed

Lines changed: 160 additions & 21 deletions

File tree

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

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/l
88
import { IListOptions, List, IListStyles, mightProducePrintableCharacter, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent } from 'vs/base/browser/ui/list/listWidget';
99
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
1010
import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass } from 'vs/base/browser/dom';
11-
import { Event, Relay, Emitter } from 'vs/base/common/event';
11+
import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event';
1212
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1313
import { KeyCode } from 'vs/base/common/keyCodes';
1414
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
@@ -22,6 +22,7 @@ import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTr
2222
import { localize } from 'vs/nls';
2323
import { disposableTimeout } from 'vs/base/common/async';
2424
import { isMacintosh } from 'vs/base/common/platform';
25+
import { values } from 'vs/base/common/map';
2526

2627
function asTreeDragAndDropData<T, TFilterData>(data: IDragAndDropData): IDragAndDropData {
2728
if (data instanceof ElementsDragAndDropData) {
@@ -641,21 +642,144 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
641642
readonly autoExpandSingleChildren?: boolean;
642643
}
643644

645+
/**
646+
* The trait concept needs to exist at the tree level, because collapsed
647+
* tree nodes will not be known by the list.
648+
*/
649+
class Trait<T> {
650+
651+
private nodes: ITreeNode<T, any>[] = [];
652+
private elements: T[] | undefined;
653+
654+
private _onDidChange = new Emitter<ITreeEvent<T>>();
655+
readonly onDidChange = this._onDidChange.event;
656+
657+
private _nodeSet: Set<ITreeNode<T, any>> | undefined;
658+
private get nodeSet(): Set<ITreeNode<T, any>> {
659+
if (!this._nodeSet) {
660+
this._nodeSet = new Set();
661+
662+
for (const node of this.nodes) {
663+
this._nodeSet.add(node);
664+
}
665+
}
666+
667+
return this._nodeSet;
668+
}
669+
670+
set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
671+
this.nodes = [...nodes];
672+
this.elements = undefined;
673+
this._nodeSet = undefined;
674+
675+
const that = this;
676+
this._onDidChange.fire({ get elements() { return that.get(); }, browserEvent });
677+
}
678+
679+
get(): T[] {
680+
if (!this.elements) {
681+
this.elements = this.nodes.map(node => node.element);
682+
}
683+
684+
return [...this.elements];
685+
}
686+
687+
has(node: ITreeNode<T, any>): boolean {
688+
return this.nodeSet.has(node);
689+
}
690+
691+
remove(nodes: ITreeNode<T, any>[]): void {
692+
const set = this.nodeSet;
693+
694+
for (const node of nodes) {
695+
set.delete(node);
696+
}
697+
698+
this.set(values(set));
699+
}
700+
}
701+
702+
/**
703+
* We use this List subclass to restore selection and focus as nodes
704+
* get rendered in the list, possibly due to a node expand() call.
705+
*/
706+
class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
707+
708+
constructor(
709+
container: HTMLElement,
710+
virtualDelegate: IListVirtualDelegate<ITreeNode<T, TFilterData>>,
711+
renderers: IListRenderer<any /* TODO@joao */, any>[],
712+
private focusTrait: Trait<T>,
713+
private selectionTrait: Trait<T>,
714+
options?: IListOptions<ITreeNode<T, TFilterData>>
715+
) {
716+
super(container, virtualDelegate, renderers, options);
717+
}
718+
719+
splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
720+
super.splice(start, deleteCount, elements);
721+
722+
if (elements.length === 0) {
723+
return;
724+
}
725+
726+
const additionalFocus: number[] = [];
727+
const additionalSelection: number[] = [];
728+
729+
elements.forEach((node, index) => {
730+
if (this.selectionTrait.has(node)) {
731+
additionalFocus.push(start + index);
732+
}
733+
734+
if (this.selectionTrait.has(node)) {
735+
additionalSelection.push(start + index);
736+
}
737+
});
738+
739+
if (additionalFocus.length > 0) {
740+
super.setFocus([...super.getFocus(), ...additionalFocus]);
741+
}
742+
743+
if (additionalSelection.length > 0) {
744+
super.setSelection([...super.getSelection(), ...additionalSelection]);
745+
}
746+
}
747+
748+
setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
749+
super.setFocus(indexes, browserEvent);
750+
751+
if (!fromAPI) {
752+
this.focusTrait.set(indexes.map(i => this.element(i)), browserEvent);
753+
}
754+
}
755+
756+
setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
757+
super.setSelection(indexes, browserEvent);
758+
759+
if (!fromAPI) {
760+
this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent);
761+
}
762+
}
763+
}
764+
644765
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
645766

646-
private view: List<ITreeNode<T, TFilterData>>;
767+
private view: TreeNodeList<T, TFilterData>;
647768
private renderers: TreeRenderer<T, TFilterData, any>[];
648769
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
649770
protected model: ITreeModel<T, TFilterData, TRef>;
771+
private focus = new Trait<T>();
772+
private selection = new Trait<T>();
773+
private eventBufferer = new EventBufferer();
650774
protected disposables: IDisposable[] = [];
651775

652776
private _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
653777
readonly onDidUpdateOptions = this._onDidUpdateOptions.event;
654778

655779
get onDidScroll(): Event<void> { return this.view.onDidScroll; }
656780

657-
get onDidChangeFocus(): Event<ITreeEvent<T>> { return Event.map(this.view.onFocusChange, asTreeEvent); }
658-
get onDidChangeSelection(): Event<ITreeEvent<T>> { return Event.map(this.view.onSelectionChange, asTreeEvent); }
781+
readonly onDidChangeFocus: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.focus.onDidChange);
782+
readonly onDidChangeSelection: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.selection.onDidChange);
659783
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.view.onDidOpen, asTreeEvent); }
660784

661785
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); }
@@ -697,11 +821,18 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
697821
this.disposables.push(filter);
698822
}
699823

700-
this.view = new List(container, treeDelegate, this.renderers, asListOptions(() => this.model, _options));
824+
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, asListOptions(() => this.model, _options));
701825

702826
this.model = this.createModel(this.view, _options);
703827
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
704828

829+
this.model.onDidSplice(e => {
830+
this.eventBufferer.bufferEvents(() => {
831+
this.focus.remove(e.deletedNodes);
832+
this.selection.remove(e.deletedNodes);
833+
});
834+
}, null, this.disposables);
835+
705836
this.view.onTap(this.reactOnMouseClick, this, this.disposables);
706837
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
707838

@@ -853,18 +984,23 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
853984
}
854985

855986
setSelection(elements: TRef[], browserEvent?: UIEvent): void {
856-
const indexes = elements.map(e => this.model.getListIndex(e));
857-
this.view.setSelection(indexes, browserEvent);
987+
const nodes = elements.map(e => this.model.getNode(e));
988+
this.selection.set(nodes, browserEvent);
989+
990+
const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
991+
this.view.setSelection(indexes, browserEvent, true);
858992
}
859993

860994
getSelection(): T[] {
861-
const nodes = this.view.getSelectedElements();
862-
return nodes.map(n => n.element);
995+
return this.selection.get();
863996
}
864997

865998
setFocus(elements: TRef[], browserEvent?: UIEvent): void {
866-
const indexes = elements.map(e => this.model.getListIndex(e));
867-
this.view.setFocus(indexes, browserEvent);
999+
const nodes = elements.map(e => this.model.getNode(e));
1000+
this.focus.set(nodes, browserEvent);
1001+
1002+
const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
1003+
this.view.setFocus(indexes, browserEvent, true);
8681004
}
8691005

8701006
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
@@ -892,8 +1028,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
8921028
}
8931029

8941030
getFocus(): T[] {
895-
const nodes = this.view.getFocusedElements();
896-
return nodes.map(n => n.element);
1031+
return this.focus.get();
8971032
}
8981033

8991034
open(elements: TRef[]): void {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
6+
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
77
import { tail2 } from 'vs/base/common/arrays';
88
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
99
import { ISequence, Iterator } from 'vs/base/common/iterator';
@@ -61,7 +61,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
6161
private filter?: ITreeFilter<T, TFilterData>;
6262
private autoExpandSingleChildren: boolean;
6363

64-
private _onDidSplice = new Emitter<void>();
64+
private _onDidSplice = new Emitter<ITreeModelSpliceEvent<T, TFilterData>>();
6565
readonly onDidSplice = this._onDidSplice.event;
6666

6767
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, rootElement: T, options: IIndexTreeModelOptions<T, TFilterData> = {}) {
@@ -127,7 +127,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
127127
}
128128

129129
const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement);
130-
this._onDidSplice.fire(undefined);
130+
this._onDidSplice.fire({ deletedNodes });
131131
return result;
132132
}
133133

@@ -144,8 +144,8 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
144144
}
145145

146146
getListIndex(location: number[]): number {
147-
const { listIndex, visible } = this.getTreeNodeWithListIndex(location);
148-
return visible ? listIndex : -1;
147+
const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location);
148+
return visible && revealed ? listIndex : -1;
149149
}
150150

151151
getListRenderCount(location: number[]): number {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ISpliceable } from 'vs/base/common/sequence';
77
import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator';
88
import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
99
import { Event } from 'vs/base/common/event';
10-
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
10+
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
1111

1212
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> {
1313
readonly sorter?: ITreeSorter<T>;
@@ -21,7 +21,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
2121
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
2222
private sorter?: ITreeSorter<ITreeElement<T>>;
2323

24-
readonly onDidSplice: Event<void>;
24+
readonly onDidSplice: Event<ITreeModelSpliceEvent<T | null, TFilterData>>;
2525
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
2626
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
2727

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,14 @@ export interface ICollapseStateChangeEvent<T, TFilterData> {
9595
deep: boolean;
9696
}
9797

98+
export interface ITreeModelSpliceEvent<T, TFilterData> {
99+
deletedNodes: ITreeNode<T, TFilterData>[];
100+
}
101+
98102
export interface ITreeModel<T, TFilterData, TRef> {
99103
readonly rootRef: TRef;
100104

101-
readonly onDidSplice: Event<void>;
105+
readonly onDidSplice: Event<ITreeModelSpliceEvent<T, TFilterData>>;
102106
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
103107
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
104108

0 commit comments

Comments
 (0)