components.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import * as React from 'react';
  8. import { getStructureThemeTypes } from '../../mol-plugin-state/helpers/structure-representation-params';
  9. import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
  10. import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
  11. import { StructureComponentRef, StructureRepresentationRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
  12. import { PluginCommands } from '../../mol-plugin/commands';
  13. import { State } from '../../mol-state';
  14. import { ParamDefinition } from '../../mol-util/param-definition';
  15. import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
  16. import { ActionMenu } from '../controls/action-menu';
  17. import { Button, ExpandGroup, IconButton, ToggleButton, ControlRow, TextInput } from '../controls/common';
  18. import { CubeOutlineSvg, IntersectSvg, SetSvg, SubtractSvg, UnionSvg, BookmarksOutlinedSvg, AddSvg, TuneSvg, RestoreSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, DeleteOutlinedSvg, MoreHorizSvg, CheckSvg } from '../controls/icons';
  19. import { ParameterControls } from '../controls/parameters';
  20. import { UpdateTransformControl } from '../state/update-transform';
  21. import { GenericEntryListControls } from './generic';
  22. interface StructureComponentControlState extends CollapsableState {
  23. isDisabled: boolean
  24. }
  25. export class StructureComponentControls extends CollapsableControls<{}, StructureComponentControlState> {
  26. protected defaultState(): StructureComponentControlState {
  27. return {
  28. header: 'Components',
  29. isCollapsed: false,
  30. isDisabled: false,
  31. brand: { accent: 'blue', svg: CubeOutlineSvg }
  32. };
  33. }
  34. componentDidMount() {
  35. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => this.setState({
  36. description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
  37. }));
  38. }
  39. renderControls() {
  40. return <>
  41. <ComponentEditorControls />
  42. <ComponentListControls />
  43. <GenericEntryListControls />
  44. </>;
  45. }
  46. }
  47. interface ComponentEditorControlsState {
  48. action?: 'preset' | 'add' | 'options',
  49. isEmpty: boolean,
  50. isBusy: boolean,
  51. canUndo: boolean
  52. }
  53. class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorControlsState> {
  54. state: ComponentEditorControlsState = {
  55. isEmpty: true,
  56. isBusy: false,
  57. canUndo: false
  58. };
  59. get isDisabled() {
  60. return this.state.isBusy || this.state.isEmpty;
  61. }
  62. componentDidMount() {
  63. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => this.setState({
  64. action: this.state.action !== 'options' || c.structures.length === 0 ? void 0 : 'options',
  65. isEmpty: c.structures.length === 0
  66. }));
  67. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  68. this.setState({ isBusy: v, action: this.state.action !== 'options' ? void 0 : 'options' });
  69. });
  70. this.subscribe(this.plugin.state.data.events.historyUpdated, ({ state }) => {
  71. this.setState({ canUndo: state.canUndo });
  72. });
  73. }
  74. private toggleAction(action: ComponentEditorControlsState['action']) {
  75. return () => this.setState({ action: this.state.action === action ? void 0 : action });
  76. }
  77. togglePreset = this.toggleAction('preset');
  78. toggleAdd = this.toggleAction('add');
  79. toggleOptions = this.toggleAction('options');
  80. hideAction = () => this.setState({ action: void 0 });
  81. get presetControls() {
  82. return <ActionMenu items={this.presetActions} onSelect={this.applyPreset} />;
  83. }
  84. get presetActions() {
  85. const pivot = this.plugin.managers.structure.component.pivotStructure;
  86. const providers = this.plugin.builders.structure.representation.getPresets(pivot?.cell.obj);
  87. return ActionMenu.createItems(providers, { label: p => p.display.name, category: p => p.display.group, description: p => p.display.description });
  88. }
  89. applyPreset: ActionMenu.OnSelect = item => {
  90. this.hideAction();
  91. if (!item) return;
  92. const mng = this.plugin.managers.structure;
  93. const { structures } = mng.hierarchy.selection;
  94. if (item.value === null) mng.component.clear(structures);
  95. else mng.component.applyPreset(structures, item.value as any);
  96. }
  97. undo = () => {
  98. const task = this.plugin.state.data.undo();
  99. if (task) this.plugin.runTask(task);
  100. }
  101. render() {
  102. const undoTitle = this.state.canUndo
  103. ? `Undo ${this.plugin.state.data.latestUndoLabel}`
  104. : 'Some mistakes of the past can be undone.';
  105. return <>
  106. <div className='msp-flex-row'>
  107. <ToggleButton icon={BookmarksOutlinedSvg} label='Preset' title='Apply a representation preset for the current structure(s).' toggle={this.togglePreset} isSelected={this.state.action === 'preset'} disabled={this.isDisabled} />
  108. <ToggleButton icon={AddSvg} label='Add' title='Add a new representation component for a selection.' toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
  109. <ToggleButton icon={TuneSvg} label='' title='Options that are applied to all applicable representations.' style={{ flex: '0 0 40px', padding: 0 }} toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.isDisabled} />
  110. <IconButton svg={RestoreSvg} className='msp-flex-item' flex='40px' onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
  111. </div>
  112. {this.state.action === 'preset' && this.presetControls}
  113. {this.state.action === 'add' && <div className='msp-control-offset'>
  114. <AddComponentControls onApply={this.hideAction} />
  115. </div>}
  116. {this.state.action === 'options' && <div className='msp-control-offset'><ComponentOptionsControls isDisabled={this.isDisabled} /></div>}
  117. </>;
  118. }
  119. }
  120. interface AddComponentControlsState {
  121. params: ParamDefinition.Params,
  122. values: StructureComponentManager.AddParams
  123. }
  124. interface AddComponentControlsProps {
  125. forSelection?: boolean,
  126. onApply: () => void
  127. }
  128. export class AddComponentControls extends PurePluginUIComponent<AddComponentControlsProps, AddComponentControlsState> {
  129. createState(): AddComponentControlsState {
  130. const params = StructureComponentManager.getAddParams(this.plugin);
  131. return { params, values: ParamDefinition.getDefaultValues(params) };
  132. }
  133. state = this.createState();
  134. get selectedStructures() {
  135. return this.plugin.managers.structure.component.currentStructures;
  136. }
  137. get currentStructures() {
  138. return this.plugin.managers.structure.hierarchy.current.structures;
  139. }
  140. apply = () => {
  141. const structures = this.props.forSelection ? this.currentStructures : this.selectedStructures;
  142. this.props.onApply();
  143. this.plugin.managers.structure.component.add(this.state.values, structures);
  144. }
  145. paramsChanged = (values: any) => this.setState({ values })
  146. render() {
  147. return <>
  148. <ParameterControls params={this.state.params} values={this.state.values} onChangeValues={this.paramsChanged} />
  149. <Button icon={AddSvg} title='Use Selection and optional Representation to create a new Component.' className='msp-btn-commit msp-btn-commit-on' onClick={this.apply} style={{ marginTop: '1px' }}>
  150. Create Component
  151. </Button>
  152. </>;
  153. }
  154. }
  155. class ComponentOptionsControls extends PurePluginUIComponent<{ isDisabled: boolean }> {
  156. componentDidMount() {
  157. this.subscribe(this.plugin.managers.structure.component.events.optionsUpdated, () => this.forceUpdate());
  158. }
  159. update = (options: StructureComponentManager.Options) => this.plugin.managers.structure.component.setOptions(options)
  160. render() {
  161. return <ParameterControls params={StructureComponentManager.OptionsParams} values={this.plugin.managers.structure.component.state.options} onChangeValues={this.update} isDisabled={this.props.isDisabled} />;
  162. }
  163. }
  164. class ComponentListControls extends PurePluginUIComponent {
  165. componentDidMount() {
  166. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => {
  167. this.forceUpdate();
  168. });
  169. }
  170. render() {
  171. const componentGroups = this.plugin.managers.structure.hierarchy.currentComponentGroups;
  172. if (componentGroups.length === 0) return null;
  173. return <div style={{ marginTop: '6px' }}>
  174. {componentGroups.map(g => <StructureComponentGroup key={g[0].cell.transform.ref} group={g} />)}
  175. </div>;
  176. }
  177. }
  178. type StructureComponentEntryActions = 'action' | 'label'
  179. class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureComponentRef[] }, { action?: StructureComponentEntryActions }> {
  180. state = { action: void 0 as StructureComponentEntryActions | undefined }
  181. get pivot() {
  182. return this.props.group[0];
  183. }
  184. componentDidMount() {
  185. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  186. if (State.ObjectEvent.isCell(e, this.pivot.cell)) this.forceUpdate();
  187. });
  188. }
  189. toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
  190. e.preventDefault();
  191. e.currentTarget.blur();
  192. this.plugin.managers.structure.component.toggleVisibility(this.props.group);
  193. }
  194. get colorByActions() {
  195. const mng = this.plugin.managers.structure.component;
  196. const repr = this.pivot.representations[0];
  197. const name = repr.cell.transform.params?.colorTheme.name;
  198. const themes = getStructureThemeTypes(this.plugin, this.pivot.cell.obj?.data);
  199. return ActionMenu.createItemsFromSelectOptions(themes, {
  200. value: o => () => mng.updateRepresentationsTheme(this.props.group, { color: o[0] as any }),
  201. selected: o => o[0] === name
  202. }) as ActionMenu.Item[];
  203. }
  204. get actions(): ActionMenu.Items {
  205. const mng = this.plugin.managers.structure.component;
  206. const ret: ActionMenu.Items = [
  207. [
  208. ActionMenu.Header('Add Representation'),
  209. ...StructureComponentManager.getRepresentationTypes(this.plugin, this.props.group[0])
  210. .map(t => ActionMenu.Item(t[1], () => mng.addRepresentation(this.props.group, t[0])))
  211. ]
  212. ];
  213. if (this.pivot.representations.length > 0) {
  214. ret.push([
  215. ActionMenu.Header('Set Coloring', { isIndependent: true }),
  216. ...this.colorByActions
  217. ]);
  218. }
  219. if (mng.canBeModified(this.props.group[0])) {
  220. ret.push([
  221. ActionMenu.Header('Modify by Selection'),
  222. ActionMenu.Item('Include', () => mng.modifyByCurrentSelection(this.props.group, 'union'), { icon: UnionSvg }),
  223. ActionMenu.Item('Subtract', () => mng.modifyByCurrentSelection(this.props.group, 'subtract'), { icon: SubtractSvg }),
  224. ActionMenu.Item('Intersect', () => mng.modifyByCurrentSelection(this.props.group, 'intersect'), { icon: IntersectSvg })
  225. ]);
  226. }
  227. ret.push(ActionMenu.Item('Select This', () => mng.selectThis(this.props.group), { icon: SetSvg }));
  228. if (mng.canBeModified(this.props.group[0])) {
  229. ret.push(
  230. ActionMenu.Item('Edit Label', this.toggleLabel)
  231. );
  232. }
  233. return ret;
  234. }
  235. selectAction: ActionMenu.OnSelect = item => {
  236. if (!item) return;
  237. this.setState({ action: void 0 });
  238. (item?.value as any)();
  239. }
  240. remove = () => this.plugin.managers.structure.hierarchy.remove(this.props.group, true);
  241. toggleAction = () => this.setState({ action: this.state.action === 'action' ? void 0 : 'action' });
  242. toggleLabel = () => this.setState({ action: this.state.action === 'label' ? void 0 : 'label' });
  243. highlight = (e: React.MouseEvent<HTMLElement>) => {
  244. e.preventDefault();
  245. if (!this.props.group[0].cell.parent) return;
  246. PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.group[0].cell.parent!, ref: this.props.group.map(c => c.cell.transform.ref) });
  247. }
  248. clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
  249. e.preventDefault();
  250. PluginCommands.Interactivity.ClearHighlights(this.plugin);
  251. }
  252. focus = () => {
  253. let allHidden = true;
  254. for (const c of this.props.group) {
  255. if (!c.cell.state.isHidden) {
  256. allHidden = false;
  257. break;
  258. }
  259. }
  260. if (allHidden) {
  261. this.plugin.managers.structure.hierarchy.toggleVisibility(this.props.group, 'show');
  262. }
  263. this.plugin.managers.camera.focusSpheres(this.props.group, e => {
  264. if (e.cell.state.isHidden) return;
  265. return e.cell.obj?.data.boundary.sphere;
  266. });
  267. }
  268. get reprLabel() {
  269. // TODO: handle generic reprs.
  270. const pivot = this.pivot;
  271. if (pivot.representations.length === 0) return 'No repr.';
  272. if (pivot.representations.length === 1) return pivot.representations[0].cell.obj?.label;
  273. return `${pivot.representations.length} reprs`;
  274. }
  275. private updateLabel = (v: string) => {
  276. this.plugin.managers.structure.component.updateLabel(this.pivot, v);
  277. }
  278. render() {
  279. const component = this.pivot;
  280. const cell = component.cell;
  281. const label = cell.obj?.label;
  282. const reprLabel = this.reprLabel;
  283. return <>
  284. <div className='msp-flex-row'>
  285. <Button noOverflow className='msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
  286. {label}
  287. <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{reprLabel}</small>
  288. </Button>
  289. <IconButton svg={cell.state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} onClick={this.toggleVisible} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
  290. <IconButton svg={DeleteOutlinedSvg} toggleState={false} onClick={this.remove} title='Remove' small className='msp-form-control' flex />
  291. <IconButton svg={MoreHorizSvg} onClick={this.toggleAction} title='Actions' toggleState={this.state.action === 'action'} className='msp-form-control' flex />
  292. </div>
  293. {this.state.action === 'label' && <div className='msp-control-offset' style={{ marginBottom: '6px' }}>
  294. <ControlRow label='Label' control={<div style={{ display: 'flex', textAlignLast: 'center' }}>
  295. <TextInput onChange={this.updateLabel} value={label} style={{ flex: '1 1 auto', minWidth: 0 }} className='msp-form-control' blurOnEnter={true} blurOnEscape={true} />
  296. <IconButton svg={CheckSvg} onClick={this.toggleLabel}className='msp-form-control msp-control-button-label' flex />
  297. </div>}/>
  298. </div>}
  299. {this.state.action === 'action' && <div className='msp-accent-offset'>
  300. <div style={{ marginBottom: '6px' }}>
  301. <ActionMenu items={this.actions} onSelect={this.selectAction} noOffset />
  302. </div>
  303. <div style={{ marginBottom: '6px' }}>
  304. {component.representations.map(r => <StructureRepresentationEntry group={this.props.group} key={r.cell.transform.ref} representation={r} />)}
  305. </div>
  306. </div>}
  307. </>;
  308. }
  309. }
  310. class StructureRepresentationEntry extends PurePluginUIComponent<{ group: StructureComponentRef[], representation: StructureRepresentationRef }> {
  311. remove = () => this.plugin.managers.structure.component.removeRepresentations(this.props.group, this.props.representation);
  312. toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
  313. e.preventDefault();
  314. e.currentTarget.blur();
  315. this.plugin.managers.structure.component.toggleVisibility(this.props.group, this.props.representation);
  316. }
  317. componentDidMount() {
  318. this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
  319. if (State.ObjectEvent.isCell(e, this.props.representation.cell)) this.forceUpdate();
  320. });
  321. }
  322. update = (params: any) => this.plugin.managers.structure.component.updateRepresentations(this.props.group, this.props.representation, params);
  323. render() {
  324. const repr = this.props.representation.cell;
  325. return <div className='msp-representation-entry'>
  326. {repr.parent && <ExpandGroup header={`${repr.obj?.label || ''} Representation`} noOffset>
  327. <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' customUpdate={this.update} noMargin />
  328. </ExpandGroup>}
  329. <IconButton svg={DeleteOutlinedSvg} onClick={this.remove} title='Remove' small className='msp-default-bg' toggleState={false} style={{
  330. position: 'absolute', top: 0, right: '32px', lineHeight: '24px', height: '24px', textAlign: 'right', width: '44px', paddingRight: '6px', background: 'none'
  331. }} />
  332. <IconButton svg={this.props.representation.cell.state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} onClick={this.toggleVisible} title='Toggle Visibility' small className='msp-default-bg' style={{
  333. position: 'absolute', top: 0, right: 0, lineHeight: '24px', height: '24px', textAlign: 'right', width: '32px', paddingRight: '6px', background: 'none'
  334. }} />
  335. </div>;
  336. }
  337. }