Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add indent guidelines to all trees #7194

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
// if not yet contributed by Monaco, check runtime css variables to learn.
// TODO: Following are not yet supported/no respective elements in theia:
// list.focusBackground, list.focusForeground, list.inactiveFocusBackground, list.filterMatchBorder,
// list.dropBackground, listFilterWidget.outline, listFilterWidget.noMatchesOutline, tree.indentGuidesStroke
// list.dropBackground, listFilterWidget.outline, listFilterWidget.noMatchesOutline
// list.invalidItemForeground,
// list.warningForeground, list.errorForeground => tree node needs an respective class
{ id: 'list.activeSelectionBackground', defaults: { dark: '#094771', light: '#0074E8' }, description: 'List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.' },
Expand All @@ -1015,6 +1015,8 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
{ id: 'list.hoverBackground', defaults: { dark: '#2A2D2E', light: '#F0F0F0' }, description: 'List/Tree background when hovering over items using the mouse.' },
{ id: 'list.hoverForeground', description: 'List/Tree foreground when hovering over items using the mouse.' },
{ id: 'list.filterMatchBackground', defaults: { dark: 'editor.findMatchHighlightBackground', light: 'editor.findMatchHighlightBackground' }, description: 'Background color of the filtered match.' },
{ id: 'tree.indentGuidesStrokeActive', defaults: { dark: '#585858', light: '#a9a9a9', hc: '#a9a9a9' }, description: "Tree Widget's stroke color for active indent guides." },
{ id: 'tree.indentGuidesStrokeHover', defaults: { dark: Color.rgba(88, 88, 88, 0.4), light: Color.rgba(169, 169, 169, 0.4), hc: Color.rgba(169, 169, 169, 0.4) }, description: 'Tree Widget\'s stroke color for hovered indent guides.' },

// Editor Group & Tabs colors should be aligned with https://code.visualstudio.com/api/references/theme-color#editor-groups-tabs
{
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export const corePreferenceSchema: PreferenceSchema = {
type: 'boolean',
default: false,
description: 'Controls whether to suppress notification popups.'
},
'workbench.tree.renderIndentGuides': {
type: 'string',
enum: ['onHover', 'none', 'always'],
default: 'onHover',
description: 'Controls whether the three should render indent guides.'
}
}
};
Expand All @@ -74,6 +80,7 @@ export interface CoreConfiguration {
'workbench.colorTheme'?: string;
'workbench.iconTheme'?: string | null;
'workbench.silentNotifications': boolean;
'workbench.tree.renderIndentGuides'?: 'onHover' | 'none' | 'always';
}

export const CorePreferences = Symbol('CorePreferences');
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/browser/style/tree.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,33 @@
.theia-tree-element-node {
width: 100%
}

.theia-TreeNodeIndent {
display: flex;
}

.theia-treeNodeIndentBlock {
width: 8px;
border-right: 1px solid transparent;
height: 22px;
}

.theia-treeNodeChildPadding {
margin-right: 3px;
}

.theia-TreeContainer:hover .theia-treeNodeIndentBlock.theia-indentGuideOnHover {
border-right: 1px solid var(--theia-tree-indentGuidesStrokeHover);
}

.theia-TreeContainer .theia-treeNodeIndentBlock.theia-treeNodeActive.theia-indentGuideOnHover {
border-right: 1px solid var(--theia-tree-indentGuidesStrokeActive);
}

.theia-TreeContainer .theia-treeNodeIndentBlock.theia-indentGuideAlways {
border-right: 1px solid var(--theia-tree-indentGuidesStrokeHover);
}

.theia-TreeContainer .theia-treeNodeIndentBlock.theia-treeNodeActive.theia-indentGuideAlways {
border-right: 1px solid var(--theia-tree-indentGuidesStrokeActive);
}
107 changes: 102 additions & 5 deletions packages/core/src/browser/tree/tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ElementExt } from '@phosphor/domutils';
import { TreeWidgetSelection } from './tree-widget-selection';
import { MaybePromise } from '../../common/types';
import { LabelProvider } from '../label-provider';
import { CorePreferences } from '../core-preferences';

