Browse Source

inline actions and updates in StateTree
mol-plugin code cleanup

David Sehnal 5 years ago
parent
commit
345b9f546c

+ 1 - 4
src/examples/proteopedia-wrapper/index.ts

@@ -17,7 +17,7 @@ import { StateBuilder, StateObject, StateSelection } from '../../mol-state';
 import { EvolutionaryConservation } from './annotation';
 import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo, StateElements } from './helpers';
 import { RxEventHelper } from '../../mol-util/rx-event-helper';
-import { ControlsWrapper, volumeStreamingControls } from './ui/controls';
+import { volumeStreamingControls } from './ui/controls';
 import { PluginState } from '../../mol-plugin/state';
 import { Scheduler } from '../../mol-task';
 import { createProteopediaCustomTheme } from './coloring';
@@ -55,9 +55,6 @@ class MolStarProteopediaWrapper {
                 initial: {
                     isExpanded: false,
                     showControls: false
-                },
-                controls: {
-                    right: ControlsWrapper
                 }
             },
             components: {

+ 2 - 15
src/examples/proteopedia-wrapper/ui/controls.tsx

@@ -6,24 +6,11 @@
 
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
-import { PluginUIComponent } from '../../../mol-plugin-ui/base';
-import { CurrentObject, PluginContextContainer } from '../../../mol-plugin-ui/plugin';
-import { AnimationControls } from '../../../mol-plugin-ui/state/animation';
-import { CameraSnapshots } from '../../../mol-plugin-ui/camera';
-import { PluginContext } from '../../../mol-plugin/context';
+import { PluginContextContainer } from '../../../mol-plugin-ui/plugin';
 import { TransformUpdaterControl } from '../../../mol-plugin-ui/state/update-transform';
+import { PluginContext } from '../../../mol-plugin/context';
 import { StateElements } from '../helpers';
 
-export class ControlsWrapper extends PluginUIComponent {
-    render() {
-        return <div className='msp-scrollable-container msp-right-controls'>
-            <CurrentObject />
-            <AnimationControls />
-            <CameraSnapshots />
-        </div>;
-    }
-}
-
 export function volumeStreamingControls(plugin: PluginContext, parent: Element) {
     ReactDOM.render(<PluginContextContainer plugin={plugin}>
             <TransformUpdaterControl nodeRef={StateElements.VolumeStreaming} />

+ 4 - 4
src/mol-plugin-state/animation/built-in.ts

@@ -123,7 +123,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
         if (!changed) return;
 
-        return plugin.updateState(update, { doNotUpdateCurrent: true });
+        return plugin.updateDataState(update, { doNotUpdateCurrent: true });
     },
     async teardown(_, plugin) {
         const state = plugin.state.data;
@@ -133,7 +133,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
         const update = state.build();
         for (const r of reprs) update.delete(r.transform.ref);
-        return plugin.updateState(update);
+        return plugin.updateDataState(update);
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.data;
@@ -190,7 +190,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         if (!changed) return;
 
-        return plugin.updateState(update, { doNotUpdateCurrent: true });
+        return plugin.updateDataState(update, { doNotUpdateCurrent: true });
     },
     async teardown(_, plugin) {
         const state = plugin.state.data;
@@ -200,7 +200,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
         const update = state.build();
         for (const r of reprs) update.delete(r.transform.ref);
-        return plugin.updateState(update);
+        return plugin.updateDataState(update);
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.data;

+ 4 - 4
src/mol-plugin-state/builder/data.ts

@@ -16,26 +16,26 @@ export class DataBuilder {
 
     async rawData(params: StateTransformer.Params<RawData>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(RawData, params, options);
-        await this.plugin.updateState(data, { revertOnError: true });
+        await this.plugin.updateDataState(data, { revertOnError: true });
         return data.selector;
     }
 
     async download(params: StateTransformer.Params<Download>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(Download, params, options);
-        await this.plugin.updateState(data, { revertOnError: true });
+        await this.plugin.updateDataState(data, { revertOnError: true });
         return data.selector;
     }
 
     async downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {        
         const data = this.dataState.build().toRoot().apply(DownloadBlob, params, options);
-        await this.plugin.updateState(data, { revertOnError: true });
+        await this.plugin.updateDataState(data, { revertOnError: true });
         return data.selector;
     }
 
     async readFile(params: StateTransformer.Params<ReadFile>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(ReadFile, params, options);
         const fileInfo = getFileInfo(params.file);
-        await this.plugin.updateState(data, { revertOnError: true });
+        await this.plugin.updateDataState(data, { revertOnError: true });
         return { data: data.selector, fileInfo };
     }
 

+ 8 - 8
src/mol-plugin-state/builder/structure.ts

@@ -44,7 +44,7 @@ export class StructureBuilder {
         const trajectory = state.build().to(data)
             .apply(StateTransforms.Data.ParseBlob, params, { state: { isGhost: true } })
             .apply(StateTransforms.Model.TrajectoryFromBlob, void 0, { tags: StructureBuilderTags.Trajectory });        
-        await this.plugin.updateState(trajectory, { revertOnError: true });
+        await this.plugin.updateDataState(trajectory, { revertOnError: true });
         return trajectory.selector;
     }
 
@@ -101,7 +101,7 @@ export class StructureBuilder {
         const model = state.build().to(trajectory)
             .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }, { tags: StructureBuilderTags.Model, state: initialState });
 
-        await this.plugin.updateState(model, { revertOnError: true });
+        await this.plugin.updateDataState(model, { revertOnError: true });
         return model.selector;
     }
 
@@ -109,7 +109,7 @@ export class StructureBuilder {
         const state = this.dataState;
         const props = state.build().to(model)
             .insert(StateTransforms.Model.CustomModelProperties, params, { tags: StructureBuilderTags.ModelProperties, isDecorator: true });
-        await this.plugin.updateState(props, { revertOnError: true });
+        await this.plugin.updateDataState(props, { revertOnError: true });
         return props.selector;
     }
 
@@ -118,7 +118,7 @@ export class StructureBuilder {
         const structure = state.build().to(model)
             .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure, state: initialState });        
 
-        await this.plugin.updateState(structure, { revertOnError: true });
+        await this.plugin.updateDataState(structure, { revertOnError: true });
         return structure.selector;
     }
 
@@ -126,7 +126,7 @@ export class StructureBuilder {
         const state = this.dataState;
         const props = state.build().to(structure)
             .insert(StateTransforms.Model.CustomStructureProperties, params, { tags: StructureBuilderTags.StructureProperties, isDecorator: true });
-        await this.plugin.updateState(props, { revertOnError: true });
+        await this.plugin.updateDataState(props, { revertOnError: true });
         return props.selector;
     }
 
@@ -145,13 +145,13 @@ export class StructureBuilder {
             tags: tags ? [...tags, StructureBuilderTags.Component, keyTag] : [StructureBuilderTags.Component, keyTag]
         });
 
-        await this.plugin.updateState(component);
+        await this.plugin.updateDataState(component);
 
         const selector = component.selector;
 
         if (!selector.isOk || selector.cell?.obj?.data.elementCount === 0) {
             const del = state.build().delete(selector.ref);
-            await this.plugin.updateState(del);
+            await this.plugin.updateDataState(del);
             return;
         }
 
@@ -205,7 +205,7 @@ export class StructureBuilder {
     
             if (!selector.isOk || selector.cell?.obj?.data.elementCount === 0) {
                 const del = state.build().delete(selector.ref);
-                await this.plugin.updateState(del);
+                await this.plugin.updateDataState(del);
                 return;
             }
     

+ 1 - 1
src/mol-plugin-state/builder/structure/representation.ts

@@ -107,7 +107,7 @@ export class StructureRepresentationBuilder {
             .to(structure)
             .apply(StructureRepresentation3D, params, { tags: RepresentationProviderTags.Representation });
 
-        await this.plugin.updateState(repr);
+        await this.plugin.updateDataState(repr);
         return  repr.selector;
     }
 

+ 2 - 2
src/mol-plugin-state/formats/trajectory.ts

@@ -35,7 +35,7 @@ export const MmcifProvider: TrajectoryFormatProvider = {
         const trajectory = state.build().to(data)
             .apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
             .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { tags: params?.trajectoryTags })
-        await plugin.updateState(trajectory, { revertOnError: true });
+        await plugin.updateDataState(trajectory, { revertOnError: true });
         return { trajectory: trajectory.selector };
     }
 }
@@ -45,7 +45,7 @@ function directTrajectory(transformer: StateTransformer<PluginStateObject.Data.S
         const state = plugin.state.data;
         const trajectory = state.build().to(data)
             .apply(transformer, void 0, { tags: params?.trajectoryTags })
-        await plugin.updateState(trajectory, { revertOnError: true });
+        await plugin.updateDataState(trajectory, { revertOnError: true });
         return { trajectory: trajectory.selector };
     }
 }

