mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-09 03:18:33 +00:00

Closes #1599 Closes #1825 Closes #649 Closes #1847 Closes #1882 Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Co-authored-by: per1234 <accounts@perglass.com> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
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<void> {
|
|
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 <div className="sketch-folder-icon file-icon"></div>;
|
|
}
|
|
const icon = this.toNodeIcon(node);
|
|
if (icon) {
|
|
return <div className={icon + ' file-icon'}></div>;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
protected override renderTailDecorations(
|
|
node: TreeNode,
|
|
props: NodeProps
|
|
): React.ReactNode {
|
|
return (
|
|
<React.Fragment>
|
|
{super.renderTailDecorations(node, props)}
|
|
{this.renderInlineCommands(node)}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
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<HTMLElement> {
|
|
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 (
|
|
<div
|
|
key={`${commandId}--${node.id}`}
|
|
className={className}
|
|
title={command?.label || command.id}
|
|
onClick={(event) => {
|
|
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<HTMLElement>
|
|
): 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<HTMLElement>): 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)
|
|
);
|
|
}
|
|
}
|