const debounce = require('lodash.debounce');

Expand All @@ -52,6 +53,12 @@ export const TREE_NODE_SEGMENT_GROW_CLASS = 'theia-TreeNodeSegmentGrow';
export const EXPANDABLE_TREE_NODE_CLASS = 'theia-ExpandableTreeNode';
export const COMPOSITE_TREE_NODE_CLASS = 'theia-CompositeTreeNode';
export const TREE_NODE_CAPTION_CLASS = 'theia-TreeNodeCaption';
export const TREE_NODE_INDENT_CLASS = 'theia-TreeNodeIndent';
export const TREE_NODE_INDENT_BLOCK_CLASS = 'theia-treeNodeIndentBlock';
export const TREE_NODE_CHILD_PADDING_CLASS = 'theia-treeNodeChildPadding';
export const TREE_NODE_ACTIVE = 'theia-treeNodeActive';
export const INDENT_GUIDE_ALWAYS = 'theia-indentGuideAlways';
export const INDENT_GUIDE_ONHOVER = 'theia-indentGuideOnHover';

export const TreeProps = Symbol('TreeProps');

Expand All @@ -73,6 +80,8 @@ export interface TreeProps {

readonly expansionTogglePadding: number;

readonly rootLevelIconPadding: number;

/**
* `true` if the tree widget support multi-selection. Otherwise, `false`. Defaults to `false`.
*/
Expand Down Expand Up @@ -116,7 +125,8 @@ export interface NodeProps {
*/
export const defaultTreeProps: TreeProps = {
leftPadding: 8,
expansionTogglePadding: 18
expansionTogglePadding: 0,
rootLevelIconPadding: 3
};

export namespace TreeWidget {
Expand Down Expand Up @@ -162,6 +172,9 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;

@inject(CorePreferences)
protected readonly corePreferences: CorePreferences;

protected shouldScrollToRow = true;

constructor(
Expand All @@ -178,6 +191,8 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.node.tabIndex = 0;
}

protected parentOfActiveIndentGuideline = new Set<String>();

@postConstruct()
protected init(): void {
if (this.props.search) {
Expand Down Expand Up @@ -217,10 +232,23 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.toDispose.pushAll([
this.model,
this.model.onChanged(() => this.updateRows()),
this.model.onSelectionChanged(() => this.updateScrollToRow({ resize: false })),
this.model.onSelectionChanged(selectedNodes => {
this.updateScrollToRow({ resize: false });
this.parentOfActiveIndentGuideline.clear();
for (const node of selectedNodes) {
this.addActiveNodeParent(node);
}
}

),
this.model.onDidChangeBusy(() => this.update()),
this.model.onNodeRefreshed(() => this.updateDecorations()),
this.model.onExpansionChanged(() => this.updateDecorations()),
this.model.onExpansionChanged(node => {
this.updateDecorations();
this.parentOfActiveIndentGuideline.clear();
this.addActiveNodeParent(node);
}),

this.decoratorService,
this.decoratorService.onDidChangeDecorations(() => this.updateDecorations()),
this.labelProvider.onDidChange(e => {
Expand Down Expand Up @@ -251,6 +279,11 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
})
]);
}
this.toDispose.push(this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'workbench.tree.renderIndentGuides') {
this.update();
}
}));
}

/**
Expand Down Expand Up @@ -776,6 +809,50 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
return iconClass.concat(additionalClasses).join(' ');
}

/**
* Add the parent id of the nodes that need tree indent guideline into a set
* @param node the tree node.
*/
protected addActiveNodeParent(node: TreeNode): void {
if (this.isExpandable(node)) {
if (node.children && node.children.length > 0) {
this.parentOfActiveIndentGuideline.add(node.id);
}
} else {
const parent = node.parent;
if (parent) {
this.parentOfActiveIndentGuideline.add(parent.id);
}
}
}

