Browse Source

wip, StructureFocus controls and manager

Alexander Rose 5 years ago
parent
commit
a7189232dd

+ 33 - 16
src/mol-plugin-state/manager/structure/focus.ts

@@ -10,15 +10,20 @@ import { arrayRemoveAtInPlace } from '../../../mol-util/array';
 import { StructureElement, Bond, Structure } from '../../../mol-model/structure';
 import { Loci } from '../../../mol-model/loci';
 import { lociLabel } from '../../../mol-theme/label';
+import { PluginStateObject } from '../../objects';
 
-export type FocusEntry = { label: string, loci: StructureElement.Loci, category?: string }
+export type FocusEntry = {
+    label: string
+    loci: StructureElement.Loci
+    category?: string
+}
 
 interface StructureFocusManagerState {
-    current?: FocusEntry,
-    history: FocusEntry[],
+    current?: FocusEntry
+    history: FocusEntry[]
 }
 
-const HISTORY_CAPACITY = 12;
+const HISTORY_CAPACITY = 4;
 
 export class StructureFocusManager extends StatefulPluginComponent<StructureFocusManagerState> {
 
@@ -30,12 +35,14 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
     get current() { return this.state.current; }
     get history() { return this.state.history; }
 
+    /** Adds to history without `.category` */
     private tryAddHistory(entry: FocusEntry) {
-        if (StructureElement.Loci.isEmpty(entry.loci)) return;
+        const { label, loci } = entry
+        if (StructureElement.Loci.isEmpty(loci)) return;
 
         let idx = 0, existingEntry: FocusEntry | undefined = void 0;
         for (const e of this.state.history) {
-            if (StructureElement.Loci.areEqual(e.loci, entry.loci)) {
+            if (StructureElement.Loci.areEqual(e.loci, loci)) {
                 existingEntry = e;
                 break;
             }
@@ -45,12 +52,12 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
         if (existingEntry) {
             // move to top, use new
             arrayRemoveAtInPlace(this.state.history, idx);
-            this.state.history.unshift(entry);
+            this.state.history.unshift({ label, loci });
             this.events.historyUpdated.next();
             return;
         }
 
-        this.state.history.unshift(entry);
+        this.state.history.unshift({ label, loci });
         if (this.state.history.length > HISTORY_CAPACITY) this.state.history.pop();
 
         this.events.historyUpdated.next();
@@ -81,6 +88,7 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
             this.clear()
             return
         }
+        loci = StructureElement.Loci.remap(loci, loci.structure.root)
 
         this.set({ loci, label: lociLabel(loci, { reverse: true, hidePrefix: true, htmlStyling: false }) })
     }
@@ -92,13 +100,6 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
         }
     }
 
-    // this.subscribeObservable(this.plugin.events.state.object.removed, o => {
-    //     if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(lastLoci)) return;
-    //     if (lastLoci.structure === o.obj.data) {
-    //         lastLoci = EmptyLoci;
-    //     }
-    // });
-
     // this.subscribeObservable(this.plugin.events.state.object.updated, o => {
     //     if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(lastLoci)) return;
     //     if (lastLoci.structure === o.oldObj.data) {
@@ -109,7 +110,23 @@ export class StructureFocusManager extends StatefulPluginComponent<StructureFocu
     constructor(plugin: PluginContext) {
         super({ history: [] });
 
-        // plugin.state.data.events.object.removed.subscribe(e => this.onRemove(e.ref));
+        plugin.state.data.events.object.removed.subscribe(o => {
+            if (!PluginStateObject.Molecule.Structure.is(o.obj)) return
+
+            if (this.current?.loci.structure === o.obj.data) {
+                this.clear()
+            }
+
+            const keep: FocusEntry[] = []
+            for (const e of this.history) {
+                if (e.loci.structure === o.obj.data) keep.push(e)
+            }
+            if (keep.length !== this.history.length) {
+                this.history.length = 0
+                this.history.push(...keep)
+                this.events.historyUpdated.next()
+            }
+        });
         // plugin.state.data.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
     }
 }

