// ***************************************************************************** // Copyright (C) 2022 Ericsson and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** import '../../../../src/browser/tree/tree-compression/tree-compression.css'; import { injectable, inject } from 'inversify'; import * as React from 'react'; import { ArrayUtils } from '../../../common/types'; import { ContextMenuRenderer } from '../../context-menu-renderer'; import { CompressionToggle, TreeCompressionService } from './tree-compression-service'; import { CompositeTreeNode, TreeNode } from '../tree'; import { NodeProps, TreeProps, TreeWidget } from '../tree-widget'; import { SelectableTreeNode, TreeSelection } from '../tree-selection'; import { ExpandableTreeNode } from '../tree-expansion'; import { TreeViewWelcomeWidget } from '../tree-view-welcome-widget'; import { CompressedTreeModel } from './compressed-tree-model'; export interface CompressedChildren { compressionChain?: ArrayUtils.HeadAndTail; } export interface CompressedNodeRow extends TreeWidget.NodeRow, CompressedChildren { } export interface CompressedNodeProps extends NodeProps, CompressedChildren { } @injectable() export class CompressedTreeWidget extends TreeViewWelcomeWidget { @inject(CompressionToggle) protected readonly compressionToggle: CompressionToggle; @inject(TreeCompressionService) protected readonly compressionService: TreeCompressionService; constructor( @inject(TreeProps) props: TreeProps, @inject(CompressedTreeModel) override readonly model: CompressedTreeModel, @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer, ) { super(props, model, contextMenuRenderer); } protected override rows = new Map(); toggleCompression(newCompression = !this.compressionToggle.compress): void { if (newCompression !== this.compressionToggle.compress) { this.compressionToggle.compress = newCompression; this.updateRows(); } } protected override shouldDisplayNode(node: TreeNode): boolean { if (this.compressionToggle.compress && this.compressionService.isCompressionParticipant(node) && !this.compressionService.isCompressionHead(node)) { return false; } return super.shouldDisplayNode(node); } protected override getDepthForNode(node: TreeNode, depths: Map): number { if (!this.compressionToggle.compress) { return super.getDepthForNode(node, depths); } const parent = this.compressionService.getCompressionHead(node.parent) ?? node.parent; const parentDepth = depths.get(parent); return parentDepth === undefined ? 0 : TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth; } protected override toNodeRow(node: TreeNode, index: number, depth: number): CompressedNodeRow { if (!this.compressionToggle.compress) { return super.toNodeRow(node, index, depth); } const row: CompressedNodeRow = { node, index, depth }; if (this.compressionService.isCompressionHead(node)) { row.compressionChain = this.compressionService.getCompressionChain(node); } return row; } protected override doRenderNodeRow({ node, depth, compressionChain }: CompressedNodeRow): React.ReactNode { const nodeProps: CompressedNodeProps = { depth, compressionChain }; return <> {this.renderIndent(node, nodeProps)} {this.renderNode(node, nodeProps)} ; } protected override rowIsSelected(node: TreeNode, props: CompressedNodeProps): boolean { if (this.compressionToggle.compress && props.compressionChain) { return props.compressionChain.some(participant => SelectableTreeNode.isSelected(participant)); } return SelectableTreeNode.isSelected(node); } protected override getCaptionAttributes(node: TreeNode, props: CompressedNodeProps): React.Attributes & React.HTMLAttributes { const operativeNode = props.compressionChain?.tail() ?? node; return super.getCaptionAttributes(operativeNode, props); } protected override getCaptionChildren(node: TreeNode, props: CompressedNodeProps): React.ReactNode { if (!this.compressionToggle.compress || !props.compressionChain) { return super.getCaptionChildren(node, props); } return props.compressionChain.map((subNode, index, self) => { const classes = ['theia-tree-compressed-label-part']; if (SelectableTreeNode.isSelected(subNode)) { classes.push('theia-tree-compressed-selected'); } const handlers = this.getCaptionChildEventHandlers(subNode, props); const caption = {super.getCaptionChildren(subNode, props)}; if (index === self.length - 1) { return caption; } return [ caption, {this.getSeparatorContent(node, props)} ]; }); } protected getCaptionChildEventHandlers(node: TreeNode, props: CompressedNodeProps): React.Attributes & React.HtmlHTMLAttributes { return { onClick: event => (event.stopPropagation(), this.handleClickEvent(node, event)), onDoubleClick: event => (event.stopPropagation(), this.handleDblClickEvent(node, event)), onContextMenu: event => (event.stopPropagation(), this.handleContextMenuEvent(node, event)), }; } protected override handleUp(event: KeyboardEvent): void { if (!this.compressionToggle.compress) { return super.handleUp(event); } const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined; this.model.selectPrevRow(type); this.node.focus(); } protected override handleDown(event: KeyboardEvent): void { if (!this.compressionToggle.compress) { return super.handleDown(event); } const type = this.props.multiSelect && this.hasShiftMask(event) ? TreeSelection.SelectionType.RANGE : undefined; this.model.selectNextRow(type); this.node.focus(); } protected override async handleLeft(event: KeyboardEvent): Promise { if (!this.compressionToggle.compress) { return super.handleLeft(event); } if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) { return; } const active = this.focusService.focusedNode; if (ExpandableTreeNode.isExpanded(active) && ( this.compressionService.isCompressionHead(active) || !this.compressionService.isCompressionParticipant(active) )) { await this.model.collapseNode(active); } else { this.model.selectParent(); } } protected override async handleRight(event: KeyboardEvent): Promise { if (!this.compressionToggle.compress) { return super.handleRight(event); } if (Boolean(this.props.multiSelect) && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) { return; } const active = this.focusService.focusedNode; if (ExpandableTreeNode.isCollapsed(active) && ( !this.compressionService.isCompressionParticipant(active) || this.compressionService.isCompressionTail(active) )) { await this.model.expandNode(active); } else if (ExpandableTreeNode.is(active)) { this.model.selectNextNode(); } } protected getSeparatorContent(node: TreeNode, props: CompressedNodeProps): React.ReactNode { return '/'; } }