Skip to content
Snippets Groups Projects
Commit fca68b68 authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: ui

parent a00e766e
No related branches found
No related tags found
No related merge requests found
...@@ -53,4 +53,55 @@ ...@@ -53,4 +53,55 @@
right: 0; right: 0;
top: 0; top: 0;
width: $row-height; 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
...@@ -84,4 +84,8 @@ ...@@ -84,4 +84,8 @@
left: $control-label-width + $control-spacing; left: $control-label-width + $control-spacing;
right: 0; right: 0;
top: 0; top: 0;
}
.msp-data-beh {
margin: $control-spacing 0 !important;
} }
\ No newline at end of file
...@@ -67,7 +67,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> { ...@@ -67,7 +67,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> {
return <ul style={{ listStyle: 'none' }} className='msp-state-list'> return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
{this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}> {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 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' /> <span className='msp-icon msp-icon-remove' />
</button> </button>
</li>)} </li>)}
......
...@@ -20,19 +20,18 @@ export class Controls extends PluginComponent<{ }, { }> { ...@@ -20,19 +20,18 @@ export class Controls extends PluginComponent<{ }, { }> {
export class TrajectoryControls extends PluginComponent { export class TrajectoryControls extends PluginComponent {
render() { render() {
return <div> return <div>
<b>Trajectory: </b> <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
<button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
state: this.plugin.state.dataState, state: this.plugin.state.dataState,
action: UpdateTrajectory.create({ action: 'advance', by: -1 }) action: UpdateTrajectory.create({ action: 'advance', by: -1 })
})}>&lt;&lt;</button> })}></button>
<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, state: this.plugin.state.dataState,
action: UpdateTrajectory.create({ action: 'reset' }) action: UpdateTrajectory.create({ action: 'reset' })
})}>Reset</button> })}></button>
<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, state: this.plugin.state.dataState,
action: UpdateTrajectory.create({ action: 'advance', by: +1 }) action: UpdateTrajectory.create({ action: 'advance', by: +1 })
})}>&gt;&gt;</button><br /> })}></button><br />
</div> </div>
} }
} }
\ No newline at end of file
...@@ -59,7 +59,7 @@ export class ViewportWrapper extends PluginComponent { ...@@ -59,7 +59,7 @@ export class ViewportWrapper extends PluginComponent {
<TrajectoryControls /> <TrajectoryControls />
</div> </div>
<ViewportControls /> <ViewportControls />
<div style={{ position: 'absolute', left: '10px', bottom: '10px', color: 'white' }}> <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
<BackgroundTaskProgress /> <BackgroundTaskProgress />
</div> </div>
</>; </>;
...@@ -79,8 +79,10 @@ export class State extends PluginComponent { ...@@ -79,8 +79,10 @@ export class State extends PluginComponent {
render() { render() {
const kind = this.plugin.state.behavior.kind.value; const kind = this.plugin.state.behavior.kind.value;
return <> return <>
<button onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> <div className='msp-btn-row-group msp-data-beh'>
<button 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'}}>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} /> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
</> </>
} }
......
...@@ -58,24 +58,15 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { ...@@ -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() { render() {
const cellState = this.cellState; 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); const children = this.props.state.tree.children.get(this.props.nodeRef);
return <div> 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 {children.size === 0
? void 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} />)} {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}
</div> </div>
} }
...@@ -103,7 +94,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State ...@@ -103,7 +94,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
} }
} else if (isCurrent) { } else if (isCurrent) {
isCurrent = false; 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(); if (e.state.transforms.has(this.props.nodeRef)) this.forceUpdate();
} }
}); });
...@@ -122,6 +113,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State ...@@ -122,6 +113,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
toggleVisible = (e: React.MouseEvent<HTMLElement>) => { toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault(); e.preventDefault();
PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); 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() { render() {
...@@ -130,22 +128,35 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State ...@@ -130,22 +128,35 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
const isCurrent = this.is(this.props.state.behaviors.currentObject.value); const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
const remove = <>[<a href='#' onClick={this.remove}>X</a>]</>
let label: any; let label: any;
if (cell.status !== 'ok' || !cell.obj) { if (cell.status !== 'ok' || !cell.obj) {
const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name; 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 { } else {
const obj = cell.obj as PluginStateObject.Any; 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 cellState = this.props.state.cellStates.get(this.props.nodeRef);
const visibility = <>[<a href='#' onClick={this.toggleVisible}>{cellState.isHidden ? 'H' : 'V'}</a>]</>;
return <> const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
{remove}{visibility} {isCurrent ? <b>{label}</b> : label} <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
...@@ -12,9 +12,10 @@ import { List } from 'immutable'; ...@@ -12,9 +12,10 @@ import { List } from 'immutable';
import { LogEntry } from 'mol-util/log-entry'; import { LogEntry } from 'mol-util/log-entry';
import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParamDefinition as PD } from 'mol-util/param-definition';
import { ParameterControls } from './controls/parameters'; import { ParameterControls } from './controls/parameters';
import { Subject } from 'rxjs';
export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { 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 }) }; updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
...@@ -28,13 +29,16 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> ...@@ -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 }> { 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 }; state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
static Params = { static Params = {
name: PD.Text(), name: PD.Text(),
description: PD.Text(), description: PD.Text(),
serverUrl: PD.Text('http://webchem.ncbr.muni.cz/molstar-state') serverUrl: PD.Text()
} }
add = () => { add = () => {
...@@ -54,6 +58,8 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC ...@@ -54,6 +58,8 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC
this.setState({ isUploading: true }); this.setState({ isUploading: true });
await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl }); 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.setState({ isUploading: false });
this.plugin.log(LogEntry.message('Snapshot uploaded.'));
UploadedEvent.next();
} }
render() { render() {
...@@ -91,7 +97,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { ...@@ -91,7 +97,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
return <ul style={{ listStyle: 'none' }} className='msp-state-list'> return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
{this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}> {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 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' /> <span className='msp-icon msp-icon-remove' />
</button> </button>
</li>)} </li>)}
...@@ -99,13 +105,14 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { ...@@ -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 }> { class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
state = { entries: List<RemoteEntry>(), isFetching: false }; state = { entries: List<RemoteEntry>(), isFetching: false };
componentDidMount() { componentDidMount() {
this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
this.refresh(); this.refresh();
this.subscribe(UploadedEvent, this.refresh);
} }
refresh = async () => { refresh = async () => {
...@@ -113,7 +120,13 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e ...@@ -113,7 +120,13 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
this.setState({ isFetching: true }); this.setState({ isFetching: true });
const req = await fetch(`${this.props.serverUrl}/list`); const req = await fetch(`${this.props.serverUrl}/list`);
const json: RemoteEntry[] = await req.json(); 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) { } catch (e) {
this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e)); this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e));
this.setState({ entries: List<RemoteEntry>(), isFetching: false }) this.setState({ entries: List<RemoteEntry>(), isFetching: false })
...@@ -124,6 +137,16 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e ...@@ -124,6 +137,16 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url }); 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() { render() {
return <div> 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> <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 ...@@ -131,6 +154,9 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
<ul style={{ listStyle: 'none' }} className='msp-state-list'> <ul style={{ listStyle: 'none' }} className='msp-state-list'>
{this.state.entries.valueSeq().map(e =><li key={e!.id}> {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 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>)} </li>)}
</ul> </ul>
</div>; </div>;
......
...@@ -22,7 +22,7 @@ export class ViewportControls extends PluginComponent { ...@@ -22,7 +22,7 @@ export class ViewportControls extends PluginComponent {
render() { render() {
return <div style={{ position: 'absolute', right: '10px', top: '10px', height: '100%', color: 'white' }}> 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> </div>
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment