Browse Source

wip: layout state

David Sehnal 6 years ago
parent
commit
77d2f23ede

+ 4 - 0
src/mol-plugin/command.ts

@@ -9,6 +9,7 @@ import { PluginCommand } from './command/base';
 import { Transform, State } from 'mol-state';
 import { StateAction } from 'mol-state/action';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
+import { PluginLayoutStateProps } from './layout';
 
 export * from './command/base';
 
@@ -38,6 +39,9 @@ export const PluginCommands = {
             OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }),
         }
     },
+    Layout: {
+        Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true })
+    },
     Camera: {
         Reset: PluginCommand<{}>({ isImmediate: true }),
         SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }),

+ 42 - 0
src/mol-plugin/component.ts

@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { BehaviorSubject, Observable, Subject } from 'rxjs';
+import { PluginContext } from './context';
+import { shallowMergeArray } from 'mol-util/object';
+
+export class PluginComponent<State> {
+    private _state: BehaviorSubject<State>;
+    private _updated = new Subject();
+
+    updateState(...states: Partial<State>[]) {
+        const latest = this.latestState;
+        const s = shallowMergeArray(latest, states);
+        if (s !== latest) {
+            this._state.next(s);
+        }
+    }
+
+    get states() {
+        return <Observable<State>>this._state;
+    }
+
+    get latestState() {
+        return this._state.value;
+    }
+
+    get updated() {
+        return <Observable<{}>>this._updated;
+    }
+
+    triggerUpdate() {
+        this._updated.next({});
+    }
+
+    constructor(public context: PluginContext, initialState: State) {
+        this._state = new BehaviorSubject<State>(initialState);
+    }
+}

+ 4 - 0
src/mol-plugin/context.ts

@@ -27,6 +27,7 @@ import { ajaxGet } from 'mol-util/data-source';
 import { CustomPropertyRegistry } from './util/custom-prop-registry';
 import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
 import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
+import { PluginLayout } from './layout';
 
 export class PluginContext {
     private disposed = false;
@@ -70,6 +71,7 @@ export class PluginContext {
     };
 
     readonly canvas3d: Canvas3D;
+    readonly layout: PluginLayout = new PluginLayout(this);
 
     readonly lociLabels: LociLabelManager;
 
@@ -87,6 +89,8 @@ export class PluginContext {
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
+            this.layout.setRoot(container);
+            if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             this.canvas3d.animate();

+ 172 - 1
src/mol-plugin/layout.ts

@@ -4,4 +4,175 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-// TODO
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginComponent } from './component';
+import { PluginContext } from './context';
+import { PluginCommands } from './command';
+
+export const PluginLayoutStateParams = {
+    isExpanded: PD.Boolean(false),
+    showControls: PD.Boolean(true)
+}
+
+export type PluginLayoutStateProps = PD.Values<typeof PluginLayoutStateParams>
+
+interface RootState {
+    top: string | null,
+    bottom: string | null,
+    left: string | null,
+    right: string | null,
+
+    width: string | null,
+    height: string | null,
+    maxWidth: string | null,
+    maxHeight: string | null,
+    margin: string | null,
+    marginLeft: string | null,
+    marginRight: string | null,
+    marginTop: string | null,
+    marginBottom: string | null,
+
+    scrollTop: number,
+    scrollLeft: number,
+    position: string | null,
+    overflow: string | null,
+    viewports: HTMLElement[],
+    zindex: string | null
+}
+
+export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
+    private updateProps(state: Partial<PluginLayoutStateProps>) {
+        let prevExpanded = !!this.latestState.isExpanded;
+        this.updateState(state);
+        if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
+
+        this.triggerUpdate();
+    }
+
+    private root: HTMLElement;
+    private rootState: RootState | undefined = void 0;
+    private expandedViewport: HTMLMetaElement;
+
+    setRoot(root: HTMLElement) {
+        this.root = root;
+        if (this.latestState.isExpanded) this.handleExpand();
+    }
+
+    private getScrollElement() {
+        if ((document as any).scrollingElement) return (document as any).scrollingElement;
+        if (document.documentElement) return document.documentElement;
+        return document.body;
+    }
+
+    private handleExpand() {
+        try {
+            let body = document.getElementsByTagName('body')[0];
+            let head = document.getElementsByTagName('head')[0];
+
+            if (!body || !head) return;
+
+            if (this.latestState.isExpanded) {
+                let children = head.children;
+                let hasExp = false;
+                let viewports: HTMLElement[] = [];
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        hasExp = true;
+                    } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
+                        viewports.push(children[i] as any);
+                    }
+                }
+
+                for (let v of viewports) {
+                    head.removeChild(v);
+                }
+
+                if (!hasExp) head.appendChild(this.expandedViewport);
+
+
+                let s = body.style;
+
+                let doc = this.getScrollElement();
+                let scrollLeft = doc.scrollLeft;
+                let scrollTop = doc.scrollTop;
+
+                this.rootState = {
+                    top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex,
+                    width: s.width, height: s.height,
+                    maxWidth: s.maxWidth, maxHeight: s.maxHeight,
+                    margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
+                };
+
+                s.overflow = 'hidden';
+                s.position = 'fixed';
+                s.top = '0';
+                s.bottom = '0';
+                s.right = '0';
+                s.left = '0';
+
+                s.width = '100%';
+                s.height = '100%';
+                s.maxWidth = '100%';
+                s.maxHeight = '100%';
+                s.margin = '0';
+                s.marginLeft = '0';
+                s.marginRight = '0';
+                s.marginTop = '0';
+                s.marginBottom = '0';
+
+                this.root.style.zIndex = '2147483647';
+            } else {
+                let children = head.children;
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        head.removeChild(this.expandedViewport);
+                        break;
+                    }
+                }
+
+                if (this.rootState) {
+                    let s = body.style, t = this.rootState;
+                    for (let v of t.viewports) {
+                        head.appendChild(v);
+                    }
+                    s.top = t.top;
+                    s.bottom = t.bottom;
+                    s.left = t.left;
+                    s.right = t.right;
+
+                    s.width = t.width;
+                    s.height = t.height;
+                    s.maxWidth = t.maxWidth;
+                    s.maxHeight = t.maxHeight;
+                    s.margin = t.margin;
+                    s.marginLeft = t.marginLeft;
+                    s.marginRight = t.marginRight;
+                    s.marginTop = t.marginTop;
+                    s.marginBottom = t.marginBottom;
+
+                    s.position = t.position;
+                    s.overflow = t.overflow;
+                    let doc = this.getScrollElement();
+                    doc.scrollTop = t.scrollTop;
+                    doc.scrollLeft = t.scrollLeft;
+                    this.rootState = void 0;
+                    this.root.style.zIndex = t.zindex;
+                }
+            }
+        } catch (e) {
+            this.context.log.error('Layout change error, you might have to reload the page.');
+            console.log('Layout change error, you might have to reload the page.', e);
+        }
+    }
+
+    constructor(context: PluginContext) {
+        super(context, PD.getDefaultValues(PluginLayoutStateParams));
+
+        PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
+
+        // <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
+        this.expandedViewport = document.createElement('meta') as any;
+        this.expandedViewport.name = 'viewport';
+        this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
+    }
+}