+ 1 - 1
src/mol-plugin-state/helpers/structure-overpaint.ts

@@ -65,7 +65,7 @@ async function eachRepr(plugin: PluginContext, components: StructureComponentRef
         }
     }
 
-    await plugin.updateState(update, { doNotUpdateCurrent: true });
+    await plugin.updateDataState(update, { doNotUpdateCurrent: true });
 }
 
 /** filter overpaint layers for given structure */

+ 5 - 5
src/mol-plugin-state/manager/structure/component.ts

@@ -62,7 +62,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
         }
 
         return this.plugin.dataTransaction(async () => {
-            await this.plugin.updateState(update);
+            await this.plugin.updateDataState(update);
             if (interactionChanged) await this.updateInterationProps();
         });
     }
@@ -96,7 +96,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
                     arraySetAdd(old.autoAttach, InteractionsProvider.descriptor.name);
                     old.properties[InteractionsProvider.descriptor.name] = this.state.options.interactions;
                 });
-                await this.plugin.updateState(b);
+                await this.plugin.updateDataState(b);
             } else {
                 const pd = this.plugin.customStructureProperties.getParams(s.cell.obj?.data);
                 const params = PD.getDefaultValues(pd);
@@ -196,7 +196,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
             update.to(repr.cell).update(params);
         }
 
-        return this.plugin.updateState(update, { canUndo: 'Update Representation' });
+        return this.plugin.updateDataState(update, { canUndo: 'Update Representation' });
     }
 
     updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> {
@@ -220,7 +220,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
             }
         }
 
-        return this.plugin.updateState(update, { canUndo: 'Update Theme' });
+        return this.plugin.updateDataState(update, { canUndo: 'Update Theme' });
     }
 
     addRepresentation(components: ReadonlyArray<StructureComponentRef>, type: string) {
@@ -318,7 +318,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
                 if (s.currentFocus.surroundings) deletes.delete(s.currentFocus.surroundings.cell.transform.ref);
             }
         }
-        return this.plugin.updateState(deletes, { canUndo: 'Clear Selections' });
+        return this.plugin.updateDataState(deletes, { canUndo: 'Clear Selections' });
     }
 
     constructor(public plugin: PluginContext) {

+ 2 - 2
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -131,7 +131,7 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
         if (refs.length === 0) return;
         const deletes = this.plugin.state.data.build();
         for (const r of refs) deletes.delete(r.cell.transform.ref);
-        return this.plugin.updateState(deletes, { canUndo: canUndo ? 'Remove' : false });
+        return this.plugin.updateDataState(deletes, { canUndo: canUndo ? 'Remove' : false });
     }
 
     createAllModels(trajectory: TrajectoryRef) {
@@ -159,7 +159,7 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
         for (const m of trajectory.models) {
             builder.delete(m.cell);
         }
-        return this.plugin.updateState(builder);
+        return this.plugin.updateDataState(builder);
     }
 
     constructor(private plugin: PluginContext) {

+ 6 - 6
src/mol-plugin-ui/controls/action-menu.tsx

@@ -16,13 +16,13 @@ export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
         const cmd = this.props;
         return <div className='msp-action-menu-options' style={{ marginTop: cmd.header ? void 0 : '1px' }}>
             {cmd.header && <ControlGroup header={cmd.header} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hide} topRightIcon='off'></ControlGroup>}
-            <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} multiselect={this.props.multiselect} />
+            <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} multiselect={this.props.multiselect} noOffset={this.props.noOffset} />
         </div>
     }
 }
 
 export namespace ActionMenu {
-    export type Props = { items: Items, onSelect: OnSelect | OnSelectMany, header?: string, current?: Item, multiselect?: boolean }
+    export type Props = { items: Items, onSelect: OnSelect | OnSelectMany, header?: string, current?: Item, multiselect?: boolean, noOffset?: boolean }
 
     export type OnSelect = (item: Item | undefined) => void
     export type OnSelectMany = (itemOrItems: Item[] | undefined) => void
@@ -119,7 +119,7 @@ export namespace ActionMenu {
     }
 }
 
