selection.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. /**
  2. * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { Close, Clear, Brush } from '@material-ui/icons';
  8. import * as React from 'react';
  9. import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
  10. import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
  11. import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
  12. import { StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
  13. import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
  14. import { memoizeLatest } from '../../mol-util/memoize';
  15. import { ParamDefinition } from '../../mol-util/param-definition';
  16. import { stripTags } from '../../mol-util/string';
  17. import { PluginUIComponent, PurePluginUIComponent } from '../base';
  18. import { ActionMenu } from '../controls/action-menu';
  19. import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
  20. import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
  21. import { Union, Subtract, Intersect, SetSvg as SetSvg, CubeSvg } from '../controls/icons';
  22. import { AddComponentControls } from './components';
  23. const StructureSelectionParams = {
  24. granularity: InteractivityManager.Params.granularity,
  25. };
  26. interface StructureSelectionActionsControlsState {
  27. isEmpty: boolean,
  28. isBusy: boolean,
  29. action?: StructureSelectionModifier | 'color' | 'add-repr'
  30. }
  31. const ActionHeader = new Map<StructureSelectionModifier, string>([
  32. ['add', 'Add/Union'],
  33. ['remove', 'Remove/Subtract'],
  34. ['intersect', 'Intersect'],
  35. ['set', 'Set']
  36. ] as const);
  37. export class StructureSelectionActionsControls extends PluginUIComponent<{}, StructureSelectionActionsControlsState> {
  38. state = {
  39. action: void 0 as StructureSelectionActionsControlsState['action'],
  40. isEmpty: true,
  41. isBusy: false,
  42. }
  43. componentDidMount() {
  44. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
  45. const isEmpty = c.hierarchy.structures.length === 0;
  46. if (this.state.isEmpty !== isEmpty) {
  47. this.setState({ isEmpty });
  48. }
  49. });
  50. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  51. this.setState({ isBusy: v, action: void 0 });
  52. });
  53. this.subscribe(this.plugin.managers.interactivity.events.propsUpdated, () => {
  54. this.forceUpdate();
  55. });
  56. }
  57. get isDisabled() {
  58. return this.state.isBusy || this.state.isEmpty;
  59. }
  60. set = (modifier: StructureSelectionModifier, selectionQuery: StructureSelectionQuery) => {
  61. this.plugin.managers.structure.selection.fromSelectionQuery(modifier, selectionQuery, false);
  62. }
  63. selectQuery: ActionMenu.OnSelect = item => {
  64. if (!item || !this.state.action) {
  65. this.setState({ action: void 0 });
  66. return;
  67. }
  68. const q = this.state.action! as StructureSelectionModifier;
  69. this.setState({ action: void 0 }, () => {
  70. this.set(q, item.value as StructureSelectionQuery);
  71. });
  72. }
  73. private queriesItems: ActionMenu.Items[] = []
  74. private queriesVersion = -1
  75. get queries () {
  76. const { registry } = this.plugin.query.structure;
  77. if (registry.version !== this.queriesVersion) {
  78. this.queriesItems = ActionMenu.createItems(registry.list, {
  79. filter: q => q !== StructureSelectionQueries.current,
  80. label: q => q.label,
  81. category: q => q.category,
  82. description: q => q.description
  83. });
  84. this.queriesVersion = registry.version;
  85. }
  86. return this.queriesItems;
  87. }
  88. private showAction(q: StructureSelectionActionsControlsState['action']) {
  89. return () => this.setState({ action: this.state.action === q ? void 0 : q });
  90. }
  91. toggleAdd = this.showAction('add')
  92. toggleRemove = this.showAction('remove')
  93. toggleIntersect = this.showAction('intersect')
  94. toggleSet = this.showAction('set')
  95. toggleColor = this.showAction('color')
  96. toggleAddRepr = this.showAction('add-repr')
  97. setGranuality: ParamOnChange = ({ value }) => {
  98. this.plugin.managers.interactivity.setProps({ granularity: value });
  99. }
  100. turnOff = () => this.plugin.selectionMode = false;
  101. render() {
  102. const granularity = this.plugin.managers.interactivity.props.granularity;
  103. return <>
  104. <div className='msp-flex-row'>
  105. <ToggleButton icon={Union} title={ActionHeader.get('add')} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
  106. <ToggleButton icon={Subtract} title={ActionHeader.get('remove')} toggle={this.toggleRemove} isSelected={this.state.action === 'remove'} disabled={this.isDisabled} />
  107. <ToggleButton icon={Intersect} title={ActionHeader.get('intersect')} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
  108. <ToggleButton icon={SetSvg} title={ActionHeader.get('set')} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
  109. <ToggleButton icon={Brush} title='Color' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} />
  110. <ToggleButton icon={CubeSvg} title='Create Representation' toggle={this.toggleAddRepr} isSelected={this.state.action === 'add-repr'} disabled={this.isDisabled} />
  111. <PureSelectControl title={`Picking Level`} param={StructureSelectionParams.granularity} name='granularity' value={granularity} onChange={this.setGranuality} isDisabled={this.isDisabled} />
  112. <IconButton svg={Close} title='Turn selection mode off' onClick={this.turnOff} />
  113. </div>
  114. {(this.state.action && this.state.action !== 'color' && this.state.action !== 'add-repr') && <div className='msp-selection-viewport-controls-actions'>
  115. <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} items={this.queries} onSelect={this.selectQuery} noOffset />
  116. </div>}
  117. {this.state.action === 'color' && <div className='msp-selection-viewport-controls-actions'>
  118. <ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor} topRightIcon={Close}>
  119. <ApplyColorControls />
  120. </ControlGroup>
  121. </div>}
  122. {this.state.action === 'add-repr' && <div className='msp-selection-viewport-controls-actions'>
  123. <ControlGroup header='Add Representation' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddRepr} topRightIcon={Close}>
  124. <AddComponentControls onApply={this.toggleAddRepr} forSelection />
  125. </ControlGroup>
  126. </div>}
  127. </>;
  128. }
  129. }
  130. export class StructureSelectionStatsControls extends PluginUIComponent<{ hideOnEmpty?: boolean }, { isEmpty: boolean, isBusy: boolean }> {
  131. state = {
  132. isEmpty: true,
  133. isBusy: false
  134. }
  135. componentDidMount() {
  136. this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
  137. this.forceUpdate();
  138. });
  139. this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
  140. const isEmpty = c.structures.length === 0;
  141. if (this.state.isEmpty !== isEmpty) {
  142. this.setState({ isEmpty });
  143. }
  144. });
  145. this.subscribe(this.plugin.behaviors.state.isBusy, v => {
  146. this.setState({ isBusy: v });
  147. });
  148. }
  149. get isDisabled() {
  150. return this.state.isBusy || this.state.isEmpty;
  151. }
  152. get stats() {
  153. const stats = this.plugin.managers.structure.selection.stats;
  154. if (stats.structureCount === 0 || stats.elementCount === 0) {
  155. return 'Nothing Selected';
  156. } else {
  157. return `${stripTags(stats.label)} Selected`;
  158. }
  159. }
  160. clear = () => this.plugin.managers.interactivity.lociSelects.deselectAll();
  161. focus = () => {
  162. if (this.plugin.managers.structure.selection.stats.elementCount === 0) return;
  163. const { sphere } = this.plugin.managers.structure.selection.getBoundary();
  164. this.plugin.managers.camera.focusSphere(sphere);
  165. }
  166. highlight = (e: React.MouseEvent<HTMLElement>) => {
  167. this.plugin.managers.interactivity.lociHighlights.clearHighlights();
  168. this.plugin.managers.structure.selection.entries.forEach(e => {
  169. this.plugin.managers.interactivity.lociHighlights.highlight({ loci: e.selection }, false);
  170. });
  171. }
  172. clearHighlight = () => {
  173. this.plugin.managers.interactivity.lociHighlights.clearHighlights();
  174. }
  175. render() {
  176. const stats = this.plugin.managers.structure.selection.stats;
  177. const empty = stats.structureCount === 0 || stats.elementCount === 0;
  178. if (empty && this.props.hideOnEmpty) return null;
  179. return <>
  180. <div className='msp-flex-row'>
  181. <Button noOverflow onClick={this.focus} title='Click to Focus Selection' disabled={empty} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}
  182. style={{ textAlignLast: !empty ? 'left' : void 0 }}>
  183. {this.stats}
  184. </Button>
  185. {!empty && <IconButton svg={Clear} onClick={this.clear} title='Clear' className='msp-form-control' flex />}
  186. </div>
  187. </>;
  188. }
  189. }
  190. interface ApplyColorControlsState {
  191. values: StructureComponentManager.ColorParams
  192. }
  193. interface ApplyColorControlsProps {
  194. onApply?: () => void
  195. }
  196. class ApplyColorControls extends PurePluginUIComponent<ApplyColorControlsProps, ApplyColorControlsState> {
  197. _params = memoizeLatest((pivot: StructureRef | undefined) => StructureComponentManager.getColorParams(this.plugin, pivot));
  198. get params() { return this._params(this.plugin.managers.structure.component.pivotStructure); }
  199. state = { values: ParamDefinition.getDefaultValues(this.params) };
  200. apply = () => {
  201. this.plugin.managers.structure.component.applyColor(this.state.values);
  202. this.props.onApply?.();
  203. }
  204. paramsChanged = (values: any) => this.setState({ values })
  205. render() {
  206. return <>
  207. <ParameterControls params={this.params} values={this.state.values} onChangeValues={this.paramsChanged} />
  208. <Button icon={Brush} className='msp-btn-commit msp-btn-commit-on' onClick={this.apply} style={{ marginTop: '1px' }}>
  209. Apply Coloring
  210. </Button>
  211. </>;
  212. }
  213. }