瀏覽代碼

mol-plugin-ui: added Button common ctrl

David Sehnal 5 年之前
父節點
當前提交
1aace4a26f

+ 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>;
     }

+ 12 - 17
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)
@@ -185,18 +185,15 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
         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'} />
+            <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[] {

+ 3 - 4
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 } from './common';
 import { DefaultColorSwatch } from '../../mol-util/color/swatches';
 
 export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
@@ -43,8 +43,7 @@ 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>;
     }
 
@@ -54,7 +53,7 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
             <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>
+                    <Button onClick={this.toggleExpanded} inline className='msp-combined-color-button' style={{ background: Color.toStyle(this.props.value) }} />
                 </div>
             </div>
             {this.state.isExpanded && <div className='msp-control-offset'>

+ 55 - 34
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;
@@ -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,
@@ -256,6 +255,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,13 +310,13 @@ export function IconButton(props: {
     title?: string,
     toggleState?: boolean,
     disabled?: boolean,
-    customClass?: string,
+    className?: string,
     style?: React.CSSProperties,
     'data-id'?: string,
     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'}`
     }
@@ -286,38 +333,12 @@ export function IconButton(props: {
         else style = props.style;
     }
 
-    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']} style={props.style}>
+    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 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 = '_'
-    }
-
-    render() {
-        return <select value='_' onChange={this.onChange} disabled={this.props.disabled}>
-            <option key='_' value='_'>{this.props.label}</option>
-            {this.props.children}
-        </select>
-    }
-}
-
-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>
-}
-
 export type ToggleButtonProps = {
     style?: React.CSSProperties,
     className?: string,

+ 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({

+ 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;
 }

+ 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>
         </>;
     }

+ 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>}

+ 8 - 9
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';
@@ -120,7 +119,7 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
                 <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' flex='40px' 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' flex />
-                <IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} customClass='msp-form-control' flex />
-                <IconButton onClick={this.toggleAction} icon='dot-3' title='Actions' toggleState={this.state.action === 'action'} customClass='msp-form-control' flex />
+                <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} />

+ 4 - 4
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';
@@ -187,11 +187,11 @@ 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}
+                <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' flex 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 flex />
-                {refs.length === 1 && <IconButton customClass='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} flex />}
+                <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'>

+ 9 - 9
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' flex='20px' title={'Move up'} />}
-            {history.length > 1 && <IconButton small={true} customClass='msp-form-control' onClick={() => this.moveHistory(e, 'down')} icon='down-thin' flex='20px' title={'Move down'} />}
-            <IconButton small={true} customClass='msp-form-control' onClick={() => this.plugin.managers.structure.selection.modifyHistory(e, 'remove')} icon='remove' flex 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>;
     }
 
@@ -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' flex title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
-                <IconButton small customClass='msp-form-control' onClick={this.delete} icon='remove' flex title='Delete' />
-                <IconButton customClass='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' flex 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} />

+ 7 - 8
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';
 
@@ -169,11 +168,11 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             <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}
+                <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' flex />}
+                </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' flex 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} />}

+ 3 - 2
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])
@@ -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>