mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-26 09:35:38 -05:00
Fix treeView to render icons (#23530)
This commit is contained in:
@@ -4,20 +4,20 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IAction, ActionRunner } from 'vs/base/common/actions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge } from 'vs/workbench/common/views';
|
||||
import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge, ResolvableTreeItem, TreeCommand } from 'vs/workbench/common/views';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
|
||||
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
@@ -44,6 +44,14 @@ import { NodeContextKey } from 'sql/workbench/contrib/views/browser/nodeContext'
|
||||
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { ColorScheme } from 'vs/platform/theme/common/theme';
|
||||
import { ThemeIcon } from 'vs/base/common/themables';
|
||||
import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/parts/views/checkbox';
|
||||
import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer';
|
||||
import { ITooltipMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
|
||||
import { CancellationToken } from 'vscode';
|
||||
import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService';
|
||||
|
||||
class Root implements ITreeItem {
|
||||
label = { label: 'root' };
|
||||
@@ -53,6 +61,18 @@ class Root implements ITreeItem {
|
||||
children: ITreeItem[] | undefined = undefined;
|
||||
}
|
||||
|
||||
function isTreeCommandEnabled(treeCommand: TreeCommand, contextKeyService: IContextKeyService): boolean {
|
||||
const command = CommandsRegistry.getCommand(treeCommand.originalId ? treeCommand.originalId : treeCommand.id);
|
||||
if (command) {
|
||||
const commandAction = MenuRegistry.getCommand(command.id);
|
||||
const precondition = commandAction && commandAction.precondition;
|
||||
if (precondition) {
|
||||
return contextKeyService.contextMatchesRules(precondition);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data.");
|
||||
|
||||
class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }
|
||||
@@ -397,6 +417,67 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this._register(focusTracker.onDidBlur(() => this.focused = false));
|
||||
}
|
||||
|
||||
private updateCheckboxes(items: ITreeItem[]) {
|
||||
const additionalItems: ITreeItem[] = [];
|
||||
|
||||
if (!this.manuallyManageCheckboxes) {
|
||||
for (const item of items) {
|
||||
if (item.checkbox !== undefined) {
|
||||
|
||||
function checkChildren(currentItem: ITreeItem) {
|
||||
for (const child of (currentItem.children ?? [])) {
|
||||
if (child.checkbox !== undefined && currentItem.checkbox !== undefined) {
|
||||
child.checkbox.isChecked = currentItem.checkbox.isChecked;
|
||||
additionalItems.push(child);
|
||||
checkChildren(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkChildren(item);
|
||||
|
||||
const visitedParents: Set<ITreeItem> = new Set();
|
||||
function checkParents(currentItem: ITreeItem) {
|
||||
if (currentItem.parent && (currentItem.parent.checkbox !== undefined) && currentItem.parent.children) {
|
||||
if (visitedParents.has(currentItem.parent)) {
|
||||
return;
|
||||
} else {
|
||||
visitedParents.add(currentItem.parent);
|
||||
}
|
||||
|
||||
let someUnchecked = false;
|
||||
let someChecked = false;
|
||||
for (const child of currentItem.parent.children) {
|
||||
if (someUnchecked && someChecked) {
|
||||
break;
|
||||
}
|
||||
if (child.checkbox !== undefined) {
|
||||
if (child.checkbox.isChecked) {
|
||||
someChecked = true;
|
||||
} else {
|
||||
someUnchecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (someChecked && !someUnchecked) {
|
||||
currentItem.parent.checkbox.isChecked = true;
|
||||
additionalItems.push(currentItem.parent);
|
||||
checkParents(currentItem.parent);
|
||||
} else if (someUnchecked && !someChecked) {
|
||||
currentItem.parent.checkbox.isChecked = false;
|
||||
additionalItems.push(currentItem.parent);
|
||||
checkParents(currentItem.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkParents(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
items = items.concat(additionalItems);
|
||||
items.forEach(item => this.tree?.rerender(item));
|
||||
this._onDidChangeCheckboxState.fire(items);
|
||||
}
|
||||
|
||||
private createTree() {
|
||||
const actionViewItemProvider = (action: IAction) => {
|
||||
if (action instanceof MenuItemAction) {
|
||||
@@ -411,7 +492,11 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
|
||||
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.id, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
|
||||
const aligner = new Aligner(this.themeService);
|
||||
const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner);
|
||||
const checkboxStateHandler = this._register(new CheckboxStateHandler());
|
||||
this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {
|
||||
this.updateCheckboxes(items);
|
||||
}));
|
||||
const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler);
|
||||
const widgetAriaLabel = this._title;
|
||||
|
||||
this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer],
|
||||
@@ -760,11 +845,13 @@ registerThemingParticipant((theme, collector) => {
|
||||
});
|
||||
|
||||
interface ITreeExplorerTemplateData {
|
||||
elementDisposable: IDisposable;
|
||||
container: HTMLElement;
|
||||
resourceLabel: IResourceLabel;
|
||||
icon: HTMLElement;
|
||||
actionBar: ActionBar;
|
||||
readonly elementDisposable: DisposableStore;
|
||||
readonly container: HTMLElement;
|
||||
readonly resourceLabel: IResourceLabel;
|
||||
readonly icon: HTMLElement;
|
||||
readonly checkboxContainer: HTMLElement;
|
||||
checkbox?: TreeItemCheckbox;
|
||||
readonly actionBar: ActionBar;
|
||||
}
|
||||
|
||||
class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {
|
||||
@@ -772,6 +859,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
static readonly TREE_TEMPLATE_ID = 'treeExplorer';
|
||||
|
||||
private _actionRunner: MultipleSelectionActionRunner | undefined;
|
||||
private _hoverDelegate: IHoverDelegate;
|
||||
private _hasCheckbox: boolean = false;
|
||||
private _renderedElements = new Map<ITreeNode<ITreeItem, FuzzyScore>, ITreeExplorerTemplateData>();
|
||||
|
||||
|
||||
constructor(
|
||||
private treeViewId: string,
|
||||
@@ -779,11 +870,21 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
private labels: ResourceLabels,
|
||||
private actionViewItemProvider: IActionViewItemProvider,
|
||||
private aligner: Aligner,
|
||||
private checkboxStateHandler: CheckboxStateHandler,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ILabelService private readonly labelService: ILabelService
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IHoverService private readonly hoverService: IHoverService,
|
||||
@ITreeViewsService private readonly treeViewsService: ITreeViewsService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
this._hoverDelegate = {
|
||||
showHover: (options: IHoverDelegateOptions) => this.hoverService.showHover(options),
|
||||
delay: <number>this.configurationService.getValue('workbench.hover.delay')
|
||||
};
|
||||
this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));
|
||||
this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));
|
||||
}
|
||||
|
||||
get templateId(): string {
|
||||
@@ -797,34 +898,59 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
|
||||
container.classList.add('custom-view-tree-node-item');
|
||||
|
||||
const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon'));
|
||||
|
||||
const resourceLabel = this.labels.create(container, { supportHighlights: true });
|
||||
const checkboxContainer = DOM.append(container, DOM.$(''));
|
||||
const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate });
|
||||
const icon = DOM.prepend(resourceLabel.element, DOM.$('.custom-view-tree-node-item-icon'));
|
||||
const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));
|
||||
const actionBar = new ActionBar(actionsContainer, {
|
||||
actionViewItemProvider: this.actionViewItemProvider
|
||||
});
|
||||
|
||||
return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None };
|
||||
return { resourceLabel, icon, checkboxContainer, actionBar, container, elementDisposable: new DisposableStore() };
|
||||
}
|
||||
|
||||
private getHover(label: string | undefined, resource: URI | null, node: ITreeItem): string | ITooltipMarkdownString | undefined {
|
||||
if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) {
|
||||
if (resource && !node.tooltip) {
|
||||
return undefined;
|
||||
} else if (node.tooltip === undefined) {
|
||||
return label;
|
||||
} else if (!isString(node.tooltip)) {
|
||||
return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderMarkdownAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover
|
||||
} else if (node.tooltip !== '') {
|
||||
return node.tooltip;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markdown: typeof node.tooltip === 'string' ? node.tooltip :
|
||||
(token: CancellationToken): Promise<IMarkdownString | string | undefined> => {
|
||||
return new Promise<IMarkdownString | string | undefined>((resolve) => {
|
||||
node.resolve(token).then(() => resolve(node.tooltip));
|
||||
});
|
||||
},
|
||||
markdownNotSupportedFallback: resource ? undefined : (label ?? '') // Passing undefined as the fallback for a resource falls back to the old native hover
|
||||
};
|
||||
}
|
||||
|
||||
renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): 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 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;
|
||||
const label = treeItemLabel ? treeItemLabel.label : undefined;
|
||||
const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {
|
||||
if ((Math.abs(start) > label.length) || (Math.abs(end) >= label.length)) {
|
||||
return ({ start: 0, end: 0 });
|
||||
}
|
||||
if (start < 0) {
|
||||
start = label.length + start;
|
||||
}
|
||||
if (end < 0) {
|
||||
end = label.length + end;
|
||||
}
|
||||
if ((start >= label.length) || (end > label.length)) {
|
||||
return ({ start: 0, end: 0 });
|
||||
}
|
||||
if (start > end) {
|
||||
const swap = start;
|
||||
start = end;
|
||||
@@ -832,56 +958,146 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
}
|
||||
return ({ start, end });
|
||||
}) : undefined;
|
||||
const isLightTheme = [ColorScheme.LIGHT, ColorScheme.HIGH_CONTRAST_LIGHT].includes(this.themeService.getColorTheme().type);
|
||||
const icon = isLightTheme ? node.icon : node.iconDark;
|
||||
const iconUrl = icon ? URI.revive(icon) : null;
|
||||
const title = node.tooltip ? isString(node.tooltip) ? node.tooltip : undefined : resource ? undefined : label;
|
||||
const sqlIcon = node.sqlIcon;
|
||||
const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;
|
||||
const iconUrl = icon ? URI.revive(icon) : undefined;
|
||||
const title = this.getHover(label, resource, node);
|
||||
|
||||
// reset
|
||||
templateData.actionBar.clear();
|
||||
templateData.icon.style.color = '';
|
||||
|
||||
if (resource || this.isFileKindThemeIcon(node.themeIcon)) {
|
||||
const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('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: matches ? matches : createMatches(element.filterData) });
|
||||
} else {
|
||||
templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) });
|
||||
let commandEnabled = true;
|
||||
if (node.command) {
|
||||
commandEnabled = isTreeCommandEnabled(node.command, this.contextKeyService);
|
||||
}
|
||||
|
||||
templateData.icon.title = title ? title : '';
|
||||
this.renderCheckbox(node, templateData);
|
||||
|
||||
if (iconUrl || sqlIcon) {
|
||||
if (resource) {
|
||||
const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
|
||||
const labelResource = resource ? resource : URI.parse('missing:_icon_resource');
|
||||
templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, {
|
||||
fileKind: this.getFileKind(node),
|
||||
title,
|
||||
hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon),
|
||||
fileDecorations,
|
||||
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
|
||||
matches: matches ? matches : createMatches(element.filterData),
|
||||
strikethrough: treeItemLabel?.strikethrough,
|
||||
disabledCommand: !commandEnabled,
|
||||
labelEscapeNewLines: true
|
||||
});
|
||||
} else {
|
||||
templateData.resourceLabel.setResource({ name: label, description }, {
|
||||
title,
|
||||
hideIcon: true,
|
||||
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
|
||||
matches: matches ? matches : createMatches(element.filterData),
|
||||
strikethrough: treeItemLabel?.strikethrough,
|
||||
disabledCommand: !commandEnabled,
|
||||
labelEscapeNewLines: true
|
||||
});
|
||||
}
|
||||
|
||||
if (iconUrl) {
|
||||
templateData.icon.className = 'custom-view-tree-node-item-icon';
|
||||
if (sqlIcon) {
|
||||
templateData.icon.classList.toggle(sqlIcon, !!sqlIcon); // tracked change
|
||||
}
|
||||
templateData.icon.classList.toggle('icon', !!sqlIcon);
|
||||
templateData.icon.style.backgroundImage = iconUrl ? DOM.asCSSUrl(iconUrl) : '';
|
||||
templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl);
|
||||
} else {
|
||||
let iconClass: string | undefined;
|
||||
if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) {
|
||||
if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {
|
||||
iconClass = ThemeIcon.asClassName(node.themeIcon);
|
||||
if (node.themeIcon.color) {
|
||||
templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';
|
||||
}
|
||||
}
|
||||
templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';
|
||||
templateData.icon.style.backgroundImage = '';
|
||||
}
|
||||
|
||||
if (!commandEnabled) {
|
||||
templateData.icon.className = templateData.icon.className + ' disabled';
|
||||
if (templateData.container.parentElement) {
|
||||
templateData.container.parentElement.className = templateData.container.parentElement.className + ' disabled';
|
||||
}
|
||||
}
|
||||
|
||||
templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle };
|
||||
templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
|
||||
|
||||
const menuActions = this.menus.getResourceActions(node);
|
||||
if (menuActions.menu) {
|
||||
templateData.elementDisposable.add(menuActions.menu);
|
||||
}
|
||||
templateData.actionBar.push(menuActions.actions, { icon: true, label: false });
|
||||
|
||||
if (this._actionRunner) {
|
||||
templateData.actionBar.actionRunner = this._actionRunner;
|
||||
}
|
||||
this.setAlignment(templateData.container, node);
|
||||
templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node)));
|
||||
this.treeViewsService.addRenderedTreeItemElement(node, templateData.container);
|
||||
|
||||
// remember rendered element
|
||||
this._renderedElements.set(element, templateData);
|
||||
}
|
||||
|
||||
private rerender() {
|
||||
// As we add items to the map during this call we can't directly use the map in the for loop
|
||||
// but have to create a copy of the keys first
|
||||
const keys = new Set(this._renderedElements.keys());
|
||||
for (const key of keys) {
|
||||
const value = this._renderedElements.get(key);
|
||||
if (value) {
|
||||
this.disposeElement(key, 0, value);
|
||||
this.renderElement(key, 0, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData) {
|
||||
if (node.checkbox) {
|
||||
// The first time we find a checkbox we want to rerender the visible tree to adapt the alignment
|
||||
if (!this._hasCheckbox) {
|
||||
this._hasCheckbox = true;
|
||||
this.rerender();
|
||||
}
|
||||
if (!templateData.checkbox) {
|
||||
const checkbox = new TreeItemCheckbox(templateData.checkboxContainer, this.checkboxStateHandler);
|
||||
templateData.checkbox = checkbox;
|
||||
}
|
||||
templateData.checkbox.render(node);
|
||||
}
|
||||
else if (templateData.checkbox) {
|
||||
templateData.checkbox.dispose();
|
||||
templateData.checkbox = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setAlignment(container: HTMLElement, treeItem: ITreeItem) {
|
||||
container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem));
|
||||
container.parentElement!.classList.toggle('align-icon-with-twisty', !this._hasCheckbox && this.aligner.alignIconWithTwisty(treeItem));
|
||||
}
|
||||
|
||||
private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean {
|
||||
// We always hide the resource label in favor of the iconUrl when it's provided.
|
||||
// When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon.
|
||||
return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon)));
|
||||
}
|
||||
|
||||
private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon {
|
||||
if (!icon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the
|
||||
// icon theme and should use whatever the icon theme has provided.
|
||||
return !(hasResource && this.isFileKindThemeIcon(icon));
|
||||
}
|
||||
|
||||
private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean {
|
||||
return icon?.id === FolderThemeIcon.id;
|
||||
}
|
||||
|
||||
private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {
|
||||
if (icon) {
|
||||
return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id;
|
||||
return icon.id === FileThemeIcon.id || this.isFolderThemeIcon(icon);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -900,7 +1116,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
}
|
||||
|
||||
disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
|
||||
templateData.elementDisposable.dispose();
|
||||
templateData.elementDisposable.clear();
|
||||
|
||||
this._renderedElements.delete(resource);
|
||||
this.treeViewsService.removeRenderedTreeItemElement(resource.element);
|
||||
|
||||
templateData.checkbox?.dispose();
|
||||
templateData.checkbox = undefined;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ITreeExplorerTemplateData): void {
|
||||
@@ -991,49 +1213,60 @@ class MultipleSelectionActionRunner extends ActionRunner {
|
||||
}
|
||||
|
||||
class TreeMenus extends Disposable implements IDisposable {
|
||||
private contextKeyService: IContextKeyService | undefined;
|
||||
private _onDidChange = new Emitter<ITreeItem>();
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
private id: string,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IMenuService private readonly menuService: IMenuService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getResourceActions(element: ITreeItem): IAction[] {
|
||||
return this.mergeActions([ // tracked change
|
||||
this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary,
|
||||
this.getActions(MenuId.DataExplorerContext, { key: 'viewItem', value: element.contextValue }).primary
|
||||
]);
|
||||
/**
|
||||
* Caller is now responsible for disposing of the menu!
|
||||
*/
|
||||
getResourceActions(element: ITreeItem): { menu?: IMenu; actions: IAction[] } {
|
||||
const actions = this.getActions(MenuId.ViewItemContext, element, true);
|
||||
return { menu: actions.menu, actions: actions.primary };
|
||||
}
|
||||
|
||||
getResourceContextActions(element: ITreeItem): IAction[] {
|
||||
return this.mergeActions([ // tracked change
|
||||
this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary,
|
||||
this.getActions(MenuId.DataExplorerContext, { key: 'viewItem', value: element.contextValue }).secondary
|
||||
]);
|
||||
return this.getActions(MenuId.ViewItemContext, element).secondary;
|
||||
}
|
||||
|
||||
private mergeActions(actions: IAction[][]): IAction[] {
|
||||
return actions.reduce((p, c) => p.concat(...c.filter(a => p.findIndex(x => x.id === a.id) === -1)), [] as IAction[]);
|
||||
public setContextKeyService(service: IContextKeyService) {
|
||||
this.contextKeyService = service;
|
||||
}
|
||||
|
||||
private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } {
|
||||
private getActions(menuId: MenuId, element: ITreeItem, listen: boolean = false): { menu?: IMenu; primary: IAction[]; secondary: IAction[] } {
|
||||
if (!this.contextKeyService) {
|
||||
return { primary: [], secondary: [] };
|
||||
}
|
||||
|
||||
const contextKeyService = this.contextKeyService.createOverlay([
|
||||
['view', this.id],
|
||||
[context.key, context.value]
|
||||
['viewItem', element.contextValue]
|
||||
]);
|
||||
|
||||
const menu = this.menuService.createMenu(menuId, contextKeyService);
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
const result = { primary, secondary };
|
||||
const result = { primary, secondary, menu };
|
||||
createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, 'inline');
|
||||
|
||||
menu.dispose();
|
||||
|
||||
if (listen) {
|
||||
this._register(menu.onDidChange(() => this._onDidChange.fire(element)));
|
||||
} else {
|
||||
menu.dispose();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
this.contextKeyService = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomTreeView extends TreeView {
|
||||
|
||||
Reference in New Issue
Block a user