+ 0 - 2
src/mol-plugin-ui/controls.tsx

@@ -21,7 +21,6 @@ import { Icon } from './controls/icons';
 import { StructureComponentControls } from './structure/components';
 import { StructureSourceControls } from './structure/source';
 import { VolumeStreamingControls } from './structure/volume';
-import { StructureFocusControls } from './structure/focus';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -286,7 +285,6 @@ export class DefaultStructureTools extends PluginUIComponent {
             <StructureSourceControls />
             <StructureSelectionControls />
             <StructureComponentControls />
-            <StructureFocusControls />
             <VolumeStreamingControls />
 
             <CustomStructureControls />

+ 4 - 4
src/mol-plugin-ui/controls/action-menu.tsx

@@ -60,10 +60,10 @@ export namespace ActionMenu {
         addOn?: (t: T) => JSX.Element | undefined
     }
 
-    export function createItems<T>(xs: ArrayLike<T>, params?: CreateItemsParams<T>) {
+    export function createItems<T>(xs: ArrayLike<T>, params?: CreateItemsParams<T>): Items[] {
         const { label, value, category, selected, icon, addOn } = params || { };
         let cats: Map<string, (ActionMenu.Item | ActionMenu.Header)[]> | undefined = void 0;
-        const items: (ActionMenu.Item | (ActionMenu.Item | ActionMenu.Header)[] | string)[] = [];
+        const items: (ActionMenu.Item | (ActionMenu.Item | ActionMenu.Header)[])[] = [];
         for (let i = 0; i < xs.length; i++) {
             const x = xs[i];
 
@@ -91,7 +91,7 @@ export namespace ActionMenu {
 
             cat!.push({ kind: 'item', label: l, value: v, icon: icon ? icon(x) : void 0, selected: selected ? selected(x) : void 0, addOn: ao });
         }
-        return items as ActionMenu.Items;
+        return items;
     }
 
     type Opt = ParamDefinition.Select<any>['options'][0];
@@ -204,7 +204,7 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
         const { header, hasCurrent } = this.state;
 
         return <div className='msp-control-group-header' style={{ marginTop: '1px' }}>
-            <button className='msp-btn msp-btn-block msp-form-control' onClick={this.toggleExpanded}>
+            <button className='msp-btn msp-btn-block msp-form-control msp-no-overflow' onClick={this.toggleExpanded}>
                 <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
                 {hasCurrent ? <b>{header?.label}</b> : header?.label}
             </button>

+ 103 - 71
src/mol-plugin-ui/structure/focus.tsx

@@ -5,105 +5,141 @@
  */
 
 import * as React from 'react';
-import { CollapsableState, CollapsableControls } from '../base';
+import { PluginUIComponent } from '../base';
 import { ToggleButton } from '../controls/common';
-import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
 import { ActionMenu } from '../controls/action-menu';
-import { stringToWords } from '../../mol-util/string';
-import { StructureElement, StructureProperties } from '../../mol-model/structure';
+import { StructureElement, StructureProperties, Structure } from '../../mol-model/structure';
 import { OrderedSet, SortedArray } from '../../mol-data/int';
 import { UnitIndex } from '../../mol-model/structure/structure/element/element';
 import { FocusEntry } from '../../mol-plugin-state/manager/structure/focus';
-import { Icon } from '../controls/icons';
+import { lociLabel } from '../../mol-theme/label';
 
-type FocusAction = 'presets' | 'history'
-
-interface StructureComponentControlState extends CollapsableState {
+interface StructureFocusControlsState {
     isBusy: boolean
-    action?: FocusAction
+    showAction: boolean
 }
 
-export class StructureFocusControls extends CollapsableControls<{}, StructureComponentControlState> {
-    protected defaultState(): StructureComponentControlState {
-        return {
-            header: 'Focus',
-            isCollapsed: false,
-            isBusy: false,
-        };
+function getFocusEntries(structure: Structure) {
+    const entityEntries = new Map<string, FocusEntry[]>()
+    const l = StructureElement.Location.create(structure)
+
+    for (const ug of structure.unitSymmetryGroups) {
+        l.unit = ug.units[0]
+        l.element = ug.elements[0]
+        const et = StructureProperties.entity.type(l)
+        if (et === 'non-polymer') {
+            for (const u of ug.units) {
+                l.unit = u
+                const idx = SortedArray.indexOf(u.elements, l.element) as UnitIndex
+                const loci = StructureElement.Loci.extendToWholeResidues(
+                    StructureElement.Loci(structure, [
+                        { unit: l.unit, indices: OrderedSet.ofSingleton(idx) }
+                    ])
+                )
+                let label = lociLabel(loci, { reverse: true, hidePrefix: true, htmlStyling: false })
+                if (ug.units.length > 1) {
+                    label += ` | ${u.conformation.operator.name}`
+                }
+                const name = StructureProperties.entity.pdbx_description(l).join(', ')
+                const item: FocusEntry = { label, category: name, loci }
+
+                if (entityEntries.has(name)) entityEntries.get(name)!.push(item)
+                else entityEntries.set(name, [item])
+            }
+        } else if (et === 'branched') {
+            // TODO split into residues
+        }
     }
 
+    const entries: FocusEntry[] = []
+    entityEntries.forEach((e, name) => {
+        if (e.length === 1) {
+            entries.push({ label: name, loci: e[0].loci })
+        } else {
+            entries.push(...e)
+        }
+    })
+
+    return entries
+}
+
+export class StructureFocusControls extends PluginUIComponent<{}, StructureFocusControlsState> {
+    state = { isBusy: false, showAction: false }
+
     componentDidMount() {
-        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
-            this.setState({
-                description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
-            });
-            // if setState is called on non-pure component, forceUpdate is reduntant
-            // this.forceUpdate();
+        this.subscribe(this.plugin.managers.structure.focus.events.changed, c => {
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.managers.structure.focus.events.historyUpdated, c => {
+            this.forceUpdate();
         });
+
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v, showAction: false })
+        })
+    }
+
+    get isDisabled() {
+        return this.state.isBusy || this.actionItems.length === 0
     }
 
-    get presetsItems() {
-        const items: FocusEntry[] = []
-        const l = StructureElement.Location.create()
+    get actionItems() {
+        const historyItems: ActionMenu.Items[] = []
+        const { history } = this.plugin.managers.structure.focus
+        if (history.length > 0) {
+            historyItems.push([
+                ActionMenu.Header('History'),
+                ...ActionMenu.createItems(history, {
+                    label: f => f.label,
+                    category: f => f.category
+                })
+            ])
+        }
+
+        const presetItems: ActionMenu.Items[] = []
         const { structures } = this.plugin.managers.structure.hierarchy.selection;
         for (const s of structures) {
             const d = s.cell.obj?.data
             if (d) {
-                l.structure = d
-                for (const ug of d.unitSymmetryGroups) {
-                    l.unit = ug.units[0]
-                    l.element = ug.elements[0]
-                    const et = StructureProperties.entity.type(l)
-                    if (et === 'non-polymer') {
-                        const idx = SortedArray.indexOf(ug.elements, l.element) as UnitIndex
-                        const loci = StructureElement.Loci(d, [{ unit: l.unit, indices: OrderedSet.ofSingleton(idx) }])
-                        items.push({
-                            label: StructureProperties.entity.pdbx_description(l).join(', '),
-                            loci: StructureElement.Loci.extendToWholeResidues(loci)
+                const entries = getFocusEntries(d)
+                if (entries.length > 0) {
+                    presetItems.push([
+                        ActionMenu.Header(d.label),
+                        ...ActionMenu.createItems(entries, {
+                            label: f => f.label,
+                            category: f => f.category
                         })
-                    }
+                    ])
                 }
             }
         }
+        if (presetItems.length === 1) {
+            const item = presetItems[0] as ActionMenu.Items[]
+            const header = item[0] as ActionMenu.Header
+            header.initiallyExpanded = true
+        }
 
-        return items
-    }
-
-    get historyItems() {
-        return this.plugin.managers.structure.focus.history
-    }
+        const items: ActionMenu.Items[] = []
+        if (historyItems.length > 0) items.push(...historyItems)
+        if (presetItems.length > 0) items.push(...presetItems)
 
-    get actionItems() {
-        let items: FocusEntry[]
-        switch (this.state.action) {
-            case 'presets': items = this.presetsItems; break
-            case 'history': items = this.historyItems; break
-            default: items = []
-        }
-        return ActionMenu.createItems(items, {
-            label: f => f.label,
-            category: f => f.category
-        })
+        return items
     }
 
     selectAction: ActionMenu.OnSelect = item => {
-        if (!item || !this.state.action) {
-            this.setState({ action: void 0 });
+        if (!item || !this.state.showAction) {
+            this.setState({ showAction: false });
             return;
         }
-        this.setState({ action: void 0 }, async () => {
+        this.setState({ showAction: false }, () => {
             const f = item.value as FocusEntry
             this.plugin.managers.structure.focus.set(f)
             this.plugin.managers.camera.focusLoci(f.loci, { durationMs: 0 })
         })
     }
 
-    private showAction(a: FocusAction) {
-        return () => this.setState({ action: this.state.action === a ? void 0 : a });
-    }
-
-    togglePresets = this.showAction('presets')
-    toggleHistory = this.showAction('history')
+    toggleAction = () => this.setState({ showAction: !this.state.showAction })
 
     focus = () => {
         const { current } = this.plugin.managers.structure.focus
@@ -119,22 +155,18 @@ export class StructureFocusControls extends CollapsableControls<{}, StructureCom
         this.plugin.managers.interactivity.lociHighlights.clearHighlights()
     }
 
-    renderControls() {
+    render() {
         const { current } = this.plugin.managers.structure.focus
         const label = current?.label || 'Nothing Focused'
 
         return <>
             <div className='msp-control-row msp-select-row'>
-                <ToggleButton icon='bookmarks' title='Preset' label='Preset' toggle={this.togglePresets} isSelected={this.state.action === 'presets'} disabled={this.state.isBusy} />
-                <ToggleButton icon='clock' title='History' label='History' toggle={this.toggleHistory} isSelected={this.state.action === 'history'} disabled={this.state.isBusy} />
-            </div>
-            {this.state.action && <ActionMenu header={stringToWords(this.state.action)} items={this.actionItems} onSelect={this.selectAction} />}
-            <div className='msp-control-row msp-row-text' style={{ marginTop: '6px' }}>
-                <button className='msp-btn msp-btn-block msp-no-overflow' onClick={this.focus} title='Click to Center Focused' disabled={!current} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights}>
-                    <Icon name='focus-on-visual' style={{ position: 'absolute', left: '5px' }} />
+                <button className='msp-btn msp-btn-block msp-no-overflow' onClick={this.focus} title='Click to Center Focused' onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}>
                     {label}
                 </button>
+                <ToggleButton icon='target' title='Focus Target' toggle={this.toggleAction} isSelected={this.state.showAction} disabled={this.isDisabled} style={{ flex: '0 0 40px' }} />
             </div>
+            {this.state.showAction && <ActionMenu items={this.actionItems} onSelect={this.selectAction} />}
         </>;
     }
 }

+ 5 - 0
src/mol-plugin-ui/structure/source.tsx

@@ -13,6 +13,7 @@ import { IconButton } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { StructureFocusControls } from './focus';
 
 interface StructureSourceControlState extends CollapsableState {
     isBusy: boolean,
@@ -258,6 +259,10 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
             {this.state.show === 'presets' && <ActionMenu items={presets} onSelect={this.applyPreset} />}
             {this.modelIndex}
             {this.structureType}
+
+            <div style={{ marginTop: '6px' }}>
+                <StructureFocusControls />
+            </div>
         </>;
     }
 }