diff --git a/src/cli/state-docs/pd-to-md.ts b/src/cli/state-docs/pd-to-md.ts index 615dcf0ac84313fc299b45dd22cc146ac4de2aa3..1e0d4ef4b5b6d05f602b010bdc029fe4385bcac8 100644 --- a/src/cli/state-docs/pd-to-md.ts +++ b/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)}`; diff --git a/src/mol-plugin-state/transforms/misc.ts b/src/mol-plugin-state/transforms/misc.ts index 1413196121c7b04ddf485c6845321149ff2326f2..209bbbd7c482fb2d08df2fd83817944517de64f6 100644 --- a/src/mol-plugin-state/transforms/misc.ts +++ b/src/mol-plugin-state/transforms/misc.ts @@ -30,4 +30,25 @@ const CreateGroup = PluginStateTransform.BuiltIn({ b.description = newParams.description; return StateTransformer.UpdateResult.Updated; } -}); \ No newline at end of file +}); + +// 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 }); +// } +// }); \ No newline at end of file diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 3afcb0ae0cd69bed3196f9dd0e8a5e213efb1224..a222967fc2394ae329ae046b015fc1909a3ebcd4 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/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 } diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index f6383c9e0050c5125692d2069bbd162b24920ec9..8596feaebb605d6b2271f261a3bb6c67510fceac 100644 --- a/src/mol-state/state.ts +++ b/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; diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index 57edb26d5add656ed3da51bb1cea988852f03ffa..f8630cb520e2de1686278415b0874774b7b5dfa4 100644 --- a/src/mol-util/param-definition.ts +++ b/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;