Browse Source

selection history updates
updated measurements ui
added focusLoci/Sphere
Loci stats label fixes

David Sehnal 5 years ago
parent
commit
034c28e487

+ 29 - 4
src/mol-plugin-state/manager/interactivity.ts

@@ -16,6 +16,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { StructureSelectionManager } from './structure/selection';
 import { PluginComponent } from '../component';
 import { shallowEqual } from '../../mol-util/object';
+import { Sphere3D } from '../../mol-math/geometry';
 
 export { InteractivityManager }
 
@@ -23,6 +24,15 @@ interface InteractivityManagerState {
     props: PD.ValuesFor<InteractivityManager.Params>
 }
 
+// TODO: make this customizable somewhere?
+const DefaultInteractivityFocusOptions = {
+    minRadius: 6,
+    extraRadius: 6,
+    durationMs: 250,
+}
+
+export type InteractivityFocusLociOptions = typeof DefaultInteractivityFocusOptions
+
 class InteractivityManager extends PluginComponent<InteractivityManagerState> {
     readonly lociSelects: InteractivityManager.LociSelectManager;
     readonly lociHighlights: InteractivityManager.LociHighlightManager;
@@ -46,11 +56,26 @@ class InteractivityManager extends PluginComponent<InteractivityManagerState> {
         this.events.propsUpdated.next();
     }
 
-    constructor(readonly ctx: PluginContext, props: Partial<InteractivityManager.Props> = {}) {
+    focusLoci(loci: StructureElement.Loci, options?: InteractivityFocusLociOptions) {
+        const { extraRadius, minRadius, durationMs } = { ...DefaultInteractivityFocusOptions, ...options };
+        const { sphere } = StructureElement.Loci.getBoundary(loci);
+        const radius = Math.max(sphere.radius + extraRadius, minRadius);
+        this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
+    }
+
+    focusSphere(sphere: Sphere3D, options?: InteractivityFocusLociOptions) {
+        if (sphere) {
+            const { extraRadius, minRadius, durationMs } = { ...DefaultInteractivityFocusOptions, ...options };
+            const radius = Math.max(sphere.radius + extraRadius, minRadius);
+            this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
+        }
+    }
+
+    constructor(readonly plugin: PluginContext, props: Partial<InteractivityManager.Props> = {}) {
         super({ props: { ...PD.getDefaultValues(InteractivityManager.Params), ...props } });
 
-        this.lociSelects = new InteractivityManager.LociSelectManager(ctx, this._props);
-        this.lociHighlights = new InteractivityManager.LociHighlightManager(ctx, this._props);
+        this.lociSelects = new InteractivityManager.LociSelectManager(plugin, this._props);
+        this.lociHighlights = new InteractivityManager.LociHighlightManager(plugin, this._props);
     }
 }
 
@@ -127,7 +152,7 @@ namespace InteractivityManager {
             this.prev.push(loci)
         }
 
-        clearHighlights() {
+        clearHighlights = () => {
             for (const p of this.prev) {
                 this.mark(p, MarkerAction.RemoveHighlight);
             }

+ 64 - 33
src/mol-plugin-state/manager/structure/selection.ts

@@ -20,10 +20,11 @@ import { arrayRemoveAtInPlace } from '../../../mol-util/array';
 import { PluginComponent } from '../../component';
 import { StructureSelectionQuery } from '../../helpers/structure-selection-query';
 import { PluginStateObject } from '../../objects';
+import { UUID } from '../../../mol-util';
 
 interface StructureSelectionManagerState {
     entries: Map<string, SelectionEntry>,
-    history: HistoryEntry[],
+    additionsHistory: StructureSelectionHistoryEntry[],
     stats?: SelectionStats
 }
 
@@ -34,13 +35,14 @@ export type StructureSelectionModifier = 'add' | 'remove' | 'intersect' | 'set'
 
 export class StructureSelectionManager extends PluginComponent<StructureSelectionManagerState> {
     readonly events = {
-        changed: this.ev<undefined>()
+        changed: this.ev<undefined>(),
+        additionsHistoryUpdated: this.ev<undefined>()
     }
 
     private referenceLoci: Loci | undefined
 
     get entries() { return this.state.entries; }
-    get history() { return this.state.history; }
+    get additionsHistory() { return this.state.additionsHistory; }
     get stats() {
         if (this.state.stats) return this.state.stats;
         this.state.stats = this.calcStats();
@@ -89,7 +91,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
 
         const sel = entry.selection;
         entry.selection = StructureElement.Loci.union(entry.selection, loci);
-        this.addHistory(loci);
+        this.tryAddHistory(loci);
         this.referenceLoci = loci
         return !StructureElement.Loci.areEqual(sel, entry.selection);
     }
@@ -102,7 +104,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
 
         const sel = entry.selection;
         entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
-        this.removeHistory(loci);
+        // this.addHistory(loci);
         this.referenceLoci = loci
         return !StructureElement.Loci.areEqual(sel, entry.selection);
     }
@@ -115,7 +117,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
 
         const sel = entry.selection;
         entry.selection = StructureElement.Loci.intersect(entry.selection, loci);
-        this.addHistory(loci);
+        // this.addHistory(loci);
         this.referenceLoci = loci
         return !StructureElement.Loci.areEqual(sel, entry.selection);
     }
@@ -128,16 +130,41 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
 
         const sel = entry.selection;
         entry.selection = loci;
-        this.addHistory(loci);
+        this.tryAddHistory(loci);
         this.referenceLoci = undefined;
         return !StructureElement.Loci.areEqual(sel, entry.selection);
     }
 
-    private addHistory(loci: StructureElement.Loci) {
+    modifyHistory(entry: StructureSelectionHistoryEntry, action: 'remove' | 'up' | 'down', modulus?: number) {
+        const idx = this.additionsHistory.indexOf(entry);
+        if (idx < 0) return;
+
+        let swapWith: number | undefined = void 0;
+
+        switch (action) {
+            case 'remove': arrayRemoveAtInPlace(this.additionsHistory, idx); break;
+            case 'up': swapWith = idx - 1; break;
+            case 'down': swapWith = idx + 1; break;
+        }
+
+        if (swapWith !== void 0) {
+            const mod = modulus ? Math.min(this.additionsHistory.length, modulus) : this.additionsHistory.length;
+            swapWith = swapWith % mod;
+            if (swapWith < 0) swapWith += mod;
+
+            const t = this.additionsHistory[idx];
+            this.additionsHistory[idx] = this.additionsHistory[swapWith];
+            this.additionsHistory[swapWith] = t;
+        }
+
+        this.events.additionsHistoryUpdated.next();
+    }
+
+    private tryAddHistory(loci: StructureElement.Loci) {
         if (Loci.isEmpty(loci)) return;
 
-        let idx = 0, entry: HistoryEntry | undefined = void 0;
-        for (const l of this.history) {
+        let idx = 0, entry: StructureSelectionHistoryEntry | undefined = void 0;
+        for (const l of this.additionsHistory) {
             if (Loci.areEqual(l.loci, loci)) {
                 entry = l;
                 break;
@@ -146,40 +173,43 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
         }
 
         if (entry) {
-            arrayRemoveAtInPlace(this.history, idx);
-            this.history.unshift(entry);
+            arrayRemoveAtInPlace(this.additionsHistory, idx);
+            this.additionsHistory.unshift(entry);
+            this.events.additionsHistoryUpdated.next();
             return;
         }
 
         const stats = StructureElement.Stats.ofLoci(loci);
-        const label = structureElementStatsLabel(stats)
+        const label = structureElementStatsLabel(stats, { reverse: true });
 
-        this.history.unshift({ loci, label });
-        if (this.history.length > HISTORY_CAPACITY) this.history.pop();
+        this.additionsHistory.unshift({ id: UUID.create22(), loci, label });
+        if (this.additionsHistory.length > HISTORY_CAPACITY) this.additionsHistory.pop();
+
+        this.events.additionsHistoryUpdated.next();
     }
 
-    private removeHistory(loci: Loci) {
-        if (Loci.isEmpty(loci)) return;
+    // private removeHistory(loci: Loci) {
+    //     if (Loci.isEmpty(loci)) return;
 
-        let idx = 0, found = false;
-        for (const l of this.history) {
-            if (Loci.areEqual(l.loci, loci)) {
-                found = true;
-                break;
-            }
-            idx++;
-        }
+    //     let idx = 0, found = false;
+    //     for (const l of this.history) {
+    //         if (Loci.areEqual(l.loci, loci)) {
+    //             found = true;
+    //             break;
+    //         }
+    //         idx++;
+    //     }
 
-        if (found) {
-            arrayRemoveAtInPlace(this.history, idx);
-        }
-    }
+    //     if (found) {
+    //         arrayRemoveAtInPlace(this.history, idx);
+    //     }
+    // }
 
     private onRemove(ref: string) {
         if (this.entries.has(ref)) {
             this.entries.delete(ref);
             // TODO: property update the latest loci
-            this.state.history = [];
+            this.state.additionsHistory = [];
             this.referenceLoci = undefined
         }
     }
@@ -191,7 +221,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
             if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
 
             // TODO: property update the latest loci & reference loci
-            this.state.history = [];
+            this.state.additionsHistory = [];
             this.referenceLoci = undefined
 
             // remap the old selection to be related to the new object if possible.
@@ -390,7 +420,7 @@ export class StructureSelectionManager extends PluginComponent<StructureSelectio
     }
 
     constructor(private plugin: PluginContext) {
-        super({ entries: new Map(), history: [], stats: SelectionStats() });
+        super({ entries: new Map(), additionsHistory: [], stats: SelectionStats() });
 
         plugin.state.dataState.events.object.removed.subscribe(e => this.onRemove(e.ref));
         plugin.state.dataState.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
@@ -430,7 +460,8 @@ class SelectionEntry {
     }
 }
 
-interface HistoryEntry {
+export interface StructureSelectionHistoryEntry {
+    id: UUID,
     loci: StructureElement.Loci,
     label: string
 }

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

@@ -4,7 +4,8 @@
     border: none;
     -moz-box-sizing: border-box;
     box-sizing: border-box;
-
+    overflow: hidden;
+    text-overflow: ellipsis;
 
     &[disabled] {
         background: $default-background;

+ 2 - 12
src/mol-plugin-ui/structure/components.tsx

@@ -22,12 +22,6 @@ interface StructureComponentControlState extends CollapsableState {
     isDisabled: boolean
 }
 
-const MeasurementFocusOptions = {
-    minRadius: 6,
-    extraRadius: 6,
-    durationMs: 250,
-}
-
 export class StructureComponentControls extends CollapsableControls<{}, StructureComponentControlState> {
     protected defaultState(): StructureComponentControlState {
         return { header: 'Representation', isCollapsed: false, isDisabled: false };
@@ -118,7 +112,7 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
             <div className='msp-control-row msp-select-row'>
                 <ToggleButton icon='bookmarks' label='Preset' toggle={this.togglePreset} isSelected={this.state.action === 'preset'} disabled={this.isDisabled} />
                 <ToggleButton icon='plus' label='Add' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
-                <ToggleButton icon='cog' label='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.isDisabled} />
+                <ToggleButton icon='cog' label='' title='Options' style={{ flex: '0 0 40px' }} toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.isDisabled} />
                 <IconButton customClass='msp-flex-item' style={{ flex: '0 0 40px' }} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} icon='ccw' title='Some mistakes of the past can be undone.' />
             </div>
             {this.state.action === 'preset' && this.presetControls}
@@ -309,11 +303,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
 
     focus = () => {
         const sphere = this.pivot.cell.obj?.data.boundary.sphere;
-        if (sphere) {
-            const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions;
-            const radius = Math.max(sphere.radius + extraRadius, minRadius);
-            PluginCommands.Camera.Focus(this.plugin, { center: sphere.center, radius, durationMs });
-        }
+        if (sphere) this.plugin.managers.interactivity.focusSphere(sphere);
     }
 
     render() {

+ 70 - 40
src/mol-plugin-ui/structure/measurements.tsx

@@ -5,27 +5,22 @@
  */
 
 import * as React from 'react';
-import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
-import { lociLabel, dihedralLabel, angleLabel, distanceLabel } from '../../mol-theme/label';
 import { Loci } from '../../mol-model/loci';
-import { FiniteArray } from '../../mol-util/type-helpers';
-import { State } from '../../mol-state';
+import { StructureElement } from '../../mol-model/structure';
+import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
+import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
-import { IconButton, ExpandGroup, ToggleButton } from '../controls/common';
 import { PluginCommands } from '../../mol-plugin/commands';
-import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
-import { ParameterControls } from '../controls/parameters';
+import { State } from '../../mol-state';
+import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label';
+import { FiniteArray } from '../../mol-util/type-helpers';
+import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
+import { ExpandGroup, IconButton, ToggleButton } from '../controls/common';
 import { Icon } from '../controls/icons';
+import { ParameterControls } from '../controls/parameters';
 
 // TODO details, options (e.g. change text for labels)
-// TODO better updates on state changes
-
-const MeasurementFocusOptions = {
-    minRadius: 8,
-    extraRadius: 4,
-    durationMs: 250,
-}
 
 interface StructureMeasurementsControlsState extends CollapsableState {
 }
@@ -78,7 +73,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
     state = { isBusy: false, action: void 0 as 'add' | 'options' | undefined }
 
     componentDidMount() {
-        this.subscribe(this.selection.events.changed, () => {
+        this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
             this.forceUpdate();
         });
 
@@ -92,40 +87,39 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
     }
 
     measureDistance = () => {
-        const loci = this.plugin.managers.structure.selection.history;
+        const loci = this.plugin.managers.structure.selection.additionsHistory;
         this.plugin.managers.structure.measurement.addDistance(loci[0].loci, loci[1].loci);
     }
 
     measureAngle = () => {
-        const loci = this.plugin.managers.structure.selection.history;
+        const loci = this.plugin.managers.structure.selection.additionsHistory;
         this.plugin.managers.structure.measurement.addAngle(loci[0].loci, loci[1].loci, loci[2].loci);
     }
 
     measureDihedral = () => {
-        const loci = this.plugin.managers.structure.selection.history;
+        const loci = this.plugin.managers.structure.selection.additionsHistory;
         this.plugin.managers.structure.measurement.addDihedral(loci[0].loci, loci[1].loci, loci[2].loci, loci[3].loci);
     }
 
     addLabel = () => {
-        const loci = this.plugin.managers.structure.selection.history;
+        const loci = this.plugin.managers.structure.selection.additionsHistory;
         this.plugin.managers.structure.measurement.addLabel(loci[0].loci);
     }
 
     addOrientation = () => {
         // TODO: this should be possible to add for the whole selection
-        const loci = this.plugin.managers.structure.selection.history;
+        const loci = this.plugin.managers.structure.selection.additionsHistory;
         this.plugin.managers.structure.measurement.addOrientation(loci[0].loci);
     }
 
-
     get actions(): ActionMenu.Items {
-        const history = this.selection.history;
+        const history = this.selection.additionsHistory;
         const ret: ActionMenu.Item[] = [
-            { label: `Label ${history.length === 0 ? '- 1 entry required' : ''}`, value: this.addLabel, disabled: history.length === 0 },
-            { label: `Orientation ${history.length === 0 ? '- 1 entry required' : ''}`, value: this.addOrientation, disabled: history.length === 0 },
-            { label: `Distance ${history.length < 2 ? '- 2 entries required' : ''}`, value: this.measureDistance, disabled: history.length < 2 },
-            { label: `Angle ${history.length < 3 ? '- 3 entries required' : ''}`, value: this.measureAngle, disabled: history.length < 3 },
-            { label: `Dihedral ${history.length < 4 ? '- 4 entries required' : ''}`, value: this.measureDihedral, disabled: history.length < 4 },
+            { label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 },
+            { label: `Orientation ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addOrientation, disabled: history.length === 0 },
+            { label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 },
+            { label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 },
+            { label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 },
         ];
         return ret;
     }
@@ -139,18 +133,56 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
     toggleAdd = () => this.setState({ action: this.state.action === 'add' ? void 0 : 'add' });
     toggleOptions = () => this.setState({ action: this.state.action === 'options' ? void 0 : 'options'  });
 
+    highlight(loci: StructureElement.Loci) {
+        this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci }, false);
+    }
+
+    moveHistory(e: StructureSelectionHistoryEntry, direction: 'up' | 'down') {
+        this.plugin.managers.structure.selection.modifyHistory(e, direction, 4);
+    }
+
+    focusLoci(loci: StructureElement.Loci) {
+        this.plugin.managers.interactivity.focusLoci(loci);
+    }
+
+    historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
+        const history = this.plugin.managers.structure.selection.additionsHistory;
+        return <div className='msp-btn-row-group' key={e.id}>
+            <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={this.plugin.managers.interactivity.lociHighlights.clearHighlights}>
+                {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
+            </button>
+            {history.length > 1 && <IconButton small={true} customClass='msp-form-control' onClick={() => this.moveHistory(e, 'up')} icon='up-thin' style={{ flex: '0 0 20px', maxWidth: '20px', padding: 0 }} title={'Move up'} />}
+            {history.length > 1 && <IconButton small={true} customClass='msp-form-control' onClick={() => this.moveHistory(e, 'down')} icon='down-thin' style={{ flex: '0 0 20px', maxWidth: '20px', padding: 0 }} title={'Move down'} />}
+            <IconButton small={true} customClass='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} icon='remove' style={{ flex: '0 0 32px' }} title={'Remove'} />
+        </div>;
+    }
+
+    add() {
+        const history = this.plugin.managers.structure.selection.additionsHistory;
+
+        const entries: JSX.Element[] = [];
+        for (let i = 0, _i = Math.min(history.length, 4); i < _i; i++) {
+            entries.push(this.historyEntry(history[i], i + 1));
+        }
+
+        return <>
+            <ActionMenu items={this.actions} onSelect={this.selectAction} />
+            {entries.length > 0 && <div className='msp-control-offset'>
+                {entries}
+            </div>}
+            {entries.length === 0 && <div className='msp-control-offset msp-help-text'>
+                <div className='msp-help-description'><Icon name='help-circle' />Add one or more selections</div>
+            </div>}
+        </>
+    }
+
     render() {
         return <>
             <div className='msp-control-row msp-select-row'>
                 <ToggleButton icon='plus' label='Add' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.state.isBusy} />
-                <ToggleButton icon='cog' label='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} />
+                <ToggleButton icon='cog' label='' title='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isBusy} style={{ flex: '0 0 40px' }} />
             </div>
-            {this.state.action === 'add' && <>
-                <ActionMenu items={this.actions} onSelect={this.selectAction} />
-                <div className='msp-control-offset msp-help-text'>
-                    <div className='msp-help-description'><Icon name='help-circle' />Latest entries from Selection History</div>
-                </div>
-            </>}
+            {this.state.action === 'add' && this.add()}
             {this.state.action === 'options' && <MeasurementsOptions />}
         </>
     }
@@ -226,9 +258,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
 
         const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections.data))
         if (sphere) {
-            const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions;
-            const radius = Math.max(sphere.radius + extraRadius, minRadius);
-            PluginCommands.Camera.Focus(this.plugin, { center: sphere.center, radius, durationMs });
+            this.plugin.managers.interactivity.focusSphere(sphere);
         }
     }
 
@@ -249,11 +279,11 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         if (!obj) return null;
 
         return <div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
-            <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus}>
+            <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus} style={{ width: 'auto' }}>
                 <span dangerouslySetInnerHTML={{ __html: this.label }} />
             </button>
-            <IconButton small={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ width: '52px' }} title='Delete' />
-            <IconButton small={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
+            <IconButton small={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ flex: '0 0 32px' }} title='Delete' />
+            <IconButton small={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ flex: '0 0 32px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
         </div>
     }
 }

+ 5 - 41
src/mol-plugin-ui/structure/selection.tsx

@@ -6,7 +6,6 @@
  */
 
 import * as React from 'react';
-import { StructureElement } from '../../mol-model/structure';
 import { StructureSelectionQueries, StructureSelectionQuery, StructureSelectionQueryList } from '../../mol-plugin-state/helpers/structure-selection-query';
 import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
 import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
@@ -17,7 +16,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
 import { stripTags } from '../../mol-util/string';
 import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
-import { ExpandGroup, ToggleButton, ControlGroup } from '../controls/common';
+import { ControlGroup, ToggleButton } from '../controls/common';
 import { Icon } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 
@@ -80,7 +79,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         if (stats.structureCount === 0 || stats.elementCount === 0) {
             return 'Nothing Selected'
         } else {
-            return `Selected ${stripTags(stats.label)}`
+            return `${stripTags(stats.label)} Selected`
         }
     }
 
@@ -94,16 +93,6 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         this.plugin.canvas3d?.camera.focus(origin, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs, dirA, dirC);
     }
 
-    focusLoci(loci: StructureElement.Loci) {
-        return () => {
-            const { extraRadius, minRadius, durationMs } = this.state
-            if (this.plugin.managers.structure.selection.stats.elementCount === 0) return
-            const { sphere } = StructureElement.Loci.getBoundary(loci)
-            const radius = Math.max(sphere.radius + extraRadius, minRadius);
-            this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs);
-        }
-    }
-
     setProps = (props: any) => {
         this.plugin.managers.interactivity.setProps(props);
     }
@@ -151,10 +140,9 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                 <ToggleButton icon='brush' title='Color' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} />
             </div>
             {(this.state.action && this.state.action !== 'color') && <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} items={this.queries} onSelect={this.selectQuery} />}
-            {this.state.action === 'color' && 
-                <ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.toggleColor} topRightIcon='off'>
-                    <ApplyColorControls />
-                </ControlGroup>}
+            {this.state.action === 'color' && <ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.toggleColor} topRightIcon='off'>
+                <ApplyColorControls />
+            </ControlGroup>}
         </>
     }
 
@@ -175,25 +163,6 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     }
 
     renderControls() {
-        const history: JSX.Element[] = [];
-
-        const mng = this.plugin.managers.structure.selection;
-
-        // TODO: fix the styles, move them to CSS
-
-        for (let i = 0, _i = Math.min(4, mng.history.length); i < _i; i++) {
-            const e = mng.history[i];
-            history.push(<li key={e!.label}>
-                <button className='msp-btn msp-btn-block msp-form-control' style={{ overflow: 'hidden' }}
-                    title='Click to focus.' onClick={this.focusLoci(e.loci)}>
-                    <span dangerouslySetInnerHTML={{ __html: e.label.split('|').reverse().join(' | ') }} />
-                </button>
-                {/* <div>
-                    <IconButton icon='remove' title='Remove' onClick={() => {}} />
-                </div> */}
-            </li>)
-        }
-
         return <>
             <ParameterControls params={StructureSelectionParams} values={this.values} onChangeObject={this.setProps} />
             {this.controls}
@@ -203,11 +172,6 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                     {this.stats}
                 </button>
             </div>
-            {history.length > 0 && <ExpandGroup header='Selection History'>
-                <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
-                    {history}
-                </ul>
-            </ExpandGroup>}
         </>
     }
 }

+ 12 - 12
src/mol-theme/label.ts

@@ -52,8 +52,8 @@ function countLabel(count: number, label: string) {
     return count === 1 ? `1 ${label}` : `${count} ${label}s`
 }
 
-function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean) {
-    return `${elementLabel(location, { granularity, hidePrefix })} <small>[+ ${countLabel(count - 1, `other ${capitalize(granularity)}`)}]</small>`
+function otherLabel(count: number, location: StructureElement.Location, granularity: LabelGranularity, hidePrefix: boolean, reverse: boolean, condensed: boolean) {
+    return `${elementLabel(location, { granularity, hidePrefix, reverse })} <small>[+ ${countLabel(count - 1, `other ${capitalize(granularity)}`)}]</small>`
 }
 
 /** Gets residue count of the model chain segments the unit is a subset of */
@@ -67,40 +67,40 @@ function getResidueCount(unit: Unit.Atomic) {
 
 export function structureElementStatsLabel(stats: StructureElement.Stats, options: Partial<LabelOptions> = {}): string {
     const o = { ...DefaultLabelOptions, ...options }
-    const label = _structureElementStatsLabel(stats, o.countsOnly, o.hidePrefix, o.condensed)
+    const label = _structureElementStatsLabel(stats, o.countsOnly, o.hidePrefix, o.condensed, o.reverse)
     return o.htmlStyling ? label : stripTags(label)
 }
 
-function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false): string {
+function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string {
     const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats
 
     if (!countsOnly && elementCount === 1 && residueCount === 0 && chainCount === 0) {
-        return elementLabel(stats.firstElementLoc, { hidePrefix, condensed, granularity: 'element' })
+        return elementLabel(stats.firstElementLoc, { hidePrefix, condensed, granularity: 'element', reverse })
     } else if (!countsOnly && elementCount === 0 && residueCount === 1 && chainCount === 0) {
-        return elementLabel(stats.firstResidueLoc, { hidePrefix, condensed, granularity: 'residue' })
+        return elementLabel(stats.firstResidueLoc, { hidePrefix, condensed, granularity: 'residue', reverse })
     } else if (!countsOnly && elementCount === 0 && residueCount === 0 && chainCount === 1) {
         const { unit } = stats.firstChainLoc
         const granularity = (Unit.isAtomic(unit) && getResidueCount(unit) === 1) ? 'residue' : 'chain'
-        return elementLabel(stats.firstChainLoc, { hidePrefix, condensed, granularity })
+        return elementLabel(stats.firstChainLoc, { hidePrefix, condensed, granularity, reverse })
     } else if (!countsOnly) {
         const label: string[] = []
         if (structureCount > 0) {
-            label.push(structureCount === 1 ? elementLabel(stats.firstStructureLoc, { hidePrefix, condensed, granularity: 'structure' }) : otherLabel(structureCount, stats.firstStructureLoc, 'structure', false))
+            label.push(structureCount === 1 ? elementLabel(stats.firstStructureLoc, { hidePrefix, condensed, granularity: 'structure', reverse }) : otherLabel(structureCount, stats.firstStructureLoc, 'structure', false, reverse, condensed))
         }
         if (chainCount > 0) {
-            label.push(chainCount === 1 ? elementLabel(stats.firstChainLoc, { condensed, granularity: 'chain' }) : otherLabel(chainCount, stats.firstChainLoc, 'chain', false))
+            label.push(chainCount === 1 ? elementLabel(stats.firstChainLoc, { condensed, granularity: 'chain', reverse }) : otherLabel(chainCount, stats.firstChainLoc, 'chain', false, reverse, condensed))
             hidePrefix = true;
         }
         if (residueCount > 0) {
-            label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, { condensed, granularity: 'residue', hidePrefix }) : otherLabel(residueCount, stats.firstResidueLoc, 'residue', hidePrefix))
+            label.push(residueCount === 1 ? elementLabel(stats.firstResidueLoc, { condensed, granularity: 'residue', hidePrefix, reverse }) : otherLabel(residueCount, stats.firstResidueLoc, 'residue', hidePrefix, reverse, condensed))
             hidePrefix = true;
         }
         if (conformationCount > 0) {
-            label.push(conformationCount === 1 ? elementLabel(stats.firstConformationLoc, { condensed, granularity: 'conformation', hidePrefix }) : otherLabel(conformationCount, stats.firstConformationLoc, 'conformation', hidePrefix))
+            label.push(conformationCount === 1 ? elementLabel(stats.firstConformationLoc, { condensed, granularity: 'conformation', hidePrefix, reverse }) : otherLabel(conformationCount, stats.firstConformationLoc, 'conformation', hidePrefix, reverse, condensed))
             hidePrefix = true;
         }
         if (elementCount > 0) {
-            label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, { condensed, granularity: 'element', hidePrefix }) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix))
+            label.push(elementCount === 1 ? elementLabel(stats.firstElementLoc, { condensed, granularity: 'element', hidePrefix, reverse }) : otherLabel(elementCount, stats.firstElementLoc, 'element', hidePrefix, reverse, condensed))
         }
         return label.join('<small> + </small>')
     } else {