From fc5934510ceb85114292809691a3e39933d2a0cf Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 20 Jun 2019 18:05:17 +0200 Subject: [PATCH 01/11] Work in progress --- .../base/parts/tree/browser/treeDefaults.ts | 23 ++ .../browser/parts/views/customView.ts | 320 ++++++++++-------- .../browser/parts/views/media/views.css | 39 ++- 3 files changed, 226 insertions(+), 156 deletions(-) diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index f91ca2bcf84ce..b77f3ada54b54 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -14,6 +14,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as _ from 'vs/base/parts/tree/browser/tree'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; +import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; export interface IKeyBindingCallback { (tree: _.ITree, event: IKeyboardEvent): void; @@ -572,3 +573,25 @@ export class CollapseAllAction extends Action { return Promise.resolve(); } } + + +export class CollapseAllAction2 extends Action { + + constructor(private viewer: AsyncDataTree, enabled: boolean) { + super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'monaco-tree-action collapse-all', enabled); + } + + public run(context?: any): Promise { + // TODO: alexr00 how do I get highlight on the new tree? + // if (this.viewer.getHighlight()) { + // return Promise.resolve(); // Global action disabled if user is in edit mode from another action + // } + this.viewer.collapseAll(); + this.viewer.setSelection([]); + this.viewer.setFocus([]); + this.viewer.domFocus(); + this.viewer.focusFirst(); + + return Promise.resolve(); + } +} diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 5a758eb786ff7..8bfe9db99d835 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -14,7 +14,7 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewsService, ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ViewContainer, ITreeItemLabel, Extensions } from 'vs/workbench/common/views'; -import { IViewletViewOptions, FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; @@ -22,19 +22,17 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as DOM from 'vs/base/browser/dom'; -import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; -import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults'; +import { CollapseAllAction2 } from 'vs/base/parts/tree/browser/treeDefaults'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder, textLinkForeground, textCodeBlockBackground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { isString } from 'vs/base/common/types'; @@ -44,6 +42,9 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer'; import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore } from 'vs/base/common/filters'; export class CustomTreeViewPanel extends ViewletPanel { @@ -168,11 +169,12 @@ export class CustomTreeView extends Disposable implements ITreeView { private treeContainer: HTMLElement; private _messageValue: string | IMarkdownString | undefined; private messageElement: HTMLDivElement; - private tree: FileIconThemableWorkbenchTree; + private tree2: WorkbenchAsyncDataTree; private treeLabels: ResourceLabels; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; private menus: TitleMenus; + private treeMenus: TreeMenus; private markdownRenderer: MarkdownRenderer; private markdownResult: IMarkdownRenderResult | null; @@ -200,7 +202,9 @@ export class CustomTreeView extends Disposable implements ITreeView { @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IProgressService private readonly progressService: IProgressService + @IProgressService private readonly progressService: IProgressService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService ) { super(); this.root = new Root(); @@ -287,7 +291,7 @@ export class CustomTreeView extends Disposable implements ITreeView { getPrimaryActions(): IAction[] { if (this.showCollapseAllAction) { - const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); + const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree2 ? new CollapseAllAction2(this.tree2, true).run() : Promise.resolve()); return [...this.menus.getTitleActions(), collapseAllAction]; } else { return this.menus.getTitleActions(); @@ -309,17 +313,11 @@ export class CustomTreeView extends Disposable implements ITreeView { this.activate(); } - if (this.tree) { - if (this.isVisible) { - DOM.show(this.tree.getHTMLElement()); - } else { - DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it - } - + if (this.tree2) { if (this.isVisible) { - this.tree.onVisible(); + DOM.show(this.tree2.getHTMLElement()); } else { - this.tree.onHidden(); + DOM.hide(this.tree2.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } if (this.isVisible && this.elementsToRefresh.length) { @@ -332,15 +330,15 @@ export class CustomTreeView extends Disposable implements ITreeView { } focus(): void { - if (this.tree && this.root.children && this.root.children.length > 0) { + if (this.tree2 && this.root.children && this.root.children.length > 0) { // Make sure the current selected element is revealed - const selectedElement = this.tree.getSelection()[0]; + const selectedElement = this.tree2.getSelection()[0]; if (selectedElement) { - this.tree.reveal(selectedElement, 0.5); + this.tree2.reveal(selectedElement, 0.5); } // Pass Focus to Viewer - this.tree.domFocus(); + this.tree2.domFocus(); } else { this.domNode.focus(); } @@ -361,18 +359,86 @@ export class CustomTreeView extends Disposable implements ITreeView { private createTree() { const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; - const menus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); + this.treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); - const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, menus, this.treeLabels, actionViewItemProvider); - const controller = this.instantiationService.createInstance(TreeController, this.id, menus); - this.tree = this._register(this.instantiationService.createInstance(FileIconThemableWorkbenchTree, this.treeContainer, { dataSource, renderer, controller }, {})); - this.tree.contextKeyService.createKey(this.id, true); - this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); - this._register(this.tree.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement()))); - this._register(this.tree.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement()))); - this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.selection))); - this.tree.setInput(this.root).then(() => this.updateContentAreas()); + const dataSource = this.instantiationService.createInstance(TreeDataSource2, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); + const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); + DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); + DOM.addClass(this.treeContainer, 'show-file-icons'); + this.tree2 = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], + dataSource, { + // accessibilityProvider: new ExplorerAccessibilityProvider(), + ariaLabel: localize('treeAriaLabel', "Files Explorer"), + // identityProvider: { + // getId: (stat: ITreeItem) => { + // if (stat instanceof NewExplorerItem) { + // return `new:${stat.resource}`; + // } + + // return stat.resource; + // } + // }, + keyboardNavigationLabelProvider: { // todo: test if this is needed. + getKeyboardNavigationLabel: (item: ITreeItem) => { + return item.label; + } + }, + multipleSelectionSupport: true, + // filter: this.filter, + // sorter: this.instantiationService.createInstance(FileSorter), + // dnd: this.instantiationService.createInstance(FileDragAndDrop), + autoExpandSingleChildren: true + }) as WorkbenchAsyncDataTree; + this._register(this.tree2); + renderer.tree = this.tree2; + + this.tree2.contextKeyService.createKey(this.id, true); + this._register(this.tree2.onDidChangeSelection(e => this.onSelection(e))); + this._register(this.tree2.onContextMenu(e => this.onContextMenu(e))); + // this._register(this.tree2.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement()))); todo: figure out where expand goes + // this._register(this.tree2.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement()))); todo: figure out where collapse goes + this._register(this.tree2.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this.tree2.setInput(this.root).then(() => this.updateContentAreas()); + } + + onContextMenu(treeEvent: ITreeContextMenuEvent): void { + const node: ITreeItem | null = treeEvent.element; + if (node === null) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.tree2.setFocus([node]); + const actions = this.treeMenus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + + getActions: () => actions, + + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree2.domFocus(); + } + }, + + getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), + + actionRunner: new MultipleSelectionActionRunner(() => this.tree2.getSelection()) + }); } private updateMessage(): void { @@ -421,15 +487,15 @@ export class CustomTreeView extends Disposable implements ITreeView { this._size = size; const treeSize = size - DOM.getTotalHeight(this.messageElement); this.treeContainer.style.height = treeSize + 'px'; - if (this.tree) { - this.tree.layout(treeSize); + if (this.tree2) { + this.tree2.layout(treeSize); } } } getOptimalWidth(): number { - if (this.tree) { - const parentNode = this.tree.getHTMLElement(); + if (this.tree2) { + const parentNode = this.tree2.getHTMLElement(); const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); return DOM.getLargestChildWidth(parentNode, childNodes); } @@ -437,7 +503,7 @@ export class CustomTreeView extends Disposable implements ITreeView { } refresh(elements?: ITreeItem[]): Promise { - if (this.dataProvider && this.tree) { + if (this.dataProvider && this.tree2) { if (!elements) { elements = [this.root]; // remove all waiting elements to refresh if root is asked to refresh @@ -466,29 +532,35 @@ export class CustomTreeView extends Disposable implements ITreeView { } expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { - if (this.tree) { + if (this.tree2) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - return this.tree.expandAll(itemOrItems); + const promises: Promise[] = []; + itemOrItems.forEach(element => { + promises.push(this.tree2.expand(element, true)); + }); + return Promise.all(promises).then(() => { + return; + }); } return Promise.resolve(undefined); } setSelection(items: ITreeItem[]): void { - if (this.tree) { - this.tree.setSelection(items, { source: 'api' }); + if (this.tree2) { + this.tree2.setSelection(items); // TODO: consider adding { source: 'api' }) } } setFocus(item: ITreeItem): void { - if (this.tree) { + if (this.tree2) { this.focus(); - this.tree.setFocus(item); + this.tree2.setFocus([item]); } } reveal(item: ITreeItem): Promise { - if (this.tree) { - return this.tree.reveal(item); + if (this.tree2) { + return Promise.resolve(this.tree2.reveal(item)); } return Promise.resolve(); } @@ -507,9 +579,11 @@ export class CustomTreeView extends Disposable implements ITreeView { private refreshing: boolean = false; private async doRefresh(elements: ITreeItem[]): Promise { - if (this.tree) { + if (this.tree2) { this.refreshing = true; - await Promise.all(elements.map(e => this.tree.refresh(e))); + for (let i = 0; i < elements.length; i++) { + await this.tree2.updateChildren(elements[i], true); // todo, is recursive needed? + } this.refreshing = false; this.updateContentAreas(); if (this.focused) { @@ -534,14 +608,14 @@ export class CustomTreeView extends Disposable implements ITreeView { if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { return; } - const selection: ITreeItem = this.tree.getSelection()[0]; + const selection: ITreeItem = this.tree2.getSelection()[0]; if (selection) { if (selection.command) { const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; const isMouseEvent = payload && payload.origin === 'mouse'; const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; - if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + if (!isMouseEvent || this.tree2.openOnSingleClick || isDoubleClick) { this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); } } @@ -549,7 +623,18 @@ export class CustomTreeView extends Disposable implements ITreeView { } } -class TreeDataSource implements IDataSource { +class CustomTreeDelegate implements IListVirtualDelegate { + + getHeight(element: ITreeItem): number { + return TreeRenderer2.ITEM_HEIGHT; + } + + getTemplateId(element: ITreeItem): string { + return TreeRenderer2.TREE_TEMPLATE_ID; + } +} + +class TreeDataSource2 implements IAsyncDataSource { constructor( private treeView: ITreeView, @@ -557,48 +642,32 @@ class TreeDataSource implements IDataSource { ) { } - getId(tree: ITree, node: ITreeItem): string { - return node.handle; + hasChildren(element: ITreeItem | ITreeItem[]): boolean { + return Array.isArray(element) || !!this.treeView.dataProvider && element.collapsibleState !== TreeItemCollapsibleState.None; } - hasChildren(tree: ITree, node: ITreeItem): boolean { - return !!this.treeView.dataProvider && node.collapsibleState !== TreeItemCollapsibleState.None; - } - - getChildren(tree: ITree, node: ITreeItem): Promise { + getChildren(element: ITreeItem | ITreeItem[]): ITreeItem[] | Promise { + if (Array.isArray(element)) { + return Promise.resolve(element); + } if (this.treeView.dataProvider) { - return this.withProgress(this.treeView.dataProvider.getChildren(node)); + return this.withProgress(this.treeView.dataProvider.getChildren(element)); } return Promise.resolve([]); } - - shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { - return node.collapsibleState === TreeItemCollapsibleState.Expanded; - } - - getParent(tree: ITree, node: any): Promise { - return Promise.resolve(null); - } -} - -interface ITreeExplorerTemplateData { - resourceLabel: IResourceLabel; - icon: HTMLElement; - actionBar: ActionBar; - aligner: Aligner; } - +// TODO: test themeing // todo@joh,sandy make this proper and contributable from extensions registerThemingParticipant((theme, collector) => { const findMatchHighlightColor = theme.getColor(editorFindMatchHighlight); if (findMatchHighlightColor) { - collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); } const findMatchHighlightColorBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightColorBorder) { - collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); } const link = theme.getColor(textLinkForeground); @@ -615,10 +684,19 @@ registerThemingParticipant((theme, collector) => { } }); -class TreeRenderer implements IRenderer { +export interface ITreeExplorerTemplateData2 { + elementDisposable: IDisposable; + container: HTMLElement; + resourceLabel: IResourceLabel; // todo: label + icon: HTMLElement; + actionBar: ActionBar; + aligner: Aligner; +} - private static readonly ITEM_HEIGHT = 22; - private static readonly TREE_TEMPLATE_ID = 'treeExplorer'; +class TreeRenderer2 implements ITreeRenderer { + static readonly ITEM_HEIGHT = 22; + static readonly TREE_TEMPLATE_ID = 'treeExplorer'; + private _tree: WorkbenchAsyncDataTree; constructor( private treeViewId: string, @@ -631,15 +709,16 @@ class TreeRenderer implements IRenderer { ) { } - getHeight(tree: ITree, element: any): number { - return TreeRenderer.ITEM_HEIGHT; + get templateId(): string { + return TreeRenderer2.TREE_TEMPLATE_ID; } - getTemplateId(tree: ITree, element: any): string { - return TreeRenderer.TREE_TEMPLATE_ID; + set tree(tree: WorkbenchAsyncDataTree) { + this._tree = tree; } - renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData { + // onDidChangeTwistieState?: Event | undefined; + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData2 { DOM.addClass(container, 'custom-view-tree-node-item'); const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); @@ -648,13 +727,15 @@ class TreeRenderer implements IRenderer { const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider, - actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) + actionRunner: new MultipleSelectionActionRunner() // In this case, no selection is used since this is an action bar item. }); - return { resourceLabel, icon, actionBar, aligner: new Aligner(container, tree, this.themeService) }; + return { resourceLabel, icon, actionBar, aligner: new Aligner(container.parentElement!, this._tree, this.themeService), container, elementDisposable: Disposable.None }; } - renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void { + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData2): void { + templateData.elementDisposable.dispose(); + const node = element.element; const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined; const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; @@ -694,7 +775,7 @@ class TreeRenderer implements IRenderer { return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; } - disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void { + disposeTemplate(templateData: ITreeExplorerTemplateData2): void { templateData.resourceLabel.dispose(); templateData.actionBar.dispose(); templateData.aligner.dispose(); @@ -707,7 +788,7 @@ class Aligner extends Disposable { constructor( private container: HTMLElement, - private tree: ITree, + private tree: WorkbenchAsyncDataTree, private themeService: IWorkbenchThemeService ) { super(); @@ -733,7 +814,8 @@ class Aligner extends Disposable { return false; } - const parent: ITreeItem = this.tree.getNavigator(this._treeItem).parent() || this.tree.getInput(); + let parent: ITreeItem | ITreeItem[] = this.tree.getParentElement(this._treeItem) || this.tree.getInput(); + parent = Array.isArray(parent) ? parent[0] : parent; if (this.hasIcon(parent)) { return false; } @@ -757,68 +839,20 @@ class Aligner extends Disposable { } } -class TreeController extends WorkbenchTreeController { - - constructor( - private treeViewId: string, - private menus: TreeMenus, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService - ) { - super({}, configurationService); - } - - protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { - return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); - } - - onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { - event.preventDefault(); - event.stopPropagation(); - - tree.setFocus(node); - const actions = this.menus.getResourceContextActions(node); - if (!actions.length) { - return true; - } - const anchor = { x: event.posx, y: event.posy }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => actions, +// protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { +// return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); +// } - getActionViewItem: (action) => { - const keybinding = this._keybindingService.lookupKeybinding(action.id); - if (keybinding) { - return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - } - return undefined; - }, - - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - tree.domFocus(); - } - }, - - getActionsContext: () => ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }), - - actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) - }); - - return true; - } -} class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => any[]) { + constructor(private getSelectedResources: (() => any[]) | undefined = undefined) { super(); } runAction(action: IAction, context: any): Promise { - if (action instanceof MenuItemAction) { + if ((action instanceof MenuItemAction) && this.getSelectedResources) { const selection = this.getSelectedResources(); const filteredSelection = selection.filter(s => s !== context); diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index ec2880220b8a2..b3489e693876a 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -53,6 +53,7 @@ /* File icons in trees */ .file-icon-themable-tree.align-icons-and-twisties .monaco-tl-twistie:not(.collapsible), +.file-icon-themable-tree .align-icon-with-twisty .monaco-tl-twistie:not(.collapsible), .file-icon-themable-tree.hide-arrows .monaco-tl-twistie { background-image: none !important; width: 0 !important; @@ -93,19 +94,31 @@ display: none; } -.customview-tree.file-icon-themable-tree .monaco-tree-row .content.align-icon-with-twisty::before { +/* .customview-tree .monaco-list-row .monaco-tl-contents::before { + background-size: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; + padding-right: 6px; + width: 16px; + height: 22px; + display: inline-block; + vertical-align: top; + content: ' '; +} */ + +.customview-tree .monaco-list-row .monaco-tl-contents.align-icon-with-twisty::before { display: none; } -.customview-tree.file-icon-themable-tree .monaco-tree-row .content:not(.align-icon-with-twisty)::before { +.customview-tree .monaco-list-row .monaco-tl-contents:not(.align-icon-with-twisty)::before { display: inline-block; } -.customview-tree .monaco-tree .monaco-tree-row { +.customview-tree .monaco-list .monaco-list-row { padding-right: 12px; } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item { display: flex; height: 22px; line-height: 22px; @@ -115,13 +128,13 @@ flex-wrap: nowrap } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel { flex: 1; text-overflow: ellipsis; overflow: hidden; } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon { background-size: 16px; background-position: left center; background-repeat: no-repeat; @@ -131,25 +144,25 @@ -webkit-font-smoothing: antialiased; } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container { flex: 1; } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after { padding-right: 0px; } -.customview-tree .monaco-tree .monaco-tree-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { display: none; } -.customview-tree .monaco-tree .monaco-tree-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, -.customview-tree .monaco-tree .monaco-tree-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, -.customview-tree .monaco-tree .monaco-tree-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { +.customview-tree .monaco-list .monaco-list-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, +.customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, +.customview-tree .monaco-list .monaco-list-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { display: block; } -.customview-tree .monaco-tree .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label { +.customview-tree .monaco-list .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label { width: 16px; height: 100%; background-position: 50% 50%; From 21f805befc1a11a698d764501eccf1b40920a5e4 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 28 Jun 2019 15:11:01 +0200 Subject: [PATCH 02/11] Hook up expand/collapse event, add keyboard nav, other clean up --- .../browser/parts/views/customView.ts | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 8bfe9db99d835..64797e47c386b 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -43,7 +43,7 @@ import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRender import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; import { FuzzyScore } from 'vs/base/common/filters'; export class CustomTreeViewPanel extends ViewletPanel { @@ -362,31 +362,24 @@ export class CustomTreeView extends Disposable implements ITreeView { this.treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource2, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); - const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); + const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider, this._onDidExpandItem, this._onDidCollapseItem); DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); DOM.addClass(this.treeContainer, 'show-file-icons'); this.tree2 = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], dataSource, { - // accessibilityProvider: new ExplorerAccessibilityProvider(), - ariaLabel: localize('treeAriaLabel', "Files Explorer"), - // identityProvider: { - // getId: (stat: ITreeItem) => { - // if (stat instanceof NewExplorerItem) { - // return `new:${stat.resource}`; - // } - - // return stat.resource; - // } - // }, - keyboardNavigationLabelProvider: { // todo: test if this is needed. - getKeyboardNavigationLabel: (item: ITreeItem) => { - return item.label; + accessibilityProvider: { + getAriaLabel(element: ITreeItem): string { + return element.label ? element.label.label : ''; } }, + ariaLabel: localize('treeAriaLabel', "Custom Tree"), multipleSelectionSupport: true, - // filter: this.filter, - // sorter: this.instantiationService.createInstance(FileSorter), - // dnd: this.instantiationService.createInstance(FileDragAndDrop), + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (item: ITreeItem) => { + return item.label ? item.label.label : undefined; // TODO: why don't I get a nice highlight? + } + }, + filter: new Filter(), autoExpandSingleChildren: true }) as WorkbenchAsyncDataTree; this._register(this.tree2); @@ -395,8 +388,6 @@ export class CustomTreeView extends Disposable implements ITreeView { this.tree2.contextKeyService.createKey(this.id, true); this._register(this.tree2.onDidChangeSelection(e => this.onSelection(e))); this._register(this.tree2.onContextMenu(e => this.onContextMenu(e))); - // this._register(this.tree2.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement()))); todo: figure out where expand goes - // this._register(this.tree2.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement()))); todo: figure out where collapse goes this._register(this.tree2.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); this.tree2.setInput(this.root).then(() => this.updateContentAreas()); } @@ -547,7 +538,7 @@ export class CustomTreeView extends Disposable implements ITreeView { setSelection(items: ITreeItem[]): void { if (this.tree2) { - this.tree2.setSelection(items); // TODO: consider adding { source: 'api' }) + this.tree2.setSelection(items); } } @@ -582,7 +573,7 @@ export class CustomTreeView extends Disposable implements ITreeView { if (this.tree2) { this.refreshing = true; for (let i = 0; i < elements.length; i++) { - await this.tree2.updateChildren(elements[i], true); // todo, is recursive needed? + await this.tree2.updateChildren(elements[i], true); } this.refreshing = false; this.updateContentAreas(); @@ -687,26 +678,38 @@ registerThemingParticipant((theme, collector) => { export interface ITreeExplorerTemplateData2 { elementDisposable: IDisposable; container: HTMLElement; - resourceLabel: IResourceLabel; // todo: label + resourceLabel: IResourceLabel; icon: HTMLElement; actionBar: ActionBar; aligner: Aligner; } -class TreeRenderer2 implements ITreeRenderer { +class TreeRenderer2 extends Disposable implements ITreeRenderer { static readonly ITEM_HEIGHT = 22; static readonly TREE_TEMPLATE_ID = 'treeExplorer'; private _tree: WorkbenchAsyncDataTree; + private _onDidChangeTwistieState = this._register(new Emitter()); + onDidChangeTwistieState: Event = this._onDidChangeTwistieState.event; constructor( private treeViewId: string, private menus: TreeMenus, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, + private _onDidExpandItem: Emitter, + private _onDidCollapseItem: Emitter, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILabelService private readonly labelService: ILabelService ) { + super(); + this._register(this.onDidChangeTwistieState(e => { + if (e.collapsibleState === TreeItemCollapsibleState.Expanded) { + this._onDidExpandItem.fire(e); + } else if (e.collapsibleState === TreeItemCollapsibleState.Collapsed) { + this._onDidCollapseItem.fire(e); + } + })); } get templateId(): string { @@ -717,7 +720,6 @@ class TreeRenderer2 implements ITreeRenderer | undefined; renderTemplate(container: HTMLElement): ITreeExplorerTemplateData2 { DOM.addClass(container, 'custom-view-tree-node-item'); @@ -782,6 +784,19 @@ class TreeRenderer2 implements ITreeRenderer { + + constructor() { } + + filter(item: ITreeItem, parentVisibility: TreeVisibility): TreeFilterResult { + if (parentVisibility === TreeVisibility.Hidden) { + return false; + } + + return true; + } +} + class Aligner extends Disposable { private _treeItem: ITreeItem; From 650f6aaf38195e57a0ab8668e67751c3fa97a142 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 28 Jun 2019 15:20:04 +0200 Subject: [PATCH 03/11] Fix keyboard nav for items with resourceUri --- src/vs/workbench/browser/parts/views/customView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 64797e47c386b..43bc0e39acc3c 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -376,7 +376,7 @@ export class CustomTreeView extends Disposable implements ITreeView { multipleSelectionSupport: true, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (item: ITreeItem) => { - return item.label ? item.label.label : undefined; // TODO: why don't I get a nice highlight? + return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); // TODO: why don't I get a nice highlight? } }, filter: new Filter(), From c329c77f26da1d46da8660a4525527f06207ed09 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 28 Jun 2019 16:20:20 +0200 Subject: [PATCH 04/11] Get hightlight working on non-resource tree items --- src/vs/workbench/browser/parts/views/customView.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 43bc0e39acc3c..2f1ec6487c8ab 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -44,7 +44,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; -import { FuzzyScore } from 'vs/base/common/filters'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; export class CustomTreeViewPanel extends ViewletPanel { @@ -679,6 +680,7 @@ export interface ITreeExplorerTemplateData2 { elementDisposable: IDisposable; container: HTMLElement; resourceLabel: IResourceLabel; + nonResourceLabel: HighlightedLabel; icon: HTMLElement; actionBar: ActionBar; aligner: Aligner; @@ -725,14 +727,14 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData2): void { @@ -742,7 +744,6 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer ({ start, end })) : undefined; const icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; const iconUrl = icon ? URI.revive(icon) : null; const title = node.tooltip ? node.tooltip : resource ? undefined : label; @@ -752,9 +753,9 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer('explorer.decorations'); - templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches }); + templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: createMatches(element.filterData) }); } else { - templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches }); + templateData.nonResourceLabel.set(label, createMatches(element.filterData), title); } templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; From eee5b7d5998d63a1a705d937ea2c69238f4a376c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 1 Jul 2019 12:57:44 +0200 Subject: [PATCH 05/11] Custom tree seems to look and behave correctly --- .../browser/parts/views/customView.ts | 36 ++++++++++++------- .../browser/parts/views/media/views.css | 14 ++++---- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 2f1ec6487c8ab..a09579161effd 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -43,7 +43,7 @@ import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRender import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeFilter, TreeVisibility, TreeFilterResult, ITreeEvent } from 'vs/base/browser/ui/tree/tree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; @@ -377,11 +377,12 @@ export class CustomTreeView extends Disposable implements ITreeView { multipleSelectionSupport: true, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (item: ITreeItem) => { - return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); // TODO: why don't I get a nice highlight? + return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); } }, filter: new Filter(), - autoExpandSingleChildren: true + autoExpandSingleChildren: true, + expandOnlyOnTwistieClick: true }) as WorkbenchAsyncDataTree; this._register(this.tree2); renderer.tree = this.tree2; @@ -390,6 +391,7 @@ export class CustomTreeView extends Disposable implements ITreeView { this._register(this.tree2.onDidChangeSelection(e => this.onSelection(e))); this._register(this.tree2.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree2.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree2.onDidOpen(e => this.onDidOpen(e))); this.tree2.setInput(this.root).then(() => this.updateContentAreas()); } @@ -613,6 +615,16 @@ export class CustomTreeView extends Disposable implements ITreeView { } } } + + private onDidOpen(e: ITreeEvent) { + e.elements.forEach(element => { + if (element.command) { + this.commandService.executeCommand(element.command.id, ...(element.command.arguments || [])); + } else { + this.tree2.expand(element); + } + }); + } } class CustomTreeDelegate implements IListVirtualDelegate { @@ -726,13 +738,17 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer('explorer.decorations'); templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: createMatches(element.filterData) }); + templateData.nonResourceLabel.element.remove(); } else { templateData.nonResourceLabel.set(label, createMatches(element.filterData), title); + templateData.resourceLabel.element.remove(); } templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; @@ -855,12 +873,6 @@ class Aligner extends Disposable { } } - -// protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { -// return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); -// } - - class MultipleSelectionActionRunner extends ActionRunner { constructor(private getSelectedResources: (() => any[]) | undefined = undefined) { diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index b3489e693876a..b5896ed862e05 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -144,25 +144,25 @@ -webkit-font-smoothing: antialiased; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel .monaco-icon-label-description-container { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel .monaco-highlighted-label { flex: 1; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel::after { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel::after { padding-right: 0px; } -.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .actions { display: none; } -.customview-tree .monaco-list .monaco-list-row:hover .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, -.customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions, -.customview-tree .monaco-list .monaco-list-row.focused .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions { +.customview-tree .monaco-list .monaco-list-row:hover .custom-view-tree-node-item .actions, +.customview-tree .monaco-list .monaco-list-row.selected .custom-view-tree-node-item .actions, +.customview-tree .monaco-list .monaco-list-row.focused .custom-view-tree-node-item .actions { display: block; } -.customview-tree .monaco-list .custom-view-tree-node-item > .custom-view-tree-node-item-resourceLabel > .actions .action-label { +.customview-tree .monaco-list .custom-view-tree-node-item .actions .action-label { width: 16px; height: 100%; background-position: 50% 50%; From 4f4a5da46aff848de8c4bb6eb53d024ddba18eb6 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 1 Jul 2019 14:50:04 +0200 Subject: [PATCH 06/11] Use newly added onDidChangeCollapseState in custom tree --- .../browser/parts/views/customView.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index a09579161effd..20df0de8ff3a9 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -363,7 +363,7 @@ export class CustomTreeView extends Disposable implements ITreeView { this.treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource2, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); - const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider, this._onDidExpandItem, this._onDidCollapseItem); + const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); DOM.addClass(this.treeContainer, 'show-file-icons'); this.tree2 = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], @@ -391,6 +391,14 @@ export class CustomTreeView extends Disposable implements ITreeView { this._register(this.tree2.onDidChangeSelection(e => this.onSelection(e))); this._register(this.tree2.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree2.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree2.onDidChangeCollapseState(e => { + const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; + if (e.node.collapsed) { + this._onDidCollapseItem.fire(element); + } else { + this._onDidExpandItem.fire(element); + } + })); this._register(this.tree2.onDidOpen(e => this.onDidOpen(e))); this.tree2.setInput(this.root).then(() => this.updateContentAreas()); } @@ -702,28 +710,17 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer; - private _onDidChangeTwistieState = this._register(new Emitter()); - onDidChangeTwistieState: Event = this._onDidChangeTwistieState.event; constructor( private treeViewId: string, private menus: TreeMenus, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, - private _onDidExpandItem: Emitter, - private _onDidCollapseItem: Emitter, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILabelService private readonly labelService: ILabelService ) { super(); - this._register(this.onDidChangeTwistieState(e => { - if (e.collapsibleState === TreeItemCollapsibleState.Expanded) { - this._onDidExpandItem.fire(e); - } else if (e.collapsibleState === TreeItemCollapsibleState.Collapsed) { - this._onDidCollapseItem.fire(e); - } - })); } get templateId(): string { From c7705234b51e23458f1ef276ce89080d0284fcf7 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 1 Jul 2019 15:21:47 +0200 Subject: [PATCH 07/11] Update names --- .../base/parts/tree/browser/treeDefaults.ts | 4 - .../browser/parts/views/customView.ts | 104 +++++++++--------- 2 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index b77f3ada54b54..3a9c4dc11249d 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -582,10 +582,6 @@ export class CollapseAllAction2 extends Action { } public run(context?: any): Promise { - // TODO: alexr00 how do I get highlight on the new tree? - // if (this.viewer.getHighlight()) { - // return Promise.resolve(); // Global action disabled if user is in edit mode from another action - // } this.viewer.collapseAll(); this.viewer.setSelection([]); this.viewer.setFocus([]); diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 20df0de8ff3a9..de39a5d0c9045 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -170,7 +170,7 @@ export class CustomTreeView extends Disposable implements ITreeView { private treeContainer: HTMLElement; private _messageValue: string | IMarkdownString | undefined; private messageElement: HTMLDivElement; - private tree2: WorkbenchAsyncDataTree; + private tree: WorkbenchAsyncDataTree; private treeLabels: ResourceLabels; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; @@ -292,7 +292,7 @@ export class CustomTreeView extends Disposable implements ITreeView { getPrimaryActions(): IAction[] { if (this.showCollapseAllAction) { - const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree2 ? new CollapseAllAction2(this.tree2, true).run() : Promise.resolve()); + const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction2(this.tree, true).run() : Promise.resolve()); return [...this.menus.getTitleActions(), collapseAllAction]; } else { return this.menus.getTitleActions(); @@ -314,11 +314,11 @@ export class CustomTreeView extends Disposable implements ITreeView { this.activate(); } - if (this.tree2) { + if (this.tree) { if (this.isVisible) { - DOM.show(this.tree2.getHTMLElement()); + DOM.show(this.tree.getHTMLElement()); } else { - DOM.hide(this.tree2.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it + DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } if (this.isVisible && this.elementsToRefresh.length) { @@ -331,15 +331,15 @@ export class CustomTreeView extends Disposable implements ITreeView { } focus(): void { - if (this.tree2 && this.root.children && this.root.children.length > 0) { + if (this.tree && this.root.children && this.root.children.length > 0) { // Make sure the current selected element is revealed - const selectedElement = this.tree2.getSelection()[0]; + const selectedElement = this.tree.getSelection()[0]; if (selectedElement) { - this.tree2.reveal(selectedElement, 0.5); + this.tree.reveal(selectedElement, 0.5); } // Pass Focus to Viewer - this.tree2.domFocus(); + this.tree.domFocus(); } else { this.domNode.focus(); } @@ -362,11 +362,11 @@ export class CustomTreeView extends Disposable implements ITreeView { const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; this.treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); - const dataSource = this.instantiationService.createInstance(TreeDataSource2, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); - const renderer = this.instantiationService.createInstance(TreeRenderer2, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); DOM.addClass(this.treeContainer, 'show-file-icons'); - this.tree2 = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], + this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], dataSource, { accessibilityProvider: { getAriaLabel(element: ITreeItem): string { @@ -384,14 +384,14 @@ export class CustomTreeView extends Disposable implements ITreeView { autoExpandSingleChildren: true, expandOnlyOnTwistieClick: true }) as WorkbenchAsyncDataTree; - this._register(this.tree2); - renderer.tree = this.tree2; - - this.tree2.contextKeyService.createKey(this.id, true); - this._register(this.tree2.onDidChangeSelection(e => this.onSelection(e))); - this._register(this.tree2.onContextMenu(e => this.onContextMenu(e))); - this._register(this.tree2.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); - this._register(this.tree2.onDidChangeCollapseState(e => { + this._register(this.tree); + renderer.tree = this.tree; + + this.tree.contextKeyService.createKey(this.id, true); + this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); + this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); + this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree.onDidChangeCollapseState(e => { const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; if (e.node.collapsed) { this._onDidCollapseItem.fire(element); @@ -399,8 +399,8 @@ export class CustomTreeView extends Disposable implements ITreeView { this._onDidExpandItem.fire(element); } })); - this._register(this.tree2.onDidOpen(e => this.onDidOpen(e))); - this.tree2.setInput(this.root).then(() => this.updateContentAreas()); + this._register(this.tree.onDidOpen(e => this.onDidOpen(e))); + this.tree.setInput(this.root).then(() => this.updateContentAreas()); } onContextMenu(treeEvent: ITreeContextMenuEvent): void { @@ -413,7 +413,7 @@ export class CustomTreeView extends Disposable implements ITreeView { event.preventDefault(); event.stopPropagation(); - this.tree2.setFocus([node]); + this.tree.setFocus([node]); const actions = this.treeMenus.getResourceContextActions(node); if (!actions.length) { return; @@ -433,13 +433,13 @@ export class CustomTreeView extends Disposable implements ITreeView { onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this.tree2.domFocus(); + this.tree.domFocus(); } }, getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), - actionRunner: new MultipleSelectionActionRunner(() => this.tree2.getSelection()) + actionRunner: new MultipleSelectionActionRunner(() => this.tree.getSelection()) }); } @@ -489,15 +489,15 @@ export class CustomTreeView extends Disposable implements ITreeView { this._size = size; const treeSize = size - DOM.getTotalHeight(this.messageElement); this.treeContainer.style.height = treeSize + 'px'; - if (this.tree2) { - this.tree2.layout(treeSize); + if (this.tree) { + this.tree.layout(treeSize); } } } getOptimalWidth(): number { - if (this.tree2) { - const parentNode = this.tree2.getHTMLElement(); + if (this.tree) { + const parentNode = this.tree.getHTMLElement(); const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); return DOM.getLargestChildWidth(parentNode, childNodes); } @@ -505,7 +505,7 @@ export class CustomTreeView extends Disposable implements ITreeView { } refresh(elements?: ITreeItem[]): Promise { - if (this.dataProvider && this.tree2) { + if (this.dataProvider && this.tree) { if (!elements) { elements = [this.root]; // remove all waiting elements to refresh if root is asked to refresh @@ -534,11 +534,11 @@ export class CustomTreeView extends Disposable implements ITreeView { } expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { - if (this.tree2) { + if (this.tree) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; const promises: Promise[] = []; itemOrItems.forEach(element => { - promises.push(this.tree2.expand(element, true)); + promises.push(this.tree.expand(element, true)); }); return Promise.all(promises).then(() => { return; @@ -548,21 +548,21 @@ export class CustomTreeView extends Disposable implements ITreeView { } setSelection(items: ITreeItem[]): void { - if (this.tree2) { - this.tree2.setSelection(items); + if (this.tree) { + this.tree.setSelection(items); } } setFocus(item: ITreeItem): void { - if (this.tree2) { + if (this.tree) { this.focus(); - this.tree2.setFocus([item]); + this.tree.setFocus([item]); } } reveal(item: ITreeItem): Promise { - if (this.tree2) { - return Promise.resolve(this.tree2.reveal(item)); + if (this.tree) { + return Promise.resolve(this.tree.reveal(item)); } return Promise.resolve(); } @@ -581,10 +581,10 @@ export class CustomTreeView extends Disposable implements ITreeView { private refreshing: boolean = false; private async doRefresh(elements: ITreeItem[]): Promise { - if (this.tree2) { + if (this.tree) { this.refreshing = true; for (let i = 0; i < elements.length; i++) { - await this.tree2.updateChildren(elements[i], true); + await this.tree.updateChildren(elements[i], true); } this.refreshing = false; this.updateContentAreas(); @@ -610,14 +610,14 @@ export class CustomTreeView extends Disposable implements ITreeView { if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { return; } - const selection: ITreeItem = this.tree2.getSelection()[0]; + const selection: ITreeItem = this.tree.getSelection()[0]; if (selection) { if (selection.command) { const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; const isMouseEvent = payload && payload.origin === 'mouse'; const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; - if (!isMouseEvent || this.tree2.openOnSingleClick || isDoubleClick) { + if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); } } @@ -629,7 +629,7 @@ export class CustomTreeView extends Disposable implements ITreeView { if (element.command) { this.commandService.executeCommand(element.command.id, ...(element.command.arguments || [])); } else { - this.tree2.expand(element); + this.tree.expand(element); } }); } @@ -638,15 +638,15 @@ export class CustomTreeView extends Disposable implements ITreeView { class CustomTreeDelegate implements IListVirtualDelegate { getHeight(element: ITreeItem): number { - return TreeRenderer2.ITEM_HEIGHT; + return TreeRenderer.ITEM_HEIGHT; } getTemplateId(element: ITreeItem): string { - return TreeRenderer2.TREE_TEMPLATE_ID; + return TreeRenderer.TREE_TEMPLATE_ID; } } -class TreeDataSource2 implements IAsyncDataSource { +class TreeDataSource implements IAsyncDataSource { constructor( private treeView: ITreeView, @@ -696,7 +696,7 @@ registerThemingParticipant((theme, collector) => { } }); -export interface ITreeExplorerTemplateData2 { +export interface ITreeExplorerTemplateData { elementDisposable: IDisposable; container: HTMLElement; resourceLabel: IResourceLabel; @@ -706,7 +706,7 @@ export interface ITreeExplorerTemplateData2 { aligner: Aligner; } -class TreeRenderer2 extends Disposable implements ITreeRenderer { +class TreeRenderer extends Disposable implements ITreeRenderer { static readonly ITEM_HEIGHT = 22; static readonly TREE_TEMPLATE_ID = 'treeExplorer'; private _tree: WorkbenchAsyncDataTree; @@ -724,14 +724,14 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer) { this._tree = tree; } - renderTemplate(container: HTMLElement): ITreeExplorerTemplateData2 { + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { DOM.addClass(container, 'custom-view-tree-node-item'); const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); @@ -750,7 +750,7 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData2): void { + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { templateData.elementDisposable.dispose(); const node = element.element; const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; @@ -793,7 +793,7 @@ class TreeRenderer2 extends Disposable implements ITreeRenderer Date: Mon, 1 Jul 2019 15:33:09 +0200 Subject: [PATCH 08/11] Tested themeing --- src/vs/workbench/browser/parts/views/customView.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index de39a5d0c9045..196f1f783291b 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -668,7 +668,6 @@ class TreeDataSource implements IAsyncDataSource { From 73dcc2ecd557d8c6925115678474abc91f3e67c1 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 1 Jul 2019 15:33:51 +0200 Subject: [PATCH 09/11] Fix old typo --- src/vs/workbench/browser/parts/views/customView.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 196f1f783291b..85680af3de5cb 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -685,9 +685,9 @@ registerThemingParticipant((theme, collector) => { if (link) { collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); } - const focustBorderColor = theme.getColor(focusBorder); - if (focustBorderColor) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focustBorderColor}; outline-offset: -1px; }`); + const focusBorderColor = theme.getColor(focusBorder); + if (focusBorderColor) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); } const codeBackground = theme.getColor(textCodeBlockBackground); if (codeBackground) { From fde8fe2da290e69c533e6b46529302356e5f9ee9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 3 Jul 2019 10:09:56 +0200 Subject: [PATCH 10/11] Respond to feedback. Almost ready --- src/vs/base/browser/ui/tree/treeDefaults.ts | 25 +++ .../base/parts/tree/browser/treeDefaults.ts | 19 -- .../api/browser/mainThreadTreeViews.ts | 5 +- .../api/browser/viewsExtensionPoint.ts | 2 +- .../browser/parts/views/customView.ts | 164 ++++++------------ 5 files changed, 83 insertions(+), 132 deletions(-) create mode 100644 src/vs/base/browser/ui/tree/treeDefaults.ts diff --git a/src/vs/base/browser/ui/tree/treeDefaults.ts b/src/vs/base/browser/ui/tree/treeDefaults.ts new file mode 100644 index 0000000000000..03b8665cd64ea --- /dev/null +++ b/src/vs/base/browser/ui/tree/treeDefaults.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; + +export class CollapseAllAction extends Action { + + constructor(private viewer: AsyncDataTree, enabled: boolean) { + super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'monaco-tree-action collapse-all', enabled); + } + + public run(context?: any): Promise { + this.viewer.collapseAll(); + this.viewer.setSelection([]); + this.viewer.setFocus([]); + this.viewer.domFocus(); + this.viewer.focusFirst(); + + return Promise.resolve(); + } +} \ No newline at end of file diff --git a/src/vs/base/parts/tree/browser/treeDefaults.ts b/src/vs/base/parts/tree/browser/treeDefaults.ts index 3a9c4dc11249d..f91ca2bcf84ce 100644 --- a/src/vs/base/parts/tree/browser/treeDefaults.ts +++ b/src/vs/base/parts/tree/browser/treeDefaults.ts @@ -14,7 +14,6 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as _ from 'vs/base/parts/tree/browser/tree'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; export interface IKeyBindingCallback { (tree: _.ITree, event: IKeyboardEvent): void; @@ -573,21 +572,3 @@ export class CollapseAllAction extends Action { return Promise.resolve(); } } - - -export class CollapseAllAction2 extends Action { - - constructor(private viewer: AsyncDataTree, enabled: boolean) { - super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'monaco-tree-action collapse-all', enabled); - } - - public run(context?: any): Promise { - this.viewer.collapseAll(); - this.viewer.setSelection([]); - this.viewer.setFocus([]); - this.viewer.domFocus(); - this.viewer.focusFirst(); - - return Promise.resolve(); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index f7aa4f1dad9cf..ba23ab500c857 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -81,7 +81,10 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie await treeView.refresh(); } for (const parent of parentChain) { - await treeView.expand(parent); + const parentItem = dataProvider.getItem(parent.handle); + if (parentItem) { + await treeView.expand(parentItem); + } } const item = dataProvider.getItem(itemIn.handle); if (item) { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 8bace46843367..96a92b8b82d17 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -383,7 +383,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, collapsed: this.showCollapsed(container), - treeView: this.instantiationService.createInstance(CustomTreeView, item.id, container), + treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name, container), order: ExtensionIdentifier.equals(extension.description.identifier, container.extensionId) ? index + 1 : undefined, extensionId: extension.description.identifier, originalContainerId: entry.key diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 85680af3de5cb..b9921136f0e79 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -32,7 +32,6 @@ import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; -import { CollapseAllAction2 } from 'vs/base/parts/tree/browser/treeDefaults'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder, textLinkForeground, textCodeBlockBackground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { isString } from 'vs/base/common/types'; @@ -43,9 +42,9 @@ import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRender import { ILabelService } from 'vs/platform/label/common/label'; import { Registry } from 'vs/platform/registry/common/platform'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeFilter, TreeVisibility, TreeFilterResult, ITreeEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeEvent } from 'vs/base/browser/ui/tree/tree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; export class CustomTreeViewPanel extends ViewletPanel { @@ -170,12 +169,11 @@ export class CustomTreeView extends Disposable implements ITreeView { private treeContainer: HTMLElement; private _messageValue: string | IMarkdownString | undefined; private messageElement: HTMLDivElement; - private tree: WorkbenchAsyncDataTree; + private tree: WorkbenchAsyncDataTree; private treeLabels: ResourceLabels; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; private menus: TitleMenus; - private treeMenus: TreeMenus; private markdownRenderer: MarkdownRenderer; private markdownResult: IMarkdownRenderResult | null; @@ -197,6 +195,7 @@ export class CustomTreeView extends Disposable implements ITreeView { constructor( private id: string, + private title: string, private viewContainer: ViewContainer, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @@ -292,7 +291,7 @@ export class CustomTreeView extends Disposable implements ITreeView { getPrimaryActions(): IAction[] { if (this.showCollapseAllAction) { - const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction2(this.tree, true).run() : Promise.resolve()); + const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); return [...this.menus.getTitleActions(), collapseAllAction]; } else { return this.menus.getTitleActions(); @@ -353,6 +352,8 @@ export class CustomTreeView extends Disposable implements ITreeView { this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); this.messageElement = DOM.append(this.domNode, DOM.$('.message')); this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); + DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); + DOM.addClass(this.treeContainer, 'show-file-icons'); const focusTracker = this._register(DOM.trackFocus(this.domNode)); this._register(focusTracker.onDidFocus(() => this.focused = true)); this._register(focusTracker.onDidBlur(() => this.focused = false)); @@ -360,36 +361,32 @@ export class CustomTreeView extends Disposable implements ITreeView { private createTree() { const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; - this.treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); + const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.viewContainer.id }, () => task)); - const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, this.treeMenus, this.treeLabels, actionViewItemProvider); - DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); - DOM.addClass(this.treeContainer, 'show-file-icons'); - this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], + const aligner = new Aligner(this.themeService); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); + + this.tree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], dataSource, { accessibilityProvider: { getAriaLabel(element: ITreeItem): string { return element.label ? element.label.label : ''; } }, - ariaLabel: localize('treeAriaLabel', "Custom Tree"), - multipleSelectionSupport: true, + ariaLabel: this.title, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (item: ITreeItem) => { return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); } }, - filter: new Filter(), - autoExpandSingleChildren: true, - expandOnlyOnTwistieClick: true - }) as WorkbenchAsyncDataTree; - this._register(this.tree); - renderer.tree = this.tree; + expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command + }) as WorkbenchAsyncDataTree); + aligner.tree = this.tree; this.tree.contextKeyService.createKey(this.id, true); this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); + this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e))); this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); this._register(this.tree.onDidChangeCollapseState(e => { const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; @@ -399,11 +396,10 @@ export class CustomTreeView extends Disposable implements ITreeView { this._onDidExpandItem.fire(element); } })); - this._register(this.tree.onDidOpen(e => this.onDidOpen(e))); this.tree.setInput(this.root).then(() => this.updateContentAreas()); } - onContextMenu(treeEvent: ITreeContextMenuEvent): void { + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent): void { const node: ITreeItem | null = treeEvent.element; if (node === null) { return; @@ -414,7 +410,7 @@ export class CustomTreeView extends Disposable implements ITreeView { event.stopPropagation(); this.tree.setFocus([node]); - const actions = this.treeMenus.getResourceContextActions(node); + const actions = treeMenus.getResourceContextActions(node); if (!actions.length) { return; } @@ -533,16 +529,12 @@ export class CustomTreeView extends Disposable implements ITreeView { return Promise.resolve(undefined); } - expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { + async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { if (this.tree) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - const promises: Promise[] = []; - itemOrItems.forEach(element => { - promises.push(this.tree.expand(element, true)); - }); - return Promise.all(promises).then(() => { - return; - }); + await Promise.all(itemOrItems.map(element => { + return this.tree.expand(element, false); + })); } return Promise.resolve(undefined); } @@ -583,9 +575,7 @@ export class CustomTreeView extends Disposable implements ITreeView { private async doRefresh(elements: ITreeItem[]): Promise { if (this.tree) { this.refreshing = true; - for (let i = 0; i < elements.length; i++) { - await this.tree.updateChildren(elements[i], true); - } + await Promise.all(elements.map(element => this.tree.updateChildren(element, true))); this.refreshing = false; this.updateContentAreas(); if (this.focused) { @@ -606,33 +596,19 @@ export class CustomTreeView extends Disposable implements ITreeView { } } - private onSelection({ payload }: any): void { - if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { + private onSelection(e: ITreeEvent): void { + if (!e.browserEvent) { return; } const selection: ITreeItem = this.tree.getSelection()[0]; if (selection) { if (selection.command) { - const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; - const isMouseEvent = payload && payload.origin === 'mouse'; - const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; - - if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + if (this.tree.openOnSingleClick) { this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); } } } } - - private onDidOpen(e: ITreeEvent) { - e.elements.forEach(element => { - if (element.command) { - this.commandService.executeCommand(element.command.id, ...(element.command.arguments || [])); - } else { - this.tree.expand(element); - } - }); - } } class CustomTreeDelegate implements IListVirtualDelegate { @@ -646,7 +622,7 @@ class CustomTreeDelegate implements IListVirtualDelegate { } } -class TreeDataSource implements IAsyncDataSource { +class TreeDataSource implements IAsyncDataSource { constructor( private treeView: ITreeView, @@ -654,20 +630,18 @@ class TreeDataSource implements IAsyncDataSource { - if (Array.isArray(element)) { - return Promise.resolve(element); - } + getChildren(element: ITreeItem): ITreeItem[] | Promise { if (this.treeView.dataProvider) { return this.withProgress(this.treeView.dataProvider.getChildren(element)); } return Promise.resolve([]); } } + // todo@joh,sandy make this proper and contributable from extensions registerThemingParticipant((theme, collector) => { @@ -695,11 +669,10 @@ registerThemingParticipant((theme, collector) => { } }); -export interface ITreeExplorerTemplateData { +interface ITreeExplorerTemplateData { elementDisposable: IDisposable; container: HTMLElement; resourceLabel: IResourceLabel; - nonResourceLabel: HighlightedLabel; icon: HTMLElement; actionBar: ActionBar; aligner: Aligner; @@ -708,13 +681,13 @@ export interface ITreeExplorerTemplateData { class TreeRenderer extends Disposable implements ITreeRenderer { static readonly ITEM_HEIGHT = 22; static readonly TREE_TEMPLATE_ID = 'treeExplorer'; - private _tree: WorkbenchAsyncDataTree; constructor( private treeViewId: string, private menus: TreeMenus, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, + private aligner: Aligner, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILabelService private readonly labelService: ILabelService @@ -726,10 +699,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer) { - this._tree = tree; - } - renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { DOM.addClass(container, 'custom-view-tree-node-item'); @@ -739,14 +708,12 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { @@ -766,18 +733,20 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: createMatches(element.filterData) }); - templateData.nonResourceLabel.element.remove(); } else { - templateData.nonResourceLabel.set(label, createMatches(element.filterData), title); - templateData.resourceLabel.element.remove(); + templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: createMatches(element.filterData) }); } templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl); templateData.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + this.setAlignment(templateData.container, node); + this._register(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + } - templateData.aligner.treeItem = node; + private setAlignment(container: HTMLElement, treeItem: ITreeItem) { + DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } private getFileKind(node: ITreeItem): FileKind { @@ -799,53 +768,26 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - - constructor() { } - - filter(item: ITreeItem, parentVisibility: TreeVisibility): TreeFilterResult { - if (parentVisibility === TreeVisibility.Hidden) { - return false; - } - - return true; - } -} - class Aligner extends Disposable { + private _tree: WorkbenchAsyncDataTree; - private _treeItem: ITreeItem; - - constructor( - private container: HTMLElement, - private tree: WorkbenchAsyncDataTree, - private themeService: IWorkbenchThemeService - ) { + constructor(private themeService: IWorkbenchThemeService) { super(); - this._register(this.themeService.onDidFileIconThemeChange(() => this.render())); - } - - set treeItem(treeItem: ITreeItem) { - this._treeItem = treeItem; - this.render(); } - private render(): void { - if (this._treeItem) { - DOM.toggleClass(this.container, 'align-icon-with-twisty', this.hasToAlignIconWithTwisty()); - } + set tree(tree: WorkbenchAsyncDataTree) { + this._tree = tree; } - private hasToAlignIconWithTwisty(): boolean { - if (this._treeItem.collapsibleState !== TreeItemCollapsibleState.None) { + public alignIconWithTwisty(treeItem: ITreeItem): boolean { + if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { return false; } - if (!this.hasIcon(this._treeItem)) { + if (!this.hasIcon(treeItem)) { return false; - } - let parent: ITreeItem | ITreeItem[] = this.tree.getParentElement(this._treeItem) || this.tree.getInput(); - parent = Array.isArray(parent) ? parent[0] : parent; + + const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { return false; } @@ -871,12 +813,12 @@ class Aligner extends Disposable { class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: (() => any[]) | undefined = undefined) { + constructor(private getSelectedResources: (() => any[])) { super(); } runAction(action: IAction, context: any): Promise { - if ((action instanceof MenuItemAction) && this.getSelectedResources) { + if (action instanceof MenuItemAction) { const selection = this.getSelectedResources(); const filteredSelection = selection.filter(s => s !== context); From 7c475c189b8e2f09998c3bc2f3e273d7d04b205c Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 8 Jul 2019 13:52:11 +0200 Subject: [PATCH 11/11] Responded to code review feedback --- .../browser/parts/views/customView.ts | 18 +++++++++--------- .../browser/parts/views/media/views.css | 12 ------------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index b9921136f0e79..eb3c8e8e0a324 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -675,7 +675,6 @@ interface ITreeExplorerTemplateData { resourceLabel: IResourceLabel; icon: HTMLElement; actionBar: ActionBar; - aligner: Aligner; } class TreeRenderer extends Disposable implements ITreeRenderer { @@ -704,16 +703,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { @@ -742,7 +738,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); this.setAlignment(templateData.container, node); - this._register(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); } private setAlignment(container: HTMLElement, treeItem: ITreeItem) { @@ -761,10 +757,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + } + disposeTemplate(templateData: ITreeExplorerTemplateData): void { templateData.resourceLabel.dispose(); templateData.actionBar.dispose(); - templateData.aligner.dispose(); + templateData.elementDisposable.dispose(); } } diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index b5896ed862e05..248fe55114182 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -94,18 +94,6 @@ display: none; } -/* .customview-tree .monaco-list-row .monaco-tl-contents::before { - background-size: 16px; - background-position: 50% 50%; - background-repeat: no-repeat; - padding-right: 6px; - width: 16px; - height: 22px; - display: inline-block; - vertical-align: top; - content: ' '; -} */ - .customview-tree .monaco-list-row .monaco-tl-contents.align-icon-with-twisty::before { display: none; }