Переглянути джерело

Merge branch 'master' of https://github.com/molstar/molstar

Alexander Rose 5 роки тому
батько
коміт
1f968b2836

+ 3 - 4
src/mol-plugin-ui/base.tsx

@@ -8,7 +8,7 @@
 import * as React from 'react';
 import { Observable, Subscription } from 'rxjs';
 import { PluginContext } from '../mol-plugin/context';
-import { Icon } from './controls/icons';
+import { Button } from './controls/common';
 
 export const PluginReactContext = React.createContext(void 0 as any as PluginContext);
 
@@ -89,11 +89,10 @@ export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends Plugi
 
         return <div className={wrapClass}>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-form-control msp-btn-block msp-no-overflow' onClick={this.toggleCollapsed}>
-                    <Icon name={this.state.isCollapsed ? 'expand' : 'collapse'} />
+                <Button icon={this.state.isCollapsed ? 'expand' : 'collapse'} noOverflow onClick={this.toggleCollapsed}>
                     {this.state.header}
                     <small style={{ margin: '0 6px' }}>{this.state.isCollapsed ? '' : this.state.description}</small>
-                </button>
+                </Button>
             </div>
             {!this.state.isCollapsed && this.renderControls()}
         </div>

+ 5 - 6
src/mol-plugin-ui/camera.tsx

@@ -10,6 +10,7 @@ import { PluginUIComponent } from './base';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { Icon } from './controls/icons';
+import { Button, IconButton } from './controls/common';
 
 export class CameraSnapshots extends PluginUIComponent<{ }, { }> {
     render() {
@@ -42,8 +43,8 @@ class CameraSnapshotControls extends PluginUIComponent<{ }, { name: string, desc
             <ParameterControls params={CameraSnapshotControls.Params} values={this.state} onEnter={this.add} onChange={p => this.setState({ [p.name]: p.value } as any)}  />
 
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Add</button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
+                <Button onClick={this.add}>Add</Button>
+                <Button onClick={this.clear}>Clear</Button>
             </div>
         </div>;
     }
@@ -67,10 +68,8 @@ class CameraSnapshotList extends PluginUIComponent<{ }, { }> {
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.state.cameraSnapshots.state.entries.valueSeq().map(e =><li key={e!.id}>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
-                <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
-                    <Icon name='remove' />
-                </button>
+                <Button onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></Button>
+                <IconButton icon='remove' onClick={this.remove(e!.id)} className='msp-state-list-remove-button' />
             </li>)}
         </ul>;
     }

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

@@ -5,9 +5,9 @@
  */
 
 import * as React from 'react'
-import { Icon, IconName } from './icons';
+import { IconName } from './icons';
 import { ParamDefinition } from '../../mol-util/param-definition';
-import { ControlGroup } from './common';
+import { ControlGroup, Button } from './common';
 
 export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
     hide = () => this.props.onSelect(void 0)
@@ -184,19 +184,16 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
     get multiselectHeader() {
         const { header, hasCurrent } = this.state;
 
-        return <div className='msp-control-group-header msp-flex-row' style={{ marginTop: '1px' }}>
-            <button className='msp-btn msp-form-control msp-flex-item msp-no-overflow' onClick={this.toggleExpanded}>
-                <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
+        return <div className='msp-flex-row msp-control-group-header'>
+            <Button icon={this.state.isExpanded ? 'collapse' : 'expand'} flex noOverflow onClick={this.toggleExpanded}>
                 {hasCurrent ? <b>{header?.label}</b> : header?.label}
-            </button>
-            <button className='msp-btn msp-form-control msp-flex-item' onClick={this.selectAll} style={{ flex: '0 0 50px', textAlign: 'right' }}>
-                <Icon name='check' />
+            </Button>
+            <Button icon='check' flex onClick={this.selectAll} style={{ flex: '0 0 50px', textAlign: 'right' }}>
                 All
-            </button>
-            <button className='msp-btn msp-form-control msp-flex-item' onClick={this.selectNone} style={{ flex: '0 0 50px', textAlign: 'right' }}>
-                <Icon name='cancel' />
+            </Button>
+            <Button icon='cancel' flex onClick={this.selectNone} style={{ flex: '0 0 50px', textAlign: 'right' }}>
                 None
-            </button>
+            </Button>
         </div>;
     }
 
@@ -204,10 +201,9 @@ 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 msp-no-overflow' onClick={this.toggleExpanded}>
-                <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
+            <Button noOverflow icon={this.state.isExpanded ? 'collapse' : 'expand'} onClick={this.toggleExpanded}>
                 {hasCurrent ? <b>{header?.label}</b> : header?.label}
-            </button>
+            </Button>
         </div>;
     }
 
@@ -240,11 +236,10 @@ const Action: React.FC<{
 
     const style: React.CSSProperties | undefined = item.addOn ? { position: 'relative' } : void 0;
 
-    return <button className='msp-btn msp-btn-block msp-form-control msp-action-menu-button msp-no-overflow' onClick={() => onSelect(multiselect ? [item] : item as any)} disabled={item.disabled} style={style}>
-        {item.icon && <Icon name={item.icon} />}
+    return <Button icon={item.icon} noOverflow className='msp-action-menu-button' onClick={() => onSelect(multiselect ? [item] : item as any)} disabled={item.disabled} style={style}>
         {isCurrent || item.selected ? <b>{item.label}</b> : item.label}
         {item.addOn}
-    </button>;
+    </Button>;
 }
 
 function isItems(x: any): x is ActionMenu.Items[] {

+ 10 - 18
src/mol-plugin-ui/controls/color.tsx

@@ -11,7 +11,7 @@ import { camelCaseToWords, stringToWords } from '../../mol-util/string';
 import * as React from 'react';
 import { _Props, _State } from '../base';
 import { ParamProps } from './parameters';
-import { TextInput } from './common';
+import { TextInput, Button, ControlRow } from './common';
 import { DefaultColorSwatch } from '../../mol-util/color/swatches';
 
 export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
@@ -43,31 +43,23 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
     swatch() {
         // const def = this.props.param.defaultValue;
         return <div className='msp-combined-color-swatch'>
-            {/* <button title='Default Color' key={def} className='msp-form-control msp-btn' data-color={def} onClick={this.onClickSwatch} style={{ background: Color.toStyle(def) }}></button> */}
-            {DefaultColorSwatch.map(c => <button key={c[1]} className='msp-form-control msp-btn' data-color={c[1]} onClick={this.onClickSwatch} style={{ background: Color.toStyle(c[1]) }}></button>)}
+            {DefaultColorSwatch.map(c => <Button key={c[1]} inline data-color={c[1]} onClick={this.onClickSwatch} style={{ background: Color.toStyle(c[1]) }} />)}
         </div>;
     }
 
     render() {
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <>
-            <div className='msp-control-row'>
-                <span title={this.props.param.description}>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded} className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }}></button>
-                </div>
-            </div>
+            <ControlRow title={this.props.param.description}
+                label={label}
+                control={<Button onClick={this.toggleExpanded} inline className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }} />} />
             {this.state.isExpanded && <div className='msp-control-offset'>
                 {this.swatch()}
-                <div className='msp-control-row'>
-                    <span>RGB</span>
-                    <div>
-                        <TextInput onChange={this.onChangeText} value={this.props.value}
-                            fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
-                            className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
-                            placeholder='e.g. 127 127 127' delayMs={250} />
-                    </div>
-                </div>
+                <ControlRow label='RGB'
+                    control={<TextInput onChange={this.onChangeText} value={this.props.value}
+                        fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
+                        className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
+                        placeholder='e.g. 127 127 127' delayMs={250} />} />
             </div>}
         </>;
     }

