import * as React from '@theia/core/shared/react'; import { inject, injectable, postConstruct, } from '@theia/core/shared/inversify'; import { TreeNode } from '@theia/core/lib/browser/tree/tree'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { NodeProps, TreeProps, TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, } from '@theia/core/lib/browser/tree/tree-widget'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { FileTreeWidget } from '@theia/filesystem/lib/browser'; import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer'; import { SketchbookTree } from './sketchbook-tree'; import { SketchbookTreeModel } from './sketchbook-tree-model'; import { ArduinoPreferences } from '../../arduino-preferences'; import { CurrentSketch, SketchesServiceClientImpl, } from '../../sketches-service-client-impl'; import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection'; import { Sketch } from '../../contributions/contribution'; import { nls } from '@theia/core/lib/common'; const customTreeProps: TreeProps = { leftPadding: 26, expansionTogglePadding: 6, }; @injectable() export class SketchbookTreeWidget extends FileTreeWidget { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(ArduinoPreferences) protected readonly arduinoPreferences: ArduinoPreferences; @inject(SketchesServiceClientImpl) protected readonly sketchServiceClient: SketchesServiceClientImpl; protected currentSketchUri = ''; constructor( @inject(TreeProps) override readonly props: TreeProps, @inject(SketchbookTreeModel) override readonly model: SketchbookTreeModel, @inject(ContextMenuRenderer) override readonly contextMenuRenderer: ContextMenuRenderer, @inject(EditorManager) readonly editorManager: EditorManager ) { super(props, model, contextMenuRenderer); this.id = 'arduino-sketchbook-tree-widget'; this.title.iconClass = 'sketchbook-tree-icon'; this.title.caption = nls.localize( 'arduino/sketch/titleLocalSketchbook', 'Local Sketchbook' ); this.title.closable = false; this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it. } @postConstruct() protected override async init(): Promise { super.init(); // cache the current open sketch uri const currentSketch = await this.sketchServiceClient.currentSketch(); this.currentSketchUri = (CurrentSketch.isValid(currentSketch) && currentSketch.uri) || ''; } protected override createNodeClassNames( node: TreeNode, props: NodeProps ): string[] { const classNames = super.createNodeClassNames(node, props); if ( SketchbookTree.SketchDirNode.is(node) && this.currentSketchUri === node?.uri.toString() ) { classNames.push('active-sketch'); } return classNames; } protected override renderIcon( node: TreeNode, props: NodeProps ): React.ReactNode { if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) { return
; } const icon = this.toNodeIcon(node); if (icon) { return
; } return undefined; } protected override renderTailDecorations( node: TreeNode, props: NodeProps ): React.ReactNode { return ( {super.renderTailDecorations(node, props)} {this.renderInlineCommands(node)} ); } protected hoveredNodeId: string | undefined; protected setHoverNodeId(id: string | undefined): void { this.hoveredNodeId = id; this.update(); } protected override createNodeAttributes( node: TreeNode, props: NodeProps ): React.Attributes & React.HTMLAttributes { return { ...super.createNodeAttributes(node, props), draggable: false, onMouseOver: () => this.setHoverNodeId(node.id), onMouseOut: () => this.setHoverNodeId(undefined), }; } protected renderInlineCommands(node: TreeNode): React.ReactNode { if (SketchbookTree.SketchDirNode.is(node) && node.commands) { return Array.from(new Set(node.commands)).map((command) => this.renderInlineCommand(command.id, node) ); } return undefined; } protected renderInlineCommand( commandId: string, node: SketchbookTree.SketchDirNode, options?: any ): React.ReactNode { const command = this.commandRegistry.getCommand(commandId); const icon = command?.iconClass; const args = { model: this.model, node: node, ...options }; if ( command && icon && this.commandRegistry.isEnabled(commandId, args) && this.commandRegistry.isVisible(commandId, args) ) { const className = [ TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, icon, 'theia-tree-view-inline-action', 'sketchbook-commands-icons', ].join(' '); return (
{ event.preventDefault(); event.stopPropagation(); this.commandRegistry.executeCommand( commandId, Object.assign(args, { event: event.nativeEvent }) ); }} /> ); } return undefined; } protected override handleClickEvent( node: TreeNode | undefined, event: React.MouseEvent ): void { if (node) { if (!!this.props.multiSelect) { const shiftMask = this.hasShiftMask(event); const ctrlCmdMask = this.hasCtrlCmdMask(event); if (SelectableTreeNode.is(node)) { if (shiftMask) { this.model.selectRange(node); } else if (ctrlCmdMask) { this.model.toggleNode(node); } else { this.model.selectNode(node); } } } else { if (SelectableTreeNode.is(node)) { this.model.selectNode(node); } } event.stopPropagation(); } } protected override doToggle(event: React.MouseEvent): void { const nodeId = event.currentTarget.getAttribute('data-node-id'); if (nodeId) { const node = this.model.getNode(nodeId); if (node && this.isExpandable(node)) { this.model.toggleNodeExpansion(node); } } event.stopPropagation(); } protected override getPaddingLeft(node: TreeNode, props: NodeProps): number { return ( props.depth * customTreeProps.leftPadding + (this.needsExpansionTogglePadding(node) ? customTreeProps.expansionTogglePadding : 0) ); } }