plugin.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { List } from 'immutable';
  7. import { PluginState } from 'mol-plugin/state';
  8. import { formatTime } from 'mol-util';
  9. import { LogEntry } from 'mol-util/log-entry';
  10. import * as React from 'react';
  11. import { PluginContext } from '../context';
  12. import { PluginReactContext, PluginUIComponent } from './base';
  13. import { CameraSnapshots } from './camera';
  14. import { LociLabelControl, TrajectoryControls } from './controls';
  15. import { StateSnapshots } from './state';
  16. import { StateObjectActions } from './state/actions';
  17. import { AnimationControls } from './state/animation';
  18. import { StateTree } from './state/tree';
  19. import { BackgroundTaskProgress } from './task';
  20. import { Viewport, ViewportControls } from './viewport';
  21. import { StateTransform } from 'mol-state';
  22. export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
  23. region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
  24. return <div className={`msp-layout-region msp-layout-${kind}`}>
  25. <div className='msp-layout-static'>
  26. {element}
  27. </div>
  28. </div>
  29. }
  30. render() {
  31. return <PluginReactContext.Provider value={this.props.plugin}>
  32. <Layout />
  33. </PluginReactContext.Provider>;
  34. }
  35. }
  36. class Layout extends PluginUIComponent {
  37. componentDidMount() {
  38. this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
  39. }
  40. region(kind: 'left' | 'right' | 'bottom' | 'main', Element: React.ComponentClass) {
  41. return <div className={`msp-layout-region msp-layout-${kind}`}>
  42. <div className='msp-layout-static'>
  43. <Element />
  44. </div>
  45. </div>;
  46. }
  47. render() {
  48. const layout = this.plugin.layout.state;
  49. const spec = this.plugin.spec.layout && this.plugin.spec.layout.controls;
  50. return <div className='msp-plugin'>
  51. <div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}>
  52. <div className={layout.showControls ? 'msp-layout-hide-top' : 'msp-layout-hide-top msp-layout-hide-right msp-layout-hide-bottom msp-layout-hide-left'}>
  53. {this.region('main', ViewportWrapper)}
  54. {layout.showControls && spec && spec.left !== 'none' && this.region('left', (spec && spec.left) || State)}
  55. {layout.showControls && spec && spec.right !== 'none' && this.region('right', (spec && spec.right) || ControlsWrapper)}
  56. {layout.showControls && spec && spec.bottom !== 'none' && this.region('bottom', (spec && spec.bottom) || Log)}
  57. </div>
  58. </div>
  59. </div>;
  60. }
  61. }
  62. export class ControlsWrapper extends PluginUIComponent {
  63. render() {
  64. return <div className='msp-scrollable-container msp-right-controls'>
  65. <CurrentObject />
  66. <AnimationControls />
  67. <CameraSnapshots />
  68. <StateSnapshots />
  69. </div>;
  70. }
  71. }
  72. export class ViewportWrapper extends PluginUIComponent {
  73. render() {
  74. return <>
  75. <Viewport />
  76. <TrajectoryControls />
  77. <ViewportControls />
  78. <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
  79. <BackgroundTaskProgress />
  80. </div>
  81. <LociLabelControl />
  82. </>;
  83. }
  84. }
  85. export class State extends PluginUIComponent {
  86. componentDidMount() {
  87. this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
  88. }
  89. set(kind: PluginState.Kind) {
  90. // TODO: do command for this?
  91. this.plugin.state.setKind(kind);
  92. }
  93. render() {
  94. const kind = this.plugin.state.behavior.kind.value;
  95. return <div className='msp-scrollable-container'>
  96. <div className='msp-btn-row-group msp-data-beh'>
  97. <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}>Data</button>
  98. <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>Behavior</button>
  99. </div>
  100. <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
  101. </div>
  102. }
  103. }
  104. export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
  105. private wrapper = React.createRef<HTMLDivElement>();
  106. componentDidMount() {
  107. this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries }));
  108. }
  109. componentDidUpdate() {
  110. this.scrollToBottom();
  111. }
  112. state = { entries: this.plugin.log.entries };
  113. private scrollToBottom() {
  114. const log = this.wrapper.current;
  115. if (log) log.scrollTop = log.scrollHeight - log.clientHeight - 1;
  116. }
  117. render() {
  118. // TODO: ability to show full log
  119. // showing more entries dramatically slows animations.
  120. const maxEntries = 10;
  121. const xs = this.state.entries, l = xs.size;
  122. const entries: JSX.Element[] = [];
  123. for (let i = Math.max(0, l - maxEntries), o = 0; i < l; i++) {
  124. const e = xs.get(i);
  125. entries.push(<li key={o++}>
  126. <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
  127. <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
  128. <div className='msp-log-entry'>{e!.message}</div>
  129. </li>);
  130. }
  131. return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
  132. <ul className='msp-list-unstyled'>{entries}</ul>
  133. </div>;
  134. }
  135. }
  136. export class CurrentObject extends PluginUIComponent {
  137. get current() {
  138. return this.plugin.state.behavior.currentObject.value;
  139. }
  140. componentDidMount() {
  141. this.subscribe(this.plugin.state.behavior.currentObject, o => {
  142. this.forceUpdate();
  143. });
  144. this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
  145. const current = this.current;
  146. if (current.ref !== ref || current.state !== state) return;
  147. this.forceUpdate();
  148. });
  149. }
  150. render() {
  151. const current = this.current;
  152. const ref = current.ref;
  153. const cell = current.state.cells.get(ref)!;
  154. const transform = cell.transform;
  155. const def = transform.transformer.definition;
  156. const display = cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name;
  157. let showActions = true;
  158. if (ref === StateTransform.RootRef) {
  159. const children = current.state.tree.children.get(ref);
  160. showActions = children.size !== 0;
  161. }
  162. if (!showActions) return null;
  163. return cell.status === 'ok' && <>
  164. <div className='msp-section-header'>{`Actions (${display})`}</div>
  165. <StateObjectActions state={current.state} nodeRef={ref} />
  166. </>;
  167. }
  168. }