From b9d18a80f98ecdc0868e301d7090eebb12dbde4e Mon Sep 17 00:00:00 2001 From: OmarSdt-EC Date: Wed, 21 Jul 2021 11:20:38 -0400 Subject: [PATCH] tree: use dot decoration on folders The following commit updates the rendering of tail decorations for composite tree nodes (nodes that contain children nodes like folders). The following updates the rendering of tail decorations for composite tree nodes (nodes that contain children nodes like folders). The updates include : - rendering a generic icon decoration (dot) for composite nodes with children with decoration data. - updates to the rendering logic to only render the decoration data with the highest priority so no duplicate generic icons are present. --- .../core/src/browser/tree/tree-widget.tsx | 47 ++++++++++++------- .../src/browser/navigator-widget.tsx | 40 ++++++++++++++-- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx index 6d54999eeb7e5..0a789e7f2e12e 100644 --- a/packages/core/src/browser/tree/tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-widget.tsx @@ -745,26 +745,30 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { * @param node the tree node. * @param icon the icon. */ - protected decorateIcon(node: TreeNode, icon: React.ReactNode | null): React.ReactNode { - // eslint-disable-next-line no-null/no-null - if (icon === null) { - // eslint-disable-next-line no-null/no-null - return null; + protected decorateIcon(node: TreeNode, icon: React.ReactNode): React.ReactNode { + if (!icon) { + return; } - const overlayIcons: React.ReactNode[] = []; - new Map(this.getDecorationData(node, 'iconOverlay').reverse().filter(notEmpty) - .map(overlay => [overlay.position, overlay] as [TreeDecoration.IconOverlayPosition, TreeDecoration.IconOverlay | TreeDecoration.IconClassOverlay])) - .forEach((overlay, position) => { - const iconClasses = [TreeDecoration.Styles.DECORATOR_SIZE_CLASS, TreeDecoration.IconOverlayPosition.getStyle(position)]; + // if multiple overlays have the same overlay.position attribute, we'll de-duplicate those and only process the first one from the decoration array + const seenPositions = new Set(); + const overlays = this.getDecorationData(node, 'iconOverlay').filter(notEmpty); + + for (const overlay of overlays) { + if (!seenPositions.has(overlay.position)) { + seenPositions.add(overlay.position); + const iconClasses = [TreeDecoration.Styles.DECORATOR_SIZE_CLASS, TreeDecoration.IconOverlayPosition.getStyle(overlay.position)]; const style = (color?: string) => color === undefined ? {} : { color }; + if (overlay.background) { - overlayIcons.push( - ); + overlayIcons.push(); } - const overlayIcon = (overlay as TreeDecoration.IconOverlay).icon || (overlay as TreeDecoration.IconClassOverlay).iconClass; + + const overlayIcon = 'icon' in overlay ? overlay.icon : overlay.iconClass; overlayIcons.push(); - }); + } + } if (overlayIcons.length > 0) { return
{icon}{overlayIcons}
; @@ -779,14 +783,23 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { * @param props the node properties. */ protected renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode { + const tailDecorations = this.getDecorationData(node, 'tailDecorations').filter(notEmpty).reduce((acc, current) => acc.concat(current), []); + if (tailDecorations.length === 0) { + return; + } + return this.renderTailDecorationsForNode(node, props, tailDecorations); + } + + protected renderTailDecorationsForNode(node: TreeNode, props: NodeProps, tailDecorations: + (TreeDecoration.TailDecoration | TreeDecoration.TailDecorationIcon | TreeDecoration.TailDecorationIconClass)[]): React.ReactNode { return - {this.getDecorationData(node, 'tailDecorations').filter(notEmpty).reduce((acc, current) => acc.concat(current), []).map((decoration, index) => { + {tailDecorations.map((decoration, index) => { const { tooltip } = decoration; const { data, fontData } = decoration as TreeDecoration.TailDecoration; const color = (decoration as TreeDecoration.TailDecorationIcon).color; - const icon = (decoration as TreeDecoration.TailDecorationIcon).icon || (decoration as TreeDecoration.TailDecorationIconClass).iconClass; const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS].join(' '); const style = fontData ? this.applyFontStyles({}, fontData) : color ? { color } : undefined; + const icon = (decoration as TreeDecoration.TailDecorationIcon).icon || (decoration as TreeDecoration.TailDecorationIconClass).iconClass; const content = data ? data : icon ? : ''; return
{content} @@ -803,7 +816,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { * * @returns the icon class name. */ - private getIconClass(iconName: string | string[], additionalClasses: string[] = []): string { + protected getIconClass(iconName: string | string[], additionalClasses: string[] = []): string { const iconClass = (typeof iconName === 'string') ? ['a', 'fa', `fa-${iconName}`] : ['a'].concat(iconName); return iconClass.concat(additionalClasses).join(' '); } diff --git a/packages/navigator/src/browser/navigator-widget.tsx b/packages/navigator/src/browser/navigator-widget.tsx index 131ea4eea620c..1e1f4157770fa 100644 --- a/packages/navigator/src/browser/navigator-widget.tsx +++ b/packages/navigator/src/browser/navigator-widget.tsx @@ -17,13 +17,17 @@ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { Message } from '@theia/core/shared/@phosphor/messaging'; import URI from '@theia/core/lib/common/uri'; -import { CommandService, SelectionService } from '@theia/core/lib/common'; -import { CorePreferences, Key, TreeModel, SelectableTreeNode, OpenerService } from '@theia/core/lib/browser'; +import { CommandService, notEmpty, SelectionService } from '@theia/core/lib/common'; +import { + CorePreferences, Key, TreeModel, SelectableTreeNode, + TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, + TreeDecoration, NodeProps, OpenerService +} from '@theia/core/lib/browser'; import { ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode } from '@theia/core/lib/browser'; -import { FileTreeWidget, FileNode, DirNode } from '@theia/filesystem/lib/browser'; +import { FileTreeWidget, FileNode, DirNode, FileStatNode } from '@theia/filesystem/lib/browser'; import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { WorkspaceNode, WorkspaceRootNode } from './navigator-tree'; @@ -152,6 +156,36 @@ export class FileNavigatorWidget extends FileTreeWidget { return super.renderTree(model); } + protected renderTailDecorations(node: TreeNode, props: NodeProps): React.ReactNode { + const tailDecorations = this.getDecorationData(node, 'tailDecorations').filter(notEmpty).reduce((acc, current) => acc.concat(current), []); + + if (tailDecorations.length === 0) { + return; + } + + // Handle rendering of directories versus file nodes. + if (FileStatNode.is(node) && node.fileStat.isDirectory) { + return this.renderTailDecorationsForDirectoryNode(node, props, tailDecorations); + } else { + return this.renderTailDecorationsForNode(node, props, tailDecorations); + } + } + + protected renderTailDecorationsForDirectoryNode(node: TreeNode, props: NodeProps, tailDecorations: + (TreeDecoration.TailDecoration | TreeDecoration.TailDecorationIcon | TreeDecoration.TailDecorationIconClass)[]): React.ReactNode { + // If the node represents a directory, we just want to use the decorationData with the highest priority (last element). + const decoration = tailDecorations[tailDecorations.length - 1]; + const { tooltip, fontData } = decoration as TreeDecoration.TailDecoration; + const color = (decoration as TreeDecoration.TailDecorationIcon).color; + const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS].join(' '); + const style = fontData ? this.applyFontStyles({}, fontData) : color ? { color } : undefined; + const content = ; + + return
+ {content} +
; + } + protected shouldShowWelcomeView(): boolean { return this.model.root === undefined; }