allow extension to provide custom icons to data explorer tree (#16868)

* expose icon path to extension for OE node

* remove node type

* pr comments
This commit is contained in:
Alan Ren
2021-08-24 17:52:19 -07:00
committed by GitHub
parent 16975da000
commit 712a1b3a65
10 changed files with 73 additions and 26 deletions

View File

@@ -42,7 +42,7 @@ export default class MainController implements vscode.Disposable {
public activate(): Promise<boolean> {
const connectionProvider = new ConnectionProvider();
const iconProvider = new IconProvider();
const objectExplorer = new ObjectExplorerProvider();
const objectExplorer = new ObjectExplorerProvider(this.context);
azdata.dataprotocol.registerConnectionProvider(connectionProvider);
azdata.dataprotocol.registerIconProvider(iconProvider);
azdata.dataprotocol.registerObjectExplorerProvider(objectExplorer);

View File

@@ -10,6 +10,9 @@ import { ProviderId } from './connectionProvider';
* This class implements the ObjectExplorerProvider interface that is responsible for providing the connection tree view content.
*/
export class ObjectExplorerProvider implements azdata.ObjectExplorerProvider {
constructor(private context: vscode.ExtensionContext) {
}
onSessionCreatedEmitter: vscode.EventEmitter<azdata.ObjectExplorerSession> = new vscode.EventEmitter<azdata.ObjectExplorerSession>();
onSessionCreated: vscode.Event<azdata.ObjectExplorerSession> = this.onSessionCreatedEmitter.event;
@@ -60,13 +63,18 @@ export class ObjectExplorerProvider implements azdata.ObjectExplorerProvider {
nodes: [
{
nodePath: 'root/1',
nodeType: 'Database',
label: 'abc1',
nodeType: '',
icon: {
light: this.context.asAbsolutePath('images/group.svg'),
dark: this.context.asAbsolutePath('images/group_inverse.svg')
},
label: 'obj 1',
isLeaf: false
}, {
nodePath: 'root/2',
nodeType: 'Database',
label: 'abc2',
nodeType: '',
icon: azdata.SqlThemeIcon.Column,
label: 'obj 2',
isLeaf: false
}
]

View File

@@ -921,4 +921,11 @@ declare module 'azdata' {
*/
physicalMemoryInMb?: number;
}
export interface NodeInfo {
/**
* Specify the icon for the node. The value could the path to the icon or and ADS icon defined in {@link SqlThemeIcon}.
*/
icon?: IconPath | SqlThemeIcon;
}
}

View File

@@ -5,6 +5,7 @@
import { nb, IConnectionProfile } from 'azdata';
import * as vsExtTypes from 'vs/workbench/api/common/extHostTypes';
import { URI } from 'vs/base/common/uri';
// SQL added extension host types
export enum ServiceOptionType {
@@ -447,6 +448,9 @@ export class TreeItem extends vsExtTypes.TreeItem {
providerHandle?: string;
}
export type ThemedIconPath = { light: string | URI; dark: string | URI };
export type IconPath = string | URI | ThemedIconPath;
export class SqlThemeIcon {
static readonly Folder = new SqlThemeIcon('Folder');

View File

@@ -23,6 +23,7 @@ import { ServerTreeRenderer } from 'sql/workbench/services/objectExplorer/browse
import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree';
import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel';
import { withNullAsUndefined } from 'vs/base/common/types';
import { instanceOfSqlThemeIcon } from 'sql/workbench/services/objectExplorer/common/nodeType';
const DefaultConnectionIconClass = 'server-page';
@@ -168,6 +169,8 @@ class TreeNodeTemplate extends Disposable {
let iconName: string | undefined = undefined;
if (element.iconType) {
iconName = (typeof element.iconType === 'string') ? element.iconType : element.iconType.id;
} else if (instanceOfSqlThemeIcon(element.icon)) {
iconName = element.icon.id;
} else {
iconName = element.nodeTypeId;
if (element.nodeStatus) {
@@ -185,10 +188,12 @@ class TreeNodeTemplate extends Disposable {
this._icon.classList.remove(...tokens);
this._icon.classList.add('icon');
let iconLowerCaseName = iconName.toLocaleLowerCase();
this._icon.classList.add(iconLowerCaseName);
if (iconLowerCaseName) {
this._icon.classList.add(iconLowerCaseName);
}
if (element.iconPath) {
iconRenderer.putIcon(this._icon, element.iconPath);
if (element.icon && !instanceOfSqlThemeIcon(element.icon)) {
iconRenderer.putIcon(this._icon, element.icon);
}
this._label.textContent = element.label;

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IconPath } from 'sql/workbench/api/common/sqlExtHostTypes';
import { createCSSRule, asCSSUrl } from 'vs/base/browser/dom';
import { hash } from 'vs/base/common/hash';
import { URI } from 'vs/base/common/uri';
@@ -10,10 +11,10 @@ import { URI } from 'vs/base/common/uri';
class IconRenderer {
private iconRegistered: Set<string> = new Set<string>();
public registerIcon(path: URI | IconPath | undefined): string | undefined {
public registerIcon(path: IconPath | undefined): string | undefined {
if (!path) { return undefined; }
let iconPath: IconPath = this.toIconPath(path);
let iconUid: string | undefined = this.getIconUid(iconPath);
const iconPath: ThemedIconUri = this.toThemedIconUri(path);
const iconUid: string | undefined = this.getIconUid(iconPath);
if (iconUid && !this.iconRegistered.has(iconUid)) {
createCSSRule(`.icon#${iconUid}`, `background: ${asCSSUrl(iconPath.light || iconPath.dark)} center center no-repeat`);
createCSSRule(`.vs-dark .icon#${iconUid}, .hc-black .icon#${iconUid}`, `background: ${asCSSUrl(iconPath.dark)} center center no-repeat`);
@@ -22,22 +23,32 @@ class IconRenderer {
return iconUid;
}
public getIconUid(path: URI | IconPath): string | undefined {
public getIconUid(path: IconPath): string | undefined {
if (!path) { return undefined; }
let iconPath: IconPath = this.toIconPath(path);
const iconPath: ThemedIconUri = this.toThemedIconUri(path);
return `icon${hash(iconPath.light.toString() + iconPath.dark.toString())}`;
}
private toIconPath(path: URI | IconPath): IconPath {
if (URI.isUri(path)) {
let singlePath = path;
return { light: singlePath, dark: singlePath };
private toThemedIconUri(path: IconPath): ThemedIconUri {
let light, dark: string | URI;
if (URI.isUri(path) || (typeof (path) === 'string')) {
light = dark = path;
} else {
return path;
light = path.light;
dark = path.dark;
}
return {
light: this.toUri(light),
dark: this.toUri(dark)
};
}
public putIcon(element: HTMLElement, path: URI | IconPath | undefined): void {
private toUri(path: string | URI): URI {
return URI.isUri(path) ? path : URI.file(path);
}
public putIcon(element: HTMLElement, path: IconPath | undefined): void {
let iconUid: string | undefined = this.registerIcon(path);
element.id = iconUid ?? '';
}
@@ -130,7 +141,7 @@ class BadgeRenderer {
export const badgeRenderer: BadgeRenderer = new BadgeRenderer();
interface IconPath {
interface ThemedIconUri {
light: URI;
dark: URI;
}

View File

@@ -614,7 +614,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
}
let node = new TreeNode(nodeInfo.nodeType, nodeInfo.label, isLeaf, nodeInfo.nodePath,
nodeInfo.nodeSubType!, nodeInfo.nodeStatus, parent, nodeInfo.metadata, nodeInfo.iconType, {
nodeInfo.nodeSubType!, nodeInfo.nodeStatus, parent, nodeInfo.metadata, nodeInfo.iconType, nodeInfo.icon, {
getChildren: (treeNode?: TreeNode) => this.getChildren(treeNode),
isExpanded: treeNode => this.isExpanded(treeNode),
setNodeExpandedState: async (treeNode, expandedState) => await this.setNodeExpandedState(treeNode, expandedState),

View File

@@ -17,6 +17,7 @@ import { badgeRenderer, iconRenderer } from 'sql/workbench/services/objectExplor
import { URI } from 'vs/base/common/uri';
import { DefaultServerGroupColor } from 'sql/workbench/services/serverGroup/common/serverGroupViewModel';
import { withNullAsUndefined } from 'vs/base/common/types';
import { instanceOfSqlThemeIcon } from 'sql/workbench/services/objectExplorer/common/nodeType';
export interface IConnectionTemplateData {
root: HTMLElement;
@@ -136,6 +137,8 @@ export class ServerTreeRenderer implements IRenderer {
let iconName: string | undefined = undefined;
if (treeNode.iconType) {
iconName = (typeof treeNode.iconType === 'string') ? treeNode.iconType : treeNode.iconType.id;
} else if (instanceOfSqlThemeIcon(treeNode.icon)) {
iconName = treeNode.icon.id;
} else {
iconName = treeNode.nodeTypeId;
if (treeNode.nodeStatus) {
@@ -153,10 +156,12 @@ export class ServerTreeRenderer implements IRenderer {
templateData.icon.classList.remove(...tokens);
templateData.icon.classList.add('icon');
let iconLowerCaseName = iconName.toLocaleLowerCase();
templateData.icon.classList.add(iconLowerCaseName);
if (iconLowerCaseName) {
templateData.icon.classList.add(iconLowerCaseName);
}
if (treeNode.iconPath) {
iconRenderer.putIcon(templateData.icon, treeNode.iconPath);
if (treeNode.icon && !instanceOfSqlThemeIcon(treeNode.icon)) {
iconRenderer.putIcon(templateData.icon, treeNode.icon);
}
templateData.label.textContent = treeNode.label;

View File

@@ -103,3 +103,8 @@ export class NodeType {
export interface SqlThemeIcon {
readonly id: string;
}
export function instanceOfSqlThemeIcon(obj: any): obj is SqlThemeIcon {
const icon = obj as SqlThemeIcon;
return icon && icon.id !== undefined;
}

View File

@@ -8,7 +8,7 @@ import { NodeType, SqlThemeIcon } from 'sql/workbench/services/objectExplorer/co
import * as azdata from 'azdata';
import * as UUID from 'vs/base/common/uuid';
import { URI } from 'vs/base/common/uri';
import { IconPath } from 'sql/workbench/api/common/sqlExtHostTypes';
export enum TreeItemCollapsibleState {
None = 0,
@@ -92,11 +92,12 @@ export class TreeNode {
public iconType?: string | SqlThemeIcon;
public iconPath?: URI | { light: URI, dark: URI };
public icon?: IconPath | SqlThemeIcon;
constructor(nodeTypeId: string, label: string, isAlwaysLeaf: boolean, nodePath: string,
nodeSubType: string, nodeStatus?: string, parent?: TreeNode, metadata?: azdata.ObjectMetadata,
iconType?: string | SqlThemeIcon,
icon?: IconPath | SqlThemeIcon,
private _objectExplorerCallbacks?: ObjectExplorerCallbacks) {
this.nodeTypeId = nodeTypeId;
this.label = label;
@@ -108,6 +109,7 @@ export class TreeNode {
this.id = UUID.generateUuid();
this.nodeSubType = nodeSubType;
this.nodeStatus = nodeStatus;
this.icon = icon;
}
public getConnectionProfile(): ConnectionProfile | undefined {
let currentNode: TreeNode = this;