+ 98 - 49
src/mol-plugin-ui/controls/common.tsx

@@ -6,7 +6,6 @@
 
 import * as React from 'react';
 import { Color } from '../../mol-util/color';
-import { PurePluginUIComponent } from '../base';
 import { IconName, Icon } from './icons';
 
 export class ControlGroup extends React.Component<{
@@ -33,11 +32,11 @@ export class ControlGroup extends React.Component<{
         // TODO: customize header style (bg color, togle button etc)
         return <div className='msp-control-group-wrapper' style={{ position: 'relative', marginTop: this.props.noTopMargin ? 0 : void 0 }}>
             <div className='msp-control-group-header' style={{ marginLeft: this.props.headerLeftMargin }}>
-                <button className='msp-btn msp-form-control msp-btn-block' onClick={this.headerClicked}>
+                <Button 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 }} />}
                     <b>{this.props.header}</b>
-                </button>
+                </Button>
             </div>
             {this.state.isExpanded && <div className={this.props.hideOffset ? '' : 'msp-control-offset'} style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
                 {this.props.children}
@@ -71,7 +70,7 @@ interface TextInputState {
 
 function _id(x: any) { return x; }
 
-export class TextInput<T = string> extends PurePluginUIComponent<TextInputProps<T>, TextInputState> {
+export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>, TextInputState> {
     private input = React.createRef<HTMLInputElement>();
     private delayHandle: any = void 0;
     private pendingValue: T | undefined = void 0;
@@ -124,7 +123,7 @@ export class TextInput<T = string> extends PurePluginUIComponent<TextInputProps<
         this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted));
     }
 
-    onKeyUp  = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if (e.charCode === 27 || e.keyCode === 27 /* esc */) {
             if (this.props.blurOnEscape && this.input.current) {
                 this.input.current.blur();
@@ -224,7 +223,7 @@ export class NumericInput extends React.PureComponent<{
     }
 }
 
-export class ExpandableGroup extends React.Component<{
+export class ExpandableControlRow extends React.Component<{
     label: string,
     colorStripe?: Color,
     pivot: JSX.Element,
@@ -238,17 +237,15 @@ export class ExpandableGroup extends React.Component<{
         const { label, pivot, controls } = this.props;
         // TODO: fix the inline CSS
         return <>
-            <div className='msp-control-row'>
-                <span>
-                    {label}
-                    <button className='msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded} title={`${this.state.isExpanded ? 'Less' : 'More'} options`}
-                        style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
-                        <Icon name={this.state.isExpanded ? 'minus' : 'plus'} style={{ display: 'inline-block' }} />
-                    </button>
-                </span>
-                <div>{pivot}</div>
-                {this.props.colorStripe && <div className='msp-expandable-group-color-stripe' style={{ backgroundColor: Color.toStyle(this.props.colorStripe) }} /> }
-            </div>
+            <ControlRow label={<>
+                {label}
+                <button className='msp-btn-link msp-btn-icon msp-control-group-expander' onClick={this.toggleExpanded} title={`${this.state.isExpanded ? 'Less' : 'More'} options`}
+                    style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
+                    <Icon name={this.state.isExpanded ? 'minus' : 'plus'} style={{ display: 'inline-block' }} />
+                </button>
+            </>} control={pivot}>
+                {this.props.colorStripe && <div className='msp-expandable-group-color-stripe' style={{ backgroundColor: Color.toStyle(this.props.colorStripe) }} />}
+            </ControlRow>
             {this.state.isExpanded && <div className='msp-control-offset'>
                 {controls}
             </div>}
@@ -256,6 +253,54 @@ export class ExpandableGroup extends React.Component<{
     }
 }
 
+export function SectionHeader(props: { icon?: IconName, title: string | JSX.Element, desc?: string }) {
+    return <div className='msp-section-header'>
+        {props.icon && <Icon name={props.icon} />}
+        {props.title} <small>{props.desc}</small>
+    </div>
+}
+
+export type ButtonProps = {
+    style?: React.CSSProperties,
+    className?: string,
+    disabled?: boolean,
+    title?: string,
+    icon?: IconName,
+    children?: React.ReactNode,
+    onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    onContextMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    onMouseEnter?: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    onMouseLeave?: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    inline?: boolean,
+    'data-id'?: string,
+    flex?: boolean | string | number,
+    noOverflow?: boolean
+}
+
+export function Button(props: ButtonProps) {
+    let className = 'msp-btn';
+    if (!props.inline) className += ' msp-btn-block';
+    if (props.noOverflow) className += ' msp-no-overflow';
+    if (props.flex) className += ' msp-flex-item';
+    if (props.className) className += ' ' + props.className;
+
+    let style: React.CSSProperties | undefined = void 0;
+    if (props.flex) {
+        if (typeof props.flex === 'number') style = { flex: `0 0 ${props.flex}px`, padding: 0, maxWidth: `${props.flex}px` };
+        else if (typeof props.flex === 'string') style = { flex: `0 0 ${props.flex}`, padding: 0, maxWidth: props.flex };
+    }
+    if (props.style) {
+        if (style) Object.assign(style, props.style);
+        else style = props.style;
+    }
+
+    return <button onClick={props.onClick} title={props.title} disabled={props.disabled} style={style} className={className} data-id={props['data-id']}
+        onContextMenu={props.onContextMenu} onMouseEnter={props.onMouseEnter} onMouseLeave={props.onMouseLeave}>
+        {props.icon && <Icon name={props.icon} />}
+        {props.children}
+    </button>;
+}
+
 export function IconButton(props: {
     icon: IconName,
     small?: boolean,
@@ -263,46 +308,33 @@ export function IconButton(props: {
     title?: string,
     toggleState?: boolean,
     disabled?: boolean,
-    customClass?: string,
+    className?: string,
     style?: React.CSSProperties,
     'data-id'?: string,
-    extraContent?: JSX.Element
+    extraContent?: JSX.Element,
+    flex?: boolean | string | number
 }) {
-    let className = `msp-btn-link msp-btn-icon${props.small ? '-small' : ''}${props.customClass ? ' ' + props.customClass : ''}`;
+    let className = `msp-btn-link msp-btn-icon${props.small ? '-small' : ''}${props.className ? ' ' + props.className : ''}`;
     if (typeof props.toggleState !== 'undefined') {
         className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
     }
     const iconStyle = props.small ? { fontSize: '80%' } : void 0;
-    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']} style={props.style}>
-        <Icon name={props.icon} style={iconStyle} />
-        {props.extraContent}
-    </button>;
-}
 
-export class ButtonSelect extends React.PureComponent<{ label: string, onChange: (value: string) => void, disabled?: boolean }> {
-    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-        e.preventDefault()
-        this.props.onChange(e.target.value)
-        e.target.value = '_'
+    let style: React.CSSProperties | undefined = void 0;
+    if (props.flex) {
+        if (typeof props.flex === 'boolean') style = { flex: '0 0 32px', padding: 0 };
+        else if (typeof props.flex === 'number') style = { flex: `0 0 ${props.flex}px`, padding: 0, maxWidth: `${props.flex}px` };
+        else style = { flex: `0 0 ${props.flex}`, padding: 0, maxWidth: props.flex };
     }
-
-    render() {
-        return <select value='_' onChange={this.onChange} disabled={this.props.disabled}>
-            <option key='_' value='_'>{this.props.label}</option>
-            {this.props.children}
-        </select>
+    if (props.style) {
+        if (style) Object.assign(style, props.style);
+        else style = props.style;
     }
-}
-
-export function Options(options: [string, string][]) {
-    return options.map(([value, label]) => <option key={value} value={value}>{label}</option>)
-}
 
-export function SectionHeader(props: { icon?: IconName, title: string | JSX.Element, desc?: string}) {
-    return <div className='msp-section-header'>
-        {props.icon && <Icon name={props.icon} />}
-        {props.title} <small>{props.desc}</small>
-    </div>
+    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']} style={style}>
+        <Icon name={props.icon} style={iconStyle} />
+        {props.extraContent}
+    </button>;
 }
 
 export type ToggleButtonProps = {
@@ -326,11 +358,10 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
         const props = this.props;
         const label = props.label;
         const className = props.isSelected ? `${props.className || ''} msp-control-current` : props.className;
-        return <button onClick={this.onClick} title={this.props.title}
+        return <Button icon={this.props.icon} onClick={this.onClick} title={this.props.title}
             disabled={props.disabled} style={props.style} className={className}>
-            <Icon name={this.props.icon} />
             {label && this.props.isSelected ? <b>{label}</b> : label}
-        </button>;
+        </Button>;
     }
 }
 
@@ -355,4 +386,22 @@ export class ExpandGroup extends React.PureComponent<{ header: string, headerSty
                     </div>)}
         </>;
     }
+}
+
+export type ControlRowProps = {
+    title?: string,
+    label?: React.ReactNode,
+    control?: React.ReactNode,
+    className?: string,
+    children?: React.ReactNode
+}
+
+export function ControlRow(props: ControlRowProps) {
+    let className = 'msp-control-row';
+    if (props.className) className += ' ' + props.className;
+    return <div className={className}>
+        <span className='msp-control-row-label' title={props.title}>{props.label}</span>
+        <div className='msp-control-row-ctrl'>{props.control}</div>
+        {props.children}
+    </div>;
 }

+ 32 - 64
src/mol-plugin-ui/controls/parameters.tsx

@@ -19,7 +19,7 @@ import { camelCaseToWords } from '../../mol-util/string';
 import { PluginUIComponent, _Props, _State } from '../base';
 import { ActionMenu } from './action-menu';
 import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
-import { ControlGroup, ExpandGroup, IconButton, NumericInput, ToggleButton } from './common';
+import { ControlGroup, ExpandGroup, IconButton, NumericInput, ToggleButton, Button, ControlRow } from './common';
 import { Icon } from './icons';
 import { legendFor } from './legend';
 import LineGraphComponent from './line-graph/line-graph-component';
@@ -201,7 +201,7 @@ export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legen
         const { legend, description } = this.props
         const Legend = legend && legendFor(legend)
 
-        return <div className='msp-control-row msp-help-text'>
+        return <div className='msp-help-text'>
             <div>
                 <div className='msp-help-description'><Icon name='help-circle' />{description}</div>
                 {Legend && <div className='msp-help-legend'><Legend legend={legend} /></div>}
@@ -225,7 +225,7 @@ export type ParamControl = React.ComponentClass<ParamProps<any>>
 function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) {
     const { props, state, control, toggleHelp, addOn } = options;
 
-    const _className = ['msp-control-row'];
+    let _className = [];
     if (props.param.shortLabel) _className.push('msp-control-label-short')
     if (props.param.twoColumns) _className.push('msp-control-col-2')
     const className = _className.join(' ');
@@ -237,8 +237,10 @@ function renderSimple(options: { props: ParamProps<any>, state: { showHelp: bool
     const desc = props.param.description;
     const hasHelp = help.description || help.legend
     return <>
-        <div className={className}>
-            <span title={desc}>
+        <ControlRow
+            className={className}
+            title={desc}
+            label={<>
                 {label}
                 {hasHelp &&
                     <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp}
@@ -247,11 +249,9 @@ function renderSimple(options: { props: ParamProps<any>, state: { showHelp: bool
                         <Icon name={state.showHelp ? 'help-circle-collapse' : 'help-circle-expand'} />
                     </button>
                 }
-            </span>
-            <div>
-                {control}
-            </div>
-        </div>
+            </>}
+            control={control}
+        />
         {hasHelp && state.showHelp && <div className='msp-control-offset'>
             <ParamHelp legend={help.legend} description={help.description} />
         </div>}
@@ -324,14 +324,7 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
     render() {
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <>
-            <div className='msp-control-row'>
-                <span>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded}>
-                        {`${this.state.message}`}
-                    </button>
-                </div>
-            </div>
+            <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{`${this.state.message}`}</button>} />
             <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
                 <LineGraphComponent
                     data={this.props.param.defaultValue}
@@ -356,14 +349,12 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         const p = getPrecision(this.props.param.step || 0.01)
-        return <div className='msp-control-row'>
-            <span title={this.props.param.description}>{label}</span>
-            <div>
-                <NumericInput
-                    value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
-                    isDisabled={this.props.isDisabled} onChange={this.update} />
-            </div>
-        </div>;
+        return <ControlRow
+            title={this.props.param.description}
+            label={label}
+            control={<NumericInput
+                value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
+                isDisabled={this.props.isDisabled} onChange={this.update} />} />;
     }
 }
 
@@ -520,15 +511,10 @@ export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>
         const p = getPrecision(this.props.param.step || 0.01)
         const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`;
         return <>
-            <div className='msp-control-row'>
-                <span>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded}>{value}</button>
-                </div>
-            </div>
-            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+            <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
+            {this.state.isExpanded && <div className='msp-control-offset'>
                 <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
-            </div>
+            </div>}
         </>;
     }
 }
@@ -725,15 +711,10 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
         const p = getPrecision(this.props.param.step || 0.01)
         const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`;
         return <>
-            <div className='msp-control-row'>
-                <span>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded}>{value}</button>
-                </div>
-            </div>
-            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+            <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
+            {this.state.isExpanded && <div className='msp-control-offset'>
                 <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
-            </div>
+            </div>}
         </>;
     }
 }
@@ -809,24 +790,17 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
         const emptyLabel = this.props.param.emptyValue;
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <>
-            <div className='msp-control-row'>
-                <span>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded}>
-                        {current.length === 0 && emptyLabel ? emptyLabel : `${current.length} of ${this.props.param.options.length}`}
-                    </button>
-                </div>
-            </div>
-            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+            <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>
+                {current.length === 0 && emptyLabel ? emptyLabel : `${current.length} of ${this.props.param.options.length}`}
+            </button>} />
+            {this.state.isExpanded && <div className='msp-control-offset'>
                 {this.props.param.options.map(([value, label]) => {
                     const sel = current.indexOf(value) >= 0;
-                    return <div key={value} className='msp-row'>
-                        <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
-                            <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
-                        </button>
-                    </div>
+                    return <Button key={value} onClick={this.toggle(value)} disabled={this.props.isDisabled} style={{ marginTop: '1px' }}>
+                        <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
+                    </Button>
                 })}
-            </div>
+            </div>}
         </>;
     }
 }
