/********************************************************************************
 * Copyright (C) 2018 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 { inject , injectable } from '@theia/core/shared/inversify' ; import { remote , FileFilter , OpenDialogOptions , SaveDialogOptions } from '@theia/core/shared/electron' ; import URI from '@theia/core/lib/common/uri' ; import { isOSX } from '@theia/core/lib/common/os' ; import { MaybeArray } from '@theia/core/lib/common/types' ; import { MessageService } from '@theia/core/lib/common/message-service' ; import { FileStat } from '../../common/files' ; import { FileAccess } from '../../common/filesystem' ; import { DefaultFileDialogService , OpenFileDialogProps , SaveFileDialogProps } from '../../browser/file-dialog' ; type DialogProperties = 'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory' | 'dontAddToRecent' ; import { FileUri } from '@theia/core/lib/node/file-uri' ; @ injectable ( ) export class ElectronFileDialogService extends DefaultFileDialogService { @ inject ( MessageService ) protected readonly messageService : MessageService ; async showOpenDialog ( props , folder ? ) ; async showOpenDialog ( props , folder ? ) ; async showOpenDialog ( props , folder ? ) { const rootNode = await this . getRootNode ( folder ) ; if ( rootNode ) { const { filePaths } = await remote . dialog . showOpenDialog ( this . toOpenDialogOptions ( rootNode . uri , props ) ) ; if ( filePaths . length === 0 ) { return undefined ; } const uris = filePaths . map ( path => FileUri . create ( path ) ) ; const canAccess = await this . canRead ( uris ) ; const result = canAccess ? uris . length === 1 ? uris [ 0 ] : uris : undefined ; return result ; } return undefined ; } async showSaveDialog ( props , folder ? ) { const rootNode = await this . getRootNode ( folder ) ; if ( rootNode ) { const { filePath } = await remote . dialog . showSaveDialog ( this . toSaveDialogOptions ( rootNode . uri , props ) ) ; if ( ! filePath ) { return undefined ; } const uri = FileUri . create ( filePath ) ; const exists = await this . fileService . exists ( uri ) ; if ( ! exists ) { return uri ; } const canWrite = await this . canReadWrite ( uri ) ; return canWrite ? uri : undefined ; } return undefined ; } protected async canReadWrite ( uris ) { for ( const uri of Array . isArray ( uris ) ? uris : [ uris ] ) { if ( ! ( await this . fileService . access ( uri , FileAccess . Constants . R_OK | FileAccess . Constants . W_OK ) ) ) { this . messageService . error ( `Cannot access resource at ${ uri . path }.` ) ; return false ; } } return true ; } protected async canRead ( uris ) { const resources = Array . isArray ( uris ) ? uris : [ uris ] ; const unreadableResourcePaths = [ ] ; await Promise . all ( resources . map ( async ( resource ) => { if ( ! await this . fileService . access ( resource , FileAccess . Constants . R_OK ) ) { unreadableResourcePaths . push ( resource . path . toString ( ) ) ; } } ) ) ; if ( unreadableResourcePaths . length > 0 ) { this . messageService . error ( `Cannot read ${ unreadableResourcePaths . length } resource(s): ${ unreadableResourcePaths . join ( ', ' ) }` ) ; } return unreadableResourcePaths . length === 0 ; } protected toDialogOptions ( uri , props , dialogTitle ) { const title = props . title || dialogTitle ; const defaultPath = FileUri . fsPath ( uri ) ; const filters = [ { name : 'All Files' , extensions : [ '*' ] } ] ; if ( props . filters ) { filters . unshift ( ... Object . keys ( props . filters ) . map ( key => ( { name : key , extensions : props . filters ! [ key ] } ) ) ) ; } return { title , defaultPath , filters } ; } protected toOpenDialogOptions ( uri , props ) { const properties = electron . dialog . toDialogProperties ( props ) ; const buttonLabel = props . openLabel ; return { ... this . toDialogOptions ( uri , props , 'Open' ) , properties , buttonLabel } ; } protected toSaveDialogOptions ( uri , props ) { const buttonLabel = props . saveLabel ; if ( props . inputValue ) { uri = uri . resolve ( props . inputValue ) ; } const defaultPath = FileUri . fsPath ( uri ) ; return { ... this . toDialogOptions ( uri , props , 'Save' ) , buttonLabel , defaultPath } ; } } export namespace electron { /**
     * Common "super" interface of the `electron.SaveDialogOptions` and `electron.OpenDialogOptions` types.
     */ export interface FileDialogProps { /**
         * The dialog title.
         */ readonly title ? : string ; /**
         * The default path, where the dialog opens. Requires an FS path.
         */ readonly defaultPath ? : string ; /**
         * Resource filter.
         */ readonly filters ? : FileFilter [ ] ; } export namespace dialog { /**
         * Converts the Theia specific `OpenFileDialogProps` into an electron specific array.
         *
         * Note: On Windows and Linux an open dialog can not be both a file selector and a directory selector,
         * so if you set properties to ['openFile', 'openDirectory'] on these platforms, a directory selector will be shown.
         *
         * See: https://github.com/electron/electron/issues/10252#issuecomment-322012159
         */ export function toDialogProperties ( props ) { if ( ! isOSX && props . canSelectFiles !== false && props . canSelectFolders === true ) { console . warn ( `Cannot have 'canSelectFiles' and 'canSelectFolders' at the same time. Fallback to 'folder' dialog. \nProps was: ${ JSON . stringify ( props ) }.` ) ; props . canSelectFiles = false ; props . canSelectFolders = true ; } const properties = [ ] ; if ( ! isOSX ) { if ( props . canSelectFiles !== false && props . canSelectFolders !== true ) { properties . push ( 'openFile' ) ; } if ( props . canSelectFolders === true && props . canSelectFiles === false ) { properties . push ( 'openDirectory' ) ; } } else { if ( props . canSelectFiles !== false ) { properties . push ( 'openFile' ) ; } if ( props . canSelectFolders === true ) { properties . push ( 'openDirectory' ) ; properties . push ( 'createDirectory' ) ; } } if ( props . canSelectMany === true ) { properties . push ( 'multiSelections' ) ; } return properties ; } } }