diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index c164a774e9bd74afdf6dabba83ff52a6250938cc..506dc4899112a82cdec5c964373bc9b57c8e2abb 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -53,4 +53,55 @@ right: 0; top: 0; width: $row-height; +} + +.msp-tree-row { + position: relative; + height: $row-height; + line-height: $row-height; + background: color-lower-contrast($control-background, 4%); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 1px; + padding-left: $row-height; + padding-right: 2 * $row-height + $control-spacing; + border-bottom-left-radius: $control-spacing; + + &-current { + background: $control-background + } +} + +.msp-tree-remove-button { + position: absolute; + right: $row-height; + top: 0; + width: $row-height; + font-size: 80%; + color: color-lower-contrast($font-color, 24%); +} + +.msp-tree-toggle-exp-button { + position: absolute; + left: 0; + top: 0; + width: $row-height; + color: color-lower-contrast($font-color, 24%); +} + +.msp-tree-visibility { + position: absolute; + right: 0; + top: 0; + width: $row-height; + font-size: 80%; + + &-hidden { + color: color-lower-contrast($font-color, 36%); + } +} + +.msp-tree-children { + margin-left: $control-spacing; } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/transformer.scss b/src/mol-plugin/skin/base/components/transformer.scss index 6d4d8356f05e4a2252d7eb9efc7f7c1f885fe3d2..cf4c5ff797bbe54ad284791e141e41d9caaad4cf 100644 --- a/src/mol-plugin/skin/base/components/transformer.scss +++ b/src/mol-plugin/skin/base/components/transformer.scss @@ -84,4 +84,8 @@ left: $control-label-width + $control-spacing; right: 0; top: 0; +} + +.msp-data-beh { + margin: $control-spacing 0 !important; } \ No newline at end of file diff --git a/src/mol-plugin/ui/camera.tsx b/src/mol-plugin/ui/camera.tsx index 35a874cad8761b65af1d46fd50e9f03cda9f9633..ddcf5995d5fff6c62de49ea2e04d0f150da79a69 100644 --- a/src/mol-plugin/ui/camera.tsx +++ b/src/mol-plugin/ui/camera.tsx @@ -67,7 +67,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> {this.plugin.state.cameraSnapshots.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)} style={{ float: 'right' }} className='msp-btn msp-btn-link msp-state-list-remove-button'> + <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> </button> </li>)} diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 610b37c92f2b55f2c71a4045e494f47c30b41e83..b3e2426fd3d4ce49f3877e2c60a77488cd05ef48 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -20,19 +20,18 @@ export class Controls extends PluginComponent<{ }, { }> { export class TrajectoryControls extends PluginComponent { render() { return <div> - <b>Trajectory: </b> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { state: this.plugin.state.dataState, action: UpdateTrajectory.create({ action: 'advance', by: -1 }) - })}><<</button> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + })}>◀</button> + <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { state: this.plugin.state.dataState, action: UpdateTrajectory.create({ action: 'reset' }) - })}>Reset</button> - <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + })}>↻</button> + <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { state: this.plugin.state.dataState, action: UpdateTrajectory.create({ action: 'advance', by: +1 }) - })}>>></button><br /> + })}>►</button><br /> </div> } } \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 409420a21cc46a98cfcba4f715d5be153c6d57d9..4e8154c03f51f4c123e953bc88218b922a96a528 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -59,7 +59,7 @@ export class ViewportWrapper extends PluginComponent { <TrajectoryControls /> </div> <ViewportControls /> - <div style={{ position: 'absolute', left: '10px', bottom: '10px', color: 'white' }}> + <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}> <BackgroundTaskProgress /> </div> </>; @@ -79,8 +79,10 @@ export class State extends PluginComponent { render() { const kind = this.plugin.state.behavior.kind.value; return <> - <button onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> - <button onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button> + <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> + </div> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> </> } diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 9634c8dcf48433cf512bde18b2efb03cf213a4f6..bd7a17f7a9382607324d0af325aa7b9dd62099ee 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -58,24 +58,15 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { }); } - toggleExpanded = (e: React.MouseEvent<HTMLElement>) => { - e.preventDefault(); - PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); - } - render() { const cellState = this.cellState; - const expander = <> - [<a href='#' onClick={this.toggleExpanded}>{cellState.isCollapsed ? '+' : '-'}</a>] - </>; - const children = this.props.state.tree.children.get(this.props.nodeRef); return <div> - {children.size === 0 ? void 0 : expander} <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} /> + <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} /> {children.size === 0 ? void 0 - : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999', display: cellState.isCollapsed ? 'none' : 'block' }}> + : <div className='msp-tree-children' style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)} </div> } @@ -103,7 +94,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State } } else if (isCurrent) { isCurrent = false; - // have to check the node wasn't remove + // have to check the node wasn't removed if (e.state.transforms.has(this.props.nodeRef)) this.forceUpdate(); } }); @@ -122,6 +113,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State toggleVisible = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + e.currentTarget.blur(); + } + + toggleExpanded = (e: React.MouseEvent<HTMLElement>) => { + e.preventDefault(); + PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + e.currentTarget.blur(); } render() { @@ -130,22 +128,35 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State const isCurrent = this.is(this.props.state.behaviors.currentObject.value); - const remove = <>[<a href='#' onClick={this.remove}>X</a>]</> let label: any; if (cell.status !== 'ok' || !cell.obj) { const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name; - label = <><b>{cell.status}</b> <a href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>; + const title = `${cell.errorText}` + label = <><b>{cell.status}</b> <a title={title} href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>; } else { const obj = cell.obj as PluginStateObject.Any; - label = <><a href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>; + const title = `${obj.label} ${obj.description ? obj.description : ''}` + label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>; } + const children = this.props.state.tree.children.get(this.props.nodeRef); const cellState = this.props.state.cellStates.get(this.props.nodeRef); - const visibility = <>[<a href='#' onClick={this.toggleVisible}>{cellState.isHidden ? 'H' : 'V'}</a>]</>; - return <> - {remove}{visibility} {isCurrent ? <b>{label}</b> : label} - </>; + const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'> + <span className='msp-icon msp-icon-remove' /> + </button>; + + const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}> + <span className='msp-icon msp-icon-visual-visibility' /> + </button>; + + return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`}> + {isCurrent ? <b>{label}</b> : label} + {children.size > 0 && <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'> + <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} /> + </button>} + {remove}{visibility} + </div> } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index 624175b93b97a26acc2582aad778fe65dff0e2b8..031a3a54b572f8b16681f5652bf43366cb9648e1 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/src/mol-plugin/ui/state.tsx @@ -12,9 +12,10 @@ import { List } from 'immutable'; import { LogEntry } from 'mol-util/log-entry'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; +import { Subject } from 'rxjs'; export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { - state = { serverUrl: 'http://webchem.ncbr.muni.cz/molstar-state' } + state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' } updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) }; @@ -28,13 +29,16 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> } } +// TODO: this is not nice: device some custom event system. +const UploadedEvent = new Subject(); + class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false }; static Params = { name: PD.Text(), description: PD.Text(), - serverUrl: PD.Text('http://webchem.ncbr.muni.cz/molstar-state') + serverUrl: PD.Text() } add = () => { @@ -54,6 +58,8 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC this.setState({ isUploading: true }); await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl }); this.setState({ isUploading: false }); + this.plugin.log(LogEntry.message('Snapshot uploaded.')); + UploadedEvent.next(); } render() { @@ -91,7 +97,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> {this.plugin.state.snapshots.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)} style={{ float: 'right' }} className='msp-btn msp-btn-link msp-state-list-remove-button'> + <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> </button> </li>)} @@ -99,13 +105,14 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { } } -type RemoteEntry = { url: string, timestamp: number, id: string, name: string, description: string } +type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string } class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { state = { entries: List<RemoteEntry>(), isFetching: false }; componentDidMount() { this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); this.refresh(); + this.subscribe(UploadedEvent, this.refresh); } refresh = async () => { @@ -113,7 +120,13 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e this.setState({ isFetching: true }); const req = await fetch(`${this.props.serverUrl}/list`); const json: RemoteEntry[] = await req.json(); - this.setState({ entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({ ...e, url: `${this.props.serverUrl}/get/${e.id}` }))), isFetching: false }) + this.setState({ + entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({ + ...e, + url: `${this.props.serverUrl}/get/${e.id}`, + removeUrl: `${this.props.serverUrl}/remove/${e.id}` + }))), + isFetching: false }) } catch (e) { this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e)); this.setState({ entries: List<RemoteEntry>(), isFetching: false }) @@ -124,6 +137,16 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url }); } + remove(url: string) { + return async () => { + this.setState({ entries: List() }); + try { + await fetch(url); + } catch { } + this.refresh(); + } + } + render() { return <div> <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button> @@ -131,6 +154,9 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e <ul style={{ listStyle: 'none' }} className='msp-state-list'> {this.state.entries.valueSeq().map(e =><li key={e!.id}> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.fetch(e!.url)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button> + <button onClick={this.remove(e!.removeUrl)} className='msp-btn msp-btn-link msp-state-list-remove-button'> + <span className='msp-icon msp-icon-remove' /> + </button> </li>)} </ul> </div>; diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 5424d514b7d005bb5adadad8ebfa89b679502eca..6d0d5ed973e84e4274762b9befa1ba7f52487ad5 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -22,7 +22,7 @@ export class ViewportControls extends PluginComponent { render() { return <div style={{ position: 'absolute', right: '10px', top: '10px', height: '100%', color: 'white' }}> - <button onClick={this.resetCamera}>Reset Camera</button> + <button className='msp-btn msp-btn-link' onClick={this.resetCamera}>↻ Camera</button> </div> } }