parameters.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import * as React from 'react'
  8. import { ParamDefinition as PD } from 'mol-util/param-definition';
  9. import { camelCaseToWords } from 'mol-util/string';
  10. import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
  11. import { Color } from 'mol-util/color';
  12. import { Slider } from './slider';
  13. import { Vec2 } from 'mol-math/linear-algebra';
  14. import LineGraphComponent from './LineGraph/LineGraphComponent';
  15. import { Slider, Slider2 } from './slider';
  16. export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
  17. params: P,
  18. values: any,
  19. onChange: ParamOnChange,
  20. isDisabled?: boolean,
  21. onEnter?: () => void
  22. }
  23. export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
  24. render() {
  25. const params = this.props.params;
  26. const values = this.props.values;
  27. return <div style={{ width: '100%' }}>
  28. {Object.keys(params).map(key => {
  29. const param = params[key];
  30. if (param.isHidden) return null;
  31. const Control = controlFor(param);
  32. if (!Control) return null;
  33. return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
  34. })}
  35. </div>;
  36. }
  37. }
  38. function controlFor(param: PD.Any): ParamControl | undefined {
  39. switch (param.type) {
  40. case 'value': return void 0;
  41. case 'boolean': return BoolControl;
  42. case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  43. ? NumberRangeControl : NumberInputControl;
  44. case 'converted': return ConvertedControl;
  45. case 'multi-select': return MultiSelectControl;
  46. case 'color': return ColorControl;
  47. case 'vec3': return Vec3Control;
  48. case 'file': return FileControl;
  49. case 'select': return SelectControl;
  50. case 'text': return TextControl;
  51. case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  52. ? BoundedIntervalControl : IntervalControl;
  53. case 'group': return GroupControl;
  54. case 'mapped': return MappedControl;
  55. case 'line-graph': return LineGraphControl;
  56. }
  57. console.warn(`${(param as any).type} has no associated UI component.`);
  58. return void 0;
  59. }
  60. // type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
  61. export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
  62. export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: string, value: P['defaultValue'], param: P, isDisabled?: boolean, onChange: ParamOnChange, onEnter?: () => void }
  63. export type ParamControl = React.ComponentClass<ParamProps<any>>
  64. export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> {
  65. protected update(value: any) {
  66. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  67. }
  68. abstract renderControl(): JSX.Element;
  69. render() {
  70. const label = this.props.param.label || camelCaseToWords(this.props.name);
  71. return <div className='msp-control-row'>
  72. <span title={this.props.param.description}>{label}</span>
  73. <div>
  74. {this.renderControl()}
  75. </div>
  76. </div>;
  77. }
  78. }
  79. export class BoolControl extends SimpleParam<PD.Boolean> {
  80. onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); }
  81. renderControl() {
  82. return <button onClick={this.onClick} disabled={this.props.isDisabled}>
  83. <span className={`msp-icon msp-icon-${this.props.value ? 'ok' : 'off'}`} />
  84. {this.props.value ? 'On' : 'Off'}
  85. </button>;
  86. }
  87. }
  88. export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
  89. state = {
  90. isExpanded: false,
  91. isOverPoint: false,
  92. message: `${this.props.param.defaultValue.length} points`,
  93. }
  94. onHover = (point?: Vec2) => {
  95. this.setState({isOverPoint: !this.state.isOverPoint});
  96. if(point){
  97. this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
  98. return;
  99. }
  100. this.setState({message: `${this.props.value.length} points`});
  101. }
  102. onDrag = (point: Vec2) => {
  103. this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
  104. }
  105. onChange = (value: PD.LineGraph['defaultValue'] ) => {
  106. this.props.onChange({ name: this.props.name, param: this.props.param, value: value});
  107. }
  108. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  109. this.setState({ isExpanded: !this.state.isExpanded });
  110. e.currentTarget.blur();
  111. }
  112. render() {
  113. const label = this.props.param.label || camelCaseToWords(this.props.name);
  114. return <>
  115. <div className='msp-control-row'>
  116. <span>{label}</span>
  117. <div>
  118. <button onClick={this.toggleExpanded}>
  119. {`${this.state.message}`}
  120. </button>
  121. </div>
  122. </div>
  123. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  124. <LineGraphComponent
  125. data={this.props.param.defaultValue}
  126. onChange={this.onChange}
  127. onHover={this.onHover}
  128. onDrag={this.onDrag}/>
  129. </div>
  130. </>;
  131. }
  132. }
  133. export class NumberInputControl extends SimpleParam<PD.Numeric> {
  134. onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
  135. renderControl() {
  136. return <span>
  137. number input TODO
  138. </span>
  139. }
  140. }
  141. export class NumberRangeControl extends SimpleParam<PD.Numeric> {
  142. onChange = (v: number) => { this.update(v); }
  143. renderControl() {
  144. return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
  145. step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />
  146. }
  147. }
  148. export class TextControl extends SimpleParam<PD.Text> {
  149. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  150. const value = e.target.value;
  151. if (value !== this.props.value) {
  152. this.update(value);
  153. }
  154. }
  155. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  156. if (!this.props.onEnter) return;
  157. if ((e.keyCode === 13 || e.charCode === 13)) {
  158. this.props.onEnter();
  159. }
  160. }
  161. renderControl() {
  162. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  163. return <input type='text'
  164. value={this.props.value || ''}
  165. placeholder={placeholder}
  166. onChange={this.onChange}
  167. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  168. disabled={this.props.isDisabled}
  169. />;
  170. }
  171. }
  172. export class SelectControl extends SimpleParam<PD.Select<any>> {
  173. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
  174. renderControl() {
  175. return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
  176. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  177. </select>;
  178. }
  179. }
  180. export class IntervalControl extends SimpleParam<PD.Interval> {
  181. onChange = (v: [number, number]) => { this.update(v); }
  182. renderControl() {
  183. return <span>interval TODO</span>;
  184. }
  185. }
  186. export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
  187. onChange = (v: [number, number]) => { this.update(v); }
  188. renderControl() {
  189. return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
  190. step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />;
  191. }
  192. }
  193. let _colors: React.ReactFragment | undefined = void 0;
  194. function ColorOptions() {
  195. if (_colors) return _colors;
  196. _colors = <>{Object.keys(ColorNames).map(name =>
  197. <option key={name} value={(ColorNames as { [k: string]: Color})[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color})[name])}` }} >
  198. {name}
  199. </option>
  200. )}</>;
  201. return _colors;
  202. }
  203. function ColorValueOption(color: Color) {
  204. return !ColorNamesValueMap.has(color) ? <option key={Color.toHexString(color)} value={color} style={{ background: `${Color.toStyle(color)}` }} >
  205. {Color.toHexString(color)}
  206. </option> : null
  207. }
  208. export class ColorControl extends SimpleParam<PD.Color> {
  209. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  210. this.update(Color(parseInt(e.target.value)));
  211. }
  212. renderControl() {
  213. return <select value={this.props.value} onChange={this.onChange} style={{ borderLeft: `16px solid ${Color.toStyle(this.props.value)}` }}>
  214. {ColorValueOption(this.props.value)}
  215. {ColorOptions()}
  216. </select>;
  217. }
  218. }
  219. export class Vec3Control extends SimpleParam<PD.Vec3> {
  220. // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  221. // this.setState({ value: e.target.value });
  222. // this.props.onChange(e.target.value);
  223. // }
  224. renderControl() {
  225. return <span>vec3 TODO</span>;
  226. }
  227. }
  228. export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
  229. change(value: File) {
  230. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  231. }
  232. onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
  233. this.change(e.target.files![0]);
  234. }
  235. render() {
  236. const value = this.props.value;
  237. // return <input disabled={this.props.isDisabled} value={void 0} type='file' multiple={false} />
  238. return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
  239. {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
  240. </div>
  241. }
  242. }
  243. export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
  244. state = { isExpanded: false }
  245. change(value: PD.MultiSelect<any>['defaultValue'] ) {
  246. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  247. }
  248. toggle(key: string) {
  249. return (e: React.MouseEvent<HTMLButtonElement>) => {
  250. if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
  251. else this.change(this.props.value.filter(v => v !== key));
  252. e.currentTarget.blur();
  253. }
  254. }
  255. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  256. this.setState({ isExpanded: !this.state.isExpanded });
  257. e.currentTarget.blur();
  258. }
  259. render() {
  260. const current = this.props.value;
  261. const label = this.props.param.label || camelCaseToWords(this.props.name);
  262. return <>
  263. <div className='msp-control-row'>
  264. <span>{label}</span>
  265. <div>
  266. <button onClick={this.toggleExpanded}>
  267. {`${current.length} of ${this.props.param.options.length}`}
  268. </button>
  269. </div>
  270. </div>
  271. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  272. {this.props.param.options.map(([value, label]) => {
  273. const sel = current.indexOf(value) >= 0;
  274. return <div key={value} className='msp-row'>
  275. <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
  276. <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
  277. </button>
  278. </div> })}
  279. </div>
  280. </>;
  281. }
  282. }
  283. export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
  284. state = { isExpanded: !!this.props.param.isExpanded }
  285. change(value: any ) {
  286. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  287. }
  288. onChangeParam: ParamOnChange = e => {
  289. this.change({ ...this.props.value, [e.name]: e.value });
  290. }
  291. toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
  292. render() {
  293. const params = this.props.param.params;
  294. const label = this.props.param.label || camelCaseToWords(this.props.name);
  295. const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
  296. if (this.props.param.isFlat) {
  297. return controls;
  298. }
  299. return <div className='msp-control-group-wrapper'>
  300. <div className='msp-control-group-header'>
  301. <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
  302. <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
  303. {label}
  304. </button>
  305. </div>
  306. {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
  307. {controls}
  308. </div>
  309. }
  310. </div>
  311. }
  312. }
  313. export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
  314. change(value: PD.Mapped<any>['defaultValue'] ) {
  315. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  316. }
  317. onChangeName: ParamOnChange = e => {
  318. // TODO: Cache values when changing types?
  319. this.change({ name: e.value, params: this.props.param.map(e.value).defaultValue });
  320. }
  321. onChangeParam: ParamOnChange = e => {
  322. this.change({ name: this.props.value.name, params: e.value });
  323. }
  324. render() {
  325. const value: PD.Mapped<any>['defaultValue'] = this.props.value;
  326. const param = this.props.param.map(value.name);
  327. const label = this.props.param.label || camelCaseToWords(this.props.name);
  328. const Mapped = controlFor(param);
  329. const select = <SelectControl param={this.props.param.select}
  330. isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
  331. name={label} value={value.name} />
  332. if (!Mapped) {
  333. return select;
  334. }
  335. return <div>
  336. {select}
  337. <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  338. </div>
  339. }
  340. }
  341. export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
  342. onChange: ParamOnChange = e => {
  343. this.props.onChange({
  344. name: this.props.name,
  345. param: this.props.param,
  346. value: this.props.param.toValue(e.value)
  347. });
  348. }
  349. render() {
  350. const value = this.props.param.fromValue(this.props.value);
  351. const Converted = controlFor(this.props.param.converted);
  352. if (!Converted) return null;
  353. return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  354. }
  355. }