Browse Source

mol-plugin-ui: ActionMenu, use in selection manager to test

David Sehnal 5 years ago
parent
commit
59235630bb

+ 1 - 0
src/mol-plugin-ui/base.tsx

@@ -25,6 +25,7 @@ export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.C
     componentWillUnmount() {
         if (!this.subs) return;
         for (const s of this.subs) s.unsubscribe();
+        this.subs = [];
     }
 
     protected init?(): void;

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

@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import { Icon } from './common';
+import { Observable, Subscription } from 'rxjs';
+
+export namespace ActionMenu {
+    export class Options extends React.PureComponent<{ toggle: Observable<OptionsParams | undefined>, hide?: Observable<any> }, { options: OptionsParams | undefined, isVisible: boolean }> { 
+        private subs: Subscription[] = [];
+
+        state = { isVisible: false, options: void 0 as OptionsParams | undefined }
+
+        componentDidMount() {
+            this.subs.push(this.props.toggle.subscribe(options => {
+                if (options && this.state.options?.items === options.items && this.state.options?.onSelect === options.onSelect) {
+                    this.setState({ isVisible: !this.state.isVisible});
+                } else {
+                    this.setState({ isVisible: !!options, options: options })
+                }
+            }));
+
+            if (this.props.hide) {
+                this.subs.push(this.props.hide.subscribe(() => this.hide()));
+            }
+        }
+
+        componentWillUnmount() {
+            if (!this.subs) return;
+            for (const s of this.subs) s.unsubscribe();
+            this.subs = [];
+        }
+
+        onSelect: OnSelect = item => {
+            this.setState({ isVisible: false, options: void 0 });
+            this.state.options?.onSelect(item.value);
+        }
+
+        hide = () => this.setState({ isVisible: false, options: void 0 });
+
+        render() {
+            if (!this.state.isVisible || !this.state.options) return null;
+            return <div className='msp-action-menu-options'>
+                {this.state.options.header && <div className='msp-control-group-header'>
+                    <button className='msp-btn msp-btn-block' onClick={this.hide}>
+                        {this.state.options.header}
+                    </button>
+                </div>}
+                <Section items={this.state.options!.items} onSelect={this.onSelect} />
+            </div>
+        }
+    }
+
+    class Section extends React.PureComponent<{ header?: string, items: Spec, onSelect: OnSelect }, { isExpanded: boolean }> { 
+        state = { isExpanded: false }
+
+        toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+            this.setState({ isExpanded: !this.state.isExpanded });
+            e.currentTarget.blur();
+        }
+
+        render() {
+            const { header, items, onSelect } = this.props;
+            if (typeof items === 'string') return null;
+            if (isItem(items)) return <Action item={items} onSelect={onSelect} />
+            return <div className='msp-control-offset'>
+                {header && <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'}`} /> 
+                        {header}
+                    </button>
+                </div>}
+                {(!header || this.state.isExpanded) && items.map((x, i) => {
+                    if (typeof x === 'string') return null;
+                    if (isItem(x)) return <Action key={i} item={x} onSelect={onSelect} />
+                    return <Section key={i} header={typeof x[0] === 'string' ? x[0] : void 0} items={x} onSelect={onSelect} />
+                })}
+            </div>;
+        }
+    }
+
+    const Action: React.FC<{ item: Item, onSelect: OnSelect }> = ({ item, onSelect }) => {
+        return <div className='msp-control-row'>
+            <button onClick={() => onSelect(item)}>
+                {item.icon && <Icon name={item.icon} />}
+                {item.name}
+            </button>
+        </div>;
+    }
+
+    type OnSelect = (item: Item) => void
+
+    function isItem(x: any): x is Item {
+        const v = x as Item;
+        return v && !!v.name && typeof v.value !== 'undefined';
+    }
+
+    export type OptionsParams = { items: Spec, header?: string, onSelect: (value: any) => void }
+    export type Spec = string | Item | [Spec]
+    export type Item = { name: string, icon?: string, value: unknown }
+
+    export function Item(name: string, value: unknown): Item
+    export function Item(name: string, icon: string, value: unknown): Item
+    export function Item(name: string, iconOrValue: any, value?: unknown): Item {
+        if (value) return { name, icon: iconOrValue, value };
+        return { name, value: iconOrValue };
+    }
+}