-type SectionProps = { items: ActionMenu.Items, onSelect: ActionMenu.OnSelect | ActionMenu.OnSelectMany, current: ActionMenu.Item | undefined, multiselect: boolean | undefined }
+type SectionProps = { items: ActionMenu.Items, onSelect: ActionMenu.OnSelect | ActionMenu.OnSelectMany, current: ActionMenu.Item | undefined, multiselect: boolean | undefined, noOffset?: boolean }
 type SectionState = { items: ActionMenu.Items, current: ActionMenu.Item | undefined, isExpanded: boolean, hasCurrent: boolean, header?: ActionMenu.Header }
 
 class Section extends React.PureComponent<SectionProps, SectionState> {
@@ -201,16 +201,16 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
 
         const { header } = this.state;
 
-        return <div>
+        return <>
             {header && (this.props.multiselect && this.state.isExpanded ? this.multiselectHeader : this.basicHeader)}
-            <div className='msp-control-offset'>
+            <div className={this.props.noOffset ? void 0 : 'msp-control-offset'}>
                 {(!header || this.state.isExpanded) && items.map((x, i) => {
                     if (isHeader(x)) return null;
                     if (isItem(x)) return <Action key={i} item={x} onSelect={onSelect} current={current} multiselect={this.props.multiselect} />
                     return <Section key={i} items={x} onSelect={onSelect} current={current} multiselect={this.props.multiselect} />
                 })}
             </div>
-        </div>;
+        </>;
     }
 }
 

+ 78 - 83
src/mol-plugin-ui/plugin.tsx

@@ -6,22 +6,17 @@
  */
 
 import { List } from 'immutable';
-import { formatTime } from '../mol-util';
-import { LogEntry } from '../mol-util/log-entry';
 import * as React from 'react';
 import { PluginContext } from '../mol-plugin/context';
+import { formatTime } from '../mol-util';
+import { LogEntry } from '../mol-util/log-entry';
 import { PluginReactContext, PluginUIComponent } from './base';
-import { LociLabels, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, DefaultStructureTools } from './controls';
-import { StateObjectActionSelect } from './state/actions';
-import { BackgroundTaskProgress } from './task';
-import { Viewport, ViewportControls } from './viewport';
-import { StateTransform } from '../mol-state';
-import { UpdateTransformControl } from './state/update-transform';
+import { AnimationViewportControls, DefaultStructureTools, LociLabels, StateSnapshotViewportControls, TrajectoryViewportControls } from './controls';
+import { LeftPanelControls } from './left-panel';
 import { SequenceView } from './sequence';
+import { BackgroundTaskProgress } from './task';
 import { Toasts } from './toast';
-import { SectionHeader, ExpandGroup } from './controls/common';
-import { LeftPanelControls } from './left-panel';
-import { StateTreeSpine } from '../mol-state/tree/spine';
+import { Viewport, ViewportControls } from './viewport';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -126,7 +121,7 @@ export class ControlsWrapper extends PluginUIComponent {
     render() {
         const StructureTools = this.plugin.spec.components?.structureTools || DefaultStructureTools;
         return <div className='msp-scrollable-container'>
-            <CurrentObject />
+            {/* <CurrentObject /> */}
             <StructureTools />
         </div>;
     }
@@ -191,74 +186,74 @@ export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
     }
 }
 
