mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-24 17:23:05 -05:00
Fix Azure tree view (#23584)
This commit is contained in:
@@ -9,10 +9,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
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, 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, ResolvableTreeItem, TreeCommand } from 'vs/workbench/common/views';
|
||||
import { IMenuService, MenuId, registerAction2, Action2, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge, ResolvableTreeItem, TreeCommand, ITreeViewDragAndDropController } 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';
|
||||
@@ -52,6 +52,14 @@ 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';
|
||||
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { CustomTreeViewDragAndDrop, RawCustomTreeViewContextKey } from 'vs/workbench/browser/parts/views/treeView';
|
||||
import { setTimeout0 } from 'vs/base/common/platform';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { AriaRole } from 'vs/base/browser/ui/aria/aria';
|
||||
import { API_OPEN_EDITOR_COMMAND_ID, API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
class Root implements ITreeItem {
|
||||
label = { label: 'root' };
|
||||
@@ -77,28 +85,30 @@ const noDataProviderMessage = localize('no-dataprovider', "There is no data prov
|
||||
|
||||
class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }
|
||||
|
||||
export class TreeView extends Disposable implements ITreeView {
|
||||
abstract class AbstractTreeView extends Disposable implements ITreeView {
|
||||
|
||||
private isVisible: boolean = false;
|
||||
private _hasIconForParentNode = false;
|
||||
private _hasIconForLeafNode = false;
|
||||
|
||||
private readonly collapseAllContextKey: RawContextKey<boolean>;
|
||||
private readonly collapseAllContext: IContextKey<boolean>;
|
||||
private readonly refreshContextKey: RawContextKey<boolean>;
|
||||
private readonly refreshContext: IContextKey<boolean>;
|
||||
private collapseAllContextKey: RawContextKey<boolean> | undefined;
|
||||
private collapseAllContext: IContextKey<boolean> | undefined;
|
||||
private collapseAllToggleContextKey: RawContextKey<boolean> | undefined;
|
||||
private collapseAllToggleContext: IContextKey<boolean> | undefined;
|
||||
private refreshContextKey: RawContextKey<boolean> | undefined;
|
||||
private refreshContext: IContextKey<boolean> | undefined;
|
||||
|
||||
private focused: boolean = false;
|
||||
private domNode!: HTMLElement;
|
||||
private treeContainer!: HTMLElement;
|
||||
private treeContainer: HTMLElement | undefined;
|
||||
private _messageValue: string | undefined;
|
||||
private _canSelectMany: boolean = false;
|
||||
private messageElement!: HTMLDivElement;
|
||||
private _manuallyManageCheckboxes: boolean = false;
|
||||
private messageElement: HTMLElement | undefined;
|
||||
private tree: Tree | undefined;
|
||||
private treeLabels: ResourceLabels | undefined;
|
||||
readonly badge: IViewBadge | undefined = undefined;
|
||||
readonly container: any | undefined = undefined;
|
||||
private _manuallyManageCheckboxes: boolean = false;
|
||||
private treeViewDnd: CustomTreeViewDragAndDrop | undefined;
|
||||
private _container: HTMLElement | undefined;
|
||||
|
||||
public readonly root: ITreeItem;
|
||||
private elementsToRefresh: ITreeItem[] = [];
|
||||
@@ -149,14 +159,31 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
@IHoverService private readonly hoverService: IHoverService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IActivityService private readonly activityService: IActivityService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this.root = new Root();
|
||||
// Try not to add anything that could be costly to this constructor. It gets called once per tree view
|
||||
// during startup, and anything added here can affect performance.
|
||||
}
|
||||
|
||||
private _isInitialized: boolean = false;
|
||||
private initialize() {
|
||||
if (this._isInitialized) {
|
||||
return;
|
||||
}
|
||||
this._isInitialized = true;
|
||||
|
||||
// Remember when adding to this method that it isn't called until the the view is visible, meaning that
|
||||
// properties could be set and events could be fired before we're initialized and that this needs to be handled.
|
||||
|
||||
this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, false);
|
||||
this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService);
|
||||
this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);
|
||||
this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, false);
|
||||
this.refreshContext = this.refreshContextKey.bindTo(contextKeyService);
|
||||
this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);
|
||||
|
||||
this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
|
||||
this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
|
||||
@@ -166,6 +193,12 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this.doRefresh([this.root]); /** soft refresh **/
|
||||
}
|
||||
}));
|
||||
|
||||
this.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id);
|
||||
if (this._dragAndDropController) {
|
||||
this.treeViewDnd.controller = this._dragAndDropController;
|
||||
}
|
||||
|
||||
this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => {
|
||||
if (views.some(v => v.id === this.id)) {
|
||||
this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } });
|
||||
@@ -173,7 +206,7 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}));
|
||||
this.registerActions();
|
||||
|
||||
this.nodeContext = this._register(instantiationService.createInstance(NodeContextKey)); // tracked change
|
||||
this.nodeContext = this._register(this.instantiationService.createInstance(NodeContextKey)); // tracked change
|
||||
this.create();
|
||||
}
|
||||
|
||||
@@ -192,6 +225,17 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
return this.viewDescriptorService.getViewLocationById(this.id)!;
|
||||
}
|
||||
|
||||
private _dragAndDropController: ITreeViewDragAndDropController | undefined;
|
||||
get dragAndDropController(): ITreeViewDragAndDropController | undefined {
|
||||
return this._dragAndDropController;
|
||||
}
|
||||
set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) {
|
||||
this._dragAndDropController = dnd;
|
||||
if (this.treeViewDnd) {
|
||||
this.treeViewDnd.controller = dnd;
|
||||
}
|
||||
}
|
||||
|
||||
private _dataProvider: ITreeViewDataProvider | undefined;
|
||||
get dataProvider(): ITreeViewDataProvider | undefined {
|
||||
return this._dataProvider;
|
||||
@@ -244,16 +288,6 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this._onDidChangeWelcomeState.fire();
|
||||
}
|
||||
|
||||
private _description: string | undefined;
|
||||
get description(): string | undefined {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
set description(_description: string | undefined) {
|
||||
this._description = _description;
|
||||
this._onDidChangeDescription.fire(this._description);
|
||||
}
|
||||
|
||||
private _message: string | undefined;
|
||||
get message(): string | undefined {
|
||||
return this._message;
|
||||
@@ -274,12 +308,63 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this._onDidChangeTitle.fire(this._title);
|
||||
}
|
||||
|
||||
private _description: string | undefined;
|
||||
get description(): string | undefined {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
set description(description: string | undefined) {
|
||||
this._description = description;
|
||||
this._onDidChangeDescription.fire(this._description);
|
||||
}
|
||||
|
||||
private _badge: IViewBadge | undefined;
|
||||
private _badgeActivity: IDisposable | undefined;
|
||||
get badge(): IViewBadge | undefined {
|
||||
return this._badge;
|
||||
}
|
||||
|
||||
set badge(badge: IViewBadge | undefined) {
|
||||
|
||||
if (this._badge?.value === badge?.value &&
|
||||
this._badge?.tooltip === badge?.tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._badgeActivity) {
|
||||
this._badgeActivity.dispose();
|
||||
this._badgeActivity = undefined;
|
||||
}
|
||||
|
||||
this._badge = badge;
|
||||
|
||||
if (badge) {
|
||||
const activity = {
|
||||
badge: new NumberBadge(badge.value, () => badge.tooltip),
|
||||
priority: 50
|
||||
};
|
||||
this._badgeActivity = this.activityService.showViewActivity(this.id, activity);
|
||||
}
|
||||
}
|
||||
|
||||
get canSelectMany(): boolean {
|
||||
return this._canSelectMany;
|
||||
}
|
||||
|
||||
set canSelectMany(canSelectMany: boolean) {
|
||||
const oldCanSelectMany = this._canSelectMany;
|
||||
this._canSelectMany = canSelectMany;
|
||||
if (this._canSelectMany !== oldCanSelectMany) {
|
||||
this.tree?.updateOptions({ multipleSelectionSupport: this.canSelectMany });
|
||||
}
|
||||
}
|
||||
|
||||
get manuallyManageCheckboxes(): boolean {
|
||||
return this._manuallyManageCheckboxes;
|
||||
}
|
||||
|
||||
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
|
||||
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
|
||||
}
|
||||
|
||||
get hasIconForParentNode(): boolean {
|
||||
@@ -294,28 +379,39 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
return this.isVisible;
|
||||
}
|
||||
|
||||
private initializeShowCollapseAllAction(startingValue: boolean = false) {
|
||||
if (!this.collapseAllContext) {
|
||||
this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the the tree view with id {0} enables collapse all.", this.id));
|
||||
this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get showCollapseAllAction(): boolean {
|
||||
return !!this.collapseAllContext.get();
|
||||
this.initializeShowCollapseAllAction();
|
||||
return !!this.collapseAllContext?.get();
|
||||
}
|
||||
|
||||
set showCollapseAllAction(showCollapseAllAction: boolean) {
|
||||
this.collapseAllContext.set(showCollapseAllAction);
|
||||
this.initializeShowCollapseAllAction(showCollapseAllAction);
|
||||
this.collapseAllContext?.set(showCollapseAllAction);
|
||||
}
|
||||
|
||||
private initializeShowRefreshAction(startingValue: boolean = false) {
|
||||
if (!this.refreshContext) {
|
||||
this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, startingValue, localize('treeView.enableRefresh', "Whether the tree view with id {0} enables refresh.", this.id));
|
||||
this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);
|
||||
}
|
||||
}
|
||||
|
||||
get showRefreshAction(): boolean {
|
||||
return !!this.refreshContext.get();
|
||||
this.initializeShowRefreshAction();
|
||||
return !!this.refreshContext?.get();
|
||||
}
|
||||
|
||||
set showRefreshAction(showRefreshAction: boolean) {
|
||||
this.refreshContext.set(showRefreshAction);
|
||||
}
|
||||
|
||||
get manuallyManageCheckboxes(): boolean {
|
||||
return this._manuallyManageCheckboxes;
|
||||
}
|
||||
|
||||
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
|
||||
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
|
||||
this.initializeShowRefreshAction(showRefreshAction);
|
||||
this.refreshContext?.set(showRefreshAction);
|
||||
}
|
||||
|
||||
private registerActions() {
|
||||
@@ -327,11 +423,11 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
title: localize('refresh', "Refresh"),
|
||||
menu: {
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey),
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.refreshContextKey),
|
||||
group: 'navigation',
|
||||
order: Number.MAX_SAFE_INTEGER - 1,
|
||||
},
|
||||
icon: { id: 'codicon/refresh' }
|
||||
icon: Codicon.refresh
|
||||
});
|
||||
}
|
||||
async run(): Promise<void> {
|
||||
@@ -345,11 +441,12 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
title: localize('collapseAll', "Collapse All"),
|
||||
menu: {
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey),
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.collapseAllContextKey),
|
||||
group: 'navigation',
|
||||
order: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
icon: { id: 'codicon/collapse-all' }
|
||||
precondition: that.collapseAllToggleContextKey,
|
||||
icon: Codicon.collapseAll
|
||||
});
|
||||
}
|
||||
async run(): Promise<void> {
|
||||
@@ -361,6 +458,11 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}
|
||||
|
||||
setVisibility(isVisible: boolean): void {
|
||||
// Throughout setVisibility we need to check if the tree view's data provider still exists.
|
||||
// This can happen because the `getChildren` call to the extension can return
|
||||
// after the tree has been disposed.
|
||||
|
||||
this.initialize();
|
||||
isVisible = !!isVisible;
|
||||
if (this.isVisible === isVisible) {
|
||||
return;
|
||||
@@ -382,15 +484,25 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}
|
||||
}
|
||||
|
||||
this._onDidChangeVisibility.fire(this.isVisible);
|
||||
setTimeout0(() => {
|
||||
if (this.dataProvider) {
|
||||
this._onDidChangeVisibility.fire(this.isVisible);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.visible) {
|
||||
this.activate();
|
||||
}
|
||||
}
|
||||
|
||||
focus(reveal: boolean = true): void {
|
||||
protected abstract activate(): void;
|
||||
|
||||
focus(reveal: boolean = true, revealItem?: ITreeItem): void {
|
||||
if (this.tree && this.root.children && this.root.children.length > 0) {
|
||||
// Make sure the current selected element is revealed
|
||||
const selectedElement = this.tree.getSelection()[0];
|
||||
if (selectedElement && reveal) {
|
||||
this.tree.reveal(selectedElement, 0.5);
|
||||
const element = revealItem ?? this.tree.getSelection()[0];
|
||||
if (element && reveal) {
|
||||
this.tree.reveal(element, 0.5);
|
||||
}
|
||||
|
||||
// Pass Focus to Viewer
|
||||
@@ -479,15 +591,7 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}
|
||||
|
||||
private createTree() {
|
||||
const actionViewItemProvider = (action: IAction) => {
|
||||
if (action instanceof MenuItemAction) {
|
||||
return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined);
|
||||
} else if (action instanceof SubmenuItemAction) {
|
||||
return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
|
||||
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, this.id, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
|
||||
@@ -499,18 +603,34 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
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],
|
||||
this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer],
|
||||
dataSource, {
|
||||
identityProvider: new TreeViewIdentityProvider(),
|
||||
accessibilityProvider: {
|
||||
getAriaLabel(element: ITreeItem): string {
|
||||
getAriaLabel(element: ITreeItem): string | null {
|
||||
if (element.accessibilityInformation) {
|
||||
return element.accessibilityInformation.label;
|
||||
}
|
||||
|
||||
return isString(element.tooltip) ? element.tooltip : element.label ? element.label.label : '';
|
||||
if (isString(element.tooltip)) {
|
||||
return element.tooltip;
|
||||
} else {
|
||||
if (element.resourceUri && !element.label) {
|
||||
// The custom tree has no good information on what should be used for the aria label.
|
||||
// Allow the tree widget's default aria label to be used.
|
||||
return null;
|
||||
}
|
||||
let buildAriaLabel: string = '';
|
||||
if (element.label) {
|
||||
buildAriaLabel += element.label.label + ' ';
|
||||
}
|
||||
if (element.description) {
|
||||
buildAriaLabel += element.description;
|
||||
}
|
||||
return buildAriaLabel;
|
||||
}
|
||||
},
|
||||
getRole(element: ITreeItem): string | undefined {
|
||||
getRole(element: ITreeItem): AriaRole | undefined {
|
||||
return element.accessibilityInformation?.role ?? 'treeitem';
|
||||
},
|
||||
getWidgetAriaLabel(): string {
|
||||
@@ -522,22 +642,33 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined);
|
||||
}
|
||||
},
|
||||
expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command,
|
||||
expandOnlyOnTwistieClick: (e: ITreeItem) => {
|
||||
return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';
|
||||
},
|
||||
collapseByDefault: (e: ITreeItem): boolean => {
|
||||
return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
|
||||
},
|
||||
multipleSelectionSupport: this.canSelectMany,
|
||||
dnd: this.treeViewDnd,
|
||||
overrideStyles: {
|
||||
listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND
|
||||
listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND
|
||||
}
|
||||
}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);
|
||||
treeMenus.setContextKeyService(this.tree.contextKeyService);
|
||||
aligner.tree = this.tree;
|
||||
const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection());
|
||||
renderer.actionRunner = actionRunner;
|
||||
|
||||
this.tree.contextKeyService.createKey<boolean>(this.id, true);
|
||||
const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);
|
||||
customTreeKey.set(true);
|
||||
this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
|
||||
this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(<any>e.elements)));
|
||||
this._register(this.tree.onDidChangeFocus(e => {
|
||||
if (e.elements.length) {
|
||||
this._onDidChangeFocus.fire(e.elements[0]);
|
||||
}
|
||||
}));
|
||||
this._register(this.tree.onDidChangeCollapseState(e => {
|
||||
if (!e.node.element) {
|
||||
return;
|
||||
@@ -550,24 +681,60 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
this._onDidExpandItem.fire(element);
|
||||
}
|
||||
}));
|
||||
|
||||
// Update resource context based on focused element
|
||||
this._register(this.tree.onDidChangeFocus(e => { // tracked change
|
||||
this.nodeContext.set({ node: e.elements[0], viewId: this.id });
|
||||
}));
|
||||
|
||||
this.tree.setInput(this.root).then(() => this.updateContentAreas());
|
||||
|
||||
this._register(this.tree.onDidOpen(e => {
|
||||
this._register(this.tree.onDidOpen(async (e) => {
|
||||
if (!e.browserEvent) {
|
||||
return;
|
||||
}
|
||||
if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {
|
||||
return;
|
||||
}
|
||||
const selection = this.tree!.getSelection();
|
||||
if ((selection.length === 1) && selection[0].command) {
|
||||
this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || []));
|
||||
const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);
|
||||
|
||||
if (command && isTreeCommandEnabled(command, this.contextKeyService)) {
|
||||
let args = command.arguments || [];
|
||||
if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {
|
||||
// Some commands owned by us should receive the
|
||||
// `IOpenEvent` as context to open properly
|
||||
args = [...args, e];
|
||||
}
|
||||
|
||||
try {
|
||||
await this.commandService.executeCommand(command.id, ...args);
|
||||
} catch (err) {
|
||||
this.notificationService.error(err);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(treeMenus.onDidChange((changed) => {
|
||||
if (this.tree?.hasNode(changed)) {
|
||||
this.tree?.rerender(changed);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {
|
||||
let command = element?.command;
|
||||
if (element && !command) {
|
||||
if ((element instanceof ResolvableTreeItem) && element.hasResolve) {
|
||||
await element.resolve(new CancellationTokenSource().token);
|
||||
command = element.command;
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
|
||||
this.hoverService.hideHover();
|
||||
const node: ITreeItem | null = treeEvent.element;
|
||||
if (node === null) {
|
||||
return;
|
||||
@@ -619,9 +786,12 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}
|
||||
|
||||
private showMessage(message: string): void {
|
||||
this._messageValue = message;
|
||||
if (!this.messageElement) {
|
||||
return;
|
||||
}
|
||||
this.messageElement.classList.remove('hide');
|
||||
this.resetMessageElement();
|
||||
this._messageValue = message;
|
||||
if (!isFalsyOrWhitespace(this._message)) {
|
||||
this.messageElement.textContent = this._messageValue;
|
||||
}
|
||||
@@ -630,25 +800,25 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
|
||||
private hideMessage(): void {
|
||||
this.resetMessageElement();
|
||||
this.messageElement.classList.add('hide');
|
||||
this.messageElement?.classList.add('hide');
|
||||
this.layout(this._height, this._width);
|
||||
}
|
||||
|
||||
private resetMessageElement(): void {
|
||||
DOM.clearNode(this.messageElement);
|
||||
if (this.messageElement) {
|
||||
DOM.clearNode(this.messageElement);
|
||||
}
|
||||
}
|
||||
|
||||
private _height: number = 0;
|
||||
private _width: number = 0;
|
||||
layout(height: number, width: number) {
|
||||
if (height && width) {
|
||||
if (height && width && this.messageElement && this.treeContainer) {
|
||||
this._height = height;
|
||||
this._width = width;
|
||||
const treeHeight = height - DOM.getTotalHeight(this.messageElement);
|
||||
this.treeContainer.style.height = treeHeight + 'px';
|
||||
if (this.tree) {
|
||||
this.tree.layout(treeHeight, width);
|
||||
}
|
||||
this.tree?.layout(treeHeight, width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,11 +831,7 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
return 0;
|
||||
}
|
||||
|
||||
isCollapsed(item: ITreeItem): boolean {
|
||||
return !!this.tree?.isCollapsed(item);
|
||||
}
|
||||
|
||||
async refresh(elements?: ITreeItem[]): Promise<void> {
|
||||
async refresh(elements?: readonly ITreeItem[]): Promise<void> {
|
||||
if (this.dataProvider && this.tree) {
|
||||
if (this.refreshing) {
|
||||
await Event.toPromise(this._onDidCompleteRefresh.event);
|
||||
@@ -699,28 +865,40 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
|
||||
async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {
|
||||
const tree = this.tree;
|
||||
if (tree) {
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
|
||||
await Promise.all(itemOrItems.map(element => {
|
||||
return tree.expand(element, false);
|
||||
}));
|
||||
for (const element of itemOrItems) {
|
||||
await tree.expand(element, false);
|
||||
}
|
||||
} catch (e) {
|
||||
// The extension could have changed the tree during the reveal.
|
||||
// Because of that, we ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
isCollapsed(item: ITreeItem): boolean {
|
||||
return !!this.tree?.isCollapsed(item);
|
||||
}
|
||||
|
||||
setSelection(items: ITreeItem[]): void {
|
||||
if (this.tree) {
|
||||
this.tree.setSelection(items);
|
||||
}
|
||||
this.tree?.setSelection(items);
|
||||
}
|
||||
|
||||
getSelection(): ITreeItem[] {
|
||||
return this.tree?.getSelection() ?? [];
|
||||
}
|
||||
|
||||
setFocus(item: ITreeItem): void {
|
||||
setFocus(item?: ITreeItem): void {
|
||||
if (this.tree) {
|
||||
this.focus();
|
||||
this.tree.setFocus([item]);
|
||||
if (item) {
|
||||
this.focus(true, item);
|
||||
this.tree.setFocus([item]);
|
||||
} else {
|
||||
this.tree.setFocus([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,31 +909,66 @@ export class TreeView extends Disposable implements ITreeView {
|
||||
}
|
||||
|
||||
private refreshing: boolean = false;
|
||||
private async doRefresh(elements: ITreeItem[]): Promise<void> {
|
||||
private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {
|
||||
const tree = this.tree;
|
||||
if (tree && this.visible) {
|
||||
this.refreshing = true;
|
||||
await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
|
||||
const oldSelection = tree.getSelection();
|
||||
try {
|
||||
await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
|
||||
} catch (e) {
|
||||
// When multiple calls are made to refresh the tree in quick succession,
|
||||
// we can get a "Tree element not found" error. This is expected.
|
||||
// Ideally this is fixable, so log instead of ignoring so the error is preserved.
|
||||
this.logService.error(e);
|
||||
}
|
||||
const newSelection = tree.getSelection();
|
||||
if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {
|
||||
this._onDidChangeSelection.fire(newSelection);
|
||||
}
|
||||
this.refreshing = false;
|
||||
this._onDidCompleteRefresh.fire();
|
||||
this.updateContentAreas();
|
||||
if (this.focused) {
|
||||
this.focus(false);
|
||||
}
|
||||
this.updateCollapseAllToggle();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeCollapseAllToggle() {
|
||||
if (!this.collapseAllToggleContext) {
|
||||
this.collapseAllToggleContextKey = new RawContextKey<boolean>(`treeView.${this.id}.toggleCollapseAll`, false, localize('treeView.toggleCollapseAll', "Whether collapse all is toggled for the tree view with id {0}.", this.id));
|
||||
this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);
|
||||
}
|
||||
}
|
||||
|
||||
private updateCollapseAllToggle() {
|
||||
if (this.showCollapseAllAction) {
|
||||
this.initializeCollapseAllToggle();
|
||||
this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&
|
||||
this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));
|
||||
}
|
||||
}
|
||||
|
||||
private updateContentAreas(): void {
|
||||
const isTreeEmpty = !this.root.children || this.root.children.length === 0;
|
||||
// Hide tree container only when there is a message and tree is empty and not refreshing
|
||||
if (this._messageValue && isTreeEmpty && !this.refreshing) {
|
||||
this.treeContainer.classList.add('hide');
|
||||
if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {
|
||||
// If there's a dnd controller then hiding the tree prevents it from being dragged into.
|
||||
if (!this.dragAndDropController) {
|
||||
this.treeContainer.classList.add('hide');
|
||||
}
|
||||
this.domNode.setAttribute('tabindex', '0');
|
||||
} else {
|
||||
} else if (this.treeContainer) {
|
||||
this.treeContainer.classList.remove('hide');
|
||||
this.domNode.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
|
||||
get container(): HTMLElement | undefined {
|
||||
return this._container;
|
||||
}
|
||||
}
|
||||
|
||||
class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {
|
||||
@@ -938,10 +1151,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
|
||||
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;
|
||||
}
|
||||
@@ -958,9 +1174,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
}
|
||||
return ({ start, end });
|
||||
}) : undefined;
|
||||
const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;
|
||||
const iconUrl = icon ? URI.revive(icon) : 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 = this.getHover(label, resource, node);
|
||||
const sqlIcon = node.sqlIcon;
|
||||
|
||||
// reset
|
||||
templateData.actionBar.clear();
|
||||
@@ -972,6 +1190,23 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
}
|
||||
|
||||
this.renderCheckbox(node, templateData);
|
||||
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) });
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
|
||||
@@ -999,14 +1234,19 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
});
|
||||
}
|
||||
|
||||
if (iconUrl) {
|
||||
if (iconUrl || sqlIcon) {
|
||||
templateData.icon.title = title ? title.toString() : '';
|
||||
templateData.icon.className = 'custom-view-tree-node-item-icon';
|
||||
templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl);
|
||||
if (sqlIcon) {
|
||||
templateData.icon.classList.toggle(sqlIcon, !!sqlIcon); // tracked change
|
||||
}
|
||||
templateData.icon.classList.toggle('icon', !!sqlIcon);
|
||||
templateData.icon.style.backgroundImage = iconUrl ? DOM.asCSSUrl(iconUrl) : '';
|
||||
} else {
|
||||
let iconClass: string | undefined;
|
||||
if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {
|
||||
iconClass = ThemeIcon.asClassName(node.themeIcon);
|
||||
if (node.themeIcon.color) {
|
||||
if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) {
|
||||
templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';
|
||||
}
|
||||
}
|
||||
@@ -1022,12 +1262,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
|
||||
}
|
||||
|
||||
templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle };
|
||||
|
||||
const menuActions = this.menus.getResourceActions(node);
|
||||
if (menuActions.menu) {
|
||||
templateData.elementDisposable.add(menuActions.menu);
|
||||
}
|
||||
templateData.actionBar.push(menuActions.actions, { icon: true, label: false });
|
||||
templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
|
||||
|
||||
if (this._actionRunner) {
|
||||
templateData.actionBar.actionRunner = this._actionRunner;
|
||||
@@ -1216,7 +1451,6 @@ 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,
|
||||
@IMenuService private readonly menuService: IMenuService
|
||||
@@ -1224,27 +1458,32 @@ class TreeMenus extends Disposable implements IDisposable {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
getResourceActions(element: ITreeItem): IAction[] {
|
||||
return this.mergeActions([ // tracked change
|
||||
this.getActions(MenuId.ViewItemContext, element, true).primary,
|
||||
this.getActions(MenuId.DataExplorerContext, element, true).primary
|
||||
]);
|
||||
}
|
||||
|
||||
getResourceContextActions(element: ITreeItem): IAction[] {
|
||||
return this.getActions(MenuId.ViewItemContext, element).secondary;
|
||||
return this.mergeActions([ // tracked change
|
||||
this.getActions(MenuId.ViewItemContext, element).secondary,
|
||||
this.getActions(MenuId.DataExplorerContext, element).secondary
|
||||
]);
|
||||
}
|
||||
|
||||
public setContextKeyService(service: IContextKeyService) {
|
||||
this.contextKeyService = service;
|
||||
}
|
||||
|
||||
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[]);
|
||||
}
|
||||
|
||||
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],
|
||||
['viewItem', element.contextValue]
|
||||
@@ -1269,7 +1508,7 @@ class TreeMenus extends Disposable implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomTreeView extends TreeView {
|
||||
export class CustomTreeView extends AbstractTreeView {
|
||||
|
||||
private activated: boolean = false;
|
||||
|
||||
@@ -1286,9 +1525,12 @@ export class CustomTreeView extends TreeView {
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IActivityService activityService: IActivityService,
|
||||
@ILogService logService: ILogService,
|
||||
) {
|
||||
super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService);
|
||||
super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService);
|
||||
}
|
||||
|
||||
override setVisibility(isVisible: boolean): void {
|
||||
@@ -1298,7 +1540,7 @@ export class CustomTreeView extends TreeView {
|
||||
}
|
||||
}
|
||||
|
||||
private activate() {
|
||||
protected activate() {
|
||||
if (!this.activated) {
|
||||
this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
|
||||
.then(() => timeout(2000))
|
||||
|
||||
Reference in New Issue
Block a user