Browse Source

ParamDefinition.ValueRef

David Sehnal 4 years ago
parent
commit
67aedd4770

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

@@ -26,6 +26,7 @@ function paramInfo(param: PD.Any, offset: number): string {
         case 'file': return `JavaScript File Handle`;
         case 'file-list': return `JavaScript FileList Handle`;
         case 'select': return `One of ${oToS(param.options)}`;
+        case 'value-ref': return `Reference to a state object.`;
         case 'text': return 'String';
         case 'interval': return `Interval [min, max]`;
         case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`;

+ 22 - 1
src/mol-plugin-state/transforms/misc.ts

@@ -30,4 +30,25 @@ const CreateGroup = PluginStateTransform.BuiltIn({
         b.description = newParams.description;
         return StateTransformer.UpdateResult.Updated;
     }
-});
+});
+
+// export { ValueRefTest };
+// type ValueRefTest = typeof ValueRefTest
+// const ValueRefTest = PluginStateTransform.BuiltIn({
+//     name: 'value-ref-test',
+//     display: { name: 'ValueRef Test' },
+//     from: SO.Root,
+//     to: SO.Data.String,
+//     params: (_, ctx: PluginContext) => {
+//         const getOptions = () => ctx.state.data.selectQ(q => q.rootsOfType(SO.Molecule.Model)).map(m => [m.transform.ref, m.obj?.label || m.transform.ref] as [string, string]);
+//         return {
+//             ref: PD.ValueRef<SO.Molecule.Model>(getOptions, ctx.state.data.tryGetCellData)
+//         };
+//     }
+// })({
+//     apply({ params }) {
+//         const model = params.ref.getValue();
+//         console.log(model);
+//         return new SO.Data.String(`Model: ${model.label}`, { label: model.label });
+//     }
+// });

+ 51 - 0
src/mol-plugin-ui/controls/parameters.tsx

@@ -185,6 +185,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'file': return FileControl;
         case 'file-list': return FileListControl;
         case 'select': return SelectControl;
+        case 'value-ref': return ValueRefControl;
         case 'text': return TextControl;
         case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
             ? BoundedIntervalControl : IntervalControl;
@@ -486,6 +487,56 @@ export class SelectControl extends React.PureComponent<ParamProps<PD.Select<stri
     }
 }
 
+export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<any>>, { showHelp: boolean, showOptions: boolean }> {
+    state = { showHelp: false, showOptions: false };
+
+    onSelect: ActionMenu.OnSelect = item => {
+        if (!item || item.value === this.props.value) {
+            this.setState({ showOptions: false });
+        } else {
+            this.setState({ showOptions: false }, () => {
+                this.props.onChange({ param: this.props.param, name: this.props.name, value: { ref: item.value } });
+            });
+        }
+    }
+
+    toggle = () => this.setState({ showOptions: !this.state.showOptions });
+
+    items = memoizeLatest((param: PD.ValueRef) => ActionMenu.createItemsFromSelectOptions(param.getOptions()));
+
+    renderControl() {
+        const items = this.items(this.props.param);
+        const current = this.props.value.ref ? ActionMenu.findItem(items, this.props.value.ref) : void 0;
+        const label = current
+            ? current.label
+            : `[Ref] ${this.props.value.ref ?? ''}`;
+
+        return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }}
+            label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />;
+    }
+
+    renderAddOn() {
+        if (!this.state.showOptions) return null;
+
+        const items = this.items(this.props.param);
+        const current = ActionMenu.findItem(items, this.props.value.ref);
+
+        return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
+    }
+
+    toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
+
+    render() {
+        return renderSimple({
+            props: this.props,
+            state: this.state,
+            control: this.renderControl(),
+            toggleHelp: this.toggleHelp,
+            addOn: this.renderAddOn()
+        });
+    }
+}
+
 export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 

+ 7 - 1
src/mol-state/state.ts

@@ -70,6 +70,12 @@ class State {
     readonly cells: State.Cells = new Map();
     private spine = new StateTreeSpine.Impl(this.cells);
 
+    tryGetCellData = <T extends StateObject>(ref: StateTransform.Ref) => {
+        const ret = this.cells.get(ref)?.obj?.data;
+        if (!ref) throw new Error(`Cell '${ref}' data undefined.`);
+        return ret as T;
+    }
+
     private historyCapacity = 5;
     private history: [StateTree, string][] = [];
 
@@ -835,6 +841,7 @@ function resolveParams(ctx: UpdateContext, transform: StateTransform, src: State
     (transform.params as any) = transform.params
         ? assignIfUndefined(transform.params, defaultValues)
         : defaultValues;
+    ParamDefinition.resolveValueRefs(definition, transform.params);
     return { definition, values: transform.params };
 }
 
@@ -876,7 +883,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
         const newParams = params.values;
         current.params = params;
 
-
         const updateKind = !!current.obj && current.obj !== StateObject.Null
             ? await updateObject(ctx, current, transform.transformer, parent, current.obj!, oldParams, newParams)
             : StateTransformer.UpdateResult.Recreate;

+ 69 - 1
src/mol-util/param-definition.ts

@@ -277,6 +277,21 @@ export namespace ParamDefinition {
     }
     function _defaultObjectListCtor(this: ObjectList) { return getDefaultValues(this.element) as any; }
 
+
+    function unsetGetValue() {
+        throw new Error('getValue not set. Fix runtime.');
+    }
+
+    // getValue needs to be assigned by a runtime because it might not be serializable
+    export interface ValueRef<T = any> extends Base<{ ref: string, getValue: () => T }> {
+        type: 'value-ref',
+        resolveRef: (ref: string) => T,
+        getOptions: () => Select<string>['options'],
+    }
+    export function ValueRef<T>(getOptions: ValueRef['getOptions'], resolveRef: ValueRef<T>['resolveRef'], info?: Info) {
+        return setInfo<ValueRef<T>>({ type: 'value-ref', defaultValue: { ref: '', getValue: unsetGetValue as any }, getOptions, resolveRef }, info);
+    }
+
     export interface Converted<T, C> extends Base<T> {
         type: 'converted',
         converted: Any,
@@ -310,7 +325,7 @@ export namespace ParamDefinition {
 
     export type Any =
         | Value<any> | Select<any> | MultiSelect<any> | BooleanParam | Text | Color | Vec3 | Mat4 | Numeric | FileParam | UrlParam | FileListParam | Interval | LineGraph
-        | ColorList | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | Script | ObjectList
+        | ColorList | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | Script | ObjectList | ValueRef
 
     export type Params = { [k: string]: Any }
     export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] }
@@ -337,6 +352,59 @@ export namespace ParamDefinition {
         return d as Values<T>;
     }
 
+    function _resolveRef(resolve: (ref: string) => any, ref: string) {
+        return () => resolve(ref);
+    }
+
+    function resolveRefValue(p: Any, value: any) {
+        if (!value) return;
+
+        if (p.type === 'value-ref') {
+            const v = value as ValueRef['defaultValue'];
+            if (!v.ref) v.getValue = () => { throw new Error('Unset ref in ValueRef value.'); };
+            else v.getValue = _resolveRef(p.resolveRef, v.ref);
+        } else if (p.type === 'group') {
+            resolveValueRefs(p.params, value);
+        } else if (p.type === 'mapped') {
+            const v = value as NamedParams;
+            const param = p.map(v.name);
+            resolveRefValue(param, v.params);
+        } else if (p.type === 'object-list') {
+            if (!hasValueRef(p.element)) return;
+            for (const e of value) {
+                resolveValueRefs(p.element, e);
+            }
+        }
+    }
+
+    function hasParamValueRef(p: Any) {
+        if (p.type === 'value-ref') {
+            return true;
+        } else if (p.type === 'group') {
+            if (hasValueRef(p.params)) return true;
+        } else if (p.type === 'mapped') {
+            for (const [o] of p.select.options) {
+                if (hasParamValueRef(p.map(o))) return true;
+            }
+        } else if (p.type === 'object-list') {
+            return hasValueRef(p.element);
+        }
+        return false;
+    }
+
+    function hasValueRef(params: Params) {
+        for (const n of Object.keys(params)) {
+            if (hasParamValueRef(params[n])) return true;
+        }
+        return false;
+    }
+
+    export function resolveValueRefs(params: Params, values: any) {
+        for (const n of Object.keys(params)) {
+            resolveRefValue(params[n], values?.[n]);
+        }
+    }
+
     export function setDefaultValues<T extends Params>(params: T, defaultValues: Values<T>) {
         for (const k of Object.keys(params)) {
             if (params[k].isOptional) continue;