From b4681da86aeab043935e16cbbb3a89897e0c87ea Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 8 Oct 2018 11:19:23 +0100 Subject: [PATCH] wip: WorkbenchObjectTree --- src/vs/base/browser/ui/tree/abstractTree.ts | 41 +++- src/vs/base/browser/ui/tree/indexTreeModel.ts | 69 +++++- .../base/browser/ui/tree/objectTreeModel.ts | 15 ++ src/vs/base/browser/ui/tree/tree.ts | 4 + src/vs/platform/list/browser/listService.ts | 87 ++++++- src/vs/workbench/electron-browser/commands.ts | 214 +++++++++++++++--- 6 files changed, 381 insertions(+), 49 deletions(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 21a7bbf3e4e..2915ad1a381 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -5,7 +5,7 @@ import 'vs/css!./tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; +import { IListOptions, List, IIdentityProvider, IMultipleSelectionController, IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { append, $ } from 'vs/base/browser/dom'; import { Event, Relay, chain, mapEvent } from 'vs/base/common/event'; @@ -183,6 +183,10 @@ export abstract class AbstractTree implements IDisposable return mapEvent(this.view.onSelectionChange, e => e.elements.map(e => e.element)); } + get onDidFocus(): Event { return this.view.onDidFocus; } + get onDidBlur(): Event { return this.view.onDidBlur; } + get onDidDispose(): Event { return this.view.onDidDispose; } + constructor( container: HTMLElement, delegate: IVirtualDelegate, @@ -211,15 +215,41 @@ export abstract class AbstractTree implements IDisposable onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); } + // Widget + // TODO@joao rename to `get domElement` getHTMLElement(): HTMLElement { return this.view.getHTMLElement(); } + domFocus(): void { + this.view.domFocus(); + } + layout(height?: number): void { this.view.layout(height); } + style(styles: IListStyles): void { + this.view.style(styles); + } + + // Tree navigation + + getParentElement(ref: TRef | null = null): T | null { + return this.model.getParentElement(ref); + } + + getFirstElementChild(ref: TRef | null = null): T | null { + return this.model.getFirstElementChild(ref); + } + + getLastElementAncestor(ref: TRef | null = null): T | null { + return this.model.getLastElementAncestor(ref); + } + + // Tree + collapse(location: TRef): boolean { return this.model.setCollapsed(location, true); } @@ -268,11 +298,11 @@ export abstract class AbstractTree implements IDisposable } focusNext(n = 1, loop = false): void { - this.view.focusNext(); + this.view.focusNext(n, loop); } focusPrevious(n = 1, loop = false): void { - this.view.focusPrevious(); + this.view.focusPrevious(n, loop); } focusNextPage(): void { @@ -296,6 +326,11 @@ export abstract class AbstractTree implements IDisposable return nodes.map(n => n.element); } + open(elements: TRef[]): void { + const indexes = elements.map(e => this.model.getListIndex(e)); + this.view.open(indexes); + } + reveal(location: TRef, relativeTop?: number): void { const index = this.model.getListIndex(location); this.view.reveal(index, relativeTop); diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index b57ffece854..c6b4304bf32 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -74,7 +74,7 @@ export class IndexTreeModel implements ITreeModel[] = []; const nodesToInsertIterator = Iterator.map(Iterator.from(toInsert), el => this.createTreeNode(el, parentNode, revealed, treeListElementsToInsert, onDidCreateNode)); @@ -109,16 +109,16 @@ export class IndexTreeModel implements ITreeModel implements ITreeModel implements ITreeModel, listIndex: number, revealed: boolean } { - const { parentNode, listIndex, revealed } = this.findParentNode(location); + /** + * Cheaper version of findNode, which doesn't require list indices. + */ + private getNode(location: number[], node: IMutableTreeNode = this.root): IMutableTreeNode { + if (location.length === 0) { + return node; + } + + const [index, ...rest] = location; + + if (index < 0 || index > node.children.length) { + throw new Error('Invalid tree location'); + } + + return this.getNode(rest, node.children[index]); + } + + private getNodeWithListIndex(location: number[]): { node: IMutableTreeNode, listIndex: number, revealed: boolean } { + const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location); const index = location[location.length - 1]; if (index < 0 || index > parentNode.children.length) { @@ -351,7 +368,7 @@ export class IndexTreeModel implements ITreeModel = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; } { + private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode; listIndex: number; revealed: boolean; } { const [index, ...rest] = location; if (index < 0 || index > node.children.length) { @@ -369,7 +386,7 @@ export class IndexTreeModel implements ITreeModel implements ITreeModel): T | null { + if (node.children.length === 0) { + return node.element; + } + + return this._getLastElementAncestor(node.children[node.children.length - 1]); + } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 03e8c131109..3eec9482540 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -45,6 +45,21 @@ export class ObjectTreeModel, TFilterData = void> imp return this.model.splice([...location, 0], Number.MAX_VALUE, children, onDidCreateNode, onDidDeleteNode); } + getParentElement(ref: T | null = null): T | null { + const location = this.getElementLocation(ref); + return this.model.getParentElement(location); + } + + getFirstElementChild(ref: T | null = null): T | null { + const location = this.getElementLocation(ref); + return this.model.getFirstElementChild(location); + } + + getLastElementAncestor(ref: T | null = null): T | null { + const location = this.getElementLocation(ref); + return this.model.getLastElementAncestor(location); + } + getListIndex(element: T): number { const location = this.getElementLocation(element); return this.model.getListIndex(location); diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 1db1b8671c9..f219267f122 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -54,4 +54,8 @@ export interface ITreeModel { getNodeLocation(node: ITreeNode): TRef; getParentNodeLocation(location: TRef): TRef | null; + + getParentElement(location: TRef): T | null; + getFirstElementChild(location: TRef): T | null; + getLastElementAncestor(location: TRef): T | null; } \ No newline at end of file diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 13880be7cf1..d276a9609c6 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -32,8 +32,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { attachInputBoxStyler, attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { ITreeRenderer, ITreeOptions as ITreeOptions2 } from 'vs/base/browser/ui/tree/abstractTree'; -export type ListWidget = List | PagedList | ITree; +export type ListWidget = List | PagedList | ITree | ObjectTree; export const IListService = createDecorator('listService'); @@ -797,6 +799,89 @@ export class HighlightingWorkbenchTree extends WorkbenchTree { } } +export class WorkbenchObjectTree, TFilterData = void> extends ObjectTree { + + readonly contextKeyService: IContextKeyService; + + protected disposables: IDisposable[]; + + private hasSelectionOrFocus: IContextKey; + private hasDoubleSelection: IContextKey; + private hasMultiSelection: IContextKey; + + private _useAltAsMultipleSelectionModifier: boolean; + + constructor( + container: HTMLElement, + delegate: IVirtualDelegate, + renderers: ITreeRenderer[], + options: ITreeOptions2, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(container, delegate, renderers, { + keyboardSupport: false, + selectOnMouseDown: true, + styleController: new DefaultStyleController(getSharedListStyleSheet()), + ...computeStyles(themeService.getTheme(), defaultListStyles), + ...handleListControllers(options, configurationService) + }); + + this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + + const listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); + listSupportsMultiSelect.set(!(options.multipleSelectionSupport === false)); + + this.hasSelectionOrFocus = WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService); + this.hasDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); + this.hasMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); + + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + + this.disposables.push(combinedDisposable([ + this.contextKeyService, + (listService as ListService).register(this), + attachListStyler(this, themeService), + this.onDidChangeSelection(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + this.hasMultiSelection.set(selection.length > 1); + this.hasDoubleSelection.set(selection.length === 2); + }), + this.onDidChangeFocus(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + }) + ])); + + this.registerListeners(); + } + + private registerListeners(): void { + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + } + })); + } + + get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } + + dispose(): void { + super.dispose(); + + this.disposables = dispose(this.disposables); + } +} + const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/electron-browser/commands.ts b/src/vs/workbench/electron-browser/commands.ts index 45fe4a8eed7..847028649ad 100644 --- a/src/vs/workbench/electron-browser/commands.ts +++ b/src/vs/workbench/electron-browser/commands.ts @@ -22,6 +22,7 @@ import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platf import { URI } from 'vs/base/common/uri'; import { IDownloadService } from 'vs/platform/download/common/download'; import { generateUuid } from 'vs/base/common/uuid'; +import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; // --- List Commands @@ -56,6 +57,17 @@ export function registerCommands(): void { } } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + list.focusNext(count); + const listFocus = list.getFocus(); + if (listFocus.length) { + list.reveal(listFocus[0]); + } + } + // Tree else if (focused) { const tree = focused; @@ -77,7 +89,7 @@ export function registerCommands(): void { handler: (accessor, arg2) => focusDown(accessor, arg2) }); - function expandMultiSelection(focused: List | PagedList | ITree, previousFocus: any): void { + function expandMultiSelection(focused: List | PagedList | ITree | ObjectTree, previousFocus: any): void { // List if (focused instanceof List || focused instanceof PagedList) { @@ -92,6 +104,19 @@ export function registerCommands(): void { } } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + const focus = list.getFocus() ? list.getFocus()[0] : void 0; + const selection = list.getSelection(); + if (selection && selection.indexOf(focus) >= 0) { + list.setSelection(selection.filter(s => s !== previousFocus)); + } else { + list.setSelection(selection.concat(focus)); + } + } + // Tree else if (focused) { const tree = focused; @@ -126,6 +151,18 @@ export function registerCommands(): void { expandMultiSelection(focused, previousFocus); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + // Focus down first + const previousFocus = list.getFocus() ? list.getFocus()[0] : void 0; + focusDown(accessor, arg2); + + // Then adjust selection + expandMultiSelection(focused, previousFocus); + } + // Tree else if (focused) { const tree = focused; @@ -158,6 +195,17 @@ export function registerCommands(): void { } } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + list.focusPrevious(count); + const listFocus = list.getFocus(); + if (listFocus.length) { + list.reveal(listFocus[0]); + } + } + // Tree else if (focused) { const tree = focused; @@ -227,18 +275,38 @@ export function registerCommands(): void { // Tree only if (focused && !(focused instanceof List || focused instanceof PagedList)) { - const tree = focused; - const focus = tree.getFocus(); + if (focused instanceof ObjectTree) { + const tree = focused; + const focusedElements = tree.getFocus(); - tree.collapse(focus).then(didCollapse => { - if (focus && !didCollapse) { - tree.focusParent({ origin: 'keyboard' }); - - return tree.reveal(tree.getFocus()); + if (focusedElements.length === 0) { + return; } - return void 0; - }); + const focus = focusedElements[0]; + + if (!tree.collapse(focus)) { + const parent = tree.getParentElement(focus); + + if (parent) { + tree.setFocus(parent); + tree.reveal(parent); + } + } + } else { + const tree = focused; + const focus = tree.getFocus(); + + tree.collapse(focus).then(didCollapse => { + if (focus && !didCollapse) { + tree.focusParent({ origin: 'keyboard' }); + + return tree.reveal(tree.getFocus()); + } + + return void 0; + }); + } } } }); @@ -253,18 +321,38 @@ export function registerCommands(): void { // Tree only if (focused && !(focused instanceof List || focused instanceof PagedList)) { - const tree = focused; - const focus = tree.getFocus(); + if (focused instanceof ObjectTree) { + const tree = focused; + const focusedElements = tree.getFocus(); - tree.expand(focus).then(didExpand => { - if (focus && !didExpand) { - tree.focusFirstChild({ origin: 'keyboard' }); - - return tree.reveal(tree.getFocus()); + if (focusedElements.length === 0) { + return; } - return void 0; - }); + const focus = focusedElements[0]; + + if (!tree.expand(focus)) { + const child = tree.getFirstElementChild(focus); + + if (child) { + tree.setFocus(child); + tree.reveal(child); + } + } + } else { + const tree = focused; + const focus = tree.getFocus(); + + tree.expand(focus).then(didExpand => { + if (focus && !didExpand) { + tree.focusFirstChild({ origin: 'keyboard' }); + + return tree.reveal(tree.getFocus()); + } + + return void 0; + }); + } } } }); @@ -288,6 +376,14 @@ export function registerCommands(): void { list.reveal(list.getFocus()[0]); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + list.focusPreviousPage(); + list.reveal(list.getFocus()[0]); + } + // Tree else if (focused) { const tree = focused; @@ -317,6 +413,14 @@ export function registerCommands(): void { list.reveal(list.getFocus()[0]); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + list.focusNextPage(); + list.reveal(list.getFocus()[0]); + } + // Tree else if (focused) { const tree = focused; @@ -357,6 +461,14 @@ export function registerCommands(): void { list.reveal(0); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + list.setFocus([0]); + list.reveal(0); + } + // Tree else if (focused) { const tree = focused; @@ -396,6 +508,19 @@ export function registerCommands(): void { list.reveal(list.length - 1); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + const last = list.getLastElementAncestor(); + + if (!last) { + return; + } + + list.setFocus([last]); + list.reveal(last); + } + // Tree else if (focused) { const tree = focused; @@ -424,6 +549,13 @@ export function registerCommands(): void { list.open(list.getFocus()); } + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + list.setSelection(list.getFocus()); + list.open(list.getFocus()); + } + // Tree else if (focused) { const tree = focused; @@ -462,11 +594,22 @@ export function registerCommands(): void { // Tree only if (focused && !(focused instanceof List || focused instanceof PagedList)) { - const tree = focused; - const focus = tree.getFocus(); + if (focused instanceof ObjectTree) { + const tree = focused; + const focus = tree.getFocus(); - if (focus) { - tree.toggleExpansion(focus); + if (focus.length === 0) { + return; + } + + tree.toggleCollapsed(focus); + } else { + const tree = focused; + const focus = tree.getFocus(); + + if (focus) { + tree.toggleExpansion(focus); + } } } } @@ -486,14 +629,19 @@ export function registerCommands(): void { if (list.getSelection().length > 0) { list.setSelection([]); - - return void 0; - } - - if (list.getFocus().length > 0) { + } else if (list.getFocus().length > 0) { list.setFocus([]); + } + } - return void 0; + // ObjectTree + else if (focused instanceof ObjectTree) { + const list = focused; + + if (list.getSelection().length > 0) { + list.setSelection([]); + } else if (list.getFocus().length > 0) { + list.setFocus([]); } } @@ -503,14 +651,8 @@ export function registerCommands(): void { if (tree.getSelection().length) { tree.clearSelection({ origin: 'keyboard' }); - - return void 0; - } - - if (tree.getFocus()) { + } else if (tree.getFocus()) { tree.clearFocus({ origin: 'keyboard' }); - - return void 0; } } }