Skip to content
Snippets Groups Projects
Commit 458eae3b authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: transform controls

parent ebca12ee
Branches
Tags
No related merge requests found
......@@ -114,6 +114,7 @@ export class PluginContext {
this.state.data.actions
.add(CreateStructureFromPDBe)
.add(StateTransforms.Data.Download)
.add(StateTransforms.Data.ParseCif)
.add(StateTransforms.Model.CreateStructureAssembly)
.add(StateTransforms.Model.CreateStructure)
.add(StateTransforms.Model.CreateModelFromTrajectory)
......
......@@ -20,7 +20,8 @@ export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root
default: () => ({ id: '1grm' }),
controls: () => ({
id: PD.Text('PDB id', '', '1grm'),
})
}),
validate: p => !p.id || !p.id.trim() ? ['Enter id.'] : void 0
},
apply({ params, state }) {
const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;
......
......@@ -10,6 +10,7 @@ import { Task } from 'mol-task';
import CIF from 'mol-io/reader/cif'
import { PluginContext } from 'mol-plugin/context';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { Transformer } from 'mol-state';
export { Download }
namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
......@@ -27,8 +28,10 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
}),
controls: () => ({
url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''),
label: PD.Text('Label', '', ''),
isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false)
})
}),
validate: p => !p.url || !p.url.trim() ? ['Enter url.'] : void 0
},
apply({ params: p }, globalCtx: PluginContext) {
return Task.create('Download', async ctx => {
......@@ -38,6 +41,14 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url })
: new SO.Data.String(data as string, { label: p.label ? p.label : p.url });
});
},
update({ oldParams, newParams, b }) {
if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate;
if (oldParams.label !== newParams.label) {
(b.label as string) = newParams.label || newParams.url;
return Transformer.UpdateResult.Updated;
}
return Transformer.UpdateResult.Unchanged;
}
});
......
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react';
import { Transform, State, Transformer } from 'mol-state';
import { StateAction } from 'mol-state/action';
import { PluginCommands } from 'mol-plugin/command';
import { PluginComponent } from './base';
import { ParameterControls, createChangeSubject, ParamChanges } from './controls/parameters';
import { Subject } from 'rxjs';
import { shallowEqual } from 'mol-util/object';
export { ActionContol }
namespace ActionContol {
export interface Props {
nodeRef: Transform.Ref,
state: State,
action?: StateAction
}
}
class ActionContol extends PluginComponent<ActionContol.Props, { params: any, initialParams: any, error?: string, busy: boolean, canApply: boolean }> {
private changes: ParamChanges;
private busy: Subject<boolean>;
cell = this.props.state.cells.get(this.props.nodeRef)!;
parentCell = (this.cell.sourceRef && this.props.state.cells.get(this.cell.sourceRef)) || void 0;
action: StateAction | Transformer = !this.props.action ? this.cell.transform.transformer : this.props.action
isUpdate = !this.props.action
getDefaultParams() {
if (this.isUpdate) {
return this.cell.transform.params;
} else {
const p = this.action.definition.params;
if (!p || !p.default) return {};
const obj = this.cell;
if (!obj.obj) return {};
return p.default(obj.obj, this.plugin);
}
}
getParamDefinitions() {
if (this.isUpdate) {
const cell = this.cell;
const def = cell.transform.transformer.definition;
if (!cell.sourceRef || !def.params || !def.params.controls) return { };
const src = this.parentCell;
if (!src || !src.obj) return { };
return def.params.controls(src.obj, this.plugin);
} else {
const p = this.action.definition.params;
if (!p || !p.controls) return {};
const cell = this.cell;
if (!cell.obj) return {};
return p.controls(cell.obj, this.plugin);
}
}
defaultState() {
const params = this.getDefaultParams();
return { error: void 0, params, initialParams: params, busy: false, canApply: !this.isUpdate };
}
apply = async () => {
this.setState({ busy: true, initialParams: this.state.params, canApply: !this.isUpdate });
try {
if (Transformer.is(this.action)) {
await this.plugin.updateTransform(this.props.state, this.props.nodeRef, this.state.params);
} else {
await PluginCommands.State.ApplyAction.dispatch(this.plugin, {
state: this.props.state,
action: this.action.create(this.state.params),
ref: this.props.nodeRef
});
}
} finally {
this.busy.next(false);
}
}
validate(params: any) {
const def = this.isUpdate ? this.cell.transform.transformer.definition.params : this.action.definition.params;
if (!def || !def.validate) return;
const cell = this.cell;
const error = def.validate(params, this.isUpdate ? this.parentCell!.obj! : cell.obj!, this.plugin);
return error && error[0];
}
init() {
this.changes = createChangeSubject();
this.subscribe(this.changes, ({ name, value }) => {
const params = { ...this.state.params, [name]: value };
const canApply = this.isUpdate ? !shallowEqual(params, this.state.initialParams) : true;
this.setState({ params, error: this.validate(params), canApply });
});
this.busy = new Subject();
this.subscribe(this.busy, busy => this.setState({ busy }));
}
onEnter = () => {
if (this.state.error) return;
this.apply();
}
refresh = () => {
this.setState({ params: this.state.initialParams, canApply: !this.isUpdate });
}
state = this.defaultState()
render() {
console.log('render', this.props.nodeRef, this.action.id);
const cell = this.cell;
if (cell.status !== 'ok' || (this.isUpdate && cell.transform.ref === Transform.RootRef)) return null;
const action = this.action;
return <div>
<div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
<ParameterControls params={this.getParamDefinitions()} values={this.state.params} changes={this.changes} onEnter={this.onEnter} isEnabled={!this.state.busy} />
<div style={{ textAlign: 'right' }}>
<span style={{ color: 'red' }}>{this.state.error}</span>
<button onClick={this.apply} disabled={!this.state.canApply || !!this.state.error || this.state.busy}>{this.isUpdate ? 'Update' : 'Create'}</button>
<button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}></button>
</div>
</div>
}
}
\ No newline at end of file
......@@ -5,9 +5,6 @@
*/
import * as React from 'react';
import { Transform, State } from 'mol-state';
import { ParametersComponent } from 'mol-app/component/parameters';
import { StateAction } from 'mol-state/action';
import { PluginCommands } from 'mol-plugin/command';
import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
import { PluginComponent } from './base';
......@@ -20,7 +17,6 @@ export class Controls extends PluginComponent<{ }, { }> {
}
}
export class TrajectoryControls extends PluginComponent {
render() {
return <div>
......@@ -40,114 +36,3 @@ export class TrajectoryControls extends PluginComponent {
</div>
}
}
\ No newline at end of file
export class _test_ApplyAction extends PluginComponent<{ nodeRef: Transform.Ref, state: State, action: StateAction }, { params: any }> {
private getObj() {
const obj = this.props.state.cells.get(this.props.nodeRef)!;
return obj;
}
private getDefaultParams() {
const p = this.props.action.definition.params;
if (!p || !p.default) return {};
const obj = this.getObj();
if (!obj.obj) return {};
return p.default(obj.obj, this.plugin);
}
private getParamDef() {
const p = this.props.action.definition.params;
if (!p || !p.controls) return {};
const obj = this.getObj();
if (!obj.obj) return {};
return p.controls(obj.obj, this.plugin);
}
private create() {
console.log('Apply Action', this.state.params);
PluginCommands.State.ApplyAction.dispatch(this.plugin, {
state: this.props.state,
action: this.props.action.create(this.state.params),
ref: this.props.nodeRef
});
// this.context.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params);
}
state = { params: this.getDefaultParams() }
render() {
const obj = this.getObj();
if (obj.status !== 'ok') {
// TODO filter this elsewhere
return <div />;
}
const action = this.props.action;
return <div key={`${this.props.nodeRef} ${this.props.action.id}`}>
<div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
<ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={(k, v) => {
this.setState({ params: { ...this.state.params, [k]: v } });
}} />
<div style={{ textAlign: 'right' }}>
<button onClick={() => this.create()}>Create</button>
</div>
</div>
}
}
export class _test_UpdateTransform extends PluginComponent<{ state: State, nodeRef: Transform.Ref }, { params: any }> {
private getCell(ref?: string) {
return this.props.state.cells.get(ref || this.props.nodeRef)!;
}
private getDefParams() {
const cell = this.getCell();
if (!cell) return {};
return cell.transform.params;
}
private getParamDef() {
const cell = this.getCell();
const def = cell.transform.transformer.definition;
if (!cell.sourceRef || !def.params || !def.params.controls) return void 0;
const src = this.getCell(cell.sourceRef);
if (!src || !src.obj) return void 0;
return def.params.controls(src.obj, this.plugin);
}
private update() {
console.log(this.props.nodeRef, this.state.params);
this.plugin.updateTransform(this.props.state, this.props.nodeRef, this.state.params);
}
// componentDidMount() {
// const t = this.context.state.data.tree.nodes.get(this.props.nodeRef)!;
// if (t) this.setState({ params: t.value.params });
// }
state = { params: this.getDefParams() };
render() {
const cell = this.getCell();
const transform = cell.transform;
if (!transform || transform.ref === Transform.RootRef) {
return <div />;
}
const params = this.getParamDef();
if (!params) return <div />;
const tr = transform.transformer;
return <div key={`${this.props.nodeRef} ${tr.id}`} style={{ marginBottom: '10ox' }}>
<div style={{ borderBottom: '1px solid #999' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</h3></div>
<ParametersComponent params={params} values={this.state.params as any} onChange={(k, v) => {
this.setState({ params: { ...this.state.params, [k]: v } });
}} />
<button onClick={() => this.update()} style={{ width: '100%' }}>Update</button>
</div>
}
}
\ No newline at end of file
/**
* Copyright (c) 2018 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 * as React from 'react'
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { Subject } from 'rxjs';
export function createChangeSubject(): ParamChanges {
return new Subject<{ param: PD.Base<any>, name: string, value: any }>();
}
export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
params: P,
values: any,
changes: ParamChanges,
isEnabled?: boolean,
onEnter?: () => void
}
export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
render() {
const common = {
changes: this.props.changes,
isEnabled: this.props.isEnabled,
onEnter: this.props.onEnter,
}
const params = this.props.params;
const values = this.props.values;
return <div style={{ width: '100%' }}>
{Object.keys(params).map(key => <ParamWrapper control={controlFor(params[key])} param={params[key]} key={key} {...common} name={key} value={values[key]} />)}
</div>;
}
}
function controlFor(param: PD.Any): ValueControl {
switch (param.type) {
case 'boolean': return BoolControl;
case 'number': return NumberControl;
case 'range': return NumberControl;
case 'multi-select': throw new Error('nyi');
case 'color': throw new Error('nyi');
case 'select': return SelectControl;
case 'text': return TextControl;
}
throw new Error('not supporter');
}
type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, changes: ParamChanges, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
export type ParamChanges = Subject<{ param: PD.Base<any>, name: string, value: any }>
type ValueControlProps<P extends PD.Base<any> = PD.Base<any>> = { value: any, param: P, isEnabled?: boolean, onChange: (v: any) => void, onEnter?: () => void }
type ValueControl = React.ComponentClass<ValueControlProps<any>>
export class ParamWrapper extends React.PureComponent<ParamWrapperProps> {
onChange = (value: any) => {
this.props.changes.next({ param: this.props.param, name: this.props.name, value });
}
render() {
return <div>
<span title={this.props.param.description}>{this.props.param.label}</span>
<div>
<this.props.control value={this.props.value} param={this.props.param} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
</div>
</div>;
}
}
export class BoolControl extends React.PureComponent<ValueControlProps> {
onClick = () => {
this.props.onChange(!this.props.value);
}
render() {
return <button onClick={this.onClick} disabled={!this.props.isEnabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
}
}
export class NumberControl extends React.PureComponent<ValueControlProps<PD.Numeric>> {
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.props.onChange(+e.target.value);
}
render() {
return <input type='range'
value={this.props.value}
min={this.props.param.min}
max={this.props.param.max}
step={this.props.param.step}
onChange={this.onChange}
/>;
}
}
export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>> {
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value !== this.props.value) {
this.props.onChange(value);
}
}
onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!this.props.onEnter) return;
if ((e.keyCode === 13 || e.charCode === 13)) {
this.props.onEnter();
}
}
render() {
return <input type='text'
value={this.props.value || ''}
onChange={this.onChange}
onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
/>;
}
}
export class SelectControl extends React.PureComponent<ValueControlProps<PD.Select<any>>> {
onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
this.setState({ value: e.target.value });
this.props.onChange(e.target.value);
}
render() {
return <select value={this.props.value} onChange={this.onChange}>
{this.props.param.options.map(([value, label]) => <option key={label} value={value}>{label}</option>)}
</select>;
}
}
\ No newline at end of file
......@@ -8,16 +8,16 @@ import * as React from 'react';
import { PluginContext } from '../context';
import { StateTree } from './state-tree';
import { Viewport, ViewportControls } from './viewport';
import { Controls, _test_UpdateTransform, _test_ApplyAction, TrajectoryControls } from './controls';
import { Controls, TrajectoryControls } from './controls';
import { PluginComponent, PluginReactContext } from './base';
import { merge } from 'rxjs';
import { State } from 'mol-state';
import { CameraSnapshots } from './camera';
import { StateSnapshots } from './state';
import { List } from 'immutable';
import { LogEntry } from 'mol-util/log-entry';
import { formatTime } from 'mol-util';
import { BackgroundTaskProgress } from './task';
import { ActionContol } from './action';
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
render() {
......@@ -86,19 +86,24 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
export class CurrentObject extends PluginComponent {
componentDidMount() {
let current: State.ObjectEvent | undefined = void 0;
// let current: State.ObjectEvent | undefined = void 0;
this.subscribe(merge(this.plugin.behaviors.state.data.currentObject, this.plugin.behaviors.state.behavior.currentObject), o => {
current = o;
// current = o;
this.forceUpdate()
});
this.subscribe(this.plugin.events.state.data.object.updated, ({ ref, state }) => {
if (!current || current.ref !== ref && current.state !== state) return;
console.log('curr event', +new Date);
const current = this.plugin.behaviors.state.data.currentObject.value;
if (current.ref !== ref || current.state !== state) return;
console.log('curr event pass', +new Date);
this.forceUpdate();
});
}
render() {
console.log('curr', +new Date);
const current = this.plugin.behaviors.state.data.currentObject.value;
const ref = current.ref;
......@@ -113,11 +118,11 @@ export class CurrentObject extends PluginComponent {
return <div>
<hr />
<h3>Update {obj.obj ? obj.obj.label : ref}</h3>
<_test_UpdateTransform key={`${ref} update`} state={current.state} nodeRef={ref} />
<ActionContol key={`${ref} update`} state={current.state} nodeRef={ref} />
<hr />
<h3>Create</h3>
{
actions.map((act, i) => <_test_ApplyAction key={`${act.id} ${ref} ${i}`}
actions.map((act, i) => <ActionContol key={`${act.id}`}
state={current.state} action={act} nodeRef={ref} />)
}
</div>;
......
......@@ -72,7 +72,7 @@ export namespace Transformer {
/** Specify default control descriptors for the parameters */
controls?(a: A, globalCtx: unknown): ControlsFor<P>,
/** Check the parameters and return a list of errors if the are not valid. */
validate?(a: A, params: P, globalCtx: unknown): string[] | undefined,
validate?(params: P, a: A, globalCtx: unknown): string[] | undefined,
/** Optional custom parameter equality. Use deep structural equal by default. */
areEqual?(oldParams: P, newParams: P): boolean
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment