diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e273690fff7ea52af83b4c44e750c6eeaa71c04f --- /dev/null +++ b/src/mol-plugin/ui/base.tsx @@ -0,0 +1,35 @@ +/** + * 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 { Observable, Subscription } from 'rxjs'; +import { PluginContext } from '../context'; + +export const PluginReactContext = React.createContext(void 0 as any as PluginContext); + +export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> { + static contextType = PluginReactContext; + readonly context: PluginContext; + + private subs: Subscription[] | undefined = void 0; + + protected subscribe<T>(obs: Observable<T>, action: (v: T) => void) { + if (typeof this.subs === 'undefined') this.subs = [] + this.subs.push(obs.subscribe(action)); + } + + componentWillUnmount() { + if (!this.subs) return; + for (const s of this.subs) s.unsubscribe(); + } + + protected init?(): void; + + constructor(props: P, context?: any) { + super(props, context); + if (this.init) this.init(); + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 044a418d7ced3a674c1e9d220357aa55d0201a4a..e6154488eeaab765b685a64425f0707fcdc472c0 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -5,29 +5,29 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; 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'; -export class Controls extends React.Component<{ plugin: PluginContext }, { id: string }> { +export class Controls extends PluginComponent<{ }, { }> { state = { id: '1grm' }; private _snap: any = void 0; private getSnapshot = () => { - this._snap = this.props.plugin.state.getSnapshot(); + this._snap = this.context.state.getSnapshot(); console.log(btoa(JSON.stringify(this._snap))); } private setSnapshot = () => { if (!this._snap) return; - this.props.plugin.state.setSnapshot(this._snap); + this.context.state.setSnapshot(this._snap); } render() { return <div> - <button onClick={() => this.props.plugin._test_centerView()}>Center View</button><br /> + <button onClick={() => this.context._test_centerView()}>Center View</button><br /> <hr /> <button onClick={this.getSnapshot}>Get Snapshot</button> <button onClick={this.setSnapshot}>Set Snapshot</button> @@ -36,27 +36,27 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s } -export class _test_TrajectoryControls extends React.Component<{ plugin: PluginContext }> { +export class _test_TrajectoryControls extends PluginComponent { render() { return <div> <b>Trajectory: </b> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { - state: this.props.plugin.state.data, + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.context, { + state: this.context.state.data, action: UpdateTrajectory.create({ action: 'advance', by: -1 }) })}><<</button> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { - state: this.props.plugin.state.data, + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.context, { + state: this.context.state.data, action: UpdateTrajectory.create({ action: 'reset' }) })}>Reset</button> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { - state: this.props.plugin.state.data, + <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.context, { + state: this.context.state.data, action: UpdateTrajectory.create({ action: 'advance', by: +1 }) })}>>></button><br /> </div> } } -export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, state: State, action: StateAction }, { params: any }> { +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; @@ -67,7 +67,7 @@ export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, if (!p || !p.default) return {}; const obj = this.getObj(); if (!obj.obj) return {}; - return p.default(obj.obj, this.props.plugin); + return p.default(obj.obj, this.context); } private getParamDef() { @@ -75,17 +75,17 @@ export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, if (!p || !p.controls) return {}; const obj = this.getObj(); if (!obj.obj) return {}; - return p.controls(obj.obj, this.props.plugin); + return p.controls(obj.obj, this.context); } private create() { console.log('Apply Action', this.state.params); - PluginCommands.State.ApplyAction.dispatch(this.props.plugin, { + PluginCommands.State.ApplyAction.dispatch(this.context, { state: this.props.state, action: this.props.action.create(this.state.params), ref: this.props.nodeRef }); - // this.props.plugin.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params); + // this.context.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params); } state = { params: this.getDefaultParams() } @@ -111,7 +111,7 @@ export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, } } -export class _test_UpdateTransform extends React.Component<{ plugin: PluginContext, state: State, nodeRef: Transform.Ref }, { params: any }> { +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)!; } @@ -130,16 +130,16 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte const src = this.getCell(cell.sourceRef); if (!src || !src.obj) return void 0; - return def.params.controls(src.obj, this.props.plugin); + return def.params.controls(src.obj, this.context); } private update() { console.log(this.props.nodeRef, this.state.params); - this.props.plugin.updateTransform(this.props.state, this.props.nodeRef, this.state.params); + this.context.updateTransform(this.props.state, this.props.nodeRef, this.state.params); } // componentDidMount() { - // const t = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!; + // const t = this.context.state.data.tree.nodes.get(this.props.nodeRef)!; // if (t) this.setState({ params: t.value.params }); // } diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index cbd04d53d5f587d416009fa8c976aed695fa2f6b..81947eab6b2a6eda3bb1b482afd177626f055bd5 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -9,43 +9,44 @@ import { PluginContext } from '../context'; import { StateTree } from './state-tree'; import { Viewport } from './viewport'; import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls } from './controls'; +import { PluginComponent, PluginReactContext } from './base'; -// TODO: base object with subscribe helpers, separate behavior list etc - -export class Plugin extends React.Component<{ plugin: PluginContext }, { }> { +export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { render() { - return <div style={{ position: 'absolute', width: '100%', height: '100%', fontFamily: 'monospace' }}> - <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll', padding: '10px' }}> - <StateTree plugin={this.props.plugin} state={this.props.plugin.state.data} /> - <h3>Behaviors</h3> - <StateTree plugin={this.props.plugin} state={this.props.plugin.state.behavior} /> - </div> - <div style={{ position: 'absolute', left: '350px', right: '300px', height: '100%' }}> - <Viewport plugin={this.props.plugin} /> - <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}> - <_test_TrajectoryControls {...this.props} /> + return <PluginReactContext.Provider value={this.props.plugin}> + <div style={{ position: 'absolute', width: '100%', height: '100%', fontFamily: 'monospace' }}> + <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll', padding: '10px' }}> + <StateTree state={this.props.plugin.state.data} /> + <h3>Behaviors</h3> + <StateTree state={this.props.plugin.state.behavior} /> + </div> + <div style={{ position: 'absolute', left: '350px', right: '300px', height: '100%' }}> + <Viewport /> + <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}> + <_test_TrajectoryControls /> + </div> + </div> + <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px' }}> + <_test_CurrentObject /> + <hr /> + <Controls /> </div> </div> - <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px' }}> - <_test_CurrentObject plugin={this.props.plugin} /> - <hr /> - <Controls plugin={this.props.plugin} /> - </div> - </div>; + </PluginReactContext.Provider>; } } -export class _test_CurrentObject extends React.Component<{ plugin: PluginContext }, { }> { - componentDidMount() { - // TODO: move to constructor? - this.props.plugin.behaviors.state.data.currentObject.subscribe(() => this.forceUpdate()); +export class _test_CurrentObject extends PluginComponent { + init() { + this.subscribe(this.context.behaviors.state.data.currentObject, () => this.forceUpdate()); } + render() { - const current = this.props.plugin.behaviors.state.data.currentObject.value; + const current = this.context.behaviors.state.data.currentObject.value; const ref = current.ref; // const n = this.props.plugin.state.data.tree.nodes.get(ref)!; - const obj = this.props.plugin.state.data.cells.get(ref)!; + const obj = this.context.state.data.cells.get(ref)!; const type = obj && obj.obj ? obj.obj.type : void 0; @@ -55,12 +56,12 @@ export class _test_CurrentObject extends React.Component<{ plugin: PluginContext return <div> <hr /> <h3>Update {ref}</h3> - <_test_UpdateTransform key={`${ref} update`} plugin={this.props.plugin} state={current.state} nodeRef={ref} /> + <_test_UpdateTransform key={`${ref} update`} state={current.state} nodeRef={ref} /> <hr /> <h3>Create</h3> { actions.map((act, i) => <_test_ApplyAction key={`${act.id} ${ref} ${i}`} - plugin={this.props.plugin} state={current.state} action={act} nodeRef={ref} />) + state={current.state} action={act} nodeRef={ref} />) } </div>; } diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 36fadc6ed4a474dd6d3d2faea408f1c9f98dbf36..12b98100f41a78b2fa3ef971457065d3a3d6a735 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -5,34 +5,34 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; import { PluginStateObject } from 'mol-plugin/state/objects'; import { State } from 'mol-state' import { PluginCommands } from 'mol-plugin/command'; +import { PluginComponent } from './base'; -export class StateTree extends React.Component<{ plugin: PluginContext, state: State }, { }> { - componentDidMount() { - // TODO: move to constructor? - this.props.state.events.changed.subscribe(() => this.forceUpdate()); +export class StateTree extends PluginComponent<{ state: State }, { }> { + init() { + this.subscribe(this.props.state.events.changed, () => this.forceUpdate()); } + render() { // const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!; const n = this.props.state.tree.root.ref; return <div> - <StateTreeNode plugin={this.props.plugin} state={this.props.state} nodeRef={n} key={n} /> + <StateTreeNode state={this.props.state} nodeRef={n} key={n} /> { /* n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />) */} </div>; } } -export class StateTreeNode extends React.Component<{ plugin: PluginContext, nodeRef: string, state: State }, { }> { +export class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { }> { render() { const n = this.props.state.tree.nodes.get(this.props.nodeRef)!; const cell = this.props.state.cells.get(this.props.nodeRef)!; const remove = <>[<a href='#' onClick={e => { e.preventDefault(); - PluginCommands.State.RemoveObject.dispatch(this.props.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.RemoveObject.dispatch(this.context, { state: this.props.state, ref: this.props.nodeRef }); }}>X</a>]</> let label: any; @@ -40,13 +40,13 @@ export class StateTreeNode extends React.Component<{ plugin: PluginContext, node const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name; label = <><b>{cell.status}</b> <a href='#' onClick={e => { e.preventDefault(); - PluginCommands.State.SetCurrentObject.dispatch(this.props.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.SetCurrentObject.dispatch(this.context, { state: this.props.state, ref: this.props.nodeRef }); }}>{name}</a>: <i>{cell.errorText}</i></>; } else { const obj = cell.obj as PluginStateObject.Any; label = <><a href='#' onClick={e => { e.preventDefault(); - PluginCommands.State.SetCurrentObject.dispatch(this.props.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.SetCurrentObject.dispatch(this.context, { state: this.props.state, ref: this.props.nodeRef }); }}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>; } @@ -56,7 +56,7 @@ export class StateTreeNode extends React.Component<{ plugin: PluginContext, node {remove} {label} {children.size === 0 ? void 0 - : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{children.map(c => <StateTreeNode plugin={this.props.plugin} state={this.props.state} nodeRef={c!} key={c} />)}</div> + : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999' }}>{children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}</div> } </div>; } diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 8bd0e59ccf19f0ae3c8ad393ff85504f126c7b11..c61d2bb8b282b4f640017fbddd779e874ea66820 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -6,21 +6,15 @@ */ import * as React from 'react'; -import { PluginContext } from '../context'; -// import { Loci, EmptyLoci, areLociEqual } from 'mol-model/loci'; -// import { MarkerAction } from 'mol-geo/geometry/marker-data'; import { ButtonsType } from 'mol-util/input/input-observer'; import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify'; - -interface ViewportProps { - plugin: PluginContext -} +import { PluginComponent } from './base'; interface ViewportState { noWebGl: boolean } -export class Viewport extends React.Component<ViewportProps, ViewportState> { +export class Viewport extends PluginComponent<{ }, ViewportState> { private container: HTMLDivElement | null = null; private canvas: HTMLCanvasElement | null = null; @@ -28,31 +22,31 @@ export class Viewport extends React.Component<ViewportProps, ViewportState> { noWebGl: false }; - handleResize() { - this.props.plugin.canvas3d.handleResize(); + private handleResize = () => { + this.context.canvas3d.handleResize(); } componentDidMount() { - if (!this.canvas || !this.container || !this.props.plugin.initViewer(this.canvas, this.container)) { + if (!this.canvas || !this.container || !this.context.initViewer(this.canvas, this.container)) { this.setState({ noWebGl: true }); } this.handleResize(); - const canvas3d = this.props.plugin.canvas3d; - canvas3d.input.resize.subscribe(() => this.handleResize()); + const canvas3d = this.context.canvas3d; + this.subscribe(canvas3d.input.resize, this.handleResize); - const idHelper = new Canvas3dIdentifyHelper(this.props.plugin, 15); + const idHelper = new Canvas3dIdentifyHelper(this.context, 15); - canvas3d.input.move.subscribe(({x, y, inside, buttons}) => { + this.subscribe(canvas3d.input.move, ({x, y, inside, buttons}) => { if (!inside || buttons) { return; } idHelper.move(x, y); }); - canvas3d.input.leave.subscribe(() => { + this.subscribe(canvas3d.input.leave, () => { idHelper.leave(); }); - canvas3d.input.click.subscribe(({x, y, buttons}) => { + this.subscribe(canvas3d.input.click, ({x, y, buttons}) => { if (buttons !== ButtonsType.Flag.Primary) return; idHelper.select(x, y); });