Browse Source

ui improvements
- state tree update/apply transform
- focus label, customize radius
- StructureComponentGroup repr label

David Sehnal 5 years ago
parent
commit
1b5eff6454

+ 2 - 2
src/mol-plugin-state/manager/structure/hierarchy-state.ts

@@ -204,7 +204,7 @@ function isTransformer(t: StateTransformer): TestCell {
 
 function noop() { }
 
-const tagMap: [TestCell, ApplyRef, LeaveRef][] = [
+const Mapping: [TestCell, ApplyRef, LeaveRef][] = [
     // Trajectory
     [isType(SO.Molecule.Trajectory), (state, cell) => {
         state.currentTrajectory = createOrUpdateRefList(state, cell, state.hierarchy.trajectories, TrajectoryRef, cell);
@@ -304,7 +304,7 @@ function _doPreOrder(ctx: VisitorCtx, root: StateTransform) {
     if (!isValidCell(cell)) return;
 
     let onLeave: undefined | ((state: BuildState) => any) = void 0;
-    for (const [test, f, l] of tagMap) {
+    for (const [test, f, l] of Mapping) {
         if (test(cell, state)) {
             const cont = f(state, cell);
             if (cont === false) {

+ 4 - 3
src/mol-plugin-ui/controls/common.tsx

@@ -15,6 +15,7 @@ export class ControlGroup extends React.Component<{
     hideExpander?: boolean,
     hideOffset?: boolean,
     topRightIcon?: IconName,
+    headerLeftMargin?: string,
     onHeaderClick?: () => void
 }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initialExpanded }
@@ -30,7 +31,7 @@ export class ControlGroup extends React.Component<{
     render() {
         // TODO: customize header style (bg color, togle button etc)
         return <div className='msp-control-group-wrapper' style={{ position: 'relative' }}>
-            <div className='msp-control-group-header'>
+            <div className='msp-control-group-header' style={{ marginLeft: this.props.headerLeftMargin }}>
                 <button className='msp-btn msp-btn-block' onClick={this.headerClicked}>
                     {!this.props.hideExpander && <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />}
                     {this.props.topRightIcon && <Icon name={this.props.topRightIcon} style={{ position: 'absolute', right: '2px', top: 0 }} />}
@@ -332,14 +333,14 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
     }
 }
 
-export class ExpandGroup extends React.PureComponent<{ header: string, headerStyle?: React.CSSProperties, initiallyExpanded?: boolean, noOffset?: boolean, marginTop?: 0 | string }, { isExpanded: boolean }> {
+export class ExpandGroup extends React.PureComponent<{ header: string, headerStyle?: React.CSSProperties, initiallyExpanded?: boolean, noOffset?: boolean, marginTop?: 0 | string, headerLeftMargin?: string }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initiallyExpanded };
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
 
     render() {
         return <>
-            <div className='msp-control-group-header' style={{ marginTop: this.props.marginTop !== void 0 ? this.props.marginTop : '1px' }}>
+            <div className='msp-control-group-header' style={{ marginTop: this.props.marginTop !== void 0 ? this.props.marginTop : '1px', marginLeft: this.props.headerLeftMargin }}>
                 <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded} style={this.props.headerStyle}>
                     <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
                     {this.props.header}

+ 4 - 0
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -199,4 +199,8 @@
     overflow: hidden;
     text-overflow: ellipsis;
     white-space: nowrap;
+}
+
+.msp-25-lower-contrast-text {
+    color: color-lower-contrast($font-color, 25%);
 }

+ 3 - 2
src/mol-plugin-ui/state/common.tsx

@@ -106,7 +106,8 @@ namespace TransformControlBase {
         applyLabel?: string,
         onApply?: () => void,
         autoHideApply?: boolean,
-        wrapInExpander?: boolean
+        wrapInExpander?: boolean,
+        expanderHeaderLeftMargin?: string
     }
 }
 
@@ -235,7 +236,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
 
         if (isEmpty || !this.props.wrapInExpander) return ctrl;
 
-        return <ExpandGroup header={this.isUpdate() ? `Update ${display === 'none' ? '' : display.name}` : `Apply ${display === 'none' ? '' : display.name}` }>
+        return <ExpandGroup header={this.isUpdate() ? `Update ${display === 'none' ? '' : display.name}` : `Apply ${display === 'none' ? '' : display.name}` } headerLeftMargin={this.props.expanderHeaderLeftMargin}>
             {ctrl}
         </ExpandGroup>;
     }

+ 15 - 22
src/mol-plugin-ui/state/tree.tsx

@@ -157,11 +157,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             }
 
             if (e.state.transforms.has(this.ref)) {
-                this._setCurrent(this.props.cell.parent.current === this.ref, !!this.props.cell.state.isCollapsed)
-                // this.setState({
-                //     isCurrent: this.props.cell.parent.current === this.ref,
-                //     isCollapsed: !!this.props.cell.state.isCollapsed
-                // });
+                this._setCurrent(this.props.cell.parent.current === this.ref, !!this.props.cell.state.isCollapsed);
             }
         });
     }
@@ -195,9 +191,9 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: this.ref });
     }
 
-    setCurrentRoot = (e: React.MouseEvent<HTMLElement>) => {
-        e.preventDefault();
-        e.currentTarget.blur();
+    setCurrentRoot = (e?: React.MouseEvent<HTMLElement>) => {
+        e?.preventDefault();
+        e?.currentTarget.blur();
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: StateTransform.RootRef });
     }
 
@@ -230,12 +226,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         e.currentTarget.blur();
     }
 
-    // toggleActions = () => {
-    //     if (this.state.action) this.setState({ action: void 0, currentAction: void 0 });
-    //     else this.setState({ action: 'options', currentAction: void 0 });
-    // }
-
-    hideAction = () => this.setState({ action: void 0, currentAction: void 0 });
+    hideApply = () => this.setState({ action: 'options', currentAction: void 0 });
 
     get actions() {
         const cell = this.props.cell;
@@ -255,14 +246,14 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         (item?.value as any)();
     }
 
-    updates() {
+    updates(margin: string) {
         const cell = this.props.cell;
         const decoratorChain = StateTreeSpine.getDecoratorChain(cell.parent, cell.transform.ref);
 
         const decorators = [];
         for (let i = decoratorChain.length - 1; i >= 0; i--) {
             const d = decoratorChain[i];
-            decorators!.push(<UpdateTransformControl key={`${d.transform.transformer.id}-${i}`} state={cell.parent} transform={d.transform} noMargin wrapInExpander />);
+            decorators!.push(<UpdateTransformControl key={`${d.transform.transformer.id}-${i}`} state={cell.parent} transform={d.transform} noMargin wrapInExpander expanderHeaderLeftMargin={margin} />);
         }
 
         return decorators;
@@ -298,13 +289,13 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             <Icon name='visual-visibility' />
         </button>;
 
-        const style: React.HTMLAttributes<HTMLDivElement>['style'] = {
+        const marginStyle: React.CSSProperties = {
             marginLeft: /* this.state.isCurrent ? void 0 :*/ `${this.props.depth * 8}px`,
             // paddingLeft: !this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`,
             borderLeft: /* isCurrent || */ this.props.depth === 0 ? 'none' : void 0
         }
 
-        const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={style}>
+        const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={marginStyle}>
             {label}
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <Icon name={cellState.isCollapsed ? 'expand' : 'collapse'} />
@@ -319,8 +310,8 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         if (this.state.action === 'apply' && this.state.currentAction) {
             return <div style={{ marginBottom: '1px' }}>
                 {row}
-                <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideAction} topRightIcon='off'>
-                    <ApplyActionControl onApply={this.hideAction} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
+                <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideApply} topRightIcon='off' headerLeftMargin={marginStyle.marginLeft as string}>
+                    <ApplyActionControl onApply={this.hideApply} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
                 </ControlGroup>
             </div>
         }
@@ -329,8 +320,10 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             let actions = this.actions;
             return <div style={{ marginBottom: '1px' }}>
                 {row}
-                {this.updates()}
-                {actions && <ActionMenu items={actions} onSelect={this.selectAction} />}
+                {this.updates(marginStyle.marginLeft as string)}
+                {actions && <div style={marginStyle}>
+                    <ActionMenu items={actions} onSelect={this.selectAction} />
+                </div>}
             </div>
         }
 

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

@@ -316,14 +316,23 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         });
     }
 
