true tree focus and selection traits

fixes #67220
This commit is contained in:
Joao Moreno
2019-01-30 21:42:01 +01:00
parent 2d1da69fea
commit 31feeba3f3
4 changed files with 160 additions and 21 deletions

View File

@@ -8,7 +8,7 @@ import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/l
import { IListOptions, List, IListStyles, mightProducePrintableCharacter, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass } from 'vs/base/browser/dom';
import { Event, Relay, Emitter } from 'vs/base/common/event';
import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
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
import { localize } from 'vs/nls';
import { disposableTimeout } from 'vs/base/common/async';
import { isMacintosh } from 'vs/base/common/platform';
import { values } from 'vs/base/common/map';
function asTreeDragAndDropData<T, TFilterData>(data: IDragAndDropData): IDragAndDropData {
if (data instanceof ElementsDragAndDropData) {
@@ -641,12 +642,135 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
readonly autoExpandSingleChildren?: boolean;
}
/**
* The trait concept needs to exist at the tree level, because collapsed
* tree nodes will not be known by the list.
*/
class Trait<T> {
private nodes: ITreeNode<T, any>[] = [];
private elements: T[] | undefined;
private _onDidChange = new Emitter<ITreeEvent<T>>();
readonly onDidChange = this._onDidChange.event;
private _nodeSet: Set<ITreeNode<T, any>> | undefined;
private get nodeSet(): Set<ITreeNode<T, any>> {
if (!this._nodeSet) {
this._nodeSet = new Set();
for (const node of this.nodes) {
this._nodeSet.add(node);
}
}
return this._nodeSet;
}
set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
this.nodes = [...nodes];
this.elements = undefined;
this._nodeSet = undefined;
const that = this;
this._onDidChange.fire({ get elements() { return that.get(); }, browserEvent });
}
get(): T[] {
if (!this.elements) {
this.elements = this.nodes.map(node => node.element);
}
return [...this.elements];
}
has(node: ITreeNode<T, any>): boolean {
return this.nodeSet.has(node);
}
remove(nodes: ITreeNode<T, any>[]): void {
const set = this.nodeSet;
for (const node of nodes) {
set.delete(node);
}
this.set(values(set));
}
}
/**
* We use this List subclass to restore selection and focus as nodes
* get rendered in the list, possibly due to a node expand() call.
*/
class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
constructor(
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<ITreeNode<T, TFilterData>>,
renderers: IListRenderer<any /* TODO@joao */, any>[],
private focusTrait: Trait<T>,
private selectionTrait: Trait<T>,
options?: IListOptions<ITreeNode<T, TFilterData>>
) {
super(container, virtualDelegate, renderers, options);
}
splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
super.splice(start, deleteCount, elements);
if (elements.length === 0) {
return;
}
const additionalFocus: number[] = [];
const additionalSelection: number[] = [];
elements.forEach((node, index) => {
if (this.selectionTrait.has(node)) {
additionalFocus.push(start + index);
}
if (this.selectionTrait.has(node)) {
additionalSelection.push(start + index);
}
});
if (additionalFocus.length > 0) {
super.setFocus([...super.getFocus(), ...additionalFocus]);
}
if (additionalSelection.length > 0) {
super.setSelection([...super.getSelection(), ...additionalSelection]);
}
}
setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
super.setFocus(indexes, browserEvent);
if (!fromAPI) {
this.focusTrait.set(indexes.map(i => this.element(i)), browserEvent);
}
}
setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
super.setSelection(indexes, browserEvent);
if (!fromAPI) {
this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent);
}
}
}
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
private view: List<ITreeNode<T, TFilterData>>;
private view: TreeNodeList<T, TFilterData>;
private renderers: TreeRenderer<T, TFilterData, any>[];
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
protected model: ITreeModel<T, TFilterData, TRef>;
private focus = new Trait<T>();
private selection = new Trait<T>();
private eventBufferer = new EventBufferer();
protected disposables: IDisposable[] = [];
private _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
@@ -654,8 +778,8 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
get onDidScroll(): Event<void> { return this.view.onDidScroll; }
get onDidChangeFocus(): Event<ITreeEvent<T>> { return Event.map(this.view.onFocusChange, asTreeEvent); }
get onDidChangeSelection(): Event<ITreeEvent<T>> { return Event.map(this.view.onSelectionChange, asTreeEvent); }
readonly onDidChangeFocus: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.focus.onDidChange);
readonly onDidChangeSelection: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.selection.onDidChange);
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.view.onDidOpen, asTreeEvent); }
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
this.disposables.push(filter);
}
this.view = new List(container, treeDelegate, this.renderers, asListOptions(() => this.model, _options));
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, asListOptions(() => this.model, _options));
this.model = this.createModel(this.view, _options);
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
this.model.onDidSplice(e => {
this.eventBufferer.bufferEvents(() => {
this.focus.remove(e.deletedNodes);
this.selection.remove(e.deletedNodes);
});
}, null, this.disposables);
this.view.onTap(this.reactOnMouseClick, this, this.disposables);
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
@@ -853,18 +984,23 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
setSelection(elements: TRef[], browserEvent?: UIEvent): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.setSelection(indexes, browserEvent);
const nodes = elements.map(e => this.model.getNode(e));
this.selection.set(nodes, browserEvent);
const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
this.view.setSelection(indexes, browserEvent, true);
}
getSelection(): T[] {
const nodes = this.view.getSelectedElements();
return nodes.map(n => n.element);
return this.selection.get();
}
setFocus(elements: TRef[], browserEvent?: UIEvent): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.setFocus(indexes, browserEvent);
const nodes = elements.map(e => this.model.getNode(e));
this.focus.set(nodes, browserEvent);
const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
this.view.setFocus(indexes, browserEvent, true);
}
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
@@ -892,8 +1028,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
getFocus(): T[] {
const nodes = this.view.getFocusedElements();
return nodes.map(n => n.element);
return this.focus.get();
}
open(elements: TRef[]): void {

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
import { tail2 } from 'vs/base/common/arrays';
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { ISequence, Iterator } from 'vs/base/common/iterator';
@@ -61,7 +61,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
private filter?: ITreeFilter<T, TFilterData>;
private autoExpandSingleChildren: boolean;
private _onDidSplice = new Emitter<void>();
private _onDidSplice = new Emitter<ITreeModelSpliceEvent<T, TFilterData>>();
readonly onDidSplice = this._onDidSplice.event;
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
}
const result = Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement);
this._onDidSplice.fire(undefined);
this._onDidSplice.fire({ deletedNodes });
return result;
}
@@ -144,8 +144,8 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
getListIndex(location: number[]): number {
const { listIndex, visible } = this.getTreeNodeWithListIndex(location);
return visible ? listIndex : -1;
const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location);
return visible && revealed ? listIndex : -1;
}
getListRenderCount(location: number[]): number {

View File

@@ -7,7 +7,7 @@ import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator';
import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> {
readonly sorter?: ITreeSorter<T>;
@@ -21,7 +21,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
private sorter?: ITreeSorter<ITreeElement<T>>;
readonly onDidSplice: Event<void>;
readonly onDidSplice: Event<ITreeModelSpliceEvent<T | null, TFilterData>>;
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;

View File

@@ -95,10 +95,14 @@ export interface ICollapseStateChangeEvent<T, TFilterData> {
deep: boolean;
}
export interface ITreeModelSpliceEvent<T, TFilterData> {
deletedNodes: ITreeNode<T, TFilterData>[];
}
export interface ITreeModel<T, TFilterData, TRef> {
readonly rootRef: TRef;
readonly onDidSplice: Event<void>;
readonly onDidSplice: Event<ITreeModelSpliceEvent<T, TFilterData>>;
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;