-export class CurrentObject extends PluginUIComponent {
-    get current() {
-        return this.plugin.state.behavior.currentObject.value;
-    }
-
-    componentDidMount() {
-        this.subscribe(this.plugin.state.behavior.currentObject, o => {
-            this.forceUpdate();
-        });
-
-        this.subscribe(this.plugin.behaviors.layout.leftPanelTabName, o => {
-            this.forceUpdate();
-        });
-
-        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
-            const current = this.current;
-            if (current.ref !== ref || current.state !== state) return;
-            this.forceUpdate();
-        });
-    }
-
-    render() {
-        const tabName = this.plugin.behaviors.layout.leftPanelTabName.value;
-        if (tabName !== 'data' && tabName !== 'settings') return null;
-
-        const current = this.current;
-        const ref = current.ref;
-        if (ref === StateTransform.RootRef) return null;
-
-        const cell = current.state.cells.get(ref)!;
-        const transform = cell.transform;
-
-        let showActions = true;
-        if (ref === StateTransform.RootRef) {
-            const children = current.state.tree.children.get(ref);
-            showActions = children.size !== 0;
-        }
-
-        if (!showActions) return null;
-
-        const actions = cell.status === 'ok' && <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} />
-
-        if (cell.status === 'error') {
-            return <>            
-                <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} />
-                <UpdateTransformControl state={current.state} transform={transform} customHeader='none' />
-                {actions}
-            </>;
-        }
-
-        if (cell.status !== 'ok') return null;
-
-        const decoratorChain = StateTreeSpine.getDecoratorChain(this.current.state, this.current.ref);
-        const parent = decoratorChain[decoratorChain.length - 1];
-
-        let decorators: JSX.Element[] | undefined = decoratorChain.length > 1 ? [] : void 0;
-        for (let i = decoratorChain.length - 2; i >= 0; i--) {
-            const d = decoratorChain[i];
-            decorators!.push(<ExpandGroup key={`${d.transform.transformer.id}-${i}`} header={d.transform.transformer.definition.display.name}>
-                <UpdateTransformControl state={current.state} transform={d.transform} customHeader='none' />
-            </ExpandGroup>);
-        }
-
-        return <>            
-            <SectionHeader icon='flow-cascade' title={`${parent.obj?.label || parent.transform.transformer.definition.display.name}`} desc={parent.transform.transformer.definition.display.name} />
-            <UpdateTransformControl state={current.state} transform={parent.transform} customHeader='none' />
-            {decorators && <div className='msp-controls-section'>{decorators}</div>}
-            {actions}
-        </>;
-    }
-}
+// export class CurrentObject extends PluginUIComponent {
+//     get current() {
+//         return this.plugin.state.behavior.currentObject.value;
+//     }
+
+//     componentDidMount() {
+//         this.subscribe(this.plugin.state.behavior.currentObject, o => {
+//             this.forceUpdate();
+//         });
+
+//         this.subscribe(this.plugin.behaviors.layout.leftPanelTabName, o => {
+//             this.forceUpdate();
+//         });
+
+//         this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+//             const current = this.current;
+//             if (current.ref !== ref || current.state !== state) return;
+//             this.forceUpdate();
+//         });
+//     }
+
+//     render() {
+//         const tabName = this.plugin.behaviors.layout.leftPanelTabName.value;
+//         if (tabName !== 'data' && tabName !== 'settings') return null;
+
+//         const current = this.current;
+//         const ref = current.ref;
+//         if (ref === StateTransform.RootRef) return null;
+
+//         const cell = current.state.cells.get(ref)!;
+//         const transform = cell.transform;
+
+//         let showActions = true;
+//         if (ref === StateTransform.RootRef) {
+//             const children = current.state.tree.children.get(ref);
+//             showActions = children.size !== 0;
+//         }
+
+//         if (!showActions) return null;
+
+//         const actions = cell.status === 'ok' && <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} />
+
+//         if (cell.status === 'error') {
+//             return <>            
+//                 <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} />
+//                 <UpdateTransformControl state={current.state} transform={transform} customHeader='none' />
+//                 {actions}
+//             </>;
+//         }
+
+//         if (cell.status !== 'ok') return null;
+
+//         const decoratorChain = StateTreeSpine.getDecoratorChain(this.current.state, this.current.ref);
+//         const parent = decoratorChain[decoratorChain.length - 1];
+
+//         let decorators: JSX.Element[] | undefined = decoratorChain.length > 1 ? [] : void 0;
+//         for (let i = decoratorChain.length - 2; i >= 0; i--) {
+//             const d = decoratorChain[i];
+//             decorators!.push(<ExpandGroup key={`${d.transform.transformer.id}-${i}`} header={d.transform.transformer.definition.display.name}>
+//                 <UpdateTransformControl state={current.state} transform={d.transform} customHeader='none' />
+//             </ExpandGroup>);
+//         }
+
+//         return <>            
+//             <SectionHeader icon='flow-cascade' title={`${parent.obj?.label || parent.transform.transformer.definition.display.name}`} desc={parent.transform.transformer.definition.display.name} />
+//             <UpdateTransformControl state={current.state} transform={parent.transform} customHeader='none' />
+//             {decorators && <div className='msp-controls-section'>{decorators}</div>}
+//             {actions}
+//         </>;
+//     }
+// }

+ 12 - 4
src/mol-plugin-ui/state/common.tsx

@@ -12,6 +12,7 @@ import { PluginContext } from '../../mol-plugin/context';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Subject } from 'rxjs';
 import { Icon } from '../controls/icons';
+import { ExpandGroup } from '../controls/common';
 
 export { StateTransformParameters, TransformControlBase };
 
@@ -100,7 +101,7 @@ namespace TransformControlBase {
     }
 }
 