+    get reprLabel() {
+        // TODO: handle generic reprs.
+        const pivot = this.pivot;
+        if (pivot.representations.length === 0) return 'No repr.';
+        if (pivot.representations.length === 1) return pivot.representations[0].cell.obj?.label;
+        return `${pivot.representations.length} reprs`;
+    }
+
     render() {
         const component = this.pivot;
         const cell = component.cell;
         const label = cell.obj?.label;
+        const reprLabel = this.reprLabel;
         return <>
             <div className='msp-btn-row-group'>
-                <button className='msp-form-control msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
-                    {label}
+                <button className='msp-form-control msp-control-button-label msp-no-overflow' title={`${label}, ${reprLabel}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
+                    {label} <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{reprLabel}</small>
                 </button>
                 <IconButton onClick={this.toggleVisible} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small customClass='msp-form-control' style={{ flex: '0 0 32px' }} />
                 <IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} customClass='msp-form-control' style={{ flex: '0 0 32px' }} />

+ 14 - 5
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -34,6 +34,8 @@ const StructureRepresentationInteractionParams = (plugin: PluginContext) => {
     const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
     return {
         bindings: PD.Value(DefaultStructureRepresentationInteractionBindings, { isHidden: true }),
+        // TODO: min = 0 to turn them off?
+        expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
         focusParams: PD.Group(reprParams, {
             label: 'Focus',
             customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'uniform' })
@@ -66,6 +68,8 @@ export enum StructureRepresentationInteractionTags {
 const TagSet: Set<StructureRepresentationInteractionTags> = new Set([StructureRepresentationInteractionTags.ResidueSel, StructureRepresentationInteractionTags.ResidueRepr, StructureRepresentationInteractionTags.SurrSel, StructureRepresentationInteractionTags.SurrRepr, StructureRepresentationInteractionTags.SurrNciRepr])
 
 export class StructureRepresentationInteractionBehavior extends PluginBehavior.WithSubscribers<StructureRepresentationInteractionProps> {
+    private get surrLabel() { return `[Focus +${this.params.expandRadius} Å Surrounding]`; }
+
     private ensureShape(cell: StateObjectCell<PluginStateObject.Molecule.Structure>) {
         const state = this.plugin.state.data, tree = state.tree;
         const builder = state.build();
@@ -76,14 +80,14 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
             refs[StructureRepresentationInteractionTags.ResidueSel] = builder
                 .to(cell) // refs['structure-interaction-group'])
                 .apply(StateTransforms.Model.StructureSelectionFromBundle,
-                    { bundle: {} as any, label: 'Focus' }, { tags: StructureRepresentationInteractionTags.ResidueSel }).ref;
+                    { bundle: StructureElement.Bundle.Empty, label: '[Focus]' }, { tags: StructureRepresentationInteractionTags.ResidueSel }).ref;
         }
 
         if (!refs[StructureRepresentationInteractionTags.SurrSel]) {
             refs[StructureRepresentationInteractionTags.SurrSel] = builder
                 .to(cell) // .to(refs['structure-interaction-group'])
                 .apply(StateTransforms.Model.StructureSelectionFromExpression,
-                    { expression: {} as any, label: 'Focus Surroundings' }, { tags: StructureRepresentationInteractionTags.SurrSel }).ref;
+                    { expression: MS.struct.generator.empty(), label: this.surrLabel }, { tags: StructureRepresentationInteractionTags.SurrSel }).ref;
         }
 
         // Representations
@@ -126,7 +130,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
             update.to(s).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression }));
         }
 
-        PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+        return PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
     }
 
     register(ref: string): void {
@@ -185,14 +189,14 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
                 const surroundings = MS.struct.modifier.includeSurroundings({
                     0: StructureElement.Bundle.toExpression(residueBundle),
-                    radius: 5,
+                    radius: this.params.expandRadius,
                     'as-whole-residues': true
                 });
 
                 const { state, builder, refs } = this.ensureShape(parent);
 
                 builder.to(refs[StructureRepresentationInteractionTags.ResidueSel]!).update(StateTransforms.Model.StructureSelectionFromBundle, old => ({ ...old, bundle: residueBundle }));
-                builder.to(refs[StructureRepresentationInteractionTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings }));
+                builder.to(refs[StructureRepresentationInteractionTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings, label: this.surrLabel }));
 
                 PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
             }
@@ -200,6 +204,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
     }
 
     async update(params: StructureRepresentationInteractionProps) {
+        let oldRadius = this.params.expandRadius;
         this.params = params;
 
         const state = this.plugin.state.data;
@@ -217,6 +222,10 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
         }
 
         await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+
+        // TODO: update properly
+        if (params.expandRadius !== oldRadius) await this.clear(StateTransform.RootRef);
+
         return true;
     }
 }