mirror of
				https://github.com/arduino/arduino-ide.git
				synced 2025-10-30 21:48:33 +00:00 
			
		
		
		
	[atl-1433][atl-1433] improve local sketchbook explorer (#446)
This commit is contained in:
		| @@ -94,7 +94,6 @@ export class FilterableListContainer< | ||||
|   } | ||||
|  | ||||
|   protected sort(items: T[]): T[] { | ||||
|     // debugger; | ||||
|     const { itemLabel, itemDeprecated } = this.props; | ||||
|     return items.sort((left, right) => { | ||||
|       // always put deprecated items at the bottom of the list | ||||
|   | ||||
| @@ -1,15 +1,29 @@ | ||||
| import { inject, injectable } from 'inversify'; | ||||
| import { inject, injectable, postConstruct } from 'inversify'; | ||||
| import URI from '@theia/core/lib/common/uri'; | ||||
| import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser'; | ||||
| import { FileService } from '@theia/filesystem/lib/browser/file-service'; | ||||
| import { ConfigService } from '../../../common/protocol'; | ||||
| import { SketchbookTree } from './sketchbook-tree'; | ||||
| import { ArduinoPreferences } from '../../arduino-preferences'; | ||||
| import { SelectableTreeNode, TreeNode } from '@theia/core/lib/browser/tree'; | ||||
| import { | ||||
|   CompositeTreeNode, | ||||
|   ExpandableTreeNode, | ||||
|   SelectableTreeNode, | ||||
|   TreeNode, | ||||
| } from '@theia/core/lib/browser/tree'; | ||||
| import { SketchbookCommands } from './sketchbook-commands'; | ||||
| import { OpenerService, open } from '@theia/core/lib/browser'; | ||||
| import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; | ||||
| import { CommandRegistry } from '@theia/core/lib/common/command'; | ||||
| import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; | ||||
| import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; | ||||
| import { ProgressService } from '@theia/core/lib/common/progress-service'; | ||||
| import { | ||||
|   WorkspaceNode, | ||||
|   WorkspaceRootNode, | ||||
| } from '@theia/navigator/lib/browser/navigator-tree'; | ||||
| import { Deferred } from '@theia/core/lib/common/promise-util'; | ||||
| import { Disposable } from '@theia/core/lib/common/disposable'; | ||||
|  | ||||
| @injectable() | ||||
| export class SketchbookTreeModel extends FileTreeModel { | ||||
| @@ -31,14 +45,217 @@ export class SketchbookTreeModel extends FileTreeModel { | ||||
|   @inject(SketchesServiceClientImpl) | ||||
|   protected readonly sketchServiceClient: SketchesServiceClientImpl; | ||||
|  | ||||
|   async updateRoot(): Promise<void> { | ||||
|     const config = await this.configService.getConfiguration(); | ||||
|     const fileStat = await this.fileService.resolve( | ||||
|       new URI(config.sketchDirUri) | ||||
|   @inject(SketchbookTree) protected readonly tree: SketchbookTree; | ||||
|   @inject(WorkspaceService) | ||||
|   protected readonly workspaceService: WorkspaceService; | ||||
|   @inject(FrontendApplicationStateService) | ||||
|   protected readonly applicationState: FrontendApplicationStateService; | ||||
|  | ||||
|   @inject(ProgressService) | ||||
|   protected readonly progressService: ProgressService; | ||||
|  | ||||
|   @postConstruct() | ||||
|   protected init(): void { | ||||
|     super.init(); | ||||
|     this.reportBusyProgress(); | ||||
|     this.initializeRoot(); | ||||
|   } | ||||
|  | ||||
|   protected readonly pendingBusyProgress = new Map<string, Deferred<void>>(); | ||||
|   protected reportBusyProgress(): void { | ||||
|     this.toDispose.push( | ||||
|       this.onDidChangeBusy((node) => { | ||||
|         const pending = this.pendingBusyProgress.get(node.id); | ||||
|         if (pending) { | ||||
|           if (!node.busy) { | ||||
|             pending.resolve(); | ||||
|             this.pendingBusyProgress.delete(node.id); | ||||
|           } | ||||
|           return; | ||||
|         } | ||||
|         if (node.busy) { | ||||
|           const progress = new Deferred<void>(); | ||||
|           this.pendingBusyProgress.set(node.id, progress); | ||||
|           this.progressService.withProgress( | ||||
|             '', | ||||
|             'explorer', | ||||
|             () => progress.promise | ||||
|           ); | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|     const showAllFiles = | ||||
|       this.arduinoPreferences['arduino.sketchbook.showAllFiles']; | ||||
|     this.tree.root = SketchbookTree.RootNode.create(fileStat, showAllFiles); | ||||
|     this.toDispose.push( | ||||
|       Disposable.create(() => { | ||||
|         for (const pending of this.pendingBusyProgress.values()) { | ||||
|           pending.resolve(); | ||||
|         } | ||||
|         this.pendingBusyProgress.clear(); | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   protected async initializeRoot(): Promise<void> { | ||||
|     await Promise.all([ | ||||
|       this.applicationState.reachedState('initialized_layout'), | ||||
|       this.workspaceService.roots, | ||||
|     ]); | ||||
|     await this.updateRoot(); | ||||
|     if (this.toDispose.disposed) { | ||||
|       return; | ||||
|     } | ||||
|     this.toDispose.push( | ||||
|       this.workspaceService.onWorkspaceChanged(() => this.updateRoot()) | ||||
|     ); | ||||
|     this.toDispose.push( | ||||
|       this.workspaceService.onWorkspaceLocationChanged(() => this.updateRoot()) | ||||
|     ); | ||||
|     this.toDispose.push( | ||||
|       this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => { | ||||
|         if (preferenceName === 'arduino.sketchbook.showAllFiles') { | ||||
|           this.updateRoot(); | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     if (this.selectedNodes.length) { | ||||
|       return; | ||||
|     } | ||||
|     const root = this.root; | ||||
|     if (CompositeTreeNode.is(root) && root.children.length === 1) { | ||||
|       const child = root.children[0]; | ||||
|       if ( | ||||
|         SelectableTreeNode.is(child) && | ||||
|         !child.selected && | ||||
|         ExpandableTreeNode.is(child) | ||||
|       ) { | ||||
|         this.selectNode(child); | ||||
|         this.expandNode(child); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   previewNode(node: TreeNode): void { | ||||
|     if (FileNode.is(node)) { | ||||
|       open(this.openerService, node.uri, { | ||||
|         mode: 'reveal', | ||||
|         preview: true, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   *getNodesByUri(uri: URI): IterableIterator<TreeNode> { | ||||
|     const workspace = this.root; | ||||
|     if (WorkspaceNode.is(workspace)) { | ||||
|       for (const root of workspace.children) { | ||||
|         const id = this.tree.createId(root, uri); | ||||
|         const node = this.getNode(id); | ||||
|         if (node) { | ||||
|           yield node; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async updateRoot(): Promise<void> { | ||||
|     this.root = await this.createRoot(); | ||||
|   } | ||||
|  | ||||
|   protected async createRoot(): Promise<TreeNode | undefined> { | ||||
|     const config = await this.configService.getConfiguration(); | ||||
|     const stat = await this.fileService.resolve(new URI(config.sketchDirUri)); | ||||
|  | ||||
|     if (this.workspaceService.opened) { | ||||
|       const isMulti = stat ? !stat.isDirectory : false; | ||||
|       const workspaceNode = isMulti | ||||
|         ? this.createMultipleRootNode() | ||||
|         : WorkspaceNode.createRoot(); | ||||
|       workspaceNode.children.push( | ||||
|         await this.tree.createWorkspaceRoot(stat, workspaceNode) | ||||
|       ); | ||||
|  | ||||
|       return workspaceNode; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Create multiple root node used to display | ||||
|    * the multiple root workspace name. | ||||
|    * | ||||
|    * @returns `WorkspaceNode` | ||||
|    */ | ||||
|   protected createMultipleRootNode(): WorkspaceNode { | ||||
|     const workspace = this.workspaceService.workspace; | ||||
|     let name = workspace ? workspace.resource.path.name : 'untitled'; | ||||
|     name += ' (Workspace)'; | ||||
|     return WorkspaceNode.createRoot(name); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Move the given source file or directory to the given target directory. | ||||
|    */ | ||||
|   async move(source: TreeNode, target: TreeNode): Promise<URI | undefined> { | ||||
|     if (source.parent && WorkspaceRootNode.is(source)) { | ||||
|       // do not support moving a root folder | ||||
|       return undefined; | ||||
|     } | ||||
|     return super.move(source, target); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Reveals node in the navigator by given file uri. | ||||
|    * | ||||
|    * @param uri uri to file which should be revealed in the navigator | ||||
|    * @returns file tree node if the file with given uri was revealed, undefined otherwise | ||||
|    */ | ||||
|   async revealFile(uri: URI): Promise<TreeNode | undefined> { | ||||
|     if (!uri.path.isAbsolute) { | ||||
|       return undefined; | ||||
|     } | ||||
|     let node = this.getNodeClosestToRootByUri(uri); | ||||
|  | ||||
|     // success stop condition | ||||
|     // we have to reach workspace root because expanded node could be inside collapsed one | ||||
|     if (WorkspaceRootNode.is(node)) { | ||||
|       if (ExpandableTreeNode.is(node)) { | ||||
|         if (!node.expanded) { | ||||
|           node = await this.expandNode(node); | ||||
|         } | ||||
|         return node; | ||||
|       } | ||||
|       // shouldn't happen, root node is always directory, i.e. expandable | ||||
|       return undefined; | ||||
|     } | ||||
|  | ||||
|     // fail stop condition | ||||
|     if (uri.path.isRoot) { | ||||
|       // file system root is reached but workspace root wasn't found, it means that | ||||
|       // given uri is not in workspace root folder or points to not existing file. | ||||
|       return undefined; | ||||
|     } | ||||
|  | ||||
|     if (await this.revealFile(uri.parent)) { | ||||
|       if (node === undefined) { | ||||
|         // get node if it wasn't mounted into navigator tree before expansion | ||||
|         node = this.getNodeClosestToRootByUri(uri); | ||||
|       } | ||||
|       if (ExpandableTreeNode.is(node) && !node.expanded) { | ||||
|         node = await this.expandNode(node); | ||||
|       } | ||||
|       return node; | ||||
|     } | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   protected getNodeClosestToRootByUri(uri: URI): TreeNode | undefined { | ||||
|     const nodes = [...this.getNodesByUri(uri)]; | ||||
|     return nodes.length > 0 | ||||
|       ? nodes.reduce( | ||||
|           ( | ||||
|             node1, | ||||
|             node2 // return the node closest to the workspace root | ||||
|           ) => (node1.id.length >= node2.id.length ? node1 : node2) | ||||
|         ) | ||||
|       : undefined; | ||||
|   } | ||||
|  | ||||
|   // selectNode gets called when the user single-clicks on an item | ||||
|   | ||||
| @@ -48,23 +48,11 @@ export class SketchbookTreeWidget extends FileTreeWidget { | ||||
|   @postConstruct() | ||||
|   protected async init(): Promise<void> { | ||||
|     super.init(); | ||||
|     this.toDispose.push( | ||||
|       this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => { | ||||
|         if (preferenceName === 'arduino.sketchbook.showAllFiles') { | ||||
|           this.updateModel(); | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|     this.updateModel(); | ||||
|     // cache the current open sketch uri | ||||
|     const currentSketch = await this.sketchServiceClient.currentSketch(); | ||||
|     this.currentSketchUri = (currentSketch && currentSketch.uri) || ''; | ||||
|   } | ||||
|  | ||||
|   async updateModel(): Promise<void> { | ||||
|     return this.model.updateRoot(); | ||||
|   } | ||||
|  | ||||
|   protected createNodeClassNames(node: TreeNode, props: NodeProps): string[] { | ||||
|     const classNames = super.createNodeClassNames(node, props); | ||||
|  | ||||
|   | ||||
| @@ -1,40 +1,37 @@ | ||||
| import { inject, injectable } from 'inversify'; | ||||
| import { LabelProvider } from '@theia/core/lib/browser/label-provider'; | ||||
| import { Command } from '@theia/core/lib/common/command'; | ||||
| import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree'; | ||||
| import { | ||||
|   DirNode, | ||||
|   FileStatNode, | ||||
|   FileTree, | ||||
| } from '@theia/filesystem/lib/browser/file-tree'; | ||||
| import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree'; | ||||
| import { DirNode, FileStatNode } from '@theia/filesystem/lib/browser/file-tree'; | ||||
| import { SketchesService } from '../../../common/protocol'; | ||||
| import { FileStat } from '@theia/filesystem/lib/common/files'; | ||||
| import { SketchbookCommands } from './sketchbook-commands'; | ||||
| import { | ||||
|   FileNavigatorTree, | ||||
|   WorkspaceNode, | ||||
| } from '@theia/navigator/lib/browser/navigator-tree'; | ||||
| import { ArduinoPreferences } from '../../arduino-preferences'; | ||||
|  | ||||
| @injectable() | ||||
| export class SketchbookTree extends FileTree { | ||||
| export class SketchbookTree extends FileNavigatorTree { | ||||
|   @inject(LabelProvider) | ||||
|   protected readonly labelProvider: LabelProvider; | ||||
|  | ||||
|   @inject(SketchesService) | ||||
|   protected readonly sketchesService: SketchesService; | ||||
|  | ||||
|   @inject(ArduinoPreferences) | ||||
|   protected readonly arduinoPreferences: ArduinoPreferences; | ||||
|  | ||||
|   async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> { | ||||
|     if (!FileStatNode.is(parent)) { | ||||
|       return super.resolveChildren(parent); | ||||
|     } | ||||
|     const { root } = this; | ||||
|     if (!root) { | ||||
|       return []; | ||||
|     } | ||||
|     if (!SketchbookTree.RootNode.is(root)) { | ||||
|       return []; | ||||
|     } | ||||
|     const showAllFiles = | ||||
|       this.arduinoPreferences['arduino.sketchbook.showAllFiles']; | ||||
|  | ||||
|     const children = ( | ||||
|       await Promise.all( | ||||
|         ( | ||||
|           await super.resolveChildren(parent) | ||||
|         ).map((node) => this.maybeDecorateNode(node, root.showAllFiles)) | ||||
|         ).map((node) => this.maybeDecorateNode(node, showAllFiles)) | ||||
|       ) | ||||
|     ).filter((node) => { | ||||
|       // filter out hidden nodes | ||||
| @@ -43,7 +40,9 @@ export class SketchbookTree extends FileTree { | ||||
|       } | ||||
|       return true; | ||||
|     }); | ||||
|     if (SketchbookTree.RootNode.is(parent)) { | ||||
|  | ||||
|     // filter out hardware and libraries | ||||
|     if (WorkspaceNode.is(parent.parent)) { | ||||
|       return children | ||||
|         .filter(DirNode.is) | ||||
|         .filter( | ||||
| @@ -53,10 +52,14 @@ export class SketchbookTree extends FileTree { | ||||
|             ) === -1 | ||||
|         ); | ||||
|     } | ||||
|     if (SketchbookTree.SketchDirNode.is(parent)) { | ||||
|       return children.filter(FileStatNode.is); | ||||
|  | ||||
|     // return the Arduino directory containing all user sketches | ||||
|     if (WorkspaceNode.is(parent)) { | ||||
|       return children; | ||||
|     } | ||||
|  | ||||
|     return children; | ||||
|     // return this.filter.filter(super.resolveChildren(parent)); | ||||
|   } | ||||
|  | ||||
|   protected async maybeDecorateNode( | ||||
| @@ -74,6 +77,9 @@ export class SketchbookTree extends FileTree { | ||||
|         }); | ||||
|         if (!showAllFiles) { | ||||
|           delete (node as any).expanded; | ||||
|           node.children = []; | ||||
|         } else { | ||||
|           node.expanded = false; | ||||
|         } | ||||
|         return node; | ||||
|       } | ||||
|   | ||||
| @@ -14,7 +14,10 @@ import { SketchbookCommands } from './sketchbook-commands'; | ||||
| import { WorkspaceService } from '../../theia/workspace/workspace-service'; | ||||
| import { | ||||
|   ContextMenuRenderer, | ||||
|   Navigatable, | ||||
|   RenderContextMenuOptions, | ||||
|   SelectableTreeNode, | ||||
|   Widget, | ||||
| } from '@theia/core/lib/browser'; | ||||
| import { | ||||
|   Disposable, | ||||
| @@ -77,6 +80,10 @@ export class SketchbookWidgetContribution | ||||
|   } | ||||
|  | ||||
|   onStart(): void { | ||||
|     this.shell.currentChanged.connect(() => | ||||
|       this.onCurrentWidgetChangedHandler() | ||||
|     ); | ||||
|  | ||||
|     this.arduinoPreferences.onPreferenceChanged(({ preferenceName }) => { | ||||
|       if (preferenceName === 'arduino.sketchbook.showAllFiles') { | ||||
|         this.mainMenuManager.update(); | ||||
| @@ -196,4 +203,27 @@ export class SketchbookWidgetContribution | ||||
|       order: '0', | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Reveals and selects node in the file navigator to which given widget is related. | ||||
|    * Does nothing if given widget undefined or doesn't have related resource. | ||||
|    * | ||||
|    * @param widget widget file resource of which should be revealed and selected | ||||
|    */ | ||||
|   async selectWidgetFileNode(widget: Widget | undefined): Promise<void> { | ||||
|     if (Navigatable.is(widget)) { | ||||
|       const resourceUri = widget.getResourceUri(); | ||||
|       if (resourceUri) { | ||||
|         const { model } = (await this.widget).getTreeWidget(); | ||||
|         const node = await model.revealFile(resourceUri); | ||||
|         if (SelectableTreeNode.is(node)) { | ||||
|           model.selectNode(node); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected onCurrentWidgetChangedHandler(): void { | ||||
|     this.selectWidgetFileNode(this.shell.currentWidget); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -38,6 +38,10 @@ export class SketchbookWidget extends BaseWidget { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   getTreeWidget(): SketchbookTreeWidget { | ||||
|     return this.localSketchbookTreeWidget; | ||||
|   } | ||||
|  | ||||
|   protected onActivateRequest(message: Message): void { | ||||
|     super.onActivateRequest(message); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Francesco Stasi
					Francesco Stasi