+ 1 - 0
src/mol-plugin-ui/controls/parameters.tsx

@@ -326,6 +326,7 @@ export class SelectControl extends SimpleParam<PD.Select<string | number>> {
             this.update(e.target.value);
         }
     }
+
     renderControl() {
         const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
         return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>

+ 30 - 0
src/mol-plugin-ui/skin/base/components/temp.scss

@@ -275,4 +275,34 @@
     .msp-transform-wrapper:last-child {
         margin-bottom: 10px;
     }
+}
+
+.msp-button-row {
+    display:flex;
+    flex-direction:row;
+    height: $row-height;
+    width: inherit;
+
+    > button {
+        margin: 0;
+        flex: 1 1 auto;
+        margin-right: 1px;
+        height: $row-height;
+
+        text-align-last: center;
+        background: none;
+        padding: 0 $control-spacing;
+        overflow: hidden;
+    }
+}
+
+.msp-action-menu-options {
+    .msp-control-row, button, .msp-icon {
+        height: 24px;
+        line-height: 24px;
+    }
+
+    button {
+        text-align: left;
+    }
 }

+ 14 - 22
src/mol-plugin-ui/structure/selection.tsx

@@ -7,13 +7,14 @@
 import * as React from 'react';
 import { CollapsableControls, CollapsableState } from '../base';
 import { StructureSelectionQueries, SelectionModifier } from '../../mol-plugin/util/structure-selection-helper';
-import { ButtonSelect, Options } from '../controls/common';
 import { PluginCommands } from '../../mol-plugin/command';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Interactivity } from '../../mol-plugin/util/interactivity';
 import { ParameterControls } from '../controls/parameters';
 import { stripTags } from '../../mol-util/string';
 import { StructureElement } from '../../mol-model/structure';
+import { ActionMenu } from '../controls/action-menu';
+import { Subject } from 'rxjs';
 
 const SSQ = StructureSelectionQueries
 const DefaultQueries: (keyof typeof SSQ)[] = [
@@ -124,28 +125,19 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     remove = (value: string) => this.set('remove', value)
     only = (value: string) => this.set('only', value)
 
-    queries = Options(Object.keys(StructureSelectionQueries)
-            .map(name => [name, SSQ[name as keyof typeof SSQ].label] as [string, string])
-            .filter(pair => DefaultQueries.includes(pair[0] as keyof typeof SSQ)));
-
-    controls = <div className='msp-control-row'>
-        <div className='msp-select-row'>
-            <ButtonSelect label='Select' onChange={this.add} disabled={this.state.isDisabled}>
-                <optgroup label='Select'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Deselect' onChange={this.remove} disabled={this.state.isDisabled}>
-                <optgroup label='Deselect'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
-            <ButtonSelect label='Only' onChange={this.only} disabled={this.state.isDisabled}>
-                <optgroup label='Only'>
-                    {this.queries}
-                </optgroup>
-            </ButtonSelect>
+    queries = Object.keys(StructureSelectionQueries)
+        .map(name => ActionMenu.Item(SSQ[name as keyof typeof SSQ].label, name))
+        .filter(item => DefaultQueries.includes(item.value as keyof typeof SSQ)) as ActionMenu.Spec;
+
+    actionMenu = new Subject<ActionMenu.OptionsParams | undefined>();
+
+    controls = <div>
+        <div className='msp-control-row msp-button-row' style={{ marginBottom: '1px' }}>
+            <button onClick={() => this.actionMenu.next({ items: this.queries, header: 'Select', onSelect: this.add }) } disabled={this.state.isDisabled}>Select</button>
+            <button onClick={() => this.actionMenu.next({ items: this.queries, header: 'Deselect', onSelect: this.remove }) } disabled={this.state.isDisabled}>Deselect</button>
+            <button onClick={() => this.actionMenu.next({ items: this.queries, header: 'Only', onSelect: this.only }) } disabled={this.state.isDisabled}>Only</button>
         </div>
+        <ActionMenu.Options toggle={this.actionMenu} hide={this.plugin.state.dataState.events.isUpdating} />
     </div>
 
     defaultState() {