-abstract class TransformControlBase<P, S extends TransformControlBase.ComponentState> extends PurePluginUIComponent<P & { noMargin?: boolean, applyLabel?: string }, S> {
+abstract class TransformControlBase<P, S extends TransformControlBase.ComponentState> extends PurePluginUIComponent<P & { noMargin?: boolean, applyLabel?: string, onApply?: () => void, wrapInExpander?: boolean }, S> {
     abstract applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
     abstract getHeader(): StateTransformer.Definition['display'] | 'none';
@@ -148,6 +149,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
         } catch {
             // eat errors because they should be handled elsewhere
         } finally {
+            this.props.onApply?.();
             this.busy.next(false);
         }
     }
@@ -190,8 +192,8 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
 
         const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
 
-        return <div className={wrapClass} style={{ marginBottom: this.props.noMargin ? 0 : void 0 }}>
-            {display !== 'none' && <div className='msp-transform-header'>
+        const ctrl = <div className={wrapClass} style={{ marginBottom: this.props.noMargin ? 0 : void 0 }}>
+            {display !== 'none' && !this.props.wrapInExpander && <div className='msp-transform-header'>
                 <button className={`msp-btn msp-btn-block${isEmpty ? '' : ' msp-btn-collapse'}`} onClick={this.toggleExpanded} title={display.description}>
                     {!isEmpty && <Icon name={this.state.isCollapsed ? 'expand' : 'collapse'} />}
                     {display.name}
@@ -213,6 +215,12 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
                     </div>
                 </div>
             </>}
-        </div>
+        </div>;
+
+        if (isEmpty || !this.props.wrapInExpander) return ctrl;
+
+        return <ExpandGroup header={this.isUpdate() ? `Update ${display === 'none' ? '' : display.name}` : `Apply ${display === 'none' ? '' : display.name}` }>
+            {ctrl}
+        </ExpandGroup>;
     }
 }

+ 85 - 18
src/mol-plugin-ui/state/tree.tsx

@@ -6,10 +6,14 @@
 
 import * as React from 'react';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
-import { State, StateTree as _StateTree, StateObject, StateTransform, StateObjectCell } from '../../mol-state'
+import { State, StateTree as _StateTree, StateObject, StateTransform, StateObjectCell, StateAction } from '../../mol-state'
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginUIComponent, _Props, _State } from '../base';
 import { Icon } from '../controls/icons';
+import { ActionMenu } from '../controls/action-menu';
+import { ApplyActionControl } from './apply-action';
+import { ControlGroup } from '../controls/common';
+import { UpdateTransformControl } from './update-transform';
 
 export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
     state = { showActions: true };
@@ -121,9 +125,14 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
     }
 }
 
-class StateTreeNodeLabel extends PluginUIComponent<
-    { cell: StateObjectCell, depth: number },
-    { isCurrent: boolean, isCollapsed: boolean }> {
+interface StateTreeNodeLabelState {
+    isCurrent: boolean,
+    isCollapsed: boolean,
+    action?: 'options' | 'apply',
+    currentAction?: StateAction
+}
+
+class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, StateTreeNodeLabelState> {
 
     is(e: State.ObjectEvent) {
         return e.ref === this.ref && e.state === this.props.cell.parent;
@@ -141,23 +150,34 @@ class StateTreeNodeLabel extends PluginUIComponent<
         this.subscribe(this.plugin.state.behavior.currentObject, e => {
             if (!this.is(e)) {
                 if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
-                    this.setState({ isCurrent: this.props.cell.parent.current === this.ref });
+                    this._setCurrent(this.props.cell.parent.current === this.ref, this.state.isCollapsed);
                 }
                 return;
             }
 
             if (e.state.transforms.has(this.ref)) {
-                this.setState({
-                    isCurrent: this.props.cell.parent.current === this.ref,
-                    isCollapsed: !!this.props.cell.state.isCollapsed
-                });
+                this._setCurrent(this.props.cell.parent.current === this.ref, !!this.props.cell.state.isCollapsed)
+                // this.setState({
+                //     isCurrent: this.props.cell.parent.current === this.ref,
+                //     isCollapsed: !!this.props.cell.state.isCollapsed
+                // });
             }
         });
     }
 
-    state = {
+    private _setCurrent(isCurrent: boolean, isCollapsed: boolean) {
+        if (isCurrent) {
+            this.setState({ isCurrent, action: 'options', currentAction: void 0, isCollapsed });
+        } else {
+            this.setState({ isCurrent, action: void 0, currentAction: void 0, isCollapsed });
+        }
+    }
+
+    state: StateTreeNodeLabelState = {
         isCurrent: this.props.cell.parent.current === this.ref,
-        isCollapsed: !!this.props.cell.state.isCollapsed
+        isCollapsed: !!this.props.cell.state.isCollapsed,
+        action: void 0,
+        currentAction: void 0 as StateAction | undefined
     }
 
     static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
@@ -165,12 +185,12 @@ class StateTreeNodeLabel extends PluginUIComponent<
         const isCollapsed = !!props.cell.state.isCollapsed;
 
         if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
-        return { isCurrent, isCollapsed };
+        return { isCurrent, isCollapsed, action: void 0, currentAction: void 0 };
     }
 
-    setCurrent = (e: React.MouseEvent<HTMLElement>) => {
-        e.preventDefault();
-        e.currentTarget.blur();
+    setCurrent = (e?: React.MouseEvent<HTMLElement>) => {
+        e?.preventDefault();
+        e?.currentTarget.blur();
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: this.ref });
     }
 
