diff --git a/src/mol-plugin/skin/base/components/controls-base.scss b/src/mol-plugin/skin/base/components/controls-base.scss index e6cc0adb87da314c8104fd0cec10cbc248aaa496..8cb9afcc63cb3de3c1afa3770d190bb7de0eda93 100644 --- a/src/mol-plugin/skin/base/components/controls-base.scss +++ b/src/mol-plugin/skin/base/components/controls-base.scss @@ -11,20 +11,30 @@ } .msp-btn-icon { + border: none; height: $row-height; width: $row-height; line-height: $row-height; padding: 0; text-align: center; + + &[disabled], &[disabled]:hover, &[disabled]:active { + color: $msp-btn-link-toggle-off-font-color; + } } .msp-btn-icon-small { + border: none; height: $row-height; width: 20px; font-size: 85%; line-height: $row-height; padding: 0; text-align: center; + + &[disabled], &[disabled]:hover, &[disabled]:active { + color: $msp-btn-link-toggle-off-font-color; + } } .msp-btn-link { diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss index f75ff2101ae6bc1261b1fb69f418f0875365c3b2..db173d4fcd351d6081a260b76a4d5ee6def2c62f 100644 --- a/src/mol-plugin/skin/base/components/misc.scss +++ b/src/mol-plugin/skin/base/components/misc.scss @@ -68,6 +68,6 @@ display: block; } -.msp-animation-section { +.msp-contols-section { margin-bottom: $control-spacing; } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 27713d16f31f81a20c6a1c16ebdb0e356c787f5f..13493870b4a06372528c64dfb17cded646da28c1 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -159,10 +159,11 @@ line-height: $row-height; float: left; margin-right: $control-spacing; + background-color: $msp-form-control-background; > span { color: $font-color; - padding-top: 1px; + margin-right: $control-spacing; font-size: 85%; display: inline-block; } @@ -172,12 +173,33 @@ line-height: $row-height; float: left; margin-right: $control-spacing; + background-color: $msp-form-control-background; > select { display: inline-block; width: 200px; } } + + .msp-animation-viewport-controls { + line-height: $row-height; + float: left; + margin-right: $control-spacing; + position: relative; + background-color: $msp-form-control-background; + + .msp-animation-viewport-controls-select { + width: 290px; + position: absolute; + left: 0; + top: $row-height + $control-spacing; + background: $control-background; + + .msp-control-row:first-child { + margin-top: 0; + } + } + } } .msp-param-object-list-item { diff --git a/src/mol-plugin/skin/base/components/viewport.scss b/src/mol-plugin/skin/base/components/viewport.scss index 768d8a12fc78587921e142ac9f6c710e54cf10fa..228e721a5d122265db9f71a2abea5b487b666c19 100644 --- a/src/mol-plugin/skin/base/components/viewport.scss +++ b/src/mol-plugin/skin/base/components/viewport.scss @@ -65,6 +65,10 @@ .msp-viewport-controls-scene-options { width: 290px; background: $control-background; + + .msp-control-group-wrapper:first-child { + padding-top: 0; + } } /* highlight */ diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss index 8f35f30ba64d1d1722b8920aefeb5590d244b406..d02b91f03929378521666f93e8facfff2fba06ac 100644 --- a/src/mol-plugin/skin/base/icons.scss +++ b/src/mol-plugin/skin/base/icons.scss @@ -184,4 +184,12 @@ .msp-icon-right-open:before { content: "\e87d"; +} + +.msp-icon-cw:before { + content: "\e890"; +} + +.msp-icon-database:before { + content: "\e8d3"; } \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index ded8952d4b0ddf6ca6dde5ee17c2cf5fea336ec0..701b5a3d1a8e198fa8e4e9399cb3c36e18e0fa11 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -14,10 +14,9 @@ import { PluginStateObject } from 'mol-plugin/state/objects'; import { StateTransforms } from 'mol-plugin/state/transforms'; import { StateTransformer } from 'mol-state'; import { ModelFromTrajectory } from 'mol-plugin/state/transforms/model'; -import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; -import { ParamDefinition } from 'mol-util/param-definition'; +import { AnimationControls } from './state/animation'; -export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, label: string }> { +export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> { state = { show: false, label: '' } private update = () => { @@ -76,18 +75,18 @@ export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, l action: UpdateTrajectory.create({ action: 'advance', by: 1 }) }); - stopAnimation = () => { - this.plugin.state.animation.stop(); - } + // stopAnimation = () => { + // this.plugin.state.animation.stop(); + // } - playAnimation = () => { - const anim = this.plugin.state.animation; - if (anim.state.params.current === AnimateModelIndex.name) { - anim.start(); - } else { - anim.play(AnimateModelIndex, ParamDefinition.getDefaultValues(AnimateModelIndex.params(this.plugin) as any as ParamDefinition.Params)) - } - } + // playAnimation = () => { + // const anim = this.plugin.state.animation; + // if (anim.state.params.current === AnimateModelIndex.name) { + // anim.start(); + // } else { + // anim.play(AnimateModelIndex, ParamDefinition.getDefaultValues(AnimateModelIndex.params(this.plugin) as any as ParamDefinition.Params)) + // } + // } render() { if (!this.state.show) return null; @@ -95,7 +94,7 @@ export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, l const isAnimating = this.plugin.behaviors.state.isAnimating.value; return <div className='msp-traj-controls'> - <IconButton icon={isAnimating ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Play'} onClick={isAnimating ? this.stopAnimation : this.playAnimation} /> + {/* <IconButton icon={isAnimating ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Play'} onClick={isAnimating ? this.stopAnimation : this.playAnimation} /> */} <IconButton icon='model-first' title='First Model' onClick={this.reset} disabled={isAnimating} /> <IconButton icon='model-prev' title='Previous Model' onClick={this.prev} disabled={isAnimating} /> <IconButton icon='model-next' title='Next Model' onClick={this.next} disabled={isAnimating} /> @@ -152,16 +151,43 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus const current = snapshots.state.current; const isPlaying = snapshots.state.isPlaying; - // TODO: better handle disabled state - return <div className='msp-state-snapshot-viewport-controls'> <select className='msp-form-control' value={current || 'none'} onChange={this.change} disabled={this.state.isBusy || isPlaying}> {!current && <option key='none' value='none'></option>} {snapshots.state.entries.valueSeq().map((e, i) => <option key={e!.snapshot.id} value={e!.snapshot.id}>{`[${i! + 1}/${count}]`} {e!.name || new Date(e!.timestamp).toLocaleString()}</option>)} </select> - <IconButton icon='left-open' title='Previous State' onClick={this.prev} disabled={this.state.isBusy || isPlaying} /> - <IconButton icon='right-open' title='Next State' onClick={this.next} disabled={this.state.isBusy || isPlaying} /> - <IconButton icon={isPlaying ? 'pause' : 'play'} title={isPlaying ? 'Pause' : 'Play'} onClick={this.togglePlay} /> + <IconButton icon={isPlaying ? 'pause' : 'play'} title={isPlaying ? 'Pause' : 'Cycle States'} onClick={this.togglePlay} + disabled={isPlaying ? false : this.state.isBusy} /> + {!isPlaying && <> + <IconButton icon='left-open' title='Previous State' onClick={this.prev} disabled={this.state.isBusy || isPlaying} /> + <IconButton icon='right-open' title='Next State' onClick={this.next} disabled={this.state.isBusy || isPlaying} /> + </>} + </div>; + } +} + +export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty: boolean, isExpanded: boolean, isUpdating: boolean, isAnimating: boolean, isPlaying: boolean }> { + state = { isEmpty: true, isExpanded: false, isUpdating: false, isAnimating: false, isPlaying: false }; + + componentDidMount() { + this.subscribe(this.plugin.state.snapshots.events.changed, () => this.setState({ isPlaying: this.plugin.state.snapshots.state.isPlaying })); + this.subscribe(this.plugin.behaviors.state.isUpdating, isUpdating => this.setState({ isUpdating, isEmpty: this.plugin.state.dataState.tree.transforms.size < 2 })); + this.subscribe(this.plugin.behaviors.state.isAnimating, isAnimating => this.setState({ isAnimating })); + } + toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); + stop = () => this.plugin.state.animation.stop(); + + render() { + // if (!this.state.show) return null; + const isAnimating = this.state.isAnimating; + + return <div className='msp-animation-viewport-controls'> + <IconButton icon={isAnimating ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Select Animation'} + onClick={isAnimating ? this.stop : this.toggleExpanded} + disabled={isAnimating ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} /> + {(this.state.isExpanded && !this.state.isUpdating) && <div className='msp-animation-viewport-controls-select'> + <AnimationControls onStart={this.toggleExpanded} /> + </div>} </div>; } } diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 7bbb92d9156ebbf3648cd000a5d4130c07b4b00b..eea9c464af6de2f2299eea037c168a5ef5859843 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -81,6 +81,12 @@ export class NumericInput extends React.PureComponent<{ } } +export function Icon(props: { + name: string +}) { + return <span className={`msp-icon msp-icon-${props.name}`} />; +} + export function IconButton(props: { icon: string, isSmall?: boolean, @@ -90,7 +96,7 @@ export function IconButton(props: { disabled?: boolean, 'data-id'?: string }) { - let className = `msp-btn msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}`; + let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}`; if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}` return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']}> <span className={`msp-icon msp-icon-${props.icon}`}/> diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 4641dc353e15196123d5196471ce435db35e3ef5..a2fd3295f695074dffe4f292115b0547c604d6d3 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -11,10 +11,9 @@ import { LogEntry } from 'mol-util/log-entry'; import * as React from 'react'; import { PluginContext } from '../context'; import { PluginReactContext, PluginUIComponent } from './base'; -import { LociLabelControl, TrajectoryControls, StateSnapshotViewportControls } from './controls'; +import { LociLabelControl, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls } from './controls'; import { StateSnapshots } from './state'; import { StateObjectActions } from './state/actions'; -import { AnimationControls } from './state/animation'; import { StateTree } from './state/tree'; import { BackgroundTaskProgress } from './task'; import { Viewport, ViewportControls } from './viewport'; @@ -73,7 +72,7 @@ export class ControlsWrapper extends PluginUIComponent { render() { return <div className='msp-scrollable-container msp-right-controls'> <CurrentObject /> - <AnimationControls /> + {/* <AnimationControlsWrapper /> */} {/* <CameraSnapshots /> */} <StateSnapshots /> </div>; @@ -85,7 +84,8 @@ export class ViewportWrapper extends PluginUIComponent { return <> <Viewport /> <div className='msp-viewport-top-left-controls'> - <TrajectoryControls /> + <AnimationViewportControls /> + <TrajectoryViewportControls /> <StateSnapshotViewportControls /> </div> <ViewportControls /> @@ -111,8 +111,12 @@ export class State extends PluginUIComponent { const kind = this.plugin.state.behavior.kind.value; return <div className='msp-scrollable-container'> <div className='msp-btn-row-group msp-data-beh'> - <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}>Data</button> - <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>Behavior</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}> + <span className='msp-icon msp-icon-database' /> Data + </button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}> + <span className='msp-icon msp-icon-tools' /> Behavior + </button> </div> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> </div> diff --git a/src/mol-plugin/ui/state/animation.tsx b/src/mol-plugin/ui/state/animation.tsx index 64ebcc6c01f8474456d893097042057ca951cc17..c9c9e79d7c9326969abe0c4fdf8d572747fb2a80 100644 --- a/src/mol-plugin/ui/state/animation.tsx +++ b/src/mol-plugin/ui/state/animation.tsx @@ -7,8 +7,20 @@ import * as React from 'react'; import { PluginUIComponent } from '../base'; import { ParameterControls, ParamOnChange } from '../controls/parameters'; +import { Icon } from '../controls/common'; -export class AnimationControls extends PluginUIComponent<{ }> { +export class AnimationControlsWrapper extends PluginUIComponent<{ }> { + render() { + const anim = this.plugin.state.animation; + if (anim.isEmpty) return null; + return <div className='msp-contols-section'> + <div className='msp-section-header'>Animations</div> + <AnimationControls /> + </div> + } +} + +export class AnimationControls extends PluginUIComponent<{ onStart?: () => void }> { componentDidMount() { this.subscribe(this.plugin.state.animation.events.updated, () => this.forceUpdate()); } @@ -24,7 +36,10 @@ export class AnimationControls extends PluginUIComponent<{ }> { startOrStop = () => { const anim = this.plugin.state.animation; if (anim.state.animationState === 'playing') anim.stop(); - else anim.start(); + else { + if (this.props.onStart) this.props.onStart(); + anim.start(); + } } render() { @@ -33,17 +48,16 @@ export class AnimationControls extends PluginUIComponent<{ }> { const isDisabled = anim.state.animationState === 'playing'; - return <div className='msp-animation-section'> - <div className='msp-section-header'>Animations</div> - + return <> <ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} /> <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' />} {anim.state.animationState === 'playing' ? 'Stop' : 'Start'} </button> </div> - </div> + </>; } } \ No newline at end of file diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 6cc2eea162c2ab17fa51773c63738892fa6318a3..859d1e421ad81d05154e84f66728cebdb638ed27 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -18,7 +18,7 @@ interface ViewportState { noWebGl: boolean } -export class ViewportControls extends PluginUIComponent { +export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded: boolean }> { state = { isSettingsExpanded: false } @@ -70,8 +70,7 @@ export class ViewportControls extends PluginUIComponent { {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br /> {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)} </div> - {this.state.isSettingsExpanded && - <div className='msp-viewport-controls-scene-options'> + {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'> <ControlGroup header='Layout' initialExpanded={true}> <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} /> </ControlGroup>