/********************************************************************************
 * Copyright (C) 2017 TypeFox 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 * as React from '@theia/core/shared/react' ; import { injectable , inject } from '@theia/core/shared/inversify' ; import { DisposableCollection , Disposable } from '@theia/core/lib/common/disposable' ; import URI from '@theia/core/lib/common/uri' ; import { UriSelection } from '@theia/core/lib/common/selection' ; import { isCancelled } from '@theia/core/lib/common/cancellation' ; import { ContextMenuRenderer , NodeProps , TreeProps , TreeNode , CompositeTreeNode , TreeViewWelcomeWidget } from '@theia/core/lib/browser' ; import { FileUploadService } from '../file-upload-service' ; import { DirNode , FileStatNode , FileStatNodeData } from './file-tree' ; import { FileTreeModel } from './file-tree-model' ; import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service' ; import { FileStat , FileType } from '../../common/files' ; export const FILE_TREE_CLASS = 'theia-FileTree' ; export const FILE_STAT_NODE_CLASS = 'theia-FileStatNode' ; export const DIR_NODE_CLASS = 'theia-DirNode' ; export const FILE_STAT_ICON_CLASS = 'theia-FileStatIcon' ; @ injectable ( ) export class FileTreeWidget extends TreeViewWelcomeWidget { protected readonly toCancelNodeExpansion = new DisposableCollection ( ) ; @ inject ( FileUploadService ) protected readonly uploadService : FileUploadService ; @ inject ( IconThemeService ) protected readonly iconThemeService : IconThemeService ; constructor ( @ inject ( TreeProps ) readonly props , @ inject ( FileTreeModel ) readonly model , @ inject ( ContextMenuRenderer ) contextMenuRenderer ) { super ( props , model , contextMenuRenderer ) ; this . addClass ( FILE_TREE_CLASS ) ; this . toDispose . push ( this . toCancelNodeExpansion ) ; } protected createNodeClassNames ( node , props ) { const classNames = super . createNodeClassNames ( node , props ) ; if ( FileStatNode . is ( node ) ) { classNames . push ( FILE_STAT_NODE_CLASS ) ; } if ( DirNode . is ( node ) ) { classNames . push ( DIR_NODE_CLASS ) ; } return classNames ; } protected renderIcon ( node , props ) { const icon = this . toNodeIcon ( node ) ; if ( icon ) { return < div className = { icon + ' file-icon' } > < /div>; } return null ; } protected createContainerAttributes ( ) { const attrs = super . createContainerAttributes ( ) ; return { ... attrs , onDragEnter : event => this . handleDragEnterEvent ( this . model . root , event ) , onDragOver : event => this . handleDragOverEvent ( this . model . root , event ) , onDragLeave : event => this . handleDragLeaveEvent ( this . model . root , event ) , onDrop : event => this . handleDropEvent ( this . model . root , event ) } ; } protected createNodeAttributes ( node , props ) { const elementAttrs = super . createNodeAttributes ( node , props ) ; return { ... elementAttrs , draggable : FileStatNode . is ( node ) , onDragStart : event => this . handleDragStartEvent ( node , event ) , onDragEnter : event => this . handleDragEnterEvent ( node , event ) , onDragOver : event => this . handleDragOverEvent ( node , event ) , onDragLeave : event => this . handleDragLeaveEvent ( node , event ) , onDrop : event => this . handleDropEvent ( node , event ) , title : this . getNodeTooltip ( node ) } ; } protected getNodeTooltip ( node ) { const uri = UriSelection . getUri ( node ) ; return uri ? uri . path . toString ( ) : undefined ; } protected handleDragStartEvent ( node , event ) { event . stopPropagation ( ) ; let selectedNodes ; if ( this . model . selectedNodes . find ( selected => TreeNode . equals ( selected , node ) ) ) { selectedNodes = [ ... this . model . selectedNodes ] ; } else { selectedNodes = [ node ] ; } this . setSelectedTreeNodesAsData ( event . dataTransfer , node , selectedNodes ) ; if ( event . dataTransfer ) { let label ; if ( selectedNodes . length === 1 ) { label = this . toNodeName ( node ) ; } else { label = String ( selectedNodes . length ) ; } const dragImage = document . createElement ( 'div' ) ; dragImage . className = 'theia-file-tree-drag-image' ; dragImage . textContent = label ; document . body . appendChild ( dragImage ) ; event . dataTransfer . setDragImage ( dragImage , - 10 , - 10 ) ; setTimeout ( ( ) => document . body . removeChild ( dragImage ) , 0 ) ; } } protected handleDragEnterEvent ( node , event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . toCancelNodeExpansion . dispose ( ) ; const containing = DirNode . getContainingDir ( node ) ; if ( ! ! containing && ! containing . selected ) { this . model . selectNode ( containing ) ; } } protected handleDragOverEvent ( node , event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; if ( ! this . toCancelNodeExpansion . disposed ) { return ; } const timer = setTimeout ( ( ) => { const containing = DirNode . getContainingDir ( node ) ; if ( ! ! containing && ! containing . expanded ) { this . model . expandNode ( containing ) ; } } , 500 ) ; this . toCancelNodeExpansion . push ( Disposable . create ( ( ) => clearTimeout ( timer ) ) ) ; } protected handleDragLeaveEvent ( node , event ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . toCancelNodeExpansion . dispose ( ) ; } protected async handleDropEvent ( node , event ) { try { event . preventDefault ( ) ; event . stopPropagation ( ) ; event . dataTransfer . dropEffect = 'copy' ; const containing = this . getDropTargetDirNode ( node ) ; if ( containing ) { const resources = this . getSelectedTreeNodesFromData ( event . dataTransfer ) ; if ( resources . length > 0 ) { for ( const treeNode of resources ) { await this . model . move ( treeNode , containing ) ; } } else { await this . uploadService . upload ( containing . uri , { source : event . dataTransfer } ) ; } } } catch ( e ) { if ( ! isCancelled ( e ) ) { console . error ( e ) ; } } } protected getDropTargetDirNode ( node ) { if ( CompositeTreeNode . is ( node ) && node . id === 'WorkspaceNodeId' ) { if ( node . children . length === 1 ) { return DirNode . getContainingDir ( node . children [ 0 ] ) ; } else if ( node . children . length > 1 ) { return DirNode . getContainingDir ( node . children [ node . children . length - 1 ] ) ; } } return DirNode . getContainingDir ( node ) ; } protected setTreeNodeAsData ( data , node ) { data . setData ( 'tree-node' , node . id ) ; } protected setSelectedTreeNodesAsData ( data , sourceNode , relatedNodes ) { this . setTreeNodeAsData ( data , sourceNode ) ; data . setData ( 'selected-tree-nodes' , JSON . stringify ( relatedNodes . map ( node => node . id ) ) ) ; } protected getTreeNodeFromData ( data ) { const id = data . getData ( 'tree-node' ) ; return this . model . getNode ( id ) ; } protected getSelectedTreeNodesFromData ( data ) { const resources = data . getData ( 'selected-tree-nodes' ) ; if ( ! resources ) { return [ ] ; } const ids = JSON . parse ( resources ) ; return ids . map ( id => this . model . getNode ( id ) ) . filter ( node => node !== undefined ) as TreeNode [ ] ; } protected get hidesExplorerArrows ( ) : boolean { const theme = this . iconThemeService . getDefinition ( this . iconThemeService . current ) ; return ! ! theme && ! ! theme . hidesExplorerArrows ; } protected renderExpansionToggle ( node , props ) { if ( this . hidesExplorerArrows ) { return null ; } return super . renderExpansionToggle ( node , props ) ; } protected getPaddingLeft ( node , props ) { if ( this . hidesExplorerArrows ) { return super . getPaddingLeft ( node , props ) + this . props . leftPadding ; } return super . getPaddingLeft ( node , props ) ; } protected needsExpansionTogglePadding ( node ) { const theme = this . iconThemeService . getDefinition ( this . iconThemeService . current ) ; if ( theme && ( theme . hidesExplorerArrows || ( theme . hasFileIcons && ! theme . hasFolderIcons ) ) ) { return false ; } return super . needsExpansionTogglePadding ( node ) ; } protected deflateForStorage ( node ) { const deflated = super . deflateForStorage ( node ) ; if ( FileStatNode . is ( node ) && FileStatNodeData . is ( deflated ) ) { deflated . uri = node . uri . toString ( ) ; delete deflated [ 'fileStat' ] ; deflated . stat = FileStat . toStat ( node . fileStat ) ; } return deflated ; } protected inflateFromStorage ( node , parent ? ) { if ( FileStatNodeData . is ( node ) ) { const fileStatNode = node as any ; const resource = new URI ( node . uri ) ; fileStatNode . uri = resource ; let stat ; if ( node . fileStat ) { stat = { type : node . fileStat . isDirectory ? FileType . Directory : FileType . File , mtime : node . fileStat . lastModification , size : node . fileStat . size } ; delete node [ 'fileStat' ] ; } else if ( node . stat ) { stat = node . stat ; delete node [ 'stat' ] ; } if ( stat ) { fileStatNode . fileStat = FileStat . fromStat ( resource , stat ) ; } } const inflated = super . inflateFromStorage ( node , parent ) ; if ( DirNode . is ( inflated ) ) { inflated . fileStat . children = [ ] ; for ( const child of inflated . children ) { if ( FileStatNode . is ( child ) ) { inflated . fileStat . children . push ( child . fileStat ) ; } } } return inflated ; } }