parameters.tsx 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524
  1. /**
  2. * Copyright (c) 2018-2021 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 { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
  9. import { Volume } from '../../mol-model/volume';
  10. import { Script } from '../../mol-script/script';
  11. import { Asset } from '../../mol-util/assets';
  12. import { Color } from '../../mol-util/color';
  13. import { ColorListEntry } from '../../mol-util/color/color';
  14. import { ColorListName, ColorListOptions, ColorListOptionsScale, ColorListOptionsSet, getColorListFromName } from '../../mol-util/color/lists';
  15. import { Legend as LegendData } from '../../mol-util/legend';
  16. import { memoize1, memoizeLatest } from '../../mol-util/memoize';
  17. import { getPrecision } from '../../mol-util/number';
  18. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  19. import { ParamMapping } from '../../mol-util/param-mapping';
  20. import { camelCaseToWords } from '../../mol-util/string';
  21. import { PluginReactContext, PluginUIComponent } from '../base';
  22. import { PluginUIContext } from '../context';
  23. import { ActionMenu } from './action-menu';
  24. import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
  25. import { Button, ControlGroup, ControlRow, ExpandGroup, IconButton, TextInput, ToggleButton } from './common';
  26. import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, WarningSvg } from './icons';
  27. import { legendFor } from './legend';
  28. import { LineGraphComponent } from './line-graph/line-graph-component';
  29. import { Slider, Slider2 } from './slider';
  30. export type ParameterControlsCategoryFilter = string | null | (string | null)[]
  31. export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
  32. params: P,
  33. values: any,
  34. onChange?: ParamsOnChange<PD.ValuesFor<P>>,
  35. onChangeValues?: (values: PD.ValuesFor<P>, prev: PD.ValuesFor<P>) => void,
  36. isDisabled?: boolean,
  37. onEnter?: () => void
  38. }
  39. export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>> {
  40. onChange: ParamOnChange = (params) => {
  41. this.props.onChange?.(params, this.props.values);
  42. if (this.props.onChangeValues) {
  43. const values = { ...this.props.values, [params.name]: params.value };
  44. this.props.onChangeValues(values, this.props.values);
  45. }
  46. };
  47. renderGroup(group: ParamInfo[]) {
  48. if (group.length === 0) return null;
  49. const values = this.props.values;
  50. let ctrls: JSX.Element[] | null = null;
  51. let category: string | undefined = void 0;
  52. for (const [key, p, Control] of group) {
  53. if (p.hideIf?.(values)) continue;
  54. if (!ctrls) ctrls = [];
  55. category = p.category;
  56. ctrls.push(<Control param={p} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />);
  57. }
  58. if (!ctrls) return null;
  59. if (category) {
  60. return [<ExpandGroup key={category} header={category}>{ctrls}</ExpandGroup>];
  61. }
  62. return ctrls;
  63. }
  64. renderPart(groups: ParamInfo[][]) {
  65. let parts: JSX.Element[] | null = null;
  66. for (const g of groups) {
  67. const ctrls = this.renderGroup(g);
  68. if (!ctrls) continue;
  69. if (!parts) parts = [];
  70. for (const c of ctrls) parts.push(c);
  71. }
  72. return parts;
  73. }
  74. paramGroups = memoizeLatest((params: PD.Params) => classifyParams(params));
  75. render() {
  76. const groups = this.paramGroups(this.props.params);
  77. const essentials = this.renderPart(groups.essentials);
  78. const advanced = this.renderPart(groups.advanced);
  79. if (essentials && advanced) {
  80. return <>
  81. {essentials}
  82. <ExpandGroup header='Advanced Options'>
  83. {advanced}
  84. </ExpandGroup>
  85. </>;
  86. } else if (essentials) {
  87. return essentials;
  88. } else {
  89. return advanced;
  90. }
  91. }
  92. }
  93. export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginUIContext> }> {
  94. setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => {
  95. const values = { ...old, [p.name]: p.value };
  96. const t = this.props.mapping.update(values, this.plugin);
  97. this.props.mapping.apply(t, this.plugin);
  98. };
  99. componentDidMount() {
  100. this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
  101. }
  102. render() {
  103. const t = this.props.mapping.getTarget(this.plugin);
  104. const values = this.props.mapping.getValues(t, this.plugin);
  105. const params = this.props.mapping.params(this.plugin) as any as PD.Params;
  106. return <ParameterControls params={params} values={values} onChange={this.setSettings} />;
  107. }
  108. }
  109. type ParamInfo = [string, PD.Any, ParamControl];
  110. function classifyParams(params: PD.Params) {
  111. function addParam(k: string, p: PD.Any, group: typeof essentials) {
  112. const ctrl = controlFor(p);
  113. if (!ctrl) return;
  114. if (!p.category) group.params[0].push([k, p, ctrl]);
  115. else {
  116. if (!group.map) group.map = new Map();
  117. let c = group.map.get(p.category);
  118. if (!c) {
  119. c = [];
  120. group.map.set(p.category, c);
  121. group.params.push(c);
  122. }
  123. c.push([k, p, ctrl]);
  124. }
  125. }
  126. function sortGroups(x: ParamInfo[], y: ParamInfo[]) {
  127. const a = x[0], b = y[0];
  128. if (!a || !a[1].category) return -1;
  129. if (!b || !b[1].category) return 1;
  130. return a[1].category < b[1].category ? -1 : 1;
  131. }
  132. const keys = Object.keys(params);
  133. const essentials: { params: ParamInfo[][], map: Map<string, ParamInfo[]> | undefined } = { params: [[]], map: void 0 };
  134. const advanced: typeof essentials = { params: [[]], map: void 0 };
  135. for (const k of keys) {
  136. const p = params[k];
  137. if (p.isHidden) continue;
  138. if (p.isEssential) addParam(k, p, essentials);
  139. else addParam(k, p, advanced);
  140. }
  141. essentials.params.sort(sortGroups);
  142. advanced.params.sort(sortGroups);
  143. return { essentials: essentials.params, advanced: advanced.params };
  144. }
  145. function controlFor(param: PD.Any): ParamControl | undefined {
  146. switch (param.type) {
  147. case 'value': return void 0;
  148. case 'boolean': return BoolControl;
  149. case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  150. ? NumberRangeControl : NumberInputControl;
  151. case 'converted': return ConvertedControl;
  152. case 'conditioned': return ConditionedControl;
  153. case 'multi-select': return MultiSelectControl;
  154. case 'color': return CombinedColorControl;
  155. case 'color-list': return param.offsets ? OffsetColorListControl : ColorListControl;
  156. case 'vec3': return Vec3Control;
  157. case 'mat4': return Mat4Control;
  158. case 'url': return UrlControl;
  159. case 'file': return FileControl;
  160. case 'file-list': return FileListControl;
  161. case 'select': return SelectControl;
  162. case 'value-ref': return ValueRefControl;
  163. case 'data-ref': return void 0;
  164. case 'text': return TextControl;
  165. case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
  166. ? BoundedIntervalControl : IntervalControl;
  167. case 'group': return GroupControl;
  168. case 'mapped': return MappedControl;
  169. case 'line-graph': return LineGraphControl;
  170. case 'script': return ScriptControl;
  171. case 'object-list': return ObjectListControl;
  172. default:
  173. const _: never = param;
  174. console.warn(`${_} has no associated UI component`);
  175. return void 0;
  176. }
  177. }
  178. export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legend?: L, description?: string }> {
  179. render() {
  180. const { legend, description } = this.props;
  181. const Legend = legend && legendFor(legend);
  182. return <div className='msp-help-text'>
  183. <div>
  184. <div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />{description}</div>
  185. {Legend && <div className='msp-help-legend'><Legend legend={legend} /></div>}
  186. </div>
  187. </div>;
  188. }
  189. }
  190. export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void
  191. export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
  192. export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
  193. name: string,
  194. value: P['defaultValue'],
  195. param: P,
  196. isDisabled?: boolean,
  197. onChange: ParamOnChange,
  198. onEnter?: () => void
  199. }
  200. export type ParamControl = React.ComponentClass<ParamProps<any>>
  201. function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) {
  202. const { props, state, control, toggleHelp, addOn } = options;
  203. const _className = [];
  204. if (props.param.shortLabel) _className.push('msp-control-label-short');
  205. if (props.param.twoColumns) _className.push('msp-control-col-2');
  206. const className = _className.join(' ');
  207. const label = props.param.label || camelCaseToWords(props.name);
  208. const help = props.param.help
  209. ? props.param.help(props.value)
  210. : { description: props.param.description, legend: props.param.legend };
  211. const hasHelp = help.description || help.legend;
  212. const desc = label + (hasHelp ? '. Click for help.' : '');
  213. return <>
  214. <ControlRow
  215. className={className}
  216. title={desc}
  217. label={<>
  218. {label}
  219. {hasHelp &&
  220. <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp}
  221. title={desc || `${state.showHelp ? 'Hide' : 'Show'} help`}
  222. style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
  223. <Icon svg={HelpOutlineSvg} />
  224. </button>
  225. }
  226. </>}
  227. control={control}
  228. />
  229. {hasHelp && state.showHelp && <div className='msp-control-offset'>
  230. <ParamHelp legend={help.legend} description={help.description} />
  231. </div>}
  232. {addOn}
  233. </>;
  234. }
  235. export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { showHelp: boolean }> {
  236. state = { showHelp: false };
  237. protected update(value: P['defaultValue']) {
  238. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  239. }
  240. abstract renderControl(): JSX.Element;
  241. renderAddOn(): JSX.Element | null { return null; }
  242. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  243. render() {
  244. return renderSimple({
  245. props: this.props,
  246. state: this.state,
  247. control: this.renderControl(),
  248. toggleHelp: this.toggleHelp,
  249. addOn: this.renderAddOn()
  250. });
  251. }
  252. }
  253. export class BoolControl extends SimpleParam<PD.BooleanParam> {
  254. onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); };
  255. renderControl() {
  256. return <button onClick={this.onClick} disabled={this.props.isDisabled}>
  257. <Icon svg={this.props.value ? CheckSvg : ClearSvg} />
  258. {this.props.value ? 'On' : 'Off'}
  259. </button>;
  260. }
  261. }
  262. export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
  263. state = {
  264. isExpanded: false,
  265. isOverPoint: false,
  266. message: `${this.props.param.defaultValue.length} points`,
  267. };
  268. private pointToLabel(point?: Vec2) {
  269. if (!point) return '';
  270. const volume = this.props.param.getVolume?.() as Volume;
  271. if (volume) {
  272. const { min, max, mean, sigma } = volume.grid.stats;
  273. const v = min + (max - min) * point[0];
  274. const s = (v - mean) / sigma;
  275. return `(${v.toFixed(2)} | ${s.toFixed(2)}σ, ${point[1].toFixed(2)})`;
  276. } else {
  277. return `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`;
  278. }
  279. }
  280. onHover = (point?: Vec2) => {
  281. this.setState({ isOverPoint: !this.state.isOverPoint });
  282. if (point) {
  283. this.setState({ message: this.pointToLabel(point) });
  284. } else {
  285. this.setState({ message: `${this.props.value.length} points` });
  286. }
  287. };
  288. onDrag = (point: Vec2) => {
  289. this.setState({ message: this.pointToLabel(point) });
  290. };
  291. onChange = (value: PD.LineGraph['defaultValue']) => {
  292. this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
  293. };
  294. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  295. this.setState({ isExpanded: !this.state.isExpanded });
  296. e.currentTarget.blur();
  297. };
  298. render() {
  299. const label = this.props.param.label || camelCaseToWords(this.props.name);
  300. return <>
  301. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{`${this.state.message}`}</button>} />
  302. <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none', marginTop: 1 }}>
  303. <LineGraphComponent
  304. data={this.props.value}
  305. volume={this.props.param.getVolume?.()}
  306. onChange={this.onChange}
  307. onHover={this.onHover}
  308. onDrag={this.onDrag} />
  309. </div>
  310. </>;
  311. }
  312. }
  313. export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> {
  314. state = { value: '0' };
  315. update = (value: number) => {
  316. const p = getPrecision(this.props.param.step || 0.01);
  317. value = parseFloat(value.toFixed(p));
  318. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  319. };
  320. render() {
  321. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  322. const label = this.props.param.label || camelCaseToWords(this.props.name);
  323. const p = getPrecision(this.props.param.step || 0.01);
  324. return <ControlRow
  325. title={this.props.param.description}
  326. label={label}
  327. control={<TextInput numeric
  328. value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
  329. isDisabled={this.props.isDisabled} onChange={this.update} />} />;
  330. }
  331. }
  332. export class NumberRangeControl extends SimpleParam<PD.Numeric> {
  333. onChange = (v: number) => { this.update(v); };
  334. renderControl() {
  335. const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value;
  336. return <Slider value={value} min={this.props.param.min!} max={this.props.param.max!}
  337. step={this.props.param.step} onChange={this.onChange} onChangeImmediate={this.props.param.immediateUpdate ? this.onChange : void 0}
  338. disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
  339. }
  340. }
  341. export class TextControl extends SimpleParam<PD.Text> {
  342. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  343. const value = e.target.value;
  344. if (value !== this.props.value) {
  345. this.update(value);
  346. }
  347. };
  348. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  349. if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
  350. if (this.props.onEnter) this.props.onEnter();
  351. }
  352. e.stopPropagation();
  353. };
  354. renderControl() {
  355. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  356. return <input type='text'
  357. value={this.props.value || ''}
  358. placeholder={placeholder}
  359. onChange={this.onChange}
  360. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  361. disabled={this.props.isDisabled}
  362. />;
  363. }
  364. }
  365. export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>> & { title?: string }> {
  366. protected update(value: string | number) {
  367. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  368. }
  369. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  370. if (typeof this.props.param.defaultValue === 'number') {
  371. this.update(parseInt(e.target.value, 10));
  372. } else {
  373. this.update(e.target.value);
  374. }
  375. };
  376. render() {
  377. const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
  378. return <select className='msp-form-control' title={this.props.title} value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
  379. {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
  380. {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
  381. </select>;
  382. }
  383. }
  384. export class SelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>>, { showHelp: boolean, showOptions: boolean }> {
  385. state = { showHelp: false, showOptions: false };
  386. onSelect: ActionMenu.OnSelect = item => {
  387. if (!item || item.value === this.props.value) {
  388. this.setState({ showOptions: false });
  389. } else {
  390. this.setState({ showOptions: false }, () => {
  391. this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value });
  392. });
  393. }
  394. };
  395. toggle = () => this.setState({ showOptions: !this.state.showOptions });
  396. cycle = () => {
  397. const { options } = this.props.param;
  398. const current = options.findIndex(o => o[0] === this.props.value);
  399. const next = current === options.length - 1 ? 0 : current + 1;
  400. this.props.onChange({ param: this.props.param, name: this.props.name, value: options[next][0] });
  401. };
  402. items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createItemsFromSelectOptions(param.options));
  403. renderControl() {
  404. const items = this.items(this.props.param);
  405. const current = this.props.value !== undefined ? ActionMenu.findItem(items, this.props.value) : void 0;
  406. const label = current
  407. ? current.label
  408. : typeof this.props.value === 'undefined'
  409. ? `${ActionMenu.getFirstItem(items)?.label || ''} [Default]`
  410. : `[Invalid] ${this.props.value}`;
  411. const toggle = this.props.param.cycle ? this.cycle : this.toggle;
  412. const textAlign = this.props.param.cycle ? 'center' : 'left';
  413. const icon = this.props.param.cycle
  414. ? (this.props.value === 'on' ? CheckSvg
  415. : this.props.value === 'off' ? ClearSvg : void 0)
  416. : void 0;
  417. return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign, overflow: 'hidden', textOverflow: 'ellipsis' }}
  418. label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} />;
  419. }
  420. renderAddOn() {
  421. if (!this.state.showOptions) return null;
  422. const items = this.items(this.props.param);
  423. const current = ActionMenu.findItem(items, this.props.value);
  424. return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
  425. }
  426. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  427. render() {
  428. return renderSimple({
  429. props: this.props,
  430. state: this.state,
  431. control: this.renderControl(),
  432. toggleHelp: this.toggleHelp,
  433. addOn: this.renderAddOn()
  434. });
  435. }
  436. }
  437. export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<any>>, { showHelp: boolean, showOptions: boolean }> {
  438. state = { showHelp: false, showOptions: false };
  439. onSelect: ActionMenu.OnSelect = item => {
  440. if (!item || item.value === this.props.value) {
  441. this.setState({ showOptions: false });
  442. } else {
  443. this.setState({ showOptions: false }, () => {
  444. this.props.onChange({ param: this.props.param, name: this.props.name, value: { ref: item.value } });
  445. });
  446. }
  447. };
  448. toggle = () => this.setState({ showOptions: !this.state.showOptions });
  449. private get items() {
  450. return ActionMenu.createItemsFromSelectOptions(this.props.param.getOptions(this.context));
  451. }
  452. renderControl() {
  453. const items = this.items;
  454. const current = this.props.value.ref ? ActionMenu.findItem(items, this.props.value.ref) : void 0;
  455. const label = current
  456. ? current.label
  457. : `[Ref] ${this.props.value.ref ?? ''}`;
  458. return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }}
  459. label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />;
  460. }
  461. renderAddOn() {
  462. if (!this.state.showOptions) return null;
  463. const items = this.items;
  464. const current = ActionMenu.findItem(items, this.props.value.ref);
  465. return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
  466. }
  467. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  468. render() {
  469. return renderSimple({
  470. props: this.props,
  471. state: this.state,
  472. control: this.renderControl(),
  473. toggleHelp: this.toggleHelp,
  474. addOn: this.renderAddOn()
  475. });
  476. }
  477. }
  478. ValueRefControl.contextType = PluginReactContext;
  479. export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
  480. state = { isExpanded: false };
  481. components = {
  482. 0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }),
  483. 1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' })
  484. };
  485. change(value: PD.MultiSelect<any>['defaultValue']) {
  486. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  487. }
  488. componentChange: ParamOnChange = ({ name, value }) => {
  489. const v = [...this.props.value];
  490. v[+name] = value;
  491. this.change(v);
  492. };
  493. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  494. this.setState({ isExpanded: !this.state.isExpanded });
  495. e.currentTarget.blur();
  496. };
  497. render() {
  498. const v = this.props.value;
  499. const label = this.props.param.label || camelCaseToWords(this.props.name);
  500. const p = getPrecision(this.props.param.step || 0.01);
  501. const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`;
  502. return <>
  503. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
  504. {this.state.isExpanded && <div className='msp-control-offset'>
  505. <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
  506. </div>}
  507. </>;
  508. }
  509. }
  510. export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
  511. onChange = (v: [number, number]) => { this.update(v); };
  512. renderControl() {
  513. return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
  514. step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
  515. }
  516. }
  517. export class ColorControl extends SimpleParam<PD.Color> {
  518. onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  519. this.update(Color(parseInt(e.target.value)));
  520. };
  521. stripStyle(): React.CSSProperties {
  522. return {
  523. background: Color.toStyle(this.props.value),
  524. position: 'absolute',
  525. bottom: '0',
  526. height: '4px',
  527. right: '0',
  528. left: '0'
  529. };
  530. }
  531. renderControl() {
  532. return <div style={{ position: 'relative' }}>
  533. <select value={this.props.value} onChange={this.onChange}>
  534. {ColorValueOption(this.props.value)}
  535. {ColorOptions()}
  536. </select>
  537. <div style={this.stripStyle()} />
  538. </div>;
  539. }
  540. }
  541. function colorEntryToStyle(e: ColorListEntry, includeOffset = false) {
  542. if (Array.isArray(e)) {
  543. if (includeOffset) return `${Color.toStyle(e[0])} ${(100 * e[1]).toFixed(2)}%`;
  544. return Color.toStyle(e[0]);
  545. }
  546. return Color.toStyle(e);
  547. }
  548. const colorGradientInterpolated = memoize1((colors: ColorListEntry[]) => {
  549. const styles = colors.map(c => colorEntryToStyle(c, true));
  550. return `linear-gradient(to right, ${styles.join(', ')})`;
  551. });
  552. const colorGradientBanded = memoize1((colors: ColorListEntry[]) => {
  553. const n = colors.length;
  554. const styles: string[] = [`${colorEntryToStyle(colors[0])} ${100 * (1 / n)}%`];
  555. // TODO: does this need to support offsets?
  556. for (let i = 1, il = n - 1; i < il; ++i) {
  557. styles.push(
  558. `${colorEntryToStyle(colors[i])} ${100 * (i / n)}%`,
  559. `${colorEntryToStyle(colors[i])} ${100 * ((i + 1) / n)}%`
  560. );
  561. }
  562. styles.push(`${colorEntryToStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`);
  563. return `linear-gradient(to right, ${styles.join(', ')})`;
  564. });
  565. function colorStripStyle(list: PD.ColorList['defaultValue'], right = '0'): React.CSSProperties {
  566. return {
  567. background: colorGradient(list.colors, list.kind === 'set'),
  568. position: 'absolute',
  569. bottom: '0',
  570. height: '4px',
  571. right,
  572. left: '0'
  573. };
  574. }
  575. function colorGradient(colors: ColorListEntry[], banded: boolean) {
  576. return banded ? colorGradientBanded(colors) : colorGradientInterpolated(colors);
  577. }
  578. function createColorListHelpers() {
  579. const addOn = (l: [ColorListName, any, any]) => {
  580. const preset = getColorListFromName(l[0]);
  581. return <div style={colorStripStyle({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list })} />;
  582. };
  583. return {
  584. ColorPresets: {
  585. all: ActionMenu.createItemsFromSelectOptions(ColorListOptions, { addOn }),
  586. scale: ActionMenu.createItemsFromSelectOptions(ColorListOptionsScale, { addOn }),
  587. set: ActionMenu.createItemsFromSelectOptions(ColorListOptionsSet, { addOn })
  588. },
  589. ColorsParam: PD.ObjectList({ color: PD.Color(0x0 as Color) }, ({ color }) => Color.toHexString(color).toUpperCase()),
  590. OffsetColorsParam: PD.ObjectList(
  591. { color: PD.Color(0x0 as Color), offset: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) },
  592. ({ color, offset }) => `${Color.toHexString(color).toUpperCase()} [${offset.toFixed(2)}]`),
  593. IsInterpolatedParam: PD.Boolean(false, { label: 'Interpolated' })
  594. };
  595. }
  596. let _colorListHelpers: ReturnType<typeof createColorListHelpers>;
  597. function ColorListHelpers() {
  598. if (_colorListHelpers) return _colorListHelpers;
  599. _colorListHelpers = createColorListHelpers();
  600. return _colorListHelpers;
  601. }
  602. export class ColorListControl extends React.PureComponent<ParamProps<PD.ColorList>, { showHelp: boolean, show?: 'edit' | 'presets' }> {
  603. state = { showHelp: false, show: void 0 as 'edit' | 'presets' | undefined };
  604. protected update(value: PD.ColorList['defaultValue']) {
  605. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  606. }
  607. toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' });
  608. togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' });
  609. renderControl() {
  610. const { value } = this.props;
  611. // TODO: fix the button right offset
  612. return <>
  613. <button onClick={this.toggleEdit} style={{ position: 'relative', paddingRight: '33px' }}>
  614. {value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`}
  615. <div style={colorStripStyle(value, '33px')} />
  616. </button>
  617. <IconButton svg={BookmarksOutlinedSvg} onClick={this.togglePresets} toggleState={this.state.show === 'presets'} title='Color Presets'
  618. style={{ padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' }} />
  619. </>;
  620. }
  621. selectPreset: ActionMenu.OnSelect = item => {
  622. if (!item) return;
  623. this.setState({ show: void 0 });
  624. const preset = getColorListFromName(item.value as ColorListName);
  625. this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
  626. };
  627. colorsChanged: ParamOnChange = ({ value }) => {
  628. this.update({
  629. kind: this.props.value.kind,
  630. colors: (value as (typeof _colorListHelpers)['ColorsParam']['defaultValue']).map(c => c.color)
  631. });
  632. };
  633. isInterpolatedChanged: ParamOnChange = ({ value }) => {
  634. this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
  635. };
  636. renderColors() {
  637. if (!this.state.show) return null;
  638. const { ColorPresets, ColorsParam, IsInterpolatedParam } = ColorListHelpers();
  639. const preset = ColorPresets[this.props.param.presetKind];
  640. if (this.state.show === 'presets') return <ActionMenu items={preset} onSelect={this.selectPreset} />;
  641. const values = this.props.value.colors.map(color => ({ color }));
  642. return <div className='msp-control-offset'>
  643. <ObjectListControl name='colors' param={ColorsParam} value={values} onChange={this.colorsChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
  644. <BoolControl name='isInterpolated' param={IsInterpolatedParam} value={this.props.value.kind === 'interpolate'} onChange={this.isInterpolatedChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
  645. </div>;
  646. }
  647. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  648. render() {
  649. return renderSimple({
  650. props: this.props,
  651. state: this.state,
  652. control: this.renderControl(),
  653. toggleHelp: this.toggleHelp,
  654. addOn: this.renderColors()
  655. });
  656. }
  657. }
  658. export class OffsetColorListControl extends React.PureComponent<ParamProps<PD.ColorList>, { showHelp: boolean, show?: 'edit' | 'presets' }> {
  659. state = { showHelp: false, show: void 0 as 'edit' | 'presets' | undefined };
  660. protected update(value: PD.ColorList['defaultValue']) {
  661. this.props.onChange({ param: this.props.param, name: this.props.name, value });
  662. }
  663. toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' });
  664. togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' });
  665. renderControl() {
  666. const { value } = this.props;
  667. // TODO: fix the button right offset
  668. return <>
  669. <button onClick={this.toggleEdit} style={{ position: 'relative', paddingRight: '33px' }}>
  670. {value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`}
  671. <div style={colorStripStyle(value, '33px')} />
  672. </button>
  673. <IconButton svg={BookmarksOutlinedSvg} onClick={this.togglePresets} toggleState={this.state.show === 'presets'} title='Color Presets'
  674. style={{ padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' }} />
  675. </>;
  676. }
  677. selectPreset: ActionMenu.OnSelect = item => {
  678. if (!item) return;
  679. this.setState({ show: void 0 });
  680. const preset = getColorListFromName(item.value as ColorListName);
  681. this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
  682. };
  683. colorsChanged: ParamOnChange = ({ value }) => {
  684. const colors = (value as (typeof _colorListHelpers)['OffsetColorsParam']['defaultValue']).map(c => [c.color, c.offset] as [Color, number]);
  685. colors.sort((a, b) => a[1] - b[1]);
  686. this.update({ kind: this.props.value.kind, colors });
  687. };
  688. isInterpolatedChanged: ParamOnChange = ({ value }) => {
  689. this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
  690. };
  691. renderColors() {
  692. if (!this.state.show) return null;
  693. const { ColorPresets, OffsetColorsParam, IsInterpolatedParam } = ColorListHelpers();
  694. const preset = ColorPresets[this.props.param.presetKind];
  695. if (this.state.show === 'presets') return <ActionMenu items={preset} onSelect={this.selectPreset} />;
  696. const colors = this.props.value.colors;
  697. const values = colors.map((color, i) => {
  698. if (Array.isArray(color)) return { color: color[0], offset: color[1] };
  699. return { color, offset: i / colors.length };
  700. });
  701. values.sort((a, b) => a.offset - b.offset);
  702. return <div className='msp-control-offset'>
  703. <ObjectListControl name='colors' param={OffsetColorsParam} value={values} onChange={this.colorsChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
  704. <BoolControl name='isInterpolated' param={IsInterpolatedParam} value={this.props.value.kind === 'interpolate'} onChange={this.isInterpolatedChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
  705. </div>;
  706. }
  707. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  708. render() {
  709. return renderSimple({
  710. props: this.props,
  711. state: this.state,
  712. control: this.renderControl(),
  713. toggleHelp: this.toggleHelp,
  714. addOn: this.renderColors()
  715. });
  716. }
  717. }
  718. export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
  719. state = { isExpanded: false };
  720. components = {
  721. 0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
  722. 1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
  723. 2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
  724. };
  725. change(value: PD.MultiSelect<any>['defaultValue']) {
  726. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  727. }
  728. componentChange: ParamOnChange = ({ name, value }) => {
  729. const v = Vec3.copy(Vec3.zero(), this.props.value);
  730. v[+name] = value;
  731. this.change(v);
  732. };
  733. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  734. this.setState({ isExpanded: !this.state.isExpanded });
  735. e.currentTarget.blur();
  736. };
  737. render() {
  738. const v = this.props.value;
  739. const label = this.props.param.label || camelCaseToWords(this.props.name);
  740. const p = getPrecision(this.props.param.step || 0.01);
  741. const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`;
  742. return <>
  743. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
  744. {this.state.isExpanded && <div className='msp-control-offset'>
  745. <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
  746. </div>}
  747. </>;
  748. }
  749. }
  750. export class Mat4Control extends React.PureComponent<ParamProps<PD.Mat4>, { isExpanded: boolean }> {
  751. state = { isExpanded: false };
  752. components = {
  753. json: PD.Text(JSON.stringify(Mat4()), { description: 'JSON array with 4x4 matrix in a column major (j * 4 + i indexing) format' })
  754. };
  755. change(value: PD.MultiSelect<any>['defaultValue']) {
  756. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  757. }
  758. componentChange: ParamOnChange = ({ name, value }) => {
  759. const v = Mat4.copy(Mat4(), this.props.value);
  760. if (name === 'json') {
  761. Mat4.copy(v, JSON.parse(value));
  762. } else {
  763. v[+name] = value;
  764. }
  765. this.change(v);
  766. };
  767. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  768. this.setState({ isExpanded: !this.state.isExpanded });
  769. e.currentTarget.blur();
  770. };
  771. changeValue(idx: number) {
  772. return (v: number) => {
  773. const m = Mat4.copy(Mat4(), this.props.value);
  774. m[idx] = v;
  775. this.change(m);
  776. };
  777. }
  778. get grid() {
  779. const v = this.props.value;
  780. const rows: React.ReactNode[] = [];
  781. for (let i = 0; i < 4; i++) {
  782. const row: React.ReactNode[] = [];
  783. for (let j = 0; j < 4; j++) {
  784. row.push(<TextInput key={j} numeric delayMs={50} value={Mat4.getValue(v, i, j)} onChange={this.changeValue(4 * j + i)} className='msp-form-control' blurOnEnter={true} isDisabled={this.props.isDisabled} />);
  785. }
  786. rows.push(<div className='msp-flex-row' key={i}>{row}</div>);
  787. }
  788. return <div className='msp-parameter-matrix'>{rows}</div>;
  789. }
  790. render() {
  791. const v = {
  792. json: JSON.stringify(this.props.value)
  793. };
  794. const label = this.props.param.label || camelCaseToWords(this.props.name);
  795. return <>
  796. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{'4\u00D74 Matrix'}</button>} />
  797. {this.state.isExpanded && <div className='msp-control-offset'>
  798. {this.grid}
  799. <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
  800. </div>}
  801. </>;
  802. }
  803. }
  804. export class UrlControl extends SimpleParam<PD.UrlParam> {
  805. onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  806. const value = e.target.value;
  807. if (value !== Asset.getUrl(this.props.value || '')) {
  808. this.update(Asset.Url(value));
  809. }
  810. };
  811. onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  812. if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
  813. if (this.props.onEnter) this.props.onEnter();
  814. }
  815. e.stopPropagation();
  816. };
  817. renderControl() {
  818. const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
  819. return <input type='text'
  820. value={Asset.getUrl(this.props.value || '')}
  821. placeholder={placeholder}
  822. onChange={this.onChange}
  823. onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
  824. disabled={this.props.isDisabled}
  825. />;
  826. }
  827. }
  828. export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
  829. state = { showHelp: false };
  830. change(value: File) {
  831. this.props.onChange({ name: this.props.name, param: this.props.param, value: Asset.File(value) });
  832. }
  833. onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
  834. this.change(e.target.files![0]);
  835. };
  836. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  837. renderControl() {
  838. const value = this.props.value;
  839. return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
  840. {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
  841. </div>;
  842. }
  843. render() {
  844. if (this.props.param.label) {
  845. return renderSimple({
  846. props: this.props,
  847. state: this.state,
  848. control: this.renderControl(),
  849. toggleHelp: this.toggleHelp,
  850. addOn: null
  851. });
  852. } else {
  853. return this.renderControl();
  854. }
  855. }
  856. }
  857. export class FileListControl extends React.PureComponent<ParamProps<PD.FileListParam>> {
  858. state = { showHelp: false };
  859. change(value: FileList) {
  860. const files: Asset.File[] = [];
  861. if (value) {
  862. for (let i = 0, il = value.length; i < il; ++i) {
  863. files.push(Asset.File(value[i]));
  864. }
  865. }
  866. this.props.onChange({ name: this.props.name, param: this.props.param, value: files });
  867. }
  868. onChangeFileList = (e: React.ChangeEvent<HTMLInputElement>) => {
  869. this.change(e.target.files!);
  870. };
  871. toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
  872. renderControl() {
  873. const value = this.props.value;
  874. const names: string[] = [];
  875. if (value) {
  876. for (const file of value) {
  877. names.push(file.name);
  878. }
  879. }
  880. const label = names.length === 0
  881. ? 'Select files...' : names.length === 1
  882. ? names[0] : `${names.length} files selected`;
  883. return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
  884. {label} <input disabled={this.props.isDisabled} onChange={this.onChangeFileList} type='file' multiple={true} accept={this.props.param.accept} />
  885. </div>;
  886. }
  887. render() {
  888. if (this.props.param.label) {
  889. return renderSimple({
  890. props: this.props,
  891. state: this.state,
  892. control: this.renderControl(),
  893. toggleHelp: this.toggleHelp,
  894. addOn: null
  895. });
  896. } else {
  897. return this.renderControl();
  898. }
  899. }
  900. }
  901. export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
  902. state = { isExpanded: false };
  903. change(value: PD.MultiSelect<any>['defaultValue']) {
  904. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  905. }
  906. toggle(key: string) {
  907. return (e: React.MouseEvent<HTMLButtonElement>) => {
  908. if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
  909. else this.change(this.props.value.filter(v => v !== key));
  910. e.currentTarget.blur();
  911. };
  912. }
  913. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  914. this.setState({ isExpanded: !this.state.isExpanded });
  915. e.currentTarget.blur();
  916. };
  917. render() {
  918. const current = this.props.value;
  919. const emptyLabel = this.props.param.emptyValue;
  920. const label = this.props.param.label || camelCaseToWords(this.props.name);
  921. return <>
  922. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>
  923. {current.length === 0 && emptyLabel ? emptyLabel : `${current.length} of ${this.props.param.options.length}`}
  924. </button>} />
  925. {this.state.isExpanded && <div className='msp-control-offset'>
  926. {this.props.param.options.map(([value, label]) => {
  927. const sel = current.indexOf(value) >= 0;
  928. return <Button key={value} onClick={this.toggle(value)} disabled={this.props.isDisabled} style={{ marginTop: '1px' }}>
  929. <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
  930. </Button>;
  931. })}
  932. </div>}
  933. </>;
  934. }
  935. }
  936. export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean, showPresets: boolean, showHelp: boolean }> {
  937. state = { isExpanded: !!this.props.param.isExpanded, showPresets: false, showHelp: false };
  938. change(value: any) {
  939. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  940. }
  941. onChangeParam: ParamOnChange = e => {
  942. this.change({ ...this.props.value, [e.name]: e.value });
  943. };
  944. toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
  945. toggleShowPresets = () => this.setState({ showPresets: !this.state.showPresets });
  946. presetItems = memoizeLatest((param: PD.Group<any>) => ActionMenu.createItemsFromSelectOptions(param.presets ?? []));
  947. onSelectPreset: ActionMenu.OnSelect = item => {
  948. this.setState({ showPresets: false });
  949. this.change(item?.value);
  950. };
  951. pivotedPresets() {
  952. if (!this.props.param.presets) return null;
  953. const label = this.props.param.label || camelCaseToWords(this.props.name);
  954. return <div className='msp-control-group-wrapper'>
  955. <div className='msp-control-group-header'>
  956. <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleShowPresets}>
  957. <Icon svg={BookmarksOutlinedSvg} />
  958. {label} Presets
  959. </button>
  960. </div>
  961. {this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
  962. </div>;
  963. }
  964. presets() {
  965. if (!this.props.param.presets) return null;
  966. return <>
  967. <div className='msp-control-group-presets-wrapper'>
  968. <div className='msp-control-group-header'>
  969. <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleShowPresets}>
  970. <Icon svg={BookmarksOutlinedSvg} />
  971. Presets
  972. </button>
  973. </div>
  974. </div>
  975. {this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
  976. </>;
  977. }
  978. pivoted() {
  979. const key = this.props.param.pivot as string;
  980. const params = this.props.param.params;
  981. const pivot = params[key];
  982. const Control = controlFor(pivot)!;
  983. const ctrl = <Control name={key} param={pivot} value={this.props.value[key]} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
  984. if (!this.state.isExpanded) {
  985. return <div className='msp-mapped-parameter-group'>
  986. {ctrl}
  987. <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
  988. </div>;
  989. }
  990. const filtered = Object.create(null);
  991. for (const k of Object.keys(params)) {
  992. if (k !== key) filtered[k] = params[k];
  993. }
  994. return <div className='msp-mapped-parameter-group'>
  995. {ctrl}
  996. <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
  997. <div className='msp-control-offset'>
  998. {this.pivotedPresets()}
  999. <ParameterControls params={filtered} onEnter={this.props.onEnter} values={this.props.value} onChange={this.onChangeParam} isDisabled={this.props.isDisabled} />
  1000. </div>
  1001. </div>;
  1002. }
  1003. render() {
  1004. const params = this.props.param.params;
  1005. // Do not show if there are no params.
  1006. if (Object.keys(params).length === 0) return null;
  1007. if (this.props.param.pivot) return this.pivoted();
  1008. const label = this.props.param.label || camelCaseToWords(this.props.name);
  1009. const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
  1010. if (this.props.inMapped) {
  1011. return <div className='msp-control-offset'>{controls}</div>;
  1012. }
  1013. if (this.props.param.isFlat) {
  1014. return controls;
  1015. }
  1016. return <div className='msp-control-group-wrapper' style={{ position: 'relative' }}>
  1017. <div className='msp-control-group-header'>
  1018. <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleExpanded}>
  1019. <Icon svg={this.state.isExpanded ? ArrowDropDownSvg : ArrowRightSvg} />
  1020. {label}
  1021. </button>
  1022. </div>
  1023. {this.presets()}
  1024. {this.state.isExpanded && <div className='msp-control-offset'>
  1025. {controls}
  1026. </div>}
  1027. </div>;
  1028. }
  1029. }
  1030. export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> {
  1031. state = { isExpanded: false };
  1032. // TODO: this could lead to a rare bug where the component is reused with different mapped control.
  1033. // I think there are currently no cases where this could happen in the UI, but still need to watch out..
  1034. private valuesCache: { [name: string]: PD.Values<any> } = {};
  1035. private setValues(name: string, values: PD.Values<any>) {
  1036. this.valuesCache[name] = values;
  1037. }
  1038. private getValues(name: string) {
  1039. if (name in this.valuesCache) {
  1040. return this.valuesCache[name];
  1041. } else {
  1042. return this.props.param.map(name).defaultValue;
  1043. }
  1044. }
  1045. change(value: PD.Mapped<any>['defaultValue']) {
  1046. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  1047. }
  1048. onChangeName: ParamOnChange = e => {
  1049. this.change({ name: e.value, params: this.getValues(e.value) });
  1050. };
  1051. onChangeParam: ParamOnChange = e => {
  1052. this.setValues(this.props.value.name, e.value);
  1053. this.change({ name: this.props.value.name, params: e.value });
  1054. };
  1055. toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
  1056. areParamsEmpty(params: PD.Params) {
  1057. for (const k of Object.keys(params)) {
  1058. if (!params[k].isHidden) return false;
  1059. }
  1060. return true;
  1061. }
  1062. render() {
  1063. const value: PD.Mapped<any>['defaultValue'] = this.props.value || this.props.param.defaultValue;
  1064. const param = this.props.param.map(value.name);
  1065. const label = this.props.param.label || camelCaseToWords(this.props.name);
  1066. const Mapped = controlFor(param);
  1067. const help = this.props.param.help;
  1068. const select = help
  1069. ? {
  1070. ...this.props.param.select,
  1071. help: (name: any) => help({ name, params: this.getValues(name) })
  1072. }
  1073. : this.props.param.select;
  1074. const Select = <SelectControl param={select}
  1075. isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
  1076. name={label} value={value.name} />;
  1077. if (!Mapped) {
  1078. return Select;
  1079. }
  1080. if (param.type === 'group' && !param.isFlat) {
  1081. if (!this.areParamsEmpty(param.params)) {
  1082. return <div className='msp-mapped-parameter-group'>
  1083. {Select}
  1084. <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
  1085. {this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
  1086. </div>;
  1087. }
  1088. return Select;
  1089. }
  1090. return <>
  1091. {Select}
  1092. <Mapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  1093. </>;
  1094. }
  1095. }
  1096. type ObjectListEditorProps = { params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }
  1097. class ObjectListEditor extends React.PureComponent<ObjectListEditorProps, { current: object }> {
  1098. state = { current: this.props.value };
  1099. onChangeParam: ParamOnChange = e => {
  1100. this.setState({ current: { ...this.state.current, [e.name]: e.value } });
  1101. };
  1102. apply = () => {
  1103. this.props.apply(this.state.current);
  1104. };
  1105. componentDidUpdate(prevProps: ObjectListEditorProps) {
  1106. if (this.props.params !== prevProps.params || this.props.value !== prevProps.value) {
  1107. this.setState({ current: this.props.value });
  1108. }
  1109. }
  1110. render() {
  1111. return <>
  1112. <ParameterControls params={this.props.params} onChange={this.onChangeParam} values={this.state.current} onEnter={this.apply} isDisabled={this.props.isDisabled} />
  1113. <button className={`msp-btn msp-btn-block msp-form-control msp-control-top-offset`} onClick={this.apply} disabled={this.props.isDisabled}>
  1114. {this.props.isUpdate ? 'Update' : 'Add'}
  1115. </button>
  1116. </>;
  1117. }
  1118. }
  1119. type ObjectListItemProps = { param: PD.ObjectList, value: object, index: number, actions: ObjectListControl['actions'], isDisabled?: boolean }
  1120. class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpanded: boolean }> {
  1121. state = { isExpanded: false };
  1122. update = (v: object) => {
  1123. // this.setState({ isExpanded: false }); // TODO auto update? mark changed state?
  1124. this.props.actions.update(v, this.props.index);
  1125. };
  1126. moveUp = () => {
  1127. this.props.actions.move(this.props.index, -1);
  1128. };
  1129. moveDown = () => {
  1130. this.props.actions.move(this.props.index, 1);
  1131. };
  1132. remove = () => {
  1133. this.setState({ isExpanded: false });
  1134. this.props.actions.remove(this.props.index);
  1135. };
  1136. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  1137. this.setState({ isExpanded: !this.state.isExpanded });
  1138. e.currentTarget.blur();
  1139. };
  1140. render() {
  1141. return <>
  1142. <div className='msp-param-object-list-item'>
  1143. <button className='msp-btn msp-btn-block msp-form-control' onClick={this.toggleExpanded}>
  1144. <span>{`${this.props.index + 1}: `}</span>
  1145. {this.props.param.getLabel(this.props.value)}
  1146. </button>
  1147. <div>
  1148. <IconButton svg={ArrowDownwardSvg} title='Move Up' onClick={this.moveUp} small={true} />
  1149. <IconButton svg={ArrowUpwardSvg} title='Move Down' onClick={this.moveDown} small={true} />
  1150. <IconButton svg={DeleteOutlinedSvg} title='Remove' onClick={this.remove} small={true} />
  1151. </div>
  1152. </div>
  1153. {this.state.isExpanded && <div className='msp-control-offset'>
  1154. <ObjectListEditor params={this.props.param.element} apply={this.update} value={this.props.value} isUpdate isDisabled={this.props.isDisabled} />
  1155. </div>}
  1156. </>;
  1157. }
  1158. }
  1159. export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
  1160. state = { isExpanded: false };
  1161. change(value: any) {
  1162. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  1163. }
  1164. add = (v: object) => {
  1165. this.change([...this.props.value, v]);
  1166. };
  1167. actions = {
  1168. update: (v: object, i: number) => {
  1169. const value = this.props.value.slice(0);
  1170. value[i] = v;
  1171. this.change(value);
  1172. },
  1173. move: (i: number, dir: -1 | 1) => {
  1174. let xs = this.props.value;
  1175. if (xs.length === 1) return;
  1176. let j = (i + dir) % xs.length;
  1177. if (j < 0) j += xs.length;
  1178. xs = xs.slice(0);
  1179. const t = xs[i];
  1180. xs[i] = xs[j];
  1181. xs[j] = t;
  1182. this.change(xs);
  1183. },
  1184. remove: (i: number) => {
  1185. const xs = this.props.value;
  1186. const update: object[] = [];
  1187. for (let j = 0; j < xs.length; j++) {
  1188. if (i !== j) update.push(xs[j]);
  1189. }
  1190. this.change(update);
  1191. }
  1192. };
  1193. toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
  1194. this.setState({ isExpanded: !this.state.isExpanded });
  1195. e.currentTarget.blur();
  1196. };
  1197. render() {
  1198. const v = this.props.value;
  1199. const label = this.props.param.label || camelCaseToWords(this.props.name);
  1200. const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
  1201. return <>
  1202. <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
  1203. {this.state.isExpanded && <div className='msp-control-offset'>
  1204. {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
  1205. <ControlGroup header='New Item'>
  1206. <ObjectListEditor params={this.props.param.element} apply={this.add} value={this.props.param.ctor()} isDisabled={this.props.isDisabled} />
  1207. </ControlGroup>
  1208. </div>}
  1209. </>;
  1210. }
  1211. }
  1212. export class ConditionedControl extends React.PureComponent<ParamProps<PD.Conditioned<any, any, any>>> {
  1213. change(value: PD.Conditioned<any, any, any>['defaultValue']) {
  1214. this.props.onChange({ name: this.props.name, param: this.props.param, value });
  1215. }
  1216. onChangeCondition: ParamOnChange = e => {
  1217. this.change(this.props.param.conditionedValue(this.props.value, e.value));
  1218. };
  1219. onChangeParam: ParamOnChange = e => {
  1220. this.change(e.value);
  1221. };
  1222. render() {
  1223. const value = this.props.value;
  1224. const condition = this.props.param.conditionForValue(value) as string;
  1225. const param = this.props.param.conditionParams[condition];
  1226. const label = this.props.param.label || camelCaseToWords(this.props.name);
  1227. const Conditioned = controlFor(param);
  1228. const select = <SelectControl param={this.props.param.select}
  1229. isDisabled={this.props.isDisabled} onChange={this.onChangeCondition} onEnter={this.props.onEnter}
  1230. name={`${label} Kind`} value={condition} />;
  1231. if (!Conditioned) {
  1232. return select;
  1233. }
  1234. return <>
  1235. {select}
  1236. <Conditioned param={param} value={value} name={label} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
  1237. </>;
  1238. }
  1239. }
  1240. export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
  1241. onChange: ParamOnChange = e => {
  1242. this.props.onChange({
  1243. name: this.props.name,
  1244. param: this.props.param,
  1245. value: this.props.param.toValue(e.value)
  1246. });
  1247. };
  1248. render() {
  1249. const value = this.props.param.fromValue(this.props.value);
  1250. const Converted = controlFor(this.props.param.converted);
  1251. if (!Converted) return null;
  1252. return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
  1253. }
  1254. }
  1255. export class ScriptControl extends React.PureComponent<ParamProps<PD.Script>> {
  1256. onChange: ParamOnChange = ({ name, value }) => {
  1257. const k = name as 'language' | 'expression';
  1258. if (value !== this.props.value[k]) {
  1259. this.props.onChange({ param: this.props.param, name: this.props.name, value: { ...this.props.value, [k]: value } });
  1260. }
  1261. };
  1262. render() {
  1263. // TODO: improve!
  1264. const selectParam: PD.Select<PD.Script['defaultValue']['language']> = {
  1265. defaultValue: this.props.value.language,
  1266. options: PD.objectToOptions(Script.Info),
  1267. type: 'select',
  1268. };
  1269. const select = <SelectControl param={selectParam}
  1270. isDisabled={this.props.isDisabled} onChange={this.onChange} onEnter={this.props.onEnter}
  1271. name='language' value={this.props.value.language} />;
  1272. const textParam: PD.Text = {
  1273. defaultValue: this.props.value.language,
  1274. type: 'text',
  1275. };
  1276. const text = <TextControl param={textParam} isDisabled={this.props.isDisabled} onChange={this.onChange} name='expression' value={this.props.value.expression} />;
  1277. return <>
  1278. {select}
  1279. {this.props.value.language !== 'mol-script' && <div className='msp-help-text' style={{ padding: '10px' }}>
  1280. <Icon svg={WarningSvg} /> Support for PyMOL, VMD, and Jmol selections is an experimental feature and may not always work as intended.
  1281. </div>}
  1282. {text}
  1283. </>;
  1284. }
  1285. }