/**
* Render indent for the file tree depending on the depth
* @param node the tree node.
* @param depth the depth of the tree node.
*/
protected renderIndent(node: TreeNode, depth: number): React.ReactNode {

const indentDivs: React.ReactNode[] = [];
const indentGuideOption = this.corePreferences['workbench.tree.renderIndentGuides'];

let nodePtr = node;
for (let i = 1; i <= depth; i++) {
if (nodePtr !== undefined && nodePtr.parent !== undefined) {
nodePtr = nodePtr.parent;
}
const needsNodeGuideline = this.parentOfActiveIndentGuideline.has(nodePtr.id);
const needsLeafPadding = (!this.isExpandable(node) && i === 1);
indentDivs.unshift(<div key={i}
className={`${TREE_NODE_INDENT_BLOCK_CLASS}
${indentGuideOption !== 'none' ? indentGuideOption === 'onHover' ? INDENT_GUIDE_ONHOVER : INDENT_GUIDE_ALWAYS : ''}
${needsLeafPadding ? TREE_NODE_CHILD_PADDING_CLASS : ''}
${indentGuideOption !== 'none' && needsNodeGuideline ? TREE_NODE_ACTIVE : ''}
`}> </div>);
}
return indentDivs;
}

/**
* Render the node given the tree node and node properties.
* @param node the tree node.
Expand All @@ -787,6 +864,9 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
}
const attributes = this.createNodeAttributes(node, props);
const content = <div className={TREE_NODE_CONTENT_CLASS}>
<div className={TREE_NODE_INDENT_CLASS}>
{this.renderIndent(node, props.depth)}
</div>
{this.renderExpansionToggle(node, props)}
{this.decorateIcon(node, this.renderIcon(node, props))}
{this.renderCaptionAffixes(node, props, 'captionPrefixes')}
Expand Down Expand Up @@ -850,18 +930,35 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
return { paddingLeft };
}

protected getPaddingLeft(node: TreeNode, props: NodeProps): number {
return props.depth * this.props.leftPadding + (this.needsExpansionTogglePadding(node) ? this.props.expansionTogglePadding : 0);
/**
* Add right padding (3px) for icons located in the root level of the tree during rendering.
* @param node the tree node.
* @param props the node properties.
*/
protected needsRootLevelIconPadding(node: TreeNode, props: NodeProps): boolean {
return (props.depth === 0 && !this.isExpandable(node));
}

/**
* Code is kept here to prevent broken api
* If the node is a composite, a toggle will be rendered.
* Otherwise we need to add the width and the left, right padding => 18px
* Add right padding (3px) for icons located in the root level of the tree during rendering
* @param node the tree node.
*/
protected needsExpansionTogglePadding(node: TreeNode): boolean {
return !this.isExpandable(node);
}

/**
* Return the left padding for a tree node.
* @param node the tree node.
* @param props the node properties.
*/
protected getPaddingLeft(node: TreeNode, props: NodeProps): number {
return this.props.leftPadding + (this.needsRootLevelIconPadding(node, props) ? this.props.rootLevelIconPadding : 0);
}

/**
* Create the tree node style.
* @param node the tree node.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ export class FileTreeWidget extends TreeWidget {
return super.getPaddingLeft(node, props);
}

protected needsRootLevelIconPadding(node: TreeNode, props: NodeProps): boolean {
const theme = this.iconThemeService.getDefinition(this.iconThemeService.current);
if (theme && (theme.hidesExplorerArrows || (theme.hasFileIcons && !theme.hasFolderIcons))) {
return false;
}
return super.needsRootLevelIconPadding(node, props);
}

// Code is kept here to prevent broken api
protected needsExpansionTogglePadding(node: TreeNode): boolean {
const theme = this.iconThemeService.getDefinition(this.iconThemeService.current);
if (theme && (theme.hidesExplorerArrows || (theme.hasFileIcons && !theme.hasFolderIcons))) {
Expand Down