Bladeren bron

mol-plugin: add custom props on demand, UI tweaks

David Sehnal 6 jaren geleden
bovenliggende
commit
72d69e91c7

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

@@ -30,7 +30,7 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { PluginLayout } from './layout';
 import { List } from 'immutable';
 import { StateTransformParameters } from './ui/state/common';
-import { DataFormatRegistry } from './state/actions/basic';
+import { DataFormatRegistry } from './state/actions/volume';
 import { PluginBehavior } from './behavior/behavior';
 
 export class PluginContext {

+ 7 - 6
src/mol-plugin/index.ts

@@ -11,10 +11,10 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { PluginCommands } from './command';
 import { PluginSpec } from './spec';
-import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
 import { AnimateModelIndex } from './state/animation/built-in';
+import { StateActions } from './state/actions';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -23,11 +23,12 @@ function getParam(name: string, regex: string): string {
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
-        PluginSpec.Action(DownloadStructure),
-        PluginSpec.Action(DownloadDensity),
-        PluginSpec.Action(OpenStructure),
-        PluginSpec.Action(OpenVolume),
-        PluginSpec.Action(CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.DownloadStructure),
+        PluginSpec.Action(StateActions.Volume.DownloadDensity),
+        PluginSpec.Action(StateActions.Structure.OpenStructure),
+        PluginSpec.Action(StateActions.Volume.OpenVolume),
+        PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.EnableModelCustomProps),
         PluginSpec.Action(StateTransforms.Data.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Data.ParseCcp4),

+ 0 - 1
src/mol-plugin/providers/custom-prop.ts

@@ -1 +0,0 @@
-// TODO

+ 0 - 1
src/mol-plugin/providers/theme.ts

@@ -1 +0,0 @@
-// TODO

+ 9 - 0
src/mol-plugin/skin/base/components/temp.scss

@@ -14,6 +14,15 @@
     // border-bottom: 1px solid $entity-color-Group; // TODO separate color
 }
 
+.msp-current-header {
+    height: $row-height;
+    line-height: $row-height;
+    margin-bottom: $control-spacing;
+    text-align: center;
+    font-weight: bold;
+    background: $default-background;
+}
+
 .msp-btn-row-group {
     display:flex;
     flex-direction:row;

+ 13 - 0
src/mol-plugin/state/actions.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as Structure from './actions/structure'
+import * as Volume from './actions/volume'
+
+export const StateActions = {
+    Structure,
+    Volume
+}

+ 180 - 0
src/mol-plugin/state/actions/structure.ts

@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { StateAction, StateBuilder, StateSelection, StateTransformer } from 'mol-state';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { Download } from '../transforms/data';
+import { StructureRepresentation3DHelpers } from '../transforms/representation';
+import { CustomModelProperties } from '../transforms/model';
+
+// TODO: "structure/volume parser provider"
+
+export { DownloadStructure };
+type DownloadStructure = typeof DownloadStructure
+const DownloadStructure = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
+    params: {
+        source: PD.MappedStatic('bcif-static', {
+            'pdbe-updated': PD.Group({
+                id: PD.Text('1cbs', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'rcsb': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'bcif-static': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'url': PD.Group({
+                url: PD.Text(''),
+                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
+                isBinary: PD.Boolean(false),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true })
+        }, {
+            options: [
+                ['pdbe-updated', 'PDBe Updated'],
+                ['rcsb', 'RCSB'],
+                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
+                ['url', 'URL']
+            ]
+        })
+    }
+})(({ params, state }, ctx: PluginContext) => {
+    const b = state.build();
+    const src = params.source;
+    let downloadParams: StateTransformer.Params<Download>;
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
+            break;
+        case 'pdbe-updated':
+            downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
+            break;
+        case 'rcsb':
+            downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
+            break;
+        case 'bcif-static':
+            downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
+    const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
+});
+
+export const OpenStructure = StateAction.build({
+    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
+    from: PluginStateObject.Root,
+    params: { file: PD.File({ accept: '.cif,.bcif' }) }
+})(({ params, state }, ctx: PluginContext) => {
+    const b = state.build();
+    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
+    const traj = createModelTree(data, 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, false));
+});
+
+function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
+    const parsed = format === 'cif'
+        ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+        : b.apply(StateTransforms.Model.TrajectoryFromPDB);
+
+    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+}
+
+function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean) {
+    let root = b;
+    if (supportProps) {
+        root = root.apply(StateTransforms.Model.CustomModelProperties);
+    }
+    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
+    complexRepresentation(ctx, structure);
+
+    return root;
+}
+
+function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) {
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+}
+
+export const CreateComplexRepresentation = StateAction.build({
+    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
+    from: PluginStateObject.Molecule.Structure
+})(({ ref, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref);
+    complexRepresentation(ctx, root);
+    return state.updateTree(root);
+});
+
+export const UpdateTrajectory = StateAction.build({
+    display: { name: 'Update Trajectory' },
+    params: {
+        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
+        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
+    }
+})(({ params, state }) => {
+    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+
+    const update = state.build();
+
+    if (params.action === 'reset') {
+        for (const m of models) {
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                () => ({ modelIndex: 0 }));
+        }
+    } else {
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                old => {
+                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                    if (modelIndex < 0) modelIndex += traj.data.length;
+                    return { modelIndex };
+                });
+        }
+    }
+
+    return state.updateTree(update);
+});
+
+export const EnableModelCustomProps = StateAction.build({
+    display: { name: 'Custom Properties', description: 'Enable the addition of custom properties to the model.' },
+    from: PluginStateObject.Molecule.Model,
+    params(a, ctx: PluginContext) {
+        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
+        return { properties: ctx.customModelProperties.getSelect(a.data) };
+    },
+    isApplicable(a, t, ctx: PluginContext) {
+        return t.transformer !== CustomModelProperties;
+    }
+})(({ ref, params, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref).insert(CustomModelProperties, params);
+    return state.updateTree(root);
+});

