parameters.tsx 13 KB

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