tree.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. /**
  2. * Copyright (c) 2018 - 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as React from 'react';
  7. import { PluginStateObject } from '../../mol-plugin-state/objects';
  8. import { State, StateTree as _StateTree, StateObject, StateTransform, StateObjectCell, StateAction } from '../../mol-state'
  9. import { PluginCommands } from '../../mol-plugin/commands';
  10. import { PluginUIComponent, _Props, _State } from '../base';
  11. import { Icon } from '../controls/icons';
  12. import { ActionMenu } from '../controls/action-menu';
  13. import { ApplyActionControl } from './apply-action';
  14. import { ControlGroup } from '../controls/common';
  15. import { UpdateTransformControl } from './update-transform';
  16. export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
  17. state = { showActions: true };
  18. componentDidMount() {
  19. this.subscribe(this.plugin.events.state.cell.created, e => {
  20. if (e.cell.transform.parent === StateTransform.RootRef) this.forceUpdate();
  21. });
  22. this.subscribe(this.plugin.events.state.cell.removed, e => {
  23. if (e.parent === StateTransform.RootRef) this.forceUpdate();
  24. });
  25. }
  26. static getDerivedStateFromProps(props: { state: State }, state: { showActions: boolean }) {
  27. const n = props.state.tree.root.ref;
  28. const children = props.state.tree.children.get(n);
  29. const showActions = children.size === 0;
  30. if (state.showActions === showActions) return null;
  31. return { showActions };
  32. }
  33. render() {
  34. const ref = this.props.state.tree.root.ref;
  35. if (this.state.showActions) {
  36. return <div style={{ margin: '10px', cursor: 'default' }}>
  37. <p>Nothing to see here.</p>
  38. <p>Structures can be loaded from the <Icon name='home' /> tab.</p>
  39. </div>
  40. }
  41. return <StateTreeNode cell={this.props.state.cells.get(ref)!} depth={0} />;
  42. }
  43. }
  44. class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, { isCollapsed: boolean }> {
  45. is(e: State.ObjectEvent) {
  46. return e.ref === this.ref && e.state === this.props.cell.parent;
  47. }
  48. get ref() {
  49. return this.props.cell.transform.ref;
  50. }
  51. componentDidMount() {
  52. this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
  53. if (this.props.cell === e.cell && this.is(e) && e.state.cells.has(this.ref)) {
  54. this.forceUpdate();
  55. // if (!!this.props.cell.state.isCollapsed !== this.state.isCollapsed) {
  56. // this.setState({ isCollapsed: !!e.cell.state.isCollapsed });
  57. // }
  58. }
  59. });
  60. this.subscribe(this.plugin.events.state.cell.created, e => {
  61. if (this.props.cell.parent === e.state && this.ref === e.cell.transform.parent) {
  62. this.forceUpdate();
  63. }
  64. });
  65. this.subscribe(this.plugin.events.state.cell.removed, e => {
  66. if (this.props.cell.parent === e.state && this.ref === e.parent) {
  67. this.forceUpdate();
  68. }
  69. });
  70. }
  71. state = {
  72. isCollapsed: !!this.props.cell.state.isCollapsed
  73. }
  74. static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null {
  75. if (!!props.cell.state.isCollapsed === state.isCollapsed) return null;
  76. return { isCollapsed: !!props.cell.state.isCollapsed };
  77. }
  78. hasDecorator(children: _StateTree.ChildSet) {
  79. if (children.size !== 1) return false;
  80. const ref = children.values().next().value;
  81. return !!this.props.cell.parent.tree.transforms.get(ref).isDecorator;
  82. }
  83. render() {
  84. const cell = this.props.cell;
  85. if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
  86. return null;
  87. }
  88. const cellState = cell.state;
  89. const children = cell.parent.tree.children.get(this.ref);
  90. const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || (!cell.state.isGhost && !this.hasDecorator(children)));
  91. if (!showLabel) {
  92. if (children.size === 0) return null;
  93. return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
  94. {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={this.props.depth} />)}
  95. </div>;
  96. }
  97. const newDepth = this.props.depth + 1;
  98. return <>
  99. <StateTreeNodeLabel cell={cell} depth={this.props.depth} />
  100. {children.size === 0
  101. ? void 0
  102. : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
  103. {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
  104. </div>
  105. }
  106. </>;
  107. }
  108. }
  109. interface StateTreeNodeLabelState {
  110. isCurrent: boolean,
  111. isCollapsed: boolean,
  112. action?: 'options' | 'apply',
  113. currentAction?: StateAction
  114. }
  115. class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, StateTreeNodeLabelState> {
  116. is(e: State.ObjectEvent) {
  117. return e.ref === this.ref && e.state === this.props.cell.parent;
  118. }
  119. get ref() {
  120. return this.props.cell.transform.ref;
  121. }
  122. componentDidMount() {
  123. this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
  124. if (this.is(e)) this.forceUpdate();
  125. });
  126. this.subscribe(this.plugin.state.behavior.currentObject, e => {
  127. if (!this.is(e)) {
  128. if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
  129. this._setCurrent(this.props.cell.parent.current === this.ref, this.state.isCollapsed);
  130. }
  131. return;
  132. }
  133. if (e.state.transforms.has(this.ref)) {
  134. this._setCurrent(this.props.cell.parent.current === this.ref, !!this.props.cell.state.isCollapsed)
  135. // this.setState({
  136. // isCurrent: this.props.cell.parent.current === this.ref,
  137. // isCollapsed: !!this.props.cell.state.isCollapsed
  138. // });
  139. }
  140. });
  141. }
  142. private _setCurrent(isCurrent: boolean, isCollapsed: boolean) {
  143. if (isCurrent) {
  144. this.setState({ isCurrent, action: 'options', currentAction: void 0, isCollapsed });
  145. } else {
  146. this.setState({ isCurrent, action: void 0, currentAction: void 0, isCollapsed });
  147. }
  148. }
  149. state: StateTreeNodeLabelState = {
  150. isCurrent: this.props.cell.parent.current === this.ref,
  151. isCollapsed: !!this.props.cell.state.isCollapsed,
  152. action: void 0,
  153. currentAction: void 0 as StateAction | undefined
  154. }
  155. static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
  156. const isCurrent = props.cell.parent.current === props.cell.transform.ref;
  157. const isCollapsed = !!props.cell.state.isCollapsed;
  158. if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
  159. return { isCurrent, isCollapsed, action: void 0, currentAction: void 0 };
  160. }
  161. setCurrent = (e?: React.MouseEvent<HTMLElement>) => {
  162. e?.preventDefault();
  163. e?.currentTarget.blur();
  164. PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: this.ref });
  165. }
  166. setCurrentRoot = (e: React.MouseEvent<HTMLElement>) => {
  167. e.preventDefault();
  168. e.currentTarget.blur();
  169. PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: StateTransform.RootRef });
  170. }
  171. remove = (e?: React.MouseEvent<HTMLElement>) => {
  172. e?.preventDefault();
  173. PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
  174. }
  175. toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
  176. e.preventDefault();
  177. PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.ref });
  178. e.currentTarget.blur();
  179. }
  180. toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
  181. e.preventDefault();
  182. PluginCommands.State.ToggleExpanded(this.plugin, { state: this.props.cell.parent, ref: this.ref });
  183. e.currentTarget.blur();
  184. }
  185. highlight = (e: React.MouseEvent<HTMLElement>) => {
  186. e.preventDefault();
  187. PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent, ref: this.ref });
  188. e.currentTarget.blur();
  189. }
  190. clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
  191. e.preventDefault();
  192. PluginCommands.Interactivity.ClearHighlights(this.plugin);
  193. e.currentTarget.blur();
  194. }
  195. // toggleActions = () => {
  196. // if (this.state.action) this.setState({ action: void 0, currentAction: void 0 });
  197. // else this.setState({ action: 'options', currentAction: void 0 });
  198. // }
  199. hideAction = () => this.setState({ action: void 0, currentAction: void 0 });
  200. get actions() {
  201. const cell = this.props.cell;
  202. const actions = [...cell.parent.actions.fromCell(cell, this.plugin)];
  203. if (actions.length === 0) return;
  204. actions.sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1);
  205. return [
  206. ActionMenu.Header('Apply Action'),
  207. ...actions.map(a => ActionMenu.Item(a.definition.display.name, () => this.setState({ action: 'apply', currentAction: a })))
  208. ];
  209. }
  210. selectAction: ActionMenu.OnSelect = item => {
  211. if (!item) return;
  212. (item?.value as any)();
  213. }
  214. render() {
  215. const cell = this.props.cell;
  216. const n = cell.transform;
  217. if (!cell) return null;
  218. const isCurrent = this.is(cell.parent.behaviors.currentObject.value);
  219. let label: any;
  220. if (cell.status === 'pending' || cell.status === 'processing') {
  221. const name = n.transformer.definition.display.name;
  222. label = <><b>[{cell.status}]</b> <span title={name}>{name}</span></>;
  223. } else if (cell.status !== 'ok' || !cell.obj) {
  224. const name = n.transformer.definition.display.name;
  225. const title = `${cell.errorText}`;
  226. // {this.state.isCurrent ? this.setCurrentRoot : this.setCurrent
  227. label = <><button className='msp-btn-link msp-btn-tree-label' title={title} onClick={this.state.isCurrent ? this.setCurrentRoot : this.setCurrent}><b>[{cell.status}]</b> {name}: <i><span>{cell.errorText}</span></i> </button></>;
  228. } else {
  229. const obj = cell.obj as PluginStateObject.Any;
  230. const title = `${obj.label} ${obj.description ? obj.description : ''}`;
  231. label = <><button className='msp-btn-link msp-btn-tree-label' title={title} onClick={this.state.isCurrent ? this.setCurrentRoot : this.setCurrent}><span>{obj.label}</span> {obj.description ? <small>{obj.description}</small> : void 0}</button></>;
  232. }
  233. const children = cell.parent.tree.children.get(this.ref);
  234. const cellState = cell.state;
  235. const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
  236. <Icon name='visual-visibility' />
  237. </button>;
  238. const style: React.HTMLAttributes<HTMLDivElement>['style'] = {
  239. marginLeft: /* this.state.isCurrent ? void 0 :*/ `${this.props.depth * 8}px`,
  240. // paddingLeft: !this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`,
  241. borderLeft: /* isCurrent || */ this.props.depth === 0 ? 'none' : void 0
  242. }
  243. const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={style}>
  244. {label}
  245. {children.size > 0 && <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
  246. <Icon name={cellState.isCollapsed ? 'expand' : 'collapse'} />
  247. </button>}
  248. {!cell.state.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
  249. <Icon name='remove' />
  250. </button>}{visibility}
  251. </div>;
  252. if (!isCurrent) return row;
  253. if (this.state.action === 'apply' && this.state.currentAction) {
  254. return <div style={{ marginBottom: '1px' }}>
  255. {row}
  256. <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideAction} topRightIcon='off'>
  257. <ApplyActionControl onApply={this.hideAction} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
  258. </ControlGroup>
  259. </div>
  260. }
  261. if (this.state.action === 'options') {
  262. let actions = this.actions;
  263. return <div style={{ marginBottom: '1px' }}>
  264. {row}
  265. <UpdateTransformControl state={cell.parent} transform={cell.transform} noMargin wrapInExpander />
  266. {actions && <ActionMenu items={actions} onSelect={this.selectAction} />}
  267. </div>
  268. }
  269. // if (this.state.isCurrent) {
  270. // return <>
  271. // {row}
  272. // <StateTreeNodeTransform {...this.props} toggleCollapsed={this.toggleUpdaterObs} />
  273. // </>
  274. // }
  275. return row;
  276. }
  277. }
  278. // class StateTreeNodeTransform extends PluginUIComponent<{ nodeRef: string, state: State, depth: number, toggleCollapsed?: Observable<any> }> {
  279. // componentDidMount() {
  280. // // this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
  281. // // if (this.props.nodeRef !== ref || this.props.state !== state) return;
  282. // // this.forceUpdate();
  283. // // });
  284. // }
  285. // render() {
  286. // const ref = this.props.nodeRef;
  287. // const cell = this.props.state.cells.get(ref)!;
  288. // const parent: StateObjectCell | undefined = (cell.sourceRef && this.props.state.cells.get(cell.sourceRef)!) || void 0;
  289. // if (!parent || parent.status !== 'ok') return null;
  290. // const transform = cell.transform;
  291. // return <UpdateTransformContol state={this.props.state} transform={transform} initiallyCollapsed={true} toggleCollapsed={this.props.toggleCollapsed} />;
  292. // }
  293. // }