Browse Source

mol-plugin: Use React context

David Sehnal 6 years ago
parent
commit
502a4b0f53

+ 35 - 0
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();
+    }
+}

+ 21 - 21
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 })
             })}>&lt;&lt;</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 })
             })}>&gt;&gt;</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 });
     // }
 

+ 28 - 27
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>;
     }

+ 11 - 11
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>;
     }

+ 11 - 17
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);
         });