UNPKG

8.67 kBTypeScriptView Raw
1// *****************************************************************************
2// Copyright (C) 2022 Ericsson and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import '../../../../src/browser/tree/tree-compression/tree-compression.css';
18import { injectable, inject } from 'inversify';
19import * as React from 'react';
20import { ArrayUtils } from '../../../common/types';
21import { ContextMenuRenderer } from '../../context-menu-renderer';
22import { CompressionToggle, TreeCompressionService } from './tree-compression-service';
23import { CompositeTreeNode, TreeNode } from '../tree';
24import { NodeProps, TreeProps, TreeWidget } from '../tree-widget';
25import { SelectableTreeNode, TreeSelection } from '../tree-selection';
26import { ExpandableTreeNode } from '../tree-expansion';
27import { TreeViewWelcomeWidget } from '../tree-view-welcome-widget';
28import { CompressedTreeModel } from './compressed-tree-model';
29
30export interface CompressedChildren {
31 compressionChain?: ArrayUtils.HeadAndTail<TreeNode>;
32}
33
34export interface CompressedNodeRow extends TreeWidget.NodeRow, CompressedChildren { }
35
36export interface CompressedNodeProps extends NodeProps, CompressedChildren { }
37
38@injectable()
39export 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}