@@ -1120,13 +1094,7 @@ export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectL
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
         return <>
-            <div className='msp-control-row'>
-                <span>{label}</span>
-                <div>
-                    <button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>
-                </div>
-            </div>
-
+            <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
             {this.state.isExpanded && <div className='msp-control-offset'>
                 {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
                 <ControlGroup header='New Item'>

+ 11 - 9
src/mol-plugin-ui/custom/volume.tsx

@@ -8,7 +8,7 @@ import { PluginUIComponent } from '../base';
 import { StateTransformParameters } from '../state/common';
 import * as React from 'react';
 import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
-import { ExpandableGroup } from '../controls/common';
+import { ExpandableControlRow } from '../controls/common';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
 import { Slider } from '../controls/slider';
@@ -38,12 +38,12 @@ function Channel(props: {
     const channel = props.channels[props.name]!;
 
     const { min, max, mean, sigma } = stats;
-    const value =  Math.round(100 * (channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue)) / 100;
+    const value = Math.round(100 * (channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue)) / 100;
     const relMin = (min - mean) / sigma;
     const relMax = (max - mean) / sigma;
     const step = toPrecision(isRelative ? Math.round(((max - min) / sigma)) / 100 : sigma / 100, 2)
 
-    return <ExpandableGroup
+    return <ExpandableControlRow
         label={props.label + (props.isRelative ? ' \u03C3' : '')}
         colorStripe={channel.color}
         pivot={<Slider value={value} min={isRelative ? relMin : min} max={isRelative ? relMax : max} step={step}
@@ -103,9 +103,11 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
     };
 
     convert(channel: any, stats: VolumeData['dataStats'], isRelative: boolean) {
-        return { ...channel, isoValue: isRelative
-            ? VolumeIsoValue.toRelative(channel.isoValue, stats)
-            : VolumeIsoValue.toAbsolute(channel.isoValue, stats) }
+        return {
+            ...channel, isoValue: isRelative
+                ? VolumeIsoValue.toRelative(channel.isoValue, stats)
+                : VolumeIsoValue.toAbsolute(channel.isoValue, stats)
+        }
     }
 
     changeOption: ParamOnChange = ({ name, value }) => {
@@ -131,8 +133,8 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                 ? old.entry.params.view.params
                 : (((this.props.info.params as VolumeStreaming.ParamDefinition)
                     .entry.map(old.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>)
-                        .params as VolumeStreaming.EntryParamDefinition)
-                            .view.map(value.name).defaultValue;
+                    .params as VolumeStreaming.EntryParamDefinition)
+                    .view.map(value.name).defaultValue;
 
             const viewParams = { ...oldView };
             if (value.name === 'selection-box') {
@@ -187,7 +189,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
         const OptionsParams = {
             entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string]), { isHidden: isOff, description: 'Which entry with volume data to display.' }),
             view: PD.MappedStatic(params.entry.params.view.name, {
-                'off': PD.Group({ 
+                'off': PD.Group({
                     isRelative: PD.Boolean(isRelative, { isHidden: true })
                 }, { description: 'Display off.' }),
                 'box': PD.Group({

+ 59 - 56
src/mol-plugin-ui/skin/base/components/controls-base.scss

@@ -1,4 +1,62 @@
+
+
+.msp-form-control {
+    display: block;
+    width: 100%;
+    background: $msp-form-control-background;
+    // color: $font-color;
+    border: none; // !important;
+    padding: 0 $control-spacing;
+    line-height: $row-height - 2px;
+    height: $row-height;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+    -webkit-box-shadow: none; // iOS <4.3 & Android <4.1
+    box-shadow: none;
+    // box-shadow: none !important;
+    background-image: none;
+
+    // Firefox
+    &::-moz-placeholder {
+        color: color-lower-contrast($font-color, 33%);
+        opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
+    }
+    &:-ms-input-placeholder { color: color-lower-contrast($font-color, 33%); } // Internet Explorer 10+
+    &::-webkit-input-placeholder  { color: color-lower-contrast($font-color, 33%); } // Safari and Chrome
+
+    &:hover {
+        color: $hover-font-color;
+        background-color: color-increase-contrast($msp-form-control-background, 5%);
+        border: none;
+        outline-offset: -1px !important;
+        outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
+    }
+
+    &:active, &:focus {
+        color: $font-color;
+        background-color: $msp-form-control-background;
+        border: none;
+        outline-offset: 0;
+        outline: none;
+    }
+
+    // Disabled and read-only inputs
+    //
+    // HTML5 says that controls under a fieldset > legend:first-child won't be
+    // disabled if the fieldset is disabled. Due to implementation difficulty, we
+    // don't honor that edge case; we style them as disabled anyway.
+    &[disabled],
+    &[readonly],
+    fieldset[disabled] & {
+        background: $default-background;
+        opacity: 0.35;
+    }
+}
+
 .msp-btn {
+    @extend .msp-form-control;
+
     display: inline-block;
     margin-bottom: 0; // For input.msp-btn
     text-align: center;
@@ -149,7 +207,7 @@
     }
 }
 
-@include msp-btn('remove', $msp-btn-remove-font-color, $msp-btn-remove-background);
+// @include msp-btn('remove', $msp-btn-remove-font-color, $msp-btn-remove-background);
 @include msp-btn('action', $font-color, $msp-btn-action-background);
 @include msp-btn('commit-on', $msp-btn-commit-on-font-color, $msp-btn-commit-on-background);
 @include msp-btn('commit-off', $msp-btn-commit-off-font-color, $msp-btn-commit-off-background);
@@ -182,61 +240,6 @@ select[size] {
     height: auto;
 }
   
-
-.msp-form-control {
-    display: block;
-    width: 100%;
-    background: $msp-form-control-background;
-    // color: $font-color;
-    border: none; // !important;
-    padding: 0 $control-spacing;
-    line-height: $row-height - 2px;
-    height: $row-height;
-    -webkit-appearance: none;
-    -moz-appearance: none;
-    appearance: none;
-    -webkit-box-shadow: none; // iOS <4.3 & Android <4.1
-    box-shadow: none;
-    // box-shadow: none !important;
-    background-image: none;
-
-    // Firefox
-    &::-moz-placeholder {
-        color: color-lower-contrast($font-color, 33%);
-        opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
-    }
-    &:-ms-input-placeholder { color: color-lower-contrast($font-color, 33%); } // Internet Explorer 10+
-    &::-webkit-input-placeholder  { color: color-lower-contrast($font-color, 33%); } // Safari and Chrome
-
-    &:hover {
-        color: $hover-font-color;
-        background-color: color-increase-contrast($msp-form-control-background, 5%);
-        border: none;
-        outline-offset: -1px !important;
-        outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important;
-    }
-
-    &:active, &:focus {
-        color: $font-color;
-        background-color: $msp-form-control-background;
-        border: none;
-        outline-offset: 0;
-        outline: none;
-    }
-
-    // Disabled and read-only inputs
-    //
-    // HTML5 says that controls under a fieldset > legend:first-child won't be
-    // disabled if the fieldset is disabled. Due to implementation difficulty, we
-    // don't honor that edge case; we style them as disabled anyway.
-    &[disabled],
-    &[readonly],
-    fieldset[disabled] & {
-        background: $default-background;
-        opacity: 0.35;
-    }
-}
-
 // Reset height for `textarea`s
 textarea.msp-form-control {
     height: auto;

+ 12 - 23
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -1,5 +1,4 @@
-
-.msp-row, .msp-control-row {
+.msp-control-row {
     position: relative;
     height: $row-height;
     background: $default-background;
@@ -13,10 +12,8 @@
         @extend .msp-btn;
         @extend .msp-btn-block;
     }
-}
 
-.msp-control-row {   
-    > span:first-child, > button.msp-control-button-label {
+    > span.msp-control-row-label, > button.msp-control-button-label {
         line-height: $row-height;
         display: block;
         width: $control-label-width + $control-spacing;
@@ -40,7 +37,7 @@
         background: $default-background;
     }
 
-    > div:nth-child(2) {
+    > div.msp-control-row-ctrl {
         position: absolute;
         left: $control-label-width + $control-spacing;
         top: 0;
@@ -52,7 +49,7 @@
         background: $msp-form-control-background;
     }
 
-    > .msp-select-row {
+    > .msp-flex-row {
         background: $default-background;
     }
 }
@@ -269,21 +266,6 @@
     margin-top: 1px;
 }
 
-.msp-control-subgroup {
-    margin-top: 1px;
-
-    .msp-control-row {
-        margin-left: $control-spacing !important;
-        > span {
-            width: $control-label-width !important;
-        }
-
-        > div:nth-child(2) {
-            left: $control-label-width !important;
-        }
-    }
-}
-
 .msp-control-group-expander {
     display: block;
     position: absolute;
@@ -324,6 +306,11 @@
 }
 
 .msp-row-text {
+    height: $row-height;
+    position: relative;
+    background: $default-background;
+    margin-top: 1px;
+
     > div {
         line-height: $row-height;
         text-align: center;
@@ -341,7 +328,9 @@
 }
 
 .msp-help-text {
-    height: auto !important;
+    position: relative;
+    background: $default-background;
+    margin-top: 1px;
 
     > div {
         padding: ($control-spacing / 2) $control-spacing;

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

@@ -208,4 +208,12 @@
 
 .msp-25-lower-contrast-text {
     color: color-lower-contrast($font-color, 25%);
+}
+
+.msp-expandable-group-color-stripe {
+    position: absolute;
+    left: 0;
+    top: $row-height - 2px;
+    width: $control-label-width + $control-spacing;
+    height: 2px;
 }

+ 48 - 21
src/mol-plugin-ui/skin/base/components/temp.scss

@@ -56,12 +56,56 @@
     }
 }
 
-.msp-select-row {
+// .msp-select-row {
+//     display:flex;
+//     flex-direction:row;
+//     height: $row-height;
+//     width: inherit;
+//     background: $default-background;
+
+//     > select, > button {
+//         margin: 0;
+//         flex: 1 1 auto;
+//         margin-right: 1px;
+//         height: $row-height;
+
+//         text-align-last: center;
+//         // padding: 0 $control-spacing;
+//         overflow: hidden;
+//     }
+
+//     > select {
+//         background: none;
+
+//         > option[value = _] {
+//             display: none;
+//         }
+//     }
+
+//     > select:last-child, > button:last-child {
+//         margin-right: 0;
+//     }
+// }
+
+.msp-flex-row {
+    margin-top: 1px;
+    background: $default-background;
+
     display:flex;
     flex-direction:row;
-    height: $row-height;
     width: inherit;
-    background: $default-background;
+    height: $row-height;
+
+    > .msp-flex-item {
+        margin: 0;
+        flex: 1 1 auto;
+        margin-right: 1px;
+        overflow: hidden;
+    }
+
+    > .msp-flex-item:last-child {
+        margin-right: 0;
+    }
 
     > select, > button {
         margin: 0;
@@ -70,7 +114,7 @@
         height: $row-height;
 
         text-align-last: center;
-        padding: 0 $control-spacing;
+        // padding: 0 $control-spacing;
         overflow: hidden;
     }
 
@@ -87,23 +131,6 @@
     }
 }
 
-.msp-flex-row {
-    display:flex;
-    flex-direction:row;
-    width: inherit;
-
-    > .msp-flex-item {
-        margin: 0;
-        flex: 1 1 auto;
-        margin-right: 1px;
-        overflow: hidden;
-    }
-
-    > .msp-flex-item:last-child {
-        margin-right: 0;
-    }
-}
-
 .msp-state-list {
     list-style: none;
     margin-top: $control-spacing;

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

@@ -134,7 +134,7 @@
 }
 
 .msp-transform-apply-wider {
-    left: $row-height + 1px;
+    margin-left: $row-height + 1px;
 }
 
 .msp-data-beh {

+ 3 - 3
src/mol-plugin-ui/state/animation.tsx

@@ -8,6 +8,7 @@ import * as React from 'react';
 import { PluginUIComponent } from '../base';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
 import { Icon } from '../controls/icons';
+import { Button } from '../controls/common';
 
 export class AnimationControlsWrapper extends PluginUIComponent<{ }> {
     render() {
@@ -53,10 +54,9 @@ export class AnimationControls extends PluginUIComponent<{ onStart?: () => void
             <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} />
 
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}>
-                    {anim.state.animationState !== 'playing' && <Icon name='play' />}
+                <Button icon={anim.state.animationState !== 'playing' ? void 0 : 'play'} onClick={this.startOrStop}>
                     {anim.state.animationState === 'playing' ? 'Stop' : 'Start'}
-                </button>
+                </Button>
             </div>
         </>;
     }

+ 13 - 14
src/mol-plugin-ui/state/common.tsx

@@ -12,7 +12,7 @@ import { PluginContext } from '../../mol-plugin/context';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Subject } from 'rxjs';
 import { Icon, IconName } from '../controls/icons';
-import { ExpandGroup, ToggleButton } from '../controls/common';
+import { ExpandGroup, ToggleButton, Button } from '../controls/common';
 
 export { StateTransformParameters, TransformControlBase };
 
@@ -184,21 +184,21 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
     }
 
     renderApply() {
-        const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
+        // const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
         const canApply = this.canApply();
 
         return this.props.autoHideApply && !canApply
             ? null
             : <div className='msp-transform-apply-wrap'>
                 <button className='msp-btn msp-btn-block msp-form-control msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'><Icon name='cw' /></button>
-                {showBack && <button className='msp-btn msp-btn-block msp-form-control msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
+                {/* {showBack && <Button className='msp-btn msp-btn-block msp-form-control msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
                     <Icon name='back' /> Back
-                </button>}
-                <div className={`msp-transform-apply${!showBack ? ' msp-transform-apply-wider' : ''}`}>
-                    <button className={`msp-btn msp-btn-block msp-form-control msp-btn-commit msp-btn-commit-${canApply ? 'on' : 'off'}`} onClick={this.apply} disabled={!canApply}>
-                        {canApply && <Icon name='ok' />}
+                </Button>}
+                <div className={`msp-transform-apply${!showBack ? ' msp-transform-apply-wider' : ''}`}> */}
+                <div className={`msp-transform-apply-wider`}>
+                    <Button icon={canApply ? 'ok' : void 0} className={`msp-btn-commit msp-btn-commit-${canApply ? 'on' : 'off'}`} onClick={this.apply} disabled={!canApply}>
                         {this.props.applyLabel || this.applyText()}
-                    </button>
+                    </Button>
                 </div>
             </div>;
     }
@@ -223,10 +223,10 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
 
         const ctrl = <div className={wrapClass} style={{ marginBottom: this.props.noMargin ? 0 : void 0 }}>
             {display !== 'none' && !this.props.wrapInExpander && <div className='msp-transform-header'>
-                <button className={`msp-btn msp-btn-block msp-form-control`} onClick={this.toggleExpanded} title={display.description}>
+                <Button onClick={this.toggleExpanded} title={display.description}>
                     {!isEmpty && <Icon name={this.state.isCollapsed ? 'expand' : 'collapse'} />}
                     {display.name}
-                </button>
+                </Button>
             </div>}
             {!isEmpty && !this.state.isCollapsed && <>
                 <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
@@ -244,11 +244,10 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
     renderSimple() {
         const info = this.getInfo();
         const canApply = this.canApply();
-        const apply = <div className='msp-control-row msp-select-row'>
-            <button disabled={this.state.busy || !canApply} onClick={this.apply}>
-                <Icon name={this.props.simpleApply?.icon} />
+        const apply = <div className='msp-flex-row'>
+            <Button icon={this.props.simpleApply?.icon} disabled={this.state.busy || !canApply} onClick={this.apply}>
                 {this.props.simpleApply?.header}
-            </button>
+            </Button>
             {!info.isEmpty && <ToggleButton icon='cog' label='' title='Options' toggle={this.toggleExpanded} isSelected={!this.state.isCollapsed} disabled={this.state.busy} style={{ flex: '0 0 40px', padding: 0 }} />}
         </div>
 

+ 11 - 14
src/mol-plugin-ui/state/snapshots.tsx

@@ -13,10 +13,9 @@ import { ParameterControls } from '../controls/parameters';
 import { ParamDefinition as PD} from '../../mol-util/param-definition';
 import { PluginState } from '../../mol-plugin/state';
 import { urlCombine } from '../../mol-util/url';
-import { IconButton, SectionHeader } from '../controls/common';
+import { IconButton, SectionHeader, Button } from '../controls/common';
 import { formatTimespan } from '../../mol-util/now';
 import { PluginConfig } from '../../mol-plugin/config';
-import { Icon } from '../controls/icons';
 
 export class StateSnapshots extends PluginUIComponent<{ }> {
     downloadToFile = () => {
@@ -37,7 +36,7 @@ export class StateSnapshots extends PluginUIComponent<{ }> {
             {this.plugin.spec.components?.remoteState !== 'none' && <RemoteStateSnapshots />}
 
             <div className='msp-btn-row-group' style={{ marginTop: '10px' }}>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.downloadToFile}>Download JSON</button>
+                <Button onClick={this.downloadToFile}>Download JSON</Button>
                 <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
                     {'Open JSON'} <input onChange={this.open} type='file' multiple={false} accept='.json' />
                 </div>
@@ -95,10 +94,8 @@ class LocalStateSnapshots extends PluginUIComponent<
             }}/>
 
             <div className='msp-btn-row-group'>
-                {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='floppy' /> Save</button> */}
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Save</button>
-                {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button> */}
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
+                <Button onClick={this.add}>Save</Button>
+                <Button onClick={this.clear}>Clear</Button>
             </div>
         </div>;
     }
@@ -143,12 +140,12 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
         const current = this.plugin.state.snapshots.state.current;
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.state.snapshots.state.entries.map(e => <li key={e!.snapshot.id}>
-                <button data-id={e!.snapshot.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.apply}>
+                <Button data-id={e!.snapshot.id} onClick={this.apply}>
                     <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>
                         {e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>
                         {`${e!.snapshot.durationInMs ? formatTimespan(e!.snapshot.durationInMs, false) + `${e!.description ? ', ' : ''}` : ''}${e!.description ? e!.description : ''}`}
                     </small>
-                </button>
+                </Button>
                 <div>
                     <IconButton data-id={e!.snapshot.id} icon='up-thin' title='Move Up' onClick={this.moveUp} small={true} />
                     <IconButton data-id={e!.snapshot.id} icon='down-thin' title='Move Down' onClick={this.moveDown} small={true} />
@@ -278,8 +275,8 @@ export class RemoteStateSnapshots extends PluginUIComponent<
                     this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
                 }} isDisabled={this.state.isBusy}/>
                 <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isBusy}><Icon name='upload' /> Upload</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isBusy}>Refresh</button>
+                    <Button icon='upload' onClick={this.upload} disabled={this.state.isBusy}>Upload</Button>
+                    <Button onClick={this.refresh} disabled={this.state.isBusy}>Refresh</Button>
                 </div>
             </>}
 
@@ -291,7 +288,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
                     this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
                 }} isDisabled={this.state.isBusy}/>
                 <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isBusy}>Refresh</button>
+                    <Button onClick={this.refresh} disabled={this.state.isBusy}>Refresh</Button>
                 </div>
             </>}
         </>;
@@ -318,10 +315,10 @@ class RemoteStateSnapshotList extends PurePluginUIComponent<
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.props.entries.valueSeq().map(e =><li key={e!.id}>
-                <button data-id={e!.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.props.fetch}
+                <Button data-id={e!.id} onClick={this.props.fetch}
                     disabled={this.props.isBusy} onContextMenu={this.open} title='Click to download, right-click to open in a new tab.'>
                     {e!.name || new Date(e!.timestamp).toLocaleString()} <small>{e!.description}</small>
-                </button>
+                </Button>
                 {!e!.isSticky && this.props.remove && <div>
                     <IconButton data-id={e!.id} icon='remove' title='Remove' onClick={this.props.remove} disabled={this.props.isBusy} />
                 </div>}

+ 9 - 10
src/mol-plugin-ui/structure/components.tsx

@@ -12,8 +12,7 @@ import { State } from '../../mol-state';
 import { ParamDefinition } from '../../mol-util/param-definition';
 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 { ExpandGroup, IconButton, ToggleButton, Button } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 import { UpdateTransformControl } from '../state/update-transform';
 import { PluginContext } from '../../mol-plugin/context';
@@ -116,11 +115,11 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
             ? `Undo ${this.plugin.state.data.latestUndoLabel}`
             : 'Some mistakes of the past can be undone.';
         return <>
-            <div className='msp-control-row msp-select-row'>
+            <div className='msp-flex-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='' title='Options' style={{ flex: '0 0 40px', padding: 0 }} toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.isDisabled} />
-                <IconButton customClass='msp-flex-item' style={{ flex: '0 0 40px', padding: 0 }} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} icon='back' title={undoTitle} />
+                <IconButton className='msp-flex-item' flex='40px' onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} icon='back' title={undoTitle} />
             </div>
             {this.state.action === 'preset' && this.presetControls}
             {this.state.action === 'add' && <div className='msp-control-offset'>
@@ -166,9 +165,9 @@ class AddComponentControls extends PurePluginUIComponent<AddComponentControlsPro
     render() {
         return <>
             <ParameterControls params={this.state.params} values={this.state.values} onChangeValues={this.paramsChanged} />
-            <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-on`} onClick={this.apply} style={{ marginTop: '1px' }}>
-                <Icon name='plus' /> Create Selection
-            </button>
+            <Button icon='plus' className='msp-btn-commit msp-btn-commit-on' onClick={this.apply} style={{ marginTop: '1px' }}>
+                Create Selection
+            </Button>
         </>;
     }
 }
@@ -347,9 +346,9 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
                     {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', padding: 0 }} />
-                <IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} customClass='msp-form-control' style={{ flex: '0 0 32px', padding: 0 }} />
-                <IconButton onClick={this.toggleAction} icon='dot-3' title='Actions' toggleState={this.state.action === 'action'} customClass='msp-form-control' style={{ flex: '0 0 32px', padding: 0 }} />
+                <IconButton onClick={this.toggleVisible} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
+                <IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} className='msp-form-control' flex />
+                <IconButton onClick={this.toggleAction} icon='dot-3' title='Actions' toggleState={this.state.action === 'action'} className='msp-form-control' flex />
             </div>
             {this.state.action === 'remove' && <div style={{ marginBottom: '6px' }}>
                 <ActionMenu items={this.removeActions} onSelect={this.selectRemoveAction} />

+ 5 - 5
src/mol-plugin-ui/structure/focus.tsx

@@ -6,7 +6,7 @@
 
 import * as React from 'react';
 import { PluginUIComponent } from '../base';
-import { ToggleButton, IconButton } from '../controls/common';
+import { ToggleButton, IconButton, Button } from '../controls/common';
 import { ActionMenu } from '../controls/action-menu';
 import { StructureElement, StructureProperties, Structure } from '../../mol-model/structure';
 import { OrderedSet, SortedArray } from '../../mol-data/int';
@@ -186,12 +186,12 @@ export class StructureFocusControls extends PluginUIComponent<{}, StructureFocus
         }
 
         return <>
-            <div className='msp-control-row msp-select-row'>
-                <button className='msp-btn msp-btn-block msp-no-overflow' onClick={this.focus} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
+            <div className='msp-flex-row'>
+                <Button noOverflow onClick={this.focus} title={title} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights} disabled={this.isDisabled || !current}
                     style={{ textAlignLast: current ? 'left' : void 0 }}>
                     {label}
-                </button>
-                {current && <IconButton onClick={this.clear} icon='cancel' title='Clear' customClass='msp-form-control' style={{ flex: '0 0 32px', padding: 0 }} disabled={this.isDisabled} />}
+                </Button>
+                {current && <IconButton onClick={this.clear} icon='cancel' title='Clear' className='msp-form-control' flex disabled={this.isDisabled} />}
                 <ToggleButton icon='book-open' title='Select Target' toggle={this.toggleAction} isSelected={this.state.showAction} disabled={this.isDisabled} style={{ flex: '0 0 40px', padding: 0 }} />
             </div>
             {this.state.showAction && <ActionMenu items={this.actionItems} onSelect={this.selectAction} />}

+ 2 - 2
src/mol-plugin-ui/structure/generic.tsx

@@ -139,8 +139,8 @@ export class GenericEntry<T extends HierarchyRef> extends PurePluginUIComponent<
                 <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} <small>{description}</small>
                 </button>
-                <IconButton customClass='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!pivot.cell.state.isHidden} title={`${pivot.cell.state.isHidden ? 'Show' : 'Hide'}`} small style={{ flex: '0 0 32px', padding: 0 }} />
-                {refs.length === 1 && <IconButton customClass='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} style={{ flex: '0 0 32px', padding: 0 }} />}
+                <IconButton className='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!pivot.cell.state.isHidden} title={`${pivot.cell.state.isHidden ? 'Show' : 'Hide'}`} small flex />
+                {refs.length === 1 && <IconButton className='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} flex />}
             </div>
             {(refs.length === 1 && this.state.showOptions) && <>
                 <div className='msp-control-offset'>

+ 10 - 10
src/mol-plugin-ui/structure/measurements.tsx

@@ -15,7 +15,7 @@ import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-t
 import { FiniteArray } from '../../mol-util/type-helpers';
 import { PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
-import { ExpandGroup, IconButton, ToggleButton } from '../controls/common';
+import { ExpandGroup, IconButton, ToggleButton, Button } from '../controls/common';
 import { Icon } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 import { UpdateTransformControl } from '../state/update-transform';
@@ -138,12 +138,12 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
     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 msp-no-overflow' 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}>
+            <Button noOverflow 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', padding: 0 }} title={'Remove'} />
+            </Button>
+            {history.length > 1 && <IconButton small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} icon='up-thin' flex='20px' title={'Move up'} />}
+            {history.length > 1 && <IconButton small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'down')} icon='down-thin' flex='20px' title={'Move down'} />}
+            <IconButton small={true} className='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} icon='remove' flex title={'Remove'} />
         </div>;
     }
 
@@ -168,7 +168,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
 
     render() {
         return <>
-            <div className='msp-control-row msp-select-row'>
+            <div className='msp-flex-row'>
                 <ToggleButton icon='plus' label='Add' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} 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', padding: 0 }} />
             </div>
@@ -286,9 +286,9 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
                 <button className='msp-form-control msp-control-button-label msp-no-overflow' title='Click to focus. Hover to highlight.' onClick={this.focus} style={{ width: 'auto', textAlign: 'left' }}>
                     <span dangerouslySetInnerHTML={{ __html: this.label }} />
                 </button>
-                <IconButton small customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ flex: '0 0 32px', padding: 0 }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
-                <IconButton small customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ flex: '0 0 32px', padding: 0 }} title='Delete' />
-                <IconButton customClass='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' style={{ flex: '0 0 32px', padding: 0 }} title='Actions' toggleState={this.state.showUpdate} />
+                <IconButton small className='msp-form-control' onClick={this.toggleVisibility} icon='eye' flex title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
+                <IconButton small className='msp-form-control' onClick={this.delete} icon='remove' flex title='Delete' />
+                <IconButton className='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' flex title='Actions' toggleState={this.state.showUpdate} />
             </div>
             {this.state.showUpdate && <>
                 <ActionMenu items={this.actions} onSelect={this.selectAction} />

+ 9 - 10
src/mol-plugin-ui/structure/selection.tsx

@@ -16,8 +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 { ControlGroup, ToggleButton, IconButton } from '../controls/common';
-import { Icon } from '../controls/icons';
+import { ControlGroup, ToggleButton, IconButton, Button } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 import { StructureMeasurementsControls } from './measurements';
 
@@ -135,7 +134,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
 
     get controls() {
         return <>
-            <div className='msp-control-row msp-select-row'>
+            <div className='msp-flex-row'>
                 <ToggleButton icon='union' title={ActionHeader.get('add')} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
                 <ToggleButton icon='subtract' title={ActionHeader.get('remove')} toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} />
                 <ToggleButton icon='intersect' title={ActionHeader.get('intersect')} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
@@ -168,12 +167,12 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         return <>
             <ParameterControls params={StructureSelectionParams} values={this.values} onChangeValues={this.setProps} />
             {this.controls}
-            <div className='msp-control-row msp-select-row' style={{ margin: '6px 0' }}>
-                <button className='msp-btn msp-btn-block msp-no-overflow' onClick={this.focus} title='Click to Focus Selection' disabled={empty}
+            <div className='msp-flex-row' style={{ margin: '6px 0' }}>
+                <Button noOverflow onClick={this.focus} title='Click to Focus Selection' disabled={empty}
                     style={{ textAlignLast: !empty ? 'left' : void 0 }}>
                     {this.stats}
-                </button>
-                {!empty && <IconButton onClick={this.clear} icon='cancel' title='Clear' customClass='msp-form-control' style={{ flex: '0 0 32px', padding: 0 }} />}
+                </Button>
+                {!empty && <IconButton onClick={this.clear} icon='cancel' title='Clear' className='msp-form-control' flex />}
             </div>
             <StructureMeasurementsControls />
         </>
@@ -204,9 +203,9 @@ class ApplyColorControls extends PurePluginUIComponent<ApplyColorControlsProps,
     render() {
         return <>
             <ParameterControls params={this.params} values={this.state.values} onChangeValues={this.paramsChanged} />
-            <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-on`} onClick={this.apply} style={{ marginTop: '1px' }}>
-                <Icon name='brush' /> Apply Coloring
-            </button>
+            <Button icon='brush' className='msp-btn-commit msp-btn-commit-on' onClick={this.apply} style={{ marginTop: '1px' }}>
+                Apply Coloring
+            </Button>
         </>;
     }
 }

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

@@ -9,7 +9,7 @@ import * as React from 'react';
 import { HierarchyRef, ModelRef, TrajectoryRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { CollapsableControls, CollapsableState } from '../base';
 import { ActionMenu } from '../controls/action-menu';
-import { IconButton } from '../controls/common';
+import { IconButton, Button } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
@@ -252,10 +252,8 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
         const label = this.label;
         return <>
             <div className='msp-btn-row-group' style={{ marginTop: '1px' }}>
-                <button className='msp-btn msp-form-control msp-flex-item msp-no-overflow' onClick={this.toggleHierarchy} style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} disabled={disabled} title={label}>
-                    {label}
-                </button>
-                {presets.length > 0 && <IconButton customClass='msp-form-control' style={{ flex: '0 0 32px', padding: 0 }} onClick={this.togglePreset} icon='bookmarks' title='Presets' toggleState={this.state.show === 'presets'} disabled={disabled} />}
+                <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
+                {presets.length > 0 && <IconButton className='msp-form-control' flex onClick={this.togglePreset} icon='bookmarks' title='Presets' toggleState={this.state.show === 'presets'} disabled={disabled} />}
             </div>
             {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectHierarchy} multiselect />}
             {this.state.show === 'presets' && <ActionMenu items={presets} onSelect={this.applyPreset} />}

+ 4 - 3
src/mol-plugin-ui/viewport/help.tsx

@@ -11,6 +11,7 @@ import { StateTransformer, StateSelection } from '../../mol-state';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { Icon } from '../controls/icons';
+import { Button } from '../controls/common';
 
 function getBindingsList(bindings: { [k: string]: Binding }) {
     return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding])
@@ -38,7 +39,7 @@ export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]:
 
 class HelpText extends React.PureComponent {
     render() {
-        return <div className='msp-control-row msp-help-text'>
+        return <div className='msp-help-text'>
             <div>{this.props.children}</div>
         </div>
     }
@@ -55,10 +56,10 @@ class HelpGroup extends React.PureComponent<{ header: string, initiallyExpanded?
     render() {
         return <div className='msp-control-group-wrapper'>
             <div className='msp-control-group-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                <Button onClick={this.toggleExpanded}>
                     <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
                     {this.props.header}
-                </button>
+                </Button>
             </div>
             {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
                 {this.props.children}

+ 3 - 3
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -9,10 +9,10 @@ import * as React from 'react';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParameterControls } from '../controls/parameters';
 import { PluginUIComponent } from '../base';
-import { Icon } from '../controls/icons';
 import { debounceTime } from 'rxjs/operators';
 import { Subject } from 'rxjs';
 import { ViewportScreenshotHelper } from '../../mol-plugin/util/viewport-screenshot';
+import { Button } from '../controls/common';
 
 interface ImageControlsState {
     showPreview: boolean
@@ -125,8 +125,8 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
                 <span>Right-click the image to Copy.</span>
             </div>
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.download} disabled={this.state.isDisabled}><Icon name='download' /> Download</button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.openTab} disabled={this.state.isDisabled}><Icon name='export' /> Open in new Tab</button>
+                <Button icon='download' onClick={this.download} disabled={this.state.isDisabled}>Download</Button>
+                <Button icon='export' onClick={this.openTab} disabled={this.state.isDisabled}>Open in new Tab</Button>
             </div>
             <ParameterControls params={this.plugin.helpers.viewportScreenshot!.params} values={this.plugin.helpers.viewportScreenshot!.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
         </div>

+ 1 - 1
src/mol-plugin/behavior/dynamic/custom-props/rcsb/ui/assembly-symmetry.tsx

@@ -55,7 +55,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
     }
 
     renderNoSymmetries() {
-        return <div className='msp-control-row msp-row-text'>
+        return <div className='msp-row-text'>
             <div>No Symmetries for Assembly</div>
         </div>;
     }