common.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 { State, StateTransform, StateTransformer, StateAction, StateObject } from '../../mol-state';
  7. import * as React from 'react';
  8. import { PurePluginUIComponent } from '../base';
  9. import { ParameterControls, ParamOnChange } from '../controls/parameters';
  10. import { PluginContext } from '../../mol-plugin/context';
  11. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  12. import { Subject } from 'rxjs';
  13. import { Icon } from '../controls/icons';
  14. import { ExpandGroup, ToggleButton, Button, IconButton } from '../controls/common';
  15. import { Refresh, ArrowRight, ArrowDropDown, Check, Tune } from '@material-ui/icons';
  16. export { StateTransformParameters, TransformControlBase };
  17. class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> {
  18. validate(params: any) {
  19. // TODO
  20. return void 0;
  21. }
  22. areInitial(params: any) {
  23. return PD.areEqual(this.props.info.params, params, this.props.info.initialValues);
  24. }
  25. onChange: ParamOnChange = ({ name, value }) => {
  26. const params = { ...this.props.params, [name]: value };
  27. this.props.events.onChange(params, this.areInitial(params), this.validate(params));
  28. };
  29. render() {
  30. return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />;
  31. }
  32. }
  33. namespace StateTransformParameters {
  34. export interface Props {
  35. info: {
  36. params: PD.Params,
  37. initialValues: any,
  38. isEmpty: boolean
  39. },
  40. events: {
  41. onChange: (params: any, areInitial: boolean, errors?: string[]) => void,
  42. onEnter: () => void,
  43. }
  44. params: any,
  45. isDisabled?: boolean,
  46. a?: StateObject,
  47. b?: StateObject
  48. }
  49. export type Class = React.ComponentClass<Props>
  50. function areParamsEmpty(params: PD.Params) {
  51. const keys = Object.keys(params);
  52. for (const k of keys) {
  53. if (!params[k].isHidden) return false;
  54. }
  55. return true;
  56. }
  57. export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: StateTransform.Ref): Props['info'] {
  58. const source = state.cells.get(nodeRef)!.obj!;
  59. const params = action.definition.params ? action.definition.params(source, plugin) : { };
  60. const initialValues = PD.getDefaultValues(params);
  61. return {
  62. initialValues,
  63. params,
  64. isEmpty: areParamsEmpty(params)
  65. };
  66. }
  67. export function infoFromTransform(plugin: PluginContext, state: State, transform: StateTransform): Props['info'] {
  68. const cell = state.cells.get(transform.ref)!;
  69. // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
  70. // const create = transform.transformer.definition.params;
  71. // const params = create ? create((source && source.obj) as any, plugin) : { };
  72. const params = (cell.params && cell.params.definition) || { };
  73. const initialValues = (cell.params && cell.params.values) || { };
  74. return {
  75. initialValues,
  76. params,
  77. isEmpty: areParamsEmpty(params)
  78. };
  79. }
  80. }
  81. namespace TransformControlBase {
  82. export interface ComponentState {
  83. params: any,
  84. error?: string,
  85. busy: boolean,
  86. isInitial: boolean,
  87. simpleOnly?: boolean,
  88. isCollapsed?: boolean
  89. }
  90. export interface CommonProps {
  91. simpleApply?: { header: string, icon?: React.FC, title?: string },
  92. noMargin?: boolean,
  93. applyLabel?: string,
  94. onApply?: () => void,
  95. autoHideApply?: boolean,
  96. wrapInExpander?: boolean,
  97. expanderHeaderLeftMargin?: string
  98. }
  99. }
  100. abstract class TransformControlBase<P, S extends TransformControlBase.ComponentState> extends PurePluginUIComponent<P & TransformControlBase.CommonProps, S> {
  101. abstract applyAction(): Promise<void>;
  102. abstract getInfo(): StateTransformParameters.Props['info'];
  103. abstract getHeader(): StateTransformer.Definition['display'] | 'none';
  104. abstract canApply(): boolean;
  105. abstract getTransformerId(): string;
  106. abstract canAutoApply(newParams: any): boolean;
  107. abstract applyText(): string;
  108. abstract isUpdate(): boolean;
  109. abstract getSourceAndTarget(): { a?: StateObject, b?: StateObject };
  110. abstract state: S;
  111. private busy: Subject<boolean> = new Subject();
  112. private onEnter = () => {
  113. if (this.state.error) return;
  114. this.apply();
  115. }
  116. private autoApplyHandle: number | undefined = void 0;
  117. private clearAutoApply() {
  118. if (this.autoApplyHandle !== void 0) {
  119. clearTimeout(this.autoApplyHandle);
  120. this.autoApplyHandle = void 0;
  121. }
  122. }
  123. events: StateTransformParameters.Props['events'] = {
  124. onEnter: this.onEnter,
  125. onChange: (params, isInitial, errors) => {
  126. this.clearAutoApply();
  127. this.setState({ params, isInitial, error: errors && errors[0] }, () => {
  128. if (!isInitial && !this.state.error && this.canAutoApply(params)) {
  129. this.clearAutoApply();
  130. this.autoApplyHandle = setTimeout(this.apply, 50) as any as number;
  131. }
  132. });
  133. }
  134. }
  135. apply = async () => {
  136. this.clearAutoApply();
  137. this.setState({ busy: true });
  138. try {
  139. await this.applyAction();
  140. } catch {
  141. // eat errors because they should be handled elsewhere
  142. } finally {
  143. this.props.onApply?.();
  144. this.busy.next(false);
  145. }
  146. }
  147. componentDidMount() {
  148. this.subscribe(this.plugin.behaviors.state.isBusy, b => {
  149. if (this.state.busy !== b) this.busy.next(b);
  150. });
  151. this.subscribe(this.busy, busy => {
  152. if (this.state.busy !== busy) this.setState({ busy });
  153. });
  154. }
  155. refresh = () => {
  156. this.setState({ params: this.getInfo().initialValues, isInitial: true, error: void 0 });
  157. }
  158. setDefault = () => {
  159. const info = this.getInfo();
  160. const params = PD.getDefaultValues(info.params);
  161. this.setState({ params, isInitial: PD.areEqual(info.params, params, info.initialValues), error: void 0 });
  162. }
  163. toggleExpanded = () => {
  164. this.setState({ isCollapsed: !this.state.isCollapsed });
  165. }
  166. renderApply() {
  167. // const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
  168. const canApply = this.canApply();
  169. if (this.props.autoHideApply && (!canApply || this.canAutoApply(this.state.params))) return null;
  170. return <div className='msp-transform-apply-wrap'>
  171. <IconButton svg={Refresh} className='msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params' />
  172. <div className={`msp-transform-apply-wider`}>
  173. <Button icon={canApply ? Check : void 0} className={`msp-btn-commit msp-btn-commit-${canApply ? 'on' : 'off'}`} onClick={this.apply} disabled={!canApply}>
  174. {this.props.applyLabel || this.applyText()}
  175. </Button>
  176. </div>
  177. </div>;
  178. }
  179. renderDefault() {
  180. const info = this.getInfo();
  181. const isEmpty = info.isEmpty && this.isUpdate();
  182. const display = this.getHeader();
  183. const tId = this.getTransformerId();
  184. const ParamEditor: StateTransformParameters.Class = this.plugin.customParamEditors.has(tId)
  185. ? this.plugin.customParamEditors.get(tId)!
  186. : StateTransformParameters;
  187. const wrapClass = this.state.isCollapsed
  188. ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
  189. : 'msp-transform-wrapper';
  190. let params = null;
  191. if (!isEmpty && !this.state.isCollapsed) {
  192. const { a, b } = this.getSourceAndTarget();
  193. const applyControl = this.renderApply();
  194. params = <>
  195. <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
  196. {applyControl}
  197. </>;
  198. }
  199. const ctrl = <div className={wrapClass} style={{ marginBottom: this.props.noMargin ? 0 : void 0 }}>
  200. {display !== 'none' && !this.props.wrapInExpander && <div className='msp-transform-header'>
  201. <Button onClick={this.toggleExpanded} title={display.description}>
  202. {!isEmpty && <Icon svg={this.state.isCollapsed ? ArrowRight : ArrowDropDown} />}
  203. {display.name}
  204. </Button>
  205. </div>}
  206. {params}
  207. </div>;
  208. if (isEmpty || !this.props.wrapInExpander) return ctrl;
  209. return <ExpandGroup header={this.isUpdate() ? `Update ${display === 'none' ? '' : display.name}` : `Apply ${display === 'none' ? '' : display.name}` } headerLeftMargin={this.props.expanderHeaderLeftMargin}>
  210. {ctrl}
  211. </ExpandGroup>;
  212. }
  213. renderSimple() {
  214. const info = this.getInfo();
  215. const canApply = this.canApply();
  216. const apply = <div className='msp-flex-row'>
  217. <Button icon={this.props.simpleApply?.icon} title={this.props.simpleApply?.title} disabled={this.state.busy || !canApply} onClick={this.apply} style={{ textAlign: 'left' }}>
  218. {this.props.simpleApply?.header}
  219. </Button>
  220. {!info.isEmpty && <ToggleButton icon={Tune} label='' title='Options' toggle={this.toggleExpanded} isSelected={!this.state.isCollapsed} disabled={this.state.busy} style={{ flex: '0 0 40px', padding: 0 }} />}
  221. </div>;
  222. if (this.state.isCollapsed) return apply;
  223. const tId = this.getTransformerId();
  224. const ParamEditor: StateTransformParameters.Class = this.plugin.customParamEditors.has(tId)
  225. ? this.plugin.customParamEditors.get(tId)!
  226. : StateTransformParameters;
  227. const { a, b } = this.getSourceAndTarget();
  228. return <>
  229. {apply}
  230. <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
  231. </>;
  232. }
  233. render() {
  234. // console.log('rendering', ((this.props as any)?.transform?.transformer || (this.props as any)?.action)?.definition.display.name, +new Date)
  235. return this.props.simpleApply ? this.renderSimple() : this.renderDefault();
  236. }
  237. }