+ 1 - 1
src/mol-plugin/skin/base/components/controls.scss

@@ -218,7 +218,7 @@
     //border-left-style: solid;
     //border-left-color: color-increase-contrast($default-background, 10%);
 
-    margin-bottom: 1px;
+    margin-bottom: 0px;
     padding-top: 1px;
 }
 

+ 3 - 1
src/mol-plugin/spec.ts

@@ -7,12 +7,14 @@
 import { StateAction } from 'mol-state/action';
 import { Transformer } from 'mol-state';
 import { StateTransformParameters } from './ui/state/common';
+import { PluginLayoutStateProps } from './layout';
 
 export { PluginSpec }
 
 interface PluginSpec {
     actions: PluginSpec.Action[],
-    behaviors: PluginSpec.Behavior[]
+    behaviors: PluginSpec.Behavior[],
+    initialLayout?: PluginLayoutStateProps
 }
 
 namespace PluginSpec {

+ 24 - 1
src/mol-plugin/ui/controls/common.tsx

@@ -4,12 +4,35 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import * as React from 'react';
+
+export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
+    state = { isExpanded: !!this.props.initialExpanded }
+
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
+    render() {
+        return <div className='msp-control-group-wrapper'>
+            <div className='msp-control-group-header'>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
+                    {this.props.header}
+                </button>
+            </div>
+            {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                {this.props.children}
+            </div>
+            }
+        </div>
+    }
+}
+
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,
 //     value: boolean,
 //     label: string,
 //     title?: string
-// }) => <div className='lm-control-row lm-toggle-button' title={props.title}> 
+// }) => <div className='lm-control-row lm-toggle-button' title={props.title}>
 //         <span>{props.label}</span>
 //         <div>
 //             <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}>

+ 18 - 12
src/mol-plugin/ui/viewport.tsx

@@ -13,6 +13,8 @@ import { PluginCommands } from 'mol-plugin/command';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { Canvas3DParams } from 'mol-canvas3d/canvas3d';
+import { PluginLayoutStateParams } from 'mol-plugin/layout';
+import { ControlGroup } from './controls/common';
 
 interface ViewportState {
     noWebGl: boolean
@@ -20,8 +22,7 @@ interface ViewportState {
 
 export class ViewportControls extends PluginComponent {
     state = {
-        isSettingsExpanded: false,
-        settings: PD.getDefaultValues(Canvas3DParams)
+        isSettingsExpanded: false
     }
 
     resetCamera = () => {
@@ -33,21 +34,21 @@ export class ViewportControls extends PluginComponent {
         e.currentTarget.blur();
     }
 
-    // hideSettings = () => {
-    //     this.setState({ isSettingsExpanded: false });
-    // }
-
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { [p.name]: p.value } });
     }
 
-    componentDidMount() {
-        if (this.plugin.canvas3d) {
-            this.setState({ settings: this.plugin.canvas3d.props });
-        }
+    setLayout = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
+    }
 
+    componentDidMount() {
         this.subscribe(this.plugin.events.canvad3d.settingsUpdated, e => {
-            this.setState({ settings: this.plugin.canvas3d.props });
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.layout.updated, () => {
+            this.forceUpdate();
         });
     }
 
@@ -60,7 +61,12 @@ export class ViewportControls extends PluginComponent {
             </div>
             {this.state.isSettingsExpanded &&
             <div className='msp-viewport-controls-scene-options'>
-                <ParameterControls params={Canvas3DParams} values={this.state.settings} onChange={this.setSettings} />
+                <ControlGroup header='Layout' initialExpanded={true}>
+                    <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} />
+                </ControlGroup>
+                <ControlGroup header='Viewport' initialExpanded={true}>
+                    <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
+                </ControlGroup>
             </div>}
         </div>
     }

+ 4 - 0
src/mol-util/object.ts

@@ -41,6 +41,10 @@ export function shallowEqual<T>(a: T, b: T) {
 }
 
 export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T {
+    return shallowMergeArray(source, rest);
+}
+
+export function shallowMergeArray<T>(source: T, rest: (Partial<T> | undefined)[]): T {
     // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
     let ret: any = source;