@@ -180,8 +200,8 @@ class StateTreeNodeLabel extends PluginUIComponent<
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: StateTransform.RootRef });
     }
 
-    remove = (e: React.MouseEvent<HTMLElement>) => {
-        e.preventDefault();
+    remove = (e?: React.MouseEvent<HTMLElement>) => {
+        e?.preventDefault();
         PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
     }
 
@@ -209,12 +229,37 @@ class StateTreeNodeLabel extends PluginUIComponent<
         e.currentTarget.blur();
     }
 
+    // toggleActions = () => {
+    //     if (this.state.action) this.setState({ action: void 0, currentAction: void 0 });
+    //     else this.setState({ action: 'options', currentAction: void 0 });
+    // }
+
+    hideAction = () => this.setState({ action: void 0, currentAction: void 0 });
+
+    get actions() {
+        const cell = this.props.cell;
+        const actions = [...cell.parent.actions.fromCell(cell, this.plugin)];
+        if (actions.length === 0) return;
+
+        actions.sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1);
+
+        return [
+            ActionMenu.Header('Apply Action'),
+            ...actions.map(a => ActionMenu.Item(a.definition.display.name, () => this.setState({ action: 'apply', currentAction: a })))
+        ];
+    }
+
+    selectAction: ActionMenu.OnSelect = item => {
+        if (!item) return;
+        (item?.value as any)();
+    }
+
     render() {
         const cell = this.props.cell;
         const n = cell.transform;
         if (!cell) return null;
 
-        const isCurrent = this.state.isCurrent; // this.is(cell.parent.behaviors.currentObject.value);
+        const isCurrent = this.is(cell.parent.behaviors.currentObject.value);
 
         let label: any;
         if (cell.status === 'pending' || cell.status === 'processing') {
@@ -223,6 +268,8 @@ class StateTreeNodeLabel extends PluginUIComponent<
         } else if (cell.status !== 'ok' || !cell.obj) {
             const name = n.transformer.definition.display.name;
             const title = `${cell.errorText}`;
+
+            // {this.state.isCurrent ? this.setCurrentRoot : this.setCurrent
             label = <><button className='msp-btn-link msp-btn-tree-label' title={title} onClick={this.state.isCurrent ? this.setCurrentRoot : this.setCurrent}><b>[{cell.status}]</b> {name}: <i><span>{cell.errorText}</span></i> </button></>;
         } else {
             const obj = cell.obj as PluginStateObject.Any;
@@ -253,6 +300,26 @@ class StateTreeNodeLabel extends PluginUIComponent<
             </button>}{visibility}
         </div>;
 
+        if (!isCurrent) return row;
+
+        if (this.state.action === 'apply' && this.state.currentAction) {
+            return <div style={{ marginBottom: '1px' }}>
+                {row}
+                <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideAction} topRightIcon='off'>
+                    <ApplyActionControl onApply={this.hideAction} plugin={this.plugin} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
+                </ControlGroup>
+            </div>
+        }
+
+        if (this.state.action === 'options') {
+            let actions = this.actions;
+            return <div style={{ marginBottom: '1px' }}>
+                {row}
+                <UpdateTransformControl state={cell.parent} transform={cell.transform} noMargin wrapInExpander />
+                {actions && <ActionMenu items={actions} onSelect={this.selectAction} />}
+            </div>
+        }
+
         // if (this.state.isCurrent) {
         //     return <>
         //         {row}

