1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import '../../../../src/browser/tree/tree-compression/tree-compression.css';
|
18 | import { injectable, inject } from 'inversify';
|
19 | import * as React from 'react';
|
20 | import { ArrayUtils } from '../../../common/types';
|
21 | import { ContextMenuRenderer } from '../../context-menu-renderer';
|
22 | import { CompressionToggle, TreeCompressionService } from './tree-compression-service';
|
23 | import { CompositeTreeNode, TreeNode } from '../tree';
|
24 | import { NodeProps, TreeProps, TreeWidget } from '../tree-widget';
|
25 | import { SelectableTreeNode, TreeSelection } from '../tree-selection';
|
26 | import { ExpandableTreeNode } from '../tree-expansion';
|
27 | import { TreeViewWelcomeWidget } from '../tree-view-welcome-widget';
|
28 | import { CompressedTreeModel } from './compressed-tree-model';
|
29 |
|
30 | export interface CompressedChildren {
|
31 | compressionChain?: ArrayUtils.HeadAndTail<TreeNode>;
|
32 | }
|
33 |
|
34 | export interface CompressedNodeRow extends TreeWidget.NodeRow, CompressedChildren { }
|
35 |
|
36 | export interface CompressedNodeProps extends NodeProps, CompressedChildren { }
|
37 |
|
38 | @injectable()
|
39 | export class CompressedTreeWidget extends TreeViewWelcomeWidget {
|
40 |
|
41 | @inject(CompressionToggle) protected readonly compressionToggle: CompressionToggle;
|
42 | @inject(TreeCompressionService) protected readonly compressionService: TreeCompressionService;
|
43 |
|
44 | constructor(
|
45 | @inject(TreeProps) props: TreeProps,
|
46 | @inject(CompressedTreeModel) override readonly model: CompressedTreeModel,
|
47 | @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer,
|
48 | ) {
|
49 | super(props, model, contextMenuRenderer);
|
50 | }
|
51 |
|
52 | protected override rows = new Map<string, CompressedNodeRow>();
|
53 |
|
54 | toggleCompression(newCompression = !this.compressionToggle.compress): void {
|
55 | if (newCompression !== this.compressionToggle.compress) {
|
56 | this.compressionToggle.compress = newCompression;
|
57 | this.updateRows();
|
58 | }
|
59 | }
|
60 |
|
61 | protected override shouldDisplayNode(node: TreeNode): boolean {
|
62 | if (this.compressionToggle.compress && this.compressionService.isCompressionParticipant(node) && !this.compressionService.isCompressionHead(node)) {
|
63 | return false;
|
64 | }
|
65 | return super.shouldDisplayNode(node);
|
66 | }
|
67 |
|
68 | protected override getDepthForNode(node: TreeNode, depths: Map<CompositeTreeNode | undefined, number>): number {
|
69 | if (!this.compressionToggle.compress) { return super.getDepthForNode(node, depths); }
|
70 | const parent = this.compressionService.getCompressionHead(node.parent) ?? node.parent;
|
71 | const parentDepth = depths.get(parent);
|
72 | return parentDepth === undefined ? 0 : TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth;
|
73 | }
|
74 |
|
75 | protected override toNodeRow(node: TreeNode, index: number, depth: number): CompressedNodeRow {
|
76 | if (!this.compressionToggle.compress) { return super.toNodeRow(node, index, depth); }
|
77 | const row: CompressedNodeRow = { node, index, depth };
|
78 | if (this.compressionService.isCompressionHead(node)) {
|
79 | row.compressionChain = this.compressionService.getCompressionChain(node);
|
80 | }
|
81 | return row;
|
82 | }
|
83 |
|
84 | protected override doRenderNodeRow({ node, depth, compressionChain }: CompressedNodeRow): React.ReactNode {
|
85 | const nodeProps: CompressedNodeProps = { depth, compressionChain };
|
86 | return <>
|
87 | {this.renderIndent(node, nodeProps)}
|
88 | {this.renderNode(node, nodeProps)}
|
89 | </>;
|
90 | }
|
91 |
|
92 | protected override rowIsSelected(node: TreeNode, props: CompressedNodeProps): boolean {
|
93 | if (this.compressionToggle.compress && props.compressionChain) {
|
94 | return props.compressionChain.some(participant => SelectableTreeNode.isSelected(participant));
|
95 | }
|
96 | return SelectableTreeNode.isSelected(node);
|
97 | }
|
98 |
|
99 | protected override getCaptionAttributes(node: TreeNode, props: CompressedNodeProps): React.Attributes & React.HTMLAttributes<HTMLElement> {
|
100 | const operativeNode = props.compressionChain?.tail() ?? node;
|
101 | return super.getCaptionAttributes(operativeNode, props);
|
102 | }
|
103 |
|
104 | protected override getCaptionChildren(node: TreeNode, props: CompressedNodeProps): React.ReactNode {
|
105 | if (!this.compressionToggle.compress || !props.compressionChain) { return super.getCaptionChildren(node, props); }
|
106 | return props.compressionChain.map((subNode, index, self) => {
|
107 | const classes = ['theia-tree-compressed-label-part'];
|
108 | if (SelectableTreeNode.isSelected(subNode)) {
|
109 | classes.push('theia-tree-compressed-selected');
|
110 | }
|
111 | const handlers = this.getCaptionChildEventHandlers(subNode, props);
|
112 | const caption = <span className={classes.join(' ')} key={subNode.id} {...handlers}>{super.getCaptionChildren(subNode, props)}</span>;
|
113 | if (index === self.length - 1) {
|
114 | return caption;
|
115 | }
|
116 | return [
|
117 | caption,
|
118 | <span className='theia-tree-compressed-label-separator' key={subNode + '-separator'}>{this.getSeparatorContent(node, props)}</span>
|
119 | ];
|
120 | });
|
121 | }
|
122 |
|
123 | protected getCaptionChildEventHandlers(node: TreeNode, props: CompressedNodeProps): React.Attributes & React.HtmlHTMLAttributes<HTMLElement> {
|
124 | return {
|
125 | onClick: event => (event.stopPropagation(), this.handleClickEvent(node, event)),
|
126 | onDoubleClick: event => (event.stopPropagation(), this.handleDblClickEvent(node, event)),
|
127 | onContextMenu: event => (event.stopPropagation(), this.handleContextMenuEvent(node, event)),
|
128 | };
|
129 | }
|
130 |
|
131 | protected override handleUp(event: KeyboardEvent): void {
|
132 | if (!this.compressionToggle.compress) { return super.handleUp(event); }
|
133 | const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined;
|
134 | this.model.selectPrevRow(type);
|
135 | this.node.focus();
|
136 | }
|
137 |
|
138 | protected override handleDown(event: KeyboardEvent): void {
|
139 | if (!this.compressionToggle.compress) { return super.handleDown(event); }
|
140 | const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined;
|
141 | this.model.selectNextRow(type);
|
142 | this.node.focus();
|
143 | }
|
144 |
|
145 | protected override async handleLeft(event: KeyboardEvent): Promise<void> {
|
146 | if (!this.compressionToggle.compress) { return super.handleLeft(event); }
|
147 | if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) {
|
148 | return;
|
149 | }
|
150 | const active = this.focusService.focusedNode;
|
151 | if (ExpandableTreeNode.isExpanded(active)
|
152 | && (
|
153 | this.compressionService.isCompressionHead(active)
|
154 | || !this.compressionService.isCompressionParticipant(active)
|
155 | )) {
|
156 | await this.model.collapseNode(active);
|
157 | } else {
|
158 | this.model.selectParent();
|
159 | }
|
160 | }
|
161 |
|
162 | protected override async handleRight(event: KeyboardEvent): Promise<void> {
|
163 | if (!this.compressionToggle.compress) { return super.handleRight(event); }
|
164 | if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) {
|
165 | return;
|
166 | }
|
167 | const active = this.focusService.focusedNode;
|
168 |
|
169 | if (ExpandableTreeNode.isCollapsed(active)
|
170 | && (
|
171 | !this.compressionService.isCompressionParticipant(active)
|
172 | || this.compressionService.isCompressionTail(active)
|
173 | )) {
|
174 | await this.model.expandNode(active);
|
175 | } else if (ExpandableTreeNode.is(active)) {
|
176 | this.model.selectNextNode();
|
177 | }
|
178 | }
|
179 |
|
180 | protected getSeparatorContent(node: TreeNode, props: CompressedNodeProps): React.ReactNode {
|
181 | return '/';
|
182 | }
|
183 | }
|