+ 6 - 158
src/mol-plugin/state/actions/basic.ts → src/mol-plugin/state/actions/volume.ts

@@ -5,169 +5,17 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { VolumeIsoValue } from 'mol-model/volume';
 import { PluginContext } from 'mol-plugin/context';
-import { StateTree, StateTransformer, StateObject, State, StateBuilder, StateSelection, StateAction } from 'mol-state';
+import { State, StateAction, StateBuilder, StateObject, StateTransformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { ColorNames } from 'mol-util/color/tables';
+import { FileInfo, getFileInfo } from 'mol-util/file-info';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { Download } from '../transforms/data';
-import { StructureRepresentation3DHelpers, VolumeRepresentation3DHelpers } from '../transforms/representation';
-import { getFileInfo, FileInfo } from 'mol-util/file-info';
-import { Task } from 'mol-task';
-import { ColorNames } from 'mol-util/color/tables';
-import { VolumeIsoValue } from 'mol-model/volume';
-
-// TODO: "structure/volume parser provider"
-
-export { DownloadStructure };
-type DownloadStructure = typeof DownloadStructure
-const DownloadStructure = StateAction.build({
-    from: PluginStateObject.Root,
-    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
-    params: {
-        source: PD.MappedStatic('bcif-static', {
-            'pdbe-updated': PD.Group({
-                id: PD.Text('1cbs', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'rcsb': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'bcif-static': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'url': PD.Group({
-                url: PD.Text(''),
-                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
-                isBinary: PD.Boolean(false),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true })
-        }, {
-            options: [
-                ['pdbe-updated', 'PDBe Updated'],
-                ['rcsb', 'RCSB'],
-                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
-                ['url', 'URL']
-            ]
-        })
-    }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const src = params.source;
-    let downloadParams: StateTransformer.Params<Download>;
-
-    switch (src.name) {
-        case 'url':
-            downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
-            break;
-        case 'pdbe-updated':
-            downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
-            break;
-        case 'rcsb':
-            downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
-            break;
-        case 'bcif-static':
-            downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
-            break;
-        default: throw new Error(`${(src as any).name} not supported.`);
-    }
-
-    const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
-    const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
-    return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
-});
-
-export const OpenStructure = StateAction.build({
-    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
-    from: PluginStateObject.Root,
-    params: { file: PD.File({ accept: '.cif,.bcif' }) }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-    const traj = createModelTree(data, 'cif');
-    return state.updateTree(createStructureTree(ctx, traj, false));
-});
-
-function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
-    const parsed = format === 'cif'
-        ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
-        : b.apply(StateTransforms.Model.TrajectoryFromPDB);
-
-    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
-}
-
-function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree {
-    let root = b;
-    if (supportProps) {
-        root = root.apply(StateTransforms.Model.CustomModelProperties);
-    }
-    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
-    complexRepresentation(ctx, structure);
-
-    return root.getTree();
-}
-
-function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) {
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
-}
-
-export const CreateComplexRepresentation = StateAction.build({
-    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
-    from: PluginStateObject.Molecule.Structure
-})(({ ref, state }, ctx: PluginContext) => {
-    const root = state.build().to(ref);
-    complexRepresentation(ctx, root);
-    return state.updateTree(root.getTree());
-});
-
-export const UpdateTrajectory = StateAction.build({
-    display: { name: 'Update Trajectory' },
-    params: {
-        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
-        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
-    }
-})(({ params, state }) => {
-    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
-
-    const update = state.build();
-
-    if (params.action === 'reset') {
-        for (const m of models) {
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                () => ({ modelIndex: 0 }));
-        }
-    } else {
-        for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
-            if (!parent || !parent.obj) continue;
-            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                old => {
-                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                    if (modelIndex < 0) modelIndex += traj.data.length;
-                    return { modelIndex };
-                });
-        }
-    }
-
-    return state.updateTree(update);
-});
-
-//
+import { VolumeRepresentation3DHelpers } from '../transforms/representation';
 
 export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> {
     private _list: { name: string, provider: DataFormatProvider<D> }[] = []

+ 1 - 1
src/mol-plugin/ui/controls.tsx

@@ -6,7 +6,7 @@
 
 import * as React from 'react';
 import { PluginCommands } from 'mol-plugin/command';
-import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
+import { UpdateTrajectory } from 'mol-plugin/state/actions/structure';
 import { PluginUIComponent } from './base';
 import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager';
 

+ 7 - 7
src/mol-plugin/ui/plugin.tsx

@@ -174,19 +174,19 @@ export class CurrentObject extends PluginUIComponent {
         const cell = current.state.cells.get(ref)!;
         const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0;
 
-        const type = cell && cell.obj ? cell.obj.type : void 0;
         const transform = cell.transform;
         const def = transform.transformer.definition;
 
-        const actions = type ? current.state.actions.fromType(type) : [];
+        const actions =  current.state.actions.fromCell(cell, this.plugin);
         return <>
-            <div className='msp-section-header'>
+            <div className='msp-current-header'>
                 {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name}
             </div>
-            { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> }
-            {cell.status === 'ok' &&
-                actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)
-            }
+            {(parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} />}
+            {cell.status === 'ok' && <>
+                <div className='msp-section-header'>Actions</div>
+                {actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)}
+                </>}
         </>;
     }
 }