+ 0 - 1
src/mol-plugin-ui/structure/components.tsx

@@ -289,7 +289,6 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         (item?.value as any)();
     }
 
-
     get removeActions(): ActionMenu.Items {
         const ret = [
             ActionMenu.Item('Remove', 'remove', () => this.plugin.managers.structure.hierarchy.remove(this.props.group, true))

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

@@ -66,7 +66,7 @@ namespace PluginBehavior {
         }
     });
 
-    const categoryMap = new Map<string, string>();
+    const categoryMap = new Map<string, keyof typeof Categories>();
     export function getCategoryId(t: StateTransformer) {
         return categoryMap.get(t.id)!;
     }

+ 0 - 5
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -29,12 +29,8 @@ const Trigger = Binding.Trigger
 const DefaultStructureRepresentationInteractionBindings = {
     clickInteractionAroundOnly: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Show the structure interaction around only the clicked element using ${triggers}.'),
 }
-// const StructureRepresentationInteractionParams = {
-//     bindings: PD.Value(DefaultStructureRepresentationInteractionBindings, { isHidden: true }),
-// }
 
 const StructureRepresentationInteractionParams = (plugin: PluginContext) => {
-    
     const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
     return {
         bindings: PD.Value(DefaultStructureRepresentationInteractionBindings, { isHidden: true }),
@@ -48,7 +44,6 @@ const StructureRepresentationInteractionParams = (plugin: PluginContext) => {
         }),
         nciParams: PD.Group(reprParams, {
             label: 'Non-covalent Int.',
-            // customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', color: 'element-symbol', size: 'uniform' })
             customDefault: createStructureRepresentationParams(plugin, void 0, {
                 type: InteractionsRepresentationProvider,
                 color: InteractionTypeColorThemeProvider,

+ 3 - 3
src/mol-plugin/behavior/static/state.ts

@@ -54,7 +54,7 @@ export function SetCurrentObject(ctx: PluginContext) {
 }
 
 export function Update(ctx: PluginContext) {
-    PluginCommands.State.Update.subscribe(ctx, ({ state, tree, options }) => ctx.updateState(tree, options));
+    PluginCommands.State.Update.subscribe(ctx, ({ state, tree, options }) => ctx.runTask(state.updateTree(tree, options)));
 }
 
 export function ApplyAction(ctx: PluginContext) {
@@ -63,8 +63,8 @@ export function ApplyAction(ctx: PluginContext) {
 
 export function RemoveObject(ctx: PluginContext) {
     function remove(state: State, ref: string) {
-        const tree = state.build().delete(ref).getTree();
-        return ctx.updateState(tree);
+        const tree = state.build().delete(ref);
+        return ctx.runTask(state.updateTree(tree));
     }
 
     PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref, removeParentGhosts }) => {

+ 14 - 2
src/mol-plugin/context.ts

@@ -202,7 +202,7 @@ export class PluginContext {
         return this.runTask(this.state.data.transaction(f, options));
     }
 
-    updateState(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>) {
+    updateDataState(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>) {
         return this.runTask(this.state.data.updateTree(tree, options));
     }
 
@@ -246,16 +246,28 @@ export class PluginContext {
     }
 
     private async initBehaviors() {
-        const tree = this.state.behaviors.build();
+        let tree = this.state.behaviors.build();
 
         for (const cat of Object.keys(PluginBehavior.Categories)) {
             tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, state: { isLocked: true } });
         }
 
+        // Init custom properties 1st
         for (const b of this.spec.behaviors) {
+            const cat = PluginBehavior.getCategoryId(b.transformer);
+            if (cat !== 'custom-props') continue;
+
             tree.to(PluginBehavior.getCategoryId(b.transformer)).apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
         }
+        await this.runTask(this.state.behaviors.updateTree(tree, { doNotUpdateCurrent: true, doNotLogTiming: true }));
+
+        tree = this.state.behaviors.build();
+        for (const b of this.spec.behaviors) {
+            const cat = PluginBehavior.getCategoryId(b.transformer);
+            if (cat === 'custom-props') continue;
 
+            tree.to(PluginBehavior.getCategoryId(b.transformer)).apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
+        }
         await this.runTask(this.state.behaviors.updateTree(tree, { doNotUpdateCurrent: true, doNotLogTiming: true }));
     }