Browse Source

support loading multiple files at once

- OpenFiles state action
- file-list param definition
Alexander Rose 5 years ago
parent
commit
a1fb4b8bf3

+ 1 - 0
src/apps/state-docs/pd-to-md.ts

@@ -22,6 +22,7 @@ function paramInfo(param: PD.Any, offset: number): string {
         case 'color-list': return `One of ${oToS(param.options)}`;
         case 'vec3': return `3D vector [x, y, z]`;
         case 'file': return `JavaScript File Handle`;
+        case 'file-list': return `JavaScript FileList Handle`;
         case 'select': return `One of ${oToS(param.options)}`;
         case 'text': return 'String';
         case 'interval': return `Interval [min, max]`;

+ 29 - 2
src/mol-plugin-ui/controls/parameters.tsx

@@ -59,6 +59,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'color-list': return ColorListControl;
         case 'vec3': return Vec3Control;
         case 'file': return FileControl;
+        case 'file-list': return FileListControl;
         case 'select': return SelectControl;
         case 'text': return TextControl;
         case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
@@ -436,7 +437,6 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
     }
 }
 
-
 export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
     change(value: File) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -449,13 +449,40 @@ export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
     render() {
         const value = this.props.value;
 
-        // return <input disabled={this.props.isDisabled} value={void 0} type='file' multiple={false} />
         return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
             {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
         </div>
     }
 }
 
+export class FileListControl extends React.PureComponent<ParamProps<PD.FileListParam>> {
+    change(value: FileList) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    onChangeFileList = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.change(e.target.files!);
+    }
+
+    render() {
+        const value = this.props.value;
+
+        const names: string[] = []
+        if (value) {
+            for (let i = 0, il = value.length; i < il; ++i) {
+                names.push(value[i].name)
+            }
+        }
+        const label = names.length === 0
+            ? 'Select files...' : names.length === 1
+                ? names[0] : `${names.length} files selected`
+
+        return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
+            {label} <input disabled={this.props.isDisabled} onChange={this.onChangeFileList} type='file' multiple={true} accept={this.props.param.accept} />
+        </div>
+    }
+}
+
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 

+ 1 - 1
src/mol-plugin/index.ts

@@ -25,7 +25,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateActions.Structure.DownloadStructure),
         PluginSpec.Action(StateActions.Structure.AddTrajectory),
         PluginSpec.Action(StateActions.Volume.DownloadDensity),
-        PluginSpec.Action(StateActions.DataFormat.OpenFile),
+        PluginSpec.Action(StateActions.DataFormat.OpenFiles),
         PluginSpec.Action(StateActions.Structure.Create3DRepresentationPreset),
         PluginSpec.Action(StateActions.Structure.Remove3DRepresentationPreset),
         PluginSpec.Action(StateActions.Structure.EnableModelCustomProps),

+ 21 - 22
src/mol-plugin/state/actions/data-format.ts

@@ -119,36 +119,35 @@ export interface DataFormatProvider<D extends PluginStateObject.Data.Binary | Pl
 
 //
 
-export const OpenFile = StateAction.build({
-    display: { name: 'Open File', description: 'Load a file and optionally create its default visuals' },
+export const OpenFiles = StateAction.build({
+    display: { name: 'Open Files', description: 'Load one or more files and optionally create default visuals' },
     from: PluginStateObject.Root,
     params: (a, ctx: PluginContext) => {
         const { extensions, options } = ctx.dataFormat.registry
         return {
-            file: PD.File({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',') + ',.gz,.zip' }),
+            files: PD.FileList({ accept: Array.from(extensions.values()).map(e => `.${e}`).join(',') + ',.gz,.zip', multiple: true }),
             format: PD.Select('auto', options),
             visuals: PD.Boolean(true, { description: 'Add default visuals' }),
         }
     }
-})(({ params, state }, ctx: PluginContext) => Task.create('Open File', async taskCtx => {
-    const info = getFileInfo(params.file)
-    const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: ctx.dataFormat.registry.binaryExtensions.has(info.ext) });
-    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
-
-    // Alternative for more complex states where the builder is not a simple StateBuilder.To<>:
-    /*
-    const dataRef = dataTree.ref;
-    await state.updateTree(dataTree).runInContext(taskCtx);
-    const dataCell = state.select(dataRef)[0];
-    */
-
-    // const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-
-    const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(info, dataStateObject) : ctx.dataFormat.registry.get(params.format)
-    const b = state.build().to(data.ref);
-    const options = { visuals: params.visuals }
-    // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
-    await provider.getDefaultBuilder(ctx, b, options, state).runInContext(taskCtx)
+})(({ params, state }, ctx: PluginContext) => Task.create('Open Files', async taskCtx => {
+    for (let i = 0, il = params.files.length; i < il; ++i) {
+        try {
+            const file = params.files[i]
+            const info = getFileInfo(file)
+            const isBinary = ctx.dataFormat.registry.binaryExtensions.has(info.ext)
+            const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file, isBinary });
+            const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
+            const provider = params.format === 'auto'
+                ? ctx.dataFormat.registry.auto(info, dataStateObject)
+                : ctx.dataFormat.registry.get(params.format)
+            const b = state.build().to(data.ref);
+            // need to await so that the enclosing Task finishes after the update is done.
+            await provider.getDefaultBuilder(ctx, b, { visuals: params.visuals }, state).runInContext(taskCtx)
+        } catch (e) {
+            ctx.log.error(e)
+        }
+    }
 }));
 
 //

+ 13 - 3
src/mol-util/param-definition.ts

@@ -119,15 +119,25 @@ export namespace ParamDefinition {
     }
 
     export interface FileParam extends Base<File> {
-        type: 'file',
+        type: 'file'
         accept?: string
     }
-    export function File(info?: Info & { accept?: string }): FileParam {
+    export function File(info?: Info & { accept?: string, multiple?: boolean }): FileParam {
         const ret = setInfo<FileParam>({ type: 'file', defaultValue: void 0 as any }, info);
         if (info && info.accept) ret.accept = info.accept;
         return ret;
     }
 
+    export interface FileListParam extends Base<FileList> {
+        type: 'file-list'
+        accept?: string
+    }
+    export function FileList(info?: Info & { accept?: string, multiple?: boolean }): FileListParam {
+        const ret = setInfo<FileListParam>({ type: 'file-list', defaultValue: void 0 as any }, info);
+        if (info && info.accept) ret.accept = info.accept;
+        return ret;
+    }
+
     export interface Range {
         /** If given treat as a range. */
         min?: number
@@ -256,7 +266,7 @@ export namespace ParamDefinition {
     }
 
     export type Any =
-        | Value<any> | Select<any> | MultiSelect<any> | BooleanParam | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph
+        | Value<any> | Select<any> | MultiSelect<any> | BooleanParam | Text | Color | Vec3 | Numeric | FileParam | FileListParam | Interval | LineGraph
         | ColorList<any> | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | Script | ObjectList
 
     export type Params = { [k: string]: Any }