diff --git a/src/mol-plugin-ui/controls/action-menu.tsx b/src/mol-plugin-ui/controls/action-menu.tsx index d495642779bc324c638920f66ccbad898e29b6cc..0d67b8c80475d366ef9b372ebc6772fee5cd8168 100644 --- a/src/mol-plugin-ui/controls/action-menu.tsx +++ b/src/mol-plugin-ui/controls/action-menu.tsx @@ -45,7 +45,8 @@ export namespace ActionMenu { menu: ActionMenu, disabled?: boolean, items: ActionMenu.Spec, - header: string, + header?: string, + label?: string, current?: ActionMenu.Item, onSelect: (value: any) => void } @@ -78,11 +79,17 @@ export namespace ActionMenu { hide = () => this.setState({ isSelected: false }); + onClick = (e: React.MouseEvent<HTMLButtonElement>) => { + e.currentTarget.blur(); + this.props.menu.toggle(this.props); + } + render() { const props = this.props; - return <button onClick={() => props.menu.toggle(props)} + const label = props.label || props.header; + return <button onClick={this.onClick} disabled={props.disabled} style={props.style} className={props.className}> - {this.state.isSelected ? <b>{props.header}</b> : props.header} + {this.state.isSelected ? <b>{label}</b> : label} </button>; } } @@ -120,19 +127,19 @@ export namespace ActionMenu { render() { if (!this.state.isVisible || this.state.command.type !== 'toggle') return null; - return <div className='msp-action-menu-options'> + return <div className='msp-action-menu-options' style={{ marginTop: '1px' }}> {this.state.command.header && <div className='msp-control-group-header' style={{ position: 'relative' }}> <button className='msp-btn msp-btn-block' onClick={this.hide}> <Icon name='off' style={{ position: 'absolute', right: '2px', top: 0 }} /> <b>{this.state.command.header}</b> </button> </div>} - <Section items={this.state.command.items} onSelect={this.onSelect} /> + <Section menu={this.props.menu} items={this.state.command.items} onSelect={this.onSelect} current={this.state.command.current} /> </div> } } - class Section extends React.PureComponent<{ header?: string, items: Spec, onSelect: OnSelect }, { isExpanded: boolean }> { + class Section extends React.PureComponent<{ menu: ActionMenu, header?: string, items: Spec, onSelect: OnSelect, current: Item | undefined }, { isExpanded: boolean }> { state = { isExpanded: false } toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => { @@ -141,9 +148,9 @@ export namespace ActionMenu { } render() { - const { header, items, onSelect } = this.props; + const { header, items, onSelect, current, menu } = this.props; if (typeof items === 'string') return null; - if (isItem(items)) return <Action item={items} onSelect={onSelect} /> + if (isItem(items)) return <Action menu={menu} item={items} onSelect={onSelect} current={current} /> return <div> {header && <div className='msp-control-group-header' style={{ marginTop: '1px' }}> <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> @@ -154,19 +161,20 @@ export namespace ActionMenu { <div className='msp-control-offset'> {(!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} /> + if (isItem(x)) return <Action menu={menu} key={i} item={x} onSelect={onSelect} current={current} /> + return <Section menu={menu} key={i} header={typeof x[0] === 'string' ? x[0] : void 0} items={x} onSelect={onSelect} current={current} /> })} </div> </div>; } } - const Action: React.FC<{ item: Item, onSelect: OnSelect }> = ({ item, onSelect }) => { + const Action: React.FC<{ menu: ActionMenu, item: Item, onSelect: OnSelect, current: Item | undefined }> = ({ menu, item, onSelect, current }) => { + const isCurrent = current === item; return <div className='msp-control-row'> - <button onClick={() => onSelect(item)}> + <button onClick={isCurrent ? () => menu.hide() : () => onSelect(item)}> {item.icon && <Icon name={item.icon} />} - {item.name} + {isCurrent ? <b>{item.name}</b> : item.name} </button> </div>; } diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 6efe77704e2f26679b40b7eec99bb8ca78df7afa..f52c546ccc97e69402aa253f5a666c0933e93173 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -22,6 +22,7 @@ import { CombinedColorControl, ColorValueOption, ColorOptions } from './color'; import { getPrecision } from '../../mol-util/number'; import { ParamMapping } from '../../mol-util/param-mapping'; import { PluginContext } from '../../mol-plugin/context'; +import { ActionMenu } from './action-menu'; export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, @@ -135,7 +136,8 @@ export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent< } abstract renderControl(): JSX.Element; - + renderAddOn(): JSX.Element | null { return null; } + private get className() { const className = ['msp-control-row']; if (this.props.param.shortLabel) className.push('msp-control-label-short') @@ -143,6 +145,7 @@ export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent< return className.join(' ') } + toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); render() { @@ -171,6 +174,7 @@ export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent< {hasHelp && this.state.isExpanded && <div className='msp-control-offset'> <ParamHelp legend={help.legend} description={help.description} /> </div>} + {this.renderAddOn()} </>; } } @@ -319,20 +323,32 @@ export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select } export class SelectControl extends SimpleParam<PD.Select<string | number>> { - onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { - if (typeof this.props.param.defaultValue === 'number') { - this.update(parseInt(e.target.value, 10)); - } else { - this.update(e.target.value); - } + menu = new ActionMenu(); + onSelect = (value: string) => { + this.update(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}> - {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>} - {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)} - </select>; + const items: ActionMenu.Item[] = []; + let current: ActionMenu.Item | undefined = void 0; + if (isInvalid) { + current = ActionMenu.Item(`[Invalid] ${this.props.value}`, this.props.value); + items.push(current); + } + for (const [value, label] of this.props.param.options) { + const item = ActionMenu.Item(label, value); + items.push(item); + if (value === this.props.value) current = item; + } + + return <ActionMenu.Toggle menu={this.menu} disabled={this.props.isDisabled} + onSelect={this.onSelect} items={items as ActionMenu.Spec} label={current?.name} + current={current} />; + } + + renderAddOn() { + return <ActionMenu.Options menu={this.menu} />; } } diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx index 290e31d956ab3f5190374c01b4acfc72c66a3785..61c627f54ac2c50dee49d48295c6816daac9d894 100644 --- a/src/mol-plugin-ui/structure/selection.tsx +++ b/src/mol-plugin-ui/structure/selection.tsx @@ -210,7 +210,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS actionMenu = new ActionMenu(); controls = <div> - <div className='msp-control-row msp-button-row' style={{ marginBottom: '1px' }}> + <div className='msp-control-row msp-button-row'> <ActionMenu.Toggle menu={this.actionMenu} items={this.queries} header='Select' onSelect={this.add} disabled={this.state.isDisabled} /> <ActionMenu.Toggle menu={this.actionMenu} items={this.queries} header='Deselect' onSelect={this.remove} disabled={this.state.isDisabled} /> <ActionMenu.Toggle menu={this.actionMenu} items={this.queries} header='Only' onSelect={this.only} disabled={this.state.isDisabled} />