diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss index fa12e3e5b73b69ecc9babf7765e1977bbf4fd96d..368fbf8e0f2d155e56eb8e43d0cf469d023ca909 100644 --- a/src/mol-plugin/skin/base/components/misc.scss +++ b/src/mol-plugin/skin/base/components/misc.scss @@ -100,4 +100,24 @@ outline: 1px solid color-increase-contrast($msp-form-control-background, 20%) !important; } } +} + +.msp-action-select { + position: relative; + + select { + padding-left: $control-spacing + $row-height; + } + + option:first-child { + color: color-lower-contrast($font-color, 15%); + } + + > .msp-icon { + display: block; + top: 0; + left: $control-spacing; + position: absolute; + line-height: $row-height; + } } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss index 38a1b4e38cf89464b5e973b5a68ea582b10ab355..7c820cdd81072724f8d7e61374954633ed39221a 100644 --- a/src/mol-plugin/skin/base/icons.scss +++ b/src/mol-plugin/skin/base/icons.scss @@ -223,4 +223,8 @@ .msp-icon-flow-cascade:before { content: "\e8d8"; +} + +.msp-icon-flow-tree:before { + content: "\e8da"; } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 6d283b897d89fb5667bb110900ec7cc5f00b3efb..72d0aeea3c78810cd8cf8dcb6d6de6a110276eb4 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -14,7 +14,7 @@ import { PluginContext } from '../context'; import { PluginReactContext, PluginUIComponent } from './base'; import { LociLabels, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, StructureToolsWrapper } from './controls'; import { StateSnapshots } from './state'; -import { StateObjectActions } from './state/actions'; +import { StateObjectActionSelect } from './state/actions'; import { StateTree } from './state/tree'; import { BackgroundTaskProgress } from './task'; import { Viewport, ViewportControls } from './viewport'; @@ -254,7 +254,11 @@ export class CurrentObject extends PluginUIComponent { </div> <UpdateTransformControl state={current.state} transform={transform} customHeader='none' /> </> } - {cell.status === 'ok' && <StateObjectActions state={current.state} nodeRef={ref} initiallyCollapsed />} + {cell.status === 'ok' && + <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} /> + } + + {/* <StateObjectActions state={current.state} nodeRef={ref} initiallyCollapsed />} */} </>; } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state/actions.tsx b/src/mol-plugin/ui/state/actions.tsx index 7ca58eb14bb953fc3375824e7f4c7fa6f1073e21..70a64f969c26314e75bc0c82052d56b02dda9c1b 100644 --- a/src/mol-plugin/ui/state/actions.tsx +++ b/src/mol-plugin/ui/state/actions.tsx @@ -7,8 +7,9 @@ import * as React from 'react'; import { PluginUIComponent } from '../base'; import { ApplyActionControl } from './apply-action'; -import { State } from '../../../mol-state'; +import { State, StateAction } from '../../../mol-state'; import { Icon } from '../controls/common'; +import { PluginContext } from '../../context'; export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRef: string, hideHeader?: boolean, initiallyCollapsed?: boolean }> { get current() { @@ -16,9 +17,9 @@ export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRe } componentDidMount() { - this.subscribe(this.plugin.state.behavior.currentObject, o => { - this.forceUpdate(); - }); + // this.subscribe(this.plugin.state.behavior.currentObject, o => { + // this.forceUpdate(); + // }); this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { const current = this.current; @@ -41,4 +42,77 @@ export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRe {actions.map((act, i) => <ApplyActionControl plugin={this.plugin} key={`${act.id}`} state={state} action={act} nodeRef={ref} initiallyCollapsed={this.props.initiallyCollapsed} />)} </div>; } +} + +interface StateObjectActionSelectProps { + plugin: PluginContext, + state: State, + nodeRef: string +} + +interface StateObjectActionSelectState { + state: State, + nodeRef: string, + version: string, + actions: readonly StateAction[], + currentActionIndex: number +} + +function createStateObjectActionSelectState(props: StateObjectActionSelectProps): StateObjectActionSelectState { + const cell = props.state.cells.get(props.nodeRef)!; + const actions = props.state.actions.fromCell(cell, props.plugin); + (actions as StateAction[]).sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1); + return { + state: props.state, + nodeRef: props.nodeRef, + version: cell.transform.version, + actions, + currentActionIndex: -1 + } +} + +export class StateObjectActionSelect extends PluginUIComponent<StateObjectActionSelectProps, StateObjectActionSelectState> { + state = createStateObjectActionSelectState(this.props); + + get current() { + return this.plugin.state.behavior.currentObject.value; + } + + static getDerivedStateFromProps(props: StateObjectActionSelectProps, state: StateObjectActionSelectState) { + if (state.state !== props.state || state.nodeRef !== props.nodeRef) return createStateObjectActionSelectState(props); + const cell = props.state.cells.get(props.nodeRef)!; + if (cell.transform.version !== state.version) return createStateObjectActionSelectState(props); + return null; + } + + componentDidMount() { + this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { + const current = this.current; + if (current.ref !== ref || current.state !== state) return; + this.setState(createStateObjectActionSelectState(this.props)); + }); + } + + onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { + this.setState({ currentActionIndex: parseInt(e.target.value, 10) }); + } + + render() { + const actions = this.state.actions; + if (actions.length === 0) return null; + + const current = this.state.currentActionIndex >= 0 && actions[this.state.currentActionIndex]; + const title = current ? current.definition.display.description : 'Select Action'; + + return <> + <div className='msp-contol-row msp-action-select'> + <select className='msp-form-control' title={title} value={this.state.currentActionIndex} onChange={this.onChange} style={{ fontWeight: 'bold' }}> + <option key={-1} value={-1} style={{ color: '#999' }}>[ Select Action ]</option> + {actions.map((a, i) => <option key={i} value={i}>{a.definition.display.name}</option>)} + </select> + <Icon name='flow-tree' /> + </div> + {current && <ApplyActionControl key={current.id} plugin={this.plugin} state={this.props.state} action={current} nodeRef={this.props.nodeRef} hideHeader />} + </>; + } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx index fe4dfd1fc4021f2e0faed6e0068a40952b618700..dea9d161387995309a09faca71290714ef05ac27 100644 --- a/src/mol-plugin/ui/state/apply-action.tsx +++ b/src/mol-plugin/ui/state/apply-action.tsx @@ -19,6 +19,7 @@ namespace ApplyActionControl { nodeRef: StateTransform.Ref, state: State, action: StateAction, + hideHeader?: boolean, initiallyCollapsed?: boolean } @@ -42,7 +43,7 @@ class ApplyActionControl extends TransformControlBase<ApplyActionControl.Props, } getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); } getTransformerId() { return this.props.state.transforms.get(this.props.nodeRef).transformer.id; } - getHeader() { return this.props.action.definition.display; } + getHeader() { return this.props.hideHeader ? 'none' : this.props.action.definition.display; } canApply() { return !this.state.error && !this.state.busy; } canAutoApply() { return false; } applyText() { return 'Apply'; }