+ 8 - 2
src/mol-state/action.ts

@@ -10,6 +10,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
 import { StateTransformer } from './transformer';
+import { StateTransform } from './transform';
 
 export { StateAction };
 
@@ -45,7 +46,7 @@ namespace StateAction {
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
         /** Test if the transform can be applied to a given node */
-        isApplicable?(a: A, globalCtx: unknown): boolean
+        isApplicable?(a: A, aTransform: StateTransform<any, A, any>, globalCtx: unknown): boolean
     }
 
     export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
@@ -69,6 +70,9 @@ namespace StateAction {
             from: def.from,
             display: def.display,
             params: def.params as StateTransformer.Definition<StateTransformer.From<T>, any, StateTransformer.Params<T>>['params'],
+            isApplicable: transformer.definition.isApplicable
+                ? (a, t, ctx) => transformer.definition.isApplicable!(a, ctx)
+                : void 0,
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 return state.updateTree(tree) as Task<void>;
@@ -80,7 +84,8 @@ namespace StateAction {
         export interface Type<A extends StateObject.Ctor, P extends { }> {
             from?: A | A[],
             params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
-            display?: string | { name: string, description?: string }
+            display?: string | { name: string, description?: string },
+            isApplicable?: DefinitionBase<StateObject.From<A>, any, P>['isApplicable']
         }
 
         export interface Root {
@@ -106,6 +111,7 @@ namespace StateAction {
                     : !!info.params
                     ? info.params as any
                     : void 0,
+                isApplicable: info.isApplicable,
                 ...(typeof def === 'function'
                     ? { run: def }
                     : def)

+ 27 - 3
src/mol-state/action/manager.ts

@@ -5,7 +5,7 @@
  */
 
 import { StateAction } from '../action';
-import { StateObject } from '../object';
+import { StateObject, StateObjectCell } from '../object';
 import { StateTransformer } from '../transformer';
 
 export { StateActionManager }
@@ -32,7 +32,31 @@ class StateActionManager {
         return this;
     }
 
-    fromType(type: StateObject.Type): ReadonlyArray<StateAction> {
-        return this.fromTypeIndex.get(type) || [];
+    fromCell(cell: StateObjectCell, ctx: unknown): ReadonlyArray<StateAction> {
+        const obj = cell.obj;
+        if (!obj) return [];
+
+        const actions = this.fromTypeIndex.get(obj.type);
+        if (!actions) return [];
+        let hasTest = false;
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                hasTest = true;
+                break;
+            }
+        }
+        if (!hasTest) return actions;
+
+        const ret: StateAction[] = [];
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                if (a.definition.isApplicable(obj, cell.transform, ctx)) {
+                    ret.push(a);
+                }
+            } else {
+                ret.push(a);
+            }
+        }
+        return ret;
     }
 }