1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504 |
- /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
- import * as React from 'react';
- import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
- import { Script } from '../../mol-script/script';
- import { Asset } from '../../mol-util/assets';
- import { Color } from '../../mol-util/color';
- import { ColorListEntry } from '../../mol-util/color/color';
- import { ColorListName, ColorListOptions, ColorListOptionsScale, ColorListOptionsSet, getColorListFromName } from '../../mol-util/color/lists';
- import { Legend as LegendData } from '../../mol-util/legend';
- import { memoize1, memoizeLatest } from '../../mol-util/memoize';
- import { getPrecision } from '../../mol-util/number';
- import { ParamDefinition as PD } from '../../mol-util/param-definition';
- import { ParamMapping } from '../../mol-util/param-mapping';
- import { camelCaseToWords } from '../../mol-util/string';
- import { PluginUIComponent } from '../base';
- import { PluginUIContext } from '../context';
- import { ActionMenu } from './action-menu';
- import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
- import { Button, ControlGroup, ControlRow, ExpandGroup, IconButton, TextInput, ToggleButton } from './common';
- import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, WarningSvg } from './icons';
- import { legendFor } from './legend';
- import { LineGraphComponent } from './line-graph/line-graph-component';
- import { Slider, Slider2 } from './slider';
- export type ParameterControlsCategoryFilter = string | null | (string | null)[]
- export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
- params: P,
- values: any,
- onChange?: ParamsOnChange<PD.ValuesFor<P>>,
- onChangeValues?: (values: PD.ValuesFor<P>, prev: PD.ValuesFor<P>) => void,
- isDisabled?: boolean,
- onEnter?: () => void
- }
- export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>> {
- onChange: ParamOnChange = (params) => {
- this.props.onChange?.(params, this.props.values);
- if (this.props.onChangeValues) {
- const values = { ...this.props.values, [params.name]: params.value };
- this.props.onChangeValues(values, this.props.values);
- }
- };
- renderGroup(group: ParamInfo[]) {
- if (group.length === 0) return null;
- const values = this.props.values;
- let ctrls: JSX.Element[] | null = null;
- let category: string | undefined = void 0;
- for (const [key, p, Control] of group) {
- if (p.hideIf?.(values)) continue;
- if (!ctrls) ctrls = [];
- category = p.category;
- ctrls.push(<Control param={p} key={key} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />);
- }
- if (!ctrls) return null;
- if (category) {
- return [<ExpandGroup key={category} header={category}>{ctrls}</ExpandGroup>];
- }
- return ctrls;
- }
- renderPart(groups: ParamInfo[][]) {
- let parts: JSX.Element[] | null = null;
- for (const g of groups) {
- const ctrls = this.renderGroup(g);
- if (!ctrls) continue;
- if (!parts) parts = [];
- for (const c of ctrls) parts.push(c);
- }
- return parts;
- }
- paramGroups = memoizeLatest((params: PD.Params) => classifyParams(params));
- render() {
- const groups = this.paramGroups(this.props.params);
- const essentials = this.renderPart(groups.essentials);
- const advanced = this.renderPart(groups.advanced);
- if (essentials && advanced) {
- return <>
- {essentials}
- <ExpandGroup header='Advanced Options'>
- {advanced}
- </ExpandGroup>
- </>;
- } else if (essentials) {
- return essentials;
- } else {
- return advanced;
- }
- }
- }
- export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: ParamMapping<S, T, PluginUIContext> }> {
- setSettings = (p: { param: PD.Base<any>, name: string, value: any }, old: any) => {
- const values = { ...old, [p.name]: p.value };
- const t = this.props.mapping.update(values, this.plugin);
- this.props.mapping.apply(t, this.plugin);
- };
- componentDidMount() {
- this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
- }
- render() {
- const t = this.props.mapping.getTarget(this.plugin);
- const values = this.props.mapping.getValues(t, this.plugin);
- const params = this.props.mapping.params(this.plugin) as any as PD.Params;
- return <ParameterControls params={params} values={values} onChange={this.setSettings} />;
- }
- }
- type ParamInfo = [string, PD.Any, ParamControl];
- function classifyParams(params: PD.Params) {
- function addParam(k: string, p: PD.Any, group: typeof essentials) {
- const ctrl = controlFor(p);
- if (!ctrl) return;
- if (!p.category) group.params[0].push([k, p, ctrl]);
- else {
- if (!group.map) group.map = new Map();
- let c = group.map.get(p.category);
- if (!c) {
- c = [];
- group.map.set(p.category, c);
- group.params.push(c);
- }
- c.push([k, p, ctrl]);
- }
- }
- function sortGroups(x: ParamInfo[], y: ParamInfo[]) {
- const a = x[0], b = y[0];
- if (!a || !a[1].category) return -1;
- if (!b || !b[1].category) return 1;
- return a[1].category < b[1].category ? -1 : 1;
- }
- const keys = Object.keys(params);
- const essentials: { params: ParamInfo[][], map: Map<string, ParamInfo[]> | undefined } = { params: [[]], map: void 0 };
- const advanced: typeof essentials = { params: [[]], map: void 0 };
- for (const k of keys) {
- const p = params[k];
- if (p.isHidden) continue;
- if (p.isEssential) addParam(k, p, essentials);
- else addParam(k, p, advanced);
- }
- essentials.params.sort(sortGroups);
- advanced.params.sort(sortGroups);
- return { essentials: essentials.params, advanced: advanced.params };
- }
- function controlFor(param: PD.Any): ParamControl | undefined {
- switch (param.type) {
- case 'value': return void 0;
- case 'boolean': return BoolControl;
- case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
- ? NumberRangeControl : NumberInputControl;
- case 'converted': return ConvertedControl;
- case 'conditioned': return ConditionedControl;
- case 'multi-select': return MultiSelectControl;
- case 'color': return CombinedColorControl;
- case 'color-list': return param.offsets ? OffsetColorListControl : ColorListControl;
- case 'vec3': return Vec3Control;
- case 'mat4': return Mat4Control;
- case 'url': return UrlControl;
- case 'file': return FileControl;
- case 'file-list': return FileListControl;
- case 'select': return SelectControl;
- case 'value-ref': return ValueRefControl;
- case 'data-ref': return void 0;
- case 'text': return TextControl;
- case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
- ? BoundedIntervalControl : IntervalControl;
- case 'group': return GroupControl;
- case 'mapped': return MappedControl;
- case 'line-graph': return LineGraphControl;
- case 'script': return ScriptControl;
- case 'object-list': return ObjectListControl;
- default:
- const _: never = param;
- console.warn(`${_} has no associated UI component`);
- return void 0;
- }
- }
- export class ParamHelp<L extends LegendData> extends React.PureComponent<{ legend?: L, description?: string }> {
- render() {
- const { legend, description } = this.props;
- const Legend = legend && legendFor(legend);
- return <div className='msp-help-text'>
- <div>
- <div className='msp-help-description'><Icon svg={HelpOutlineSvg} inline />{description}</div>
- {Legend && <div className='msp-help-legend'><Legend legend={legend} /></div>}
- </div>
- </div>;
- }
- }
- export type ParamsOnChange<P> = (params: { param: PD.Base<any>, name: string, value: any }, values: Readonly<P>) => void
- export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
- export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> {
- name: string,
- value: P['defaultValue'],
- param: P,
- isDisabled?: boolean,
- onChange: ParamOnChange,
- onEnter?: () => void
- }
- export type ParamControl = React.ComponentClass<ParamProps<any>>
- function renderSimple(options: { props: ParamProps<any>, state: { showHelp: boolean }, control: JSX.Element, addOn: JSX.Element | null, toggleHelp: () => void }) {
- const { props, state, control, toggleHelp, addOn } = options;
- const _className = [];
- if (props.param.shortLabel) _className.push('msp-control-label-short');
- if (props.param.twoColumns) _className.push('msp-control-col-2');
- const className = _className.join(' ');
- const label = props.param.label || camelCaseToWords(props.name);
- const help = props.param.help
- ? props.param.help(props.value)
- : { description: props.param.description, legend: props.param.legend };
- const hasHelp = help.description || help.legend;
- const desc = label + (hasHelp ? '. Click for help.' : '');
- return <>
- <ControlRow
- className={className}
- title={desc}
- label={<>
- {label}
- {hasHelp &&
- <button className='msp-help msp-btn-link msp-btn-icon msp-control-group-expander' onClick={toggleHelp}
- title={desc || `${state.showHelp ? 'Hide' : 'Show'} help`}
- style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
- <Icon svg={HelpOutlineSvg} />
- </button>
- }
- </>}
- control={control}
- />
- {hasHelp && state.showHelp && <div className='msp-control-offset'>
- <ParamHelp legend={help.legend} description={help.description} />
- </div>}
- {addOn}
- </>;
- }
- export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>, { showHelp: boolean }> {
- state = { showHelp: false };
- protected update(value: P['defaultValue']) {
- this.props.onChange({ param: this.props.param, name: this.props.name, value });
- }
- abstract renderControl(): JSX.Element;
- renderAddOn(): JSX.Element | null { return null; }
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- render() {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: this.renderAddOn()
- });
- }
- }
- export class BoolControl extends SimpleParam<PD.BooleanParam> {
- onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); };
- renderControl() {
- return <button onClick={this.onClick} disabled={this.props.isDisabled}>
- <Icon svg={this.props.value ? CheckSvg : ClearSvg} />
- {this.props.value ? 'On' : 'Off'}
- </button>;
- }
- }
- export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
- state = {
- isExpanded: false,
- isOverPoint: false,
- message: `${this.props.param.defaultValue.length} points`,
- };
- onHover = (point?: Vec2) => {
- this.setState({ isOverPoint: !this.state.isOverPoint });
- if (point) {
- this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
- return;
- }
- this.setState({ message: `${this.props.value.length} points` });
- };
- onDrag = (point: Vec2) => {
- this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
- };
- onChange = (value: PD.LineGraph['defaultValue']) => {
- this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{`${this.state.message}`}</button>} />
- <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
- <LineGraphComponent
- data={this.props.param.defaultValue}
- onChange={this.onChange}
- onHover={this.onHover}
- onDrag={this.onDrag} />
- </div>
- </>;
- }
- }
- export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> {
- state = { value: '0' };
- update = (value: number) => {
- const p = getPrecision(this.props.param.step || 0.01);
- value = parseFloat(value.toFixed(p));
- this.props.onChange({ param: this.props.param, name: this.props.name, value });
- };
- render() {
- const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const p = getPrecision(this.props.param.step || 0.01);
- return <ControlRow
- title={this.props.param.description}
- label={label}
- control={<TextInput numeric
- value={parseFloat(this.props.value.toFixed(p))} onEnter={this.props.onEnter} placeholder={placeholder}
- isDisabled={this.props.isDisabled} onChange={this.update} />} />;
- }
- }
- export class NumberRangeControl extends SimpleParam<PD.Numeric> {
- onChange = (v: number) => { this.update(v); };
- renderControl() {
- const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value;
- return <Slider value={value} min={this.props.param.min!} max={this.props.param.max!}
- step={this.props.param.step} onChange={this.onChange} onChangeImmediate={this.props.param.immediateUpdate ? this.onChange : void 0}
- disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
- }
- }
- export class TextControl extends SimpleParam<PD.Text> {
- onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const value = e.target.value;
- if (value !== this.props.value) {
- this.update(value);
- }
- };
- onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
- if (this.props.onEnter) this.props.onEnter();
- }
- e.stopPropagation();
- };
- renderControl() {
- const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
- return <input type='text'
- value={this.props.value || ''}
- placeholder={placeholder}
- onChange={this.onChange}
- onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
- disabled={this.props.isDisabled}
- />;
- }
- }
- export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>> & { title?: string }> {
- protected update(value: string | number) {
- this.props.onChange({ param: this.props.param, name: this.props.name, value });
- }
- onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
- if (typeof this.props.param.defaultValue === 'number') {
- this.update(parseInt(e.target.value, 10));
- } else {
- this.update(e.target.value);
- }
- };
- render() {
- const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
- 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}>
- {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
- {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
- </select>;
- }
- }
- export class SelectControl extends React.PureComponent<ParamProps<PD.Select<string | number>>, { showHelp: boolean, showOptions: boolean }> {
- state = { showHelp: false, showOptions: false };
- onSelect: ActionMenu.OnSelect = item => {
- if (!item || item.value === this.props.value) {
- this.setState({ showOptions: false });
- } else {
- this.setState({ showOptions: false }, () => {
- this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value });
- });
- }
- };
- toggle = () => this.setState({ showOptions: !this.state.showOptions });
- cycle = () => {
- const { options } = this.props.param;
- const current = options.findIndex(o => o[0] === this.props.value);
- const next = current === options.length - 1 ? 0 : current + 1;
- this.props.onChange({ param: this.props.param, name: this.props.name, value: options[next][0] });
- };
- items = memoizeLatest((param: PD.Select<any>) => ActionMenu.createItemsFromSelectOptions(param.options));
- renderControl() {
- const items = this.items(this.props.param);
- const current = this.props.value !== undefined ? ActionMenu.findItem(items, this.props.value) : void 0;
- const label = current
- ? current.label
- : typeof this.props.value === 'undefined'
- ? `${ActionMenu.getFirstItem(items)?.label || ''} [Default]`
- : `[Invalid] ${this.props.value}`;
- const toggle = this.props.param.cycle ? this.cycle : this.toggle;
- const textAlign = this.props.param.cycle ? 'center' : 'left';
- const icon = this.props.param.cycle
- ? (this.props.value === 'on' ? CheckSvg
- : this.props.value === 'off' ? ClearSvg : void 0)
- : void 0;
- return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign, overflow: 'hidden', textOverflow: 'ellipsis' }}
- label={label} title={label as string} icon={icon} toggle={toggle} isSelected={this.state.showOptions} />;
- }
- renderAddOn() {
- if (!this.state.showOptions) return null;
- const items = this.items(this.props.param);
- const current = ActionMenu.findItem(items, this.props.value);
- return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
- }
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- render() {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: this.renderAddOn()
- });
- }
- }
- export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<any>>, { showHelp: boolean, showOptions: boolean }> {
- state = { showHelp: false, showOptions: false };
- onSelect: ActionMenu.OnSelect = item => {
- if (!item || item.value === this.props.value) {
- this.setState({ showOptions: false });
- } else {
- this.setState({ showOptions: false }, () => {
- this.props.onChange({ param: this.props.param, name: this.props.name, value: { ref: item.value } });
- });
- }
- };
- toggle = () => this.setState({ showOptions: !this.state.showOptions });
- items = memoizeLatest((param: PD.ValueRef) => ActionMenu.createItemsFromSelectOptions(param.getOptions()));
- renderControl() {
- const items = this.items(this.props.param);
- const current = this.props.value.ref ? ActionMenu.findItem(items, this.props.value.ref) : void 0;
- const label = current
- ? current.label
- : `[Ref] ${this.props.value.ref ?? ''}`;
- return <ToggleButton disabled={this.props.isDisabled} style={{ textAlign: 'left', overflow: 'hidden', textOverflow: 'ellipsis' }}
- label={label} title={label as string} toggle={this.toggle} isSelected={this.state.showOptions} />;
- }
- renderAddOn() {
- if (!this.state.showOptions) return null;
- const items = this.items(this.props.param);
- const current = ActionMenu.findItem(items, this.props.value.ref);
- return <ActionMenu items={items} current={current} onSelect={this.onSelect} />;
- }
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- render() {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: this.renderAddOn()
- });
- }
- }
- export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- components = {
- 0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }),
- 1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' })
- };
- change(value: PD.MultiSelect<any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- componentChange: ParamOnChange = ({ name, value }) => {
- const v = [...this.props.value];
- v[+name] = value;
- this.change(v);
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- const v = this.props.value;
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const p = getPrecision(this.props.param.step || 0.01);
- const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}]`;
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
- {this.state.isExpanded && <div className='msp-control-offset'>
- <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
- </div>}
- </>;
- }
- }
- export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
- onChange = (v: [number, number]) => { this.update(v); };
- renderControl() {
- return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
- step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
- }
- }
- export class ColorControl extends SimpleParam<PD.Color> {
- onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
- this.update(Color(parseInt(e.target.value)));
- };
- stripStyle(): React.CSSProperties {
- return {
- background: Color.toStyle(this.props.value),
- position: 'absolute',
- bottom: '0',
- height: '4px',
- right: '0',
- left: '0'
- };
- }
- renderControl() {
- return <div style={{ position: 'relative' }}>
- <select value={this.props.value} onChange={this.onChange}>
- {ColorValueOption(this.props.value)}
- {ColorOptions()}
- </select>
- <div style={this.stripStyle()} />
- </div>;
- }
- }
- function colorEntryToStyle(e: ColorListEntry, includeOffset = false) {
- if (Array.isArray(e)) {
- if (includeOffset) return `${Color.toStyle(e[0])} ${(100 * e[1]).toFixed(2)}%`;
- return Color.toStyle(e[0]);
- }
- return Color.toStyle(e);
- }
- const colorGradientInterpolated = memoize1((colors: ColorListEntry[]) => {
- const styles = colors.map(c => colorEntryToStyle(c, true));
- return `linear-gradient(to right, ${styles.join(', ')})`;
- });
- const colorGradientBanded = memoize1((colors: ColorListEntry[]) => {
- const n = colors.length;
- const styles: string[] = [`${colorEntryToStyle(colors[0])} ${100 * (1 / n)}%`];
- // TODO: does this need to support offsets?
- for (let i = 1, il = n - 1; i < il; ++i) {
- styles.push(
- `${colorEntryToStyle(colors[i])} ${100 * (i / n)}%`,
- `${colorEntryToStyle(colors[i])} ${100 * ((i + 1) / n)}%`
- );
- }
- styles.push(`${colorEntryToStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`);
- return `linear-gradient(to right, ${styles.join(', ')})`;
- });
- function colorStripStyle(list: PD.ColorList['defaultValue'], right = '0'): React.CSSProperties {
- return {
- background: colorGradient(list.colors, list.kind === 'set'),
- position: 'absolute',
- bottom: '0',
- height: '4px',
- right,
- left: '0'
- };
- }
- function colorGradient(colors: ColorListEntry[], banded: boolean) {
- return banded ? colorGradientBanded(colors) : colorGradientInterpolated(colors);
- }
- function createColorListHelpers() {
- const addOn = (l: [ColorListName, any, any]) => {
- const preset = getColorListFromName(l[0]);
- return <div style={colorStripStyle({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list })} />;
- };
- return {
- ColorPresets: {
- all: ActionMenu.createItemsFromSelectOptions(ColorListOptions, { addOn }),
- scale: ActionMenu.createItemsFromSelectOptions(ColorListOptionsScale, { addOn }),
- set: ActionMenu.createItemsFromSelectOptions(ColorListOptionsSet, { addOn })
- },
- ColorsParam: PD.ObjectList({ color: PD.Color(0x0 as Color) }, ({ color }) => Color.toHexString(color).toUpperCase()),
- OffsetColorsParam: PD.ObjectList(
- { color: PD.Color(0x0 as Color), offset: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) },
- ({ color, offset }) => `${Color.toHexString(color).toUpperCase()} [${offset.toFixed(2)}]`),
- IsInterpolatedParam: PD.Boolean(false, { label: 'Interpolated' })
- };
- }
- let _colorListHelpers: ReturnType<typeof createColorListHelpers>;
- function ColorListHelpers() {
- if (_colorListHelpers) return _colorListHelpers;
- _colorListHelpers = createColorListHelpers();
- return _colorListHelpers;
- }
- export class ColorListControl extends React.PureComponent<ParamProps<PD.ColorList>, { showHelp: boolean, show?: 'edit' | 'presets' }> {
- state = { showHelp: false, show: void 0 as 'edit' | 'presets' | undefined };
- protected update(value: PD.ColorList['defaultValue']) {
- this.props.onChange({ param: this.props.param, name: this.props.name, value });
- }
- toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' });
- togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' });
- renderControl() {
- const { value } = this.props;
- // TODO: fix the button right offset
- return <>
- <button onClick={this.toggleEdit} style={{ position: 'relative', paddingRight: '33px' }}>
- {value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`}
- <div style={colorStripStyle(value, '33px')} />
- </button>
- <IconButton svg={BookmarksOutlinedSvg} onClick={this.togglePresets} toggleState={this.state.show === 'presets'} title='Color Presets'
- style={{ padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' }} />
- </>;
- }
- selectPreset: ActionMenu.OnSelect = item => {
- if (!item) return;
- this.setState({ show: void 0 });
- const preset = getColorListFromName(item.value as ColorListName);
- this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
- };
- colorsChanged: ParamOnChange = ({ value }) => {
- this.update({
- kind: this.props.value.kind,
- colors: (value as (typeof _colorListHelpers)['ColorsParam']['defaultValue']).map(c => c.color)
- });
- };
- isInterpolatedChanged: ParamOnChange = ({ value }) => {
- this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
- };
- renderColors() {
- if (!this.state.show) return null;
- const { ColorPresets, ColorsParam, IsInterpolatedParam } = ColorListHelpers();
- const preset = ColorPresets[this.props.param.presetKind];
- if (this.state.show === 'presets') return <ActionMenu items={preset} onSelect={this.selectPreset} />;
- const values = this.props.value.colors.map(color => ({ color }));
- return <div className='msp-control-offset'>
- <ObjectListControl name='colors' param={ColorsParam} value={values} onChange={this.colorsChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
- <BoolControl name='isInterpolated' param={IsInterpolatedParam} value={this.props.value.kind === 'interpolate'} onChange={this.isInterpolatedChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
- </div>;
- }
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- render() {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: this.renderColors()
- });
- }
- }
- export class OffsetColorListControl extends React.PureComponent<ParamProps<PD.ColorList>, { showHelp: boolean, show?: 'edit' | 'presets' }> {
- state = { showHelp: false, show: void 0 as 'edit' | 'presets' | undefined };
- protected update(value: PD.ColorList['defaultValue']) {
- this.props.onChange({ param: this.props.param, name: this.props.name, value });
- }
- toggleEdit = () => this.setState({ show: this.state.show === 'edit' ? void 0 : 'edit' });
- togglePresets = () => this.setState({ show: this.state.show === 'presets' ? void 0 : 'presets' });
- renderControl() {
- const { value } = this.props;
- // TODO: fix the button right offset
- return <>
- <button onClick={this.toggleEdit} style={{ position: 'relative', paddingRight: '33px' }}>
- {value.colors.length === 1 ? '1 color' : `${value.colors.length} colors`}
- <div style={colorStripStyle(value, '33px')} />
- </button>
- <IconButton svg={BookmarksOutlinedSvg} onClick={this.togglePresets} toggleState={this.state.show === 'presets'} title='Color Presets'
- style={{ padding: 0, position: 'absolute', right: 0, top: 0, width: '32px' }} />
- </>;
- }
- selectPreset: ActionMenu.OnSelect = item => {
- if (!item) return;
- this.setState({ show: void 0 });
- const preset = getColorListFromName(item.value as ColorListName);
- this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
- };
- colorsChanged: ParamOnChange = ({ value }) => {
- const colors = (value as (typeof _colorListHelpers)['OffsetColorsParam']['defaultValue']).map(c => [c.color, c.offset] as [Color, number]);
- colors.sort((a, b) => a[1] - b[1]);
- this.update({ kind: this.props.value.kind, colors });
- };
- isInterpolatedChanged: ParamOnChange = ({ value }) => {
- this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
- };
- renderColors() {
- if (!this.state.show) return null;
- const { ColorPresets, OffsetColorsParam, IsInterpolatedParam } = ColorListHelpers();
- const preset = ColorPresets[this.props.param.presetKind];
- if (this.state.show === 'presets') return <ActionMenu items={preset} onSelect={this.selectPreset} />;
- const colors = this.props.value.colors;
- const values = colors.map((color, i) => {
- if (Array.isArray(color)) return { color: color[0], offset: color[1] };
- return { color, offset: i / colors.length };
- });
- values.sort((a, b) => a.offset - b.offset);
- return <div className='msp-control-offset'>
- <ObjectListControl name='colors' param={OffsetColorsParam} value={values} onChange={this.colorsChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
- <BoolControl name='isInterpolated' param={IsInterpolatedParam} value={this.props.value.kind === 'interpolate'} onChange={this.isInterpolatedChanged} isDisabled={this.props.isDisabled} onEnter={this.props.onEnter} />
- </div>;
- }
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- render() {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: this.renderColors()
- });
- }
- }
- export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- components = {
- 0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
- 1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
- 2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
- };
- change(value: PD.MultiSelect<any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- componentChange: ParamOnChange = ({ name, value }) => {
- const v = Vec3.copy(Vec3.zero(), this.props.value);
- v[+name] = value;
- this.change(v);
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- const v = this.props.value;
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const p = getPrecision(this.props.param.step || 0.01);
- const value = `[${v[0].toFixed(p)}, ${v[1].toFixed(p)}, ${v[2].toFixed(p)}]`;
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
- {this.state.isExpanded && <div className='msp-control-offset'>
- <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
- </div>}
- </>;
- }
- }
- export class Mat4Control extends React.PureComponent<ParamProps<PD.Mat4>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- components = {
- json: PD.Text(JSON.stringify(Mat4()), { description: 'JSON array with 4x4 matrix in a column major (j * 4 + i indexing) format' })
- };
- change(value: PD.MultiSelect<any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- componentChange: ParamOnChange = ({ name, value }) => {
- const v = Mat4.copy(Mat4(), this.props.value);
- if (name === 'json') {
- Mat4.copy(v, JSON.parse(value));
- } else {
- v[+name] = value;
- }
- this.change(v);
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- changeValue(idx: number) {
- return (v: number) => {
- const m = Mat4.copy(Mat4(), this.props.value);
- m[idx] = v;
- this.change(m);
- };
- }
- get grid() {
- const v = this.props.value;
- const rows: React.ReactNode[] = [];
- for (let i = 0; i < 4; i++) {
- const row: React.ReactNode[] = [];
- for (let j = 0; j < 4; j++) {
- 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} />);
- }
- rows.push(<div className='msp-flex-row' key={i}>{row}</div>);
- }
- return <div className='msp-parameter-matrix'>{rows}</div>;
- }
- render() {
- const v = {
- json: JSON.stringify(this.props.value)
- };
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{'4\u00D74 Matrix'}</button>} />
- {this.state.isExpanded && <div className='msp-control-offset'>
- {this.grid}
- <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
- </div>}
- </>;
- }
- }
- export class UrlControl extends SimpleParam<PD.UrlParam> {
- onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const value = e.target.value;
- if (value !== Asset.getUrl(this.props.value || '')) {
- this.update(Asset.Url(value));
- }
- };
- onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
- if (this.props.onEnter) this.props.onEnter();
- }
- e.stopPropagation();
- };
- renderControl() {
- const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
- return <input type='text'
- value={Asset.getUrl(this.props.value || '')}
- placeholder={placeholder}
- onChange={this.onChange}
- onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
- disabled={this.props.isDisabled}
- />;
- }
- }
- export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
- state = { showHelp: false };
- change(value: File) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value: Asset.File(value) });
- }
- onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.change(e.target.files![0]);
- };
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- renderControl() {
- const value = this.props.value;
- return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
- {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
- </div>;
- }
- render() {
- if (this.props.param.label) {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: null
- });
- } else {
- return this.renderControl();
- }
- }
- }
- export class FileListControl extends React.PureComponent<ParamProps<PD.FileListParam>> {
- state = { showHelp: false };
- change(value: FileList) {
- const files: Asset.File[] = [];
- if (value) {
- for (let i = 0, il = value.length; i < il; ++i) {
- files.push(Asset.File(value[i]));
- }
- }
- this.props.onChange({ name: this.props.name, param: this.props.param, value: files });
- }
- onChangeFileList = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.change(e.target.files!);
- };
- toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
- renderControl() {
- const value = this.props.value;
- const names: string[] = [];
- if (value) {
- for (const file of value) {
- names.push(file.name);
- }
- }
- const label = names.length === 0
- ? 'Select files...' : names.length === 1
- ? names[0] : `${names.length} files selected`;
- return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
- {label} <input disabled={this.props.isDisabled} onChange={this.onChangeFileList} type='file' multiple={true} accept={this.props.param.accept} />
- </div>;
- }
- render() {
- if (this.props.param.label) {
- return renderSimple({
- props: this.props,
- state: this.state,
- control: this.renderControl(),
- toggleHelp: this.toggleHelp,
- addOn: null
- });
- } else {
- return this.renderControl();
- }
- }
- }
- export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- change(value: PD.MultiSelect<any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- toggle(key: string) {
- return (e: React.MouseEvent<HTMLButtonElement>) => {
- if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
- else this.change(this.props.value.filter(v => v !== key));
- e.currentTarget.blur();
- };
- }
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- const current = this.props.value;
- const emptyLabel = this.props.param.emptyValue;
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>
- {current.length === 0 && emptyLabel ? emptyLabel : `${current.length} of ${this.props.param.options.length}`}
- </button>} />
- {this.state.isExpanded && <div className='msp-control-offset'>
- {this.props.param.options.map(([value, label]) => {
- const sel = current.indexOf(value) >= 0;
- return <Button key={value} onClick={this.toggle(value)} disabled={this.props.isDisabled} style={{ marginTop: '1px' }}>
- <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
- </Button>;
- })}
- </div>}
- </>;
- }
- }
- export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean, showPresets: boolean, showHelp: boolean }> {
- state = { isExpanded: !!this.props.param.isExpanded, showPresets: false, showHelp: false };
- change(value: any) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- onChangeParam: ParamOnChange = e => {
- this.change({ ...this.props.value, [e.name]: e.value });
- };
- toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
- toggleShowPresets = () => this.setState({ showPresets: !this.state.showPresets });
- presetItems = memoizeLatest((param: PD.Group<any>) => ActionMenu.createItemsFromSelectOptions(param.presets ?? []));
- onSelectPreset: ActionMenu.OnSelect = item => {
- this.setState({ showPresets: false });
- this.change(item?.value);
- };
- pivotedPresets() {
- if (!this.props.param.presets) return null;
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- return <div className='msp-control-group-wrapper'>
- <div className='msp-control-group-header'>
- <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleShowPresets}>
- <Icon svg={BookmarksOutlinedSvg} />
- {label} Presets
- </button>
- </div>
- {this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
- </div>;
- }
- presets() {
- if (!this.props.param.presets) return null;
- return <>
- <div className='msp-control-group-presets-wrapper'>
- <div className='msp-control-group-header'>
- <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleShowPresets}>
- <Icon svg={BookmarksOutlinedSvg} />
- Presets
- </button>
- </div>
- </div>
- {this.state.showPresets && <ActionMenu items={this.presetItems(this.props.param)} onSelect={this.onSelectPreset} />}
- </>;
- }
- pivoted() {
- const key = this.props.param.pivot as string;
- const params = this.props.param.params;
- const pivot = params[key];
- const Control = controlFor(pivot)!;
- const ctrl = <Control name={key} param={pivot} value={this.props.value[key]} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
- if (!this.state.isExpanded) {
- return <div className='msp-mapped-parameter-group'>
- {ctrl}
- <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
- </div>;
- }
- const filtered = Object.create(null);
- for (const k of Object.keys(params)) {
- if (k !== key) filtered[k] = params[k];
- }
- return <div className='msp-mapped-parameter-group'>
- {ctrl}
- <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`More Options`} />
- <div className='msp-control-offset'>
- {this.pivotedPresets()}
- <ParameterControls params={filtered} onEnter={this.props.onEnter} values={this.props.value} onChange={this.onChangeParam} isDisabled={this.props.isDisabled} />
- </div>
- </div>;
- }
- render() {
- const params = this.props.param.params;
- // Do not show if there are no params.
- if (Object.keys(params).length === 0) return null;
- if (this.props.param.pivot) return this.pivoted();
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
- if (this.props.inMapped) {
- return <div className='msp-control-offset'>{controls}</div>;
- }
- if (this.props.param.isFlat) {
- return controls;
- }
- return <div className='msp-control-group-wrapper' style={{ position: 'relative' }}>
- <div className='msp-control-group-header'>
- <button className='msp-btn msp-form-control msp-btn-block' onClick={this.toggleExpanded}>
- <Icon svg={this.state.isExpanded ? ArrowDropDownSvg : ArrowRightSvg} />
- {label}
- </button>
- </div>
- {this.presets()}
- {this.state.isExpanded && <div className='msp-control-offset'>
- {controls}
- </div>}
- </div>;
- }
- }
- export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- // TODO: this could lead to a rare bug where the component is reused with different mapped control.
- // I think there are currently no cases where this could happen in the UI, but still need to watch out..
- private valuesCache: { [name: string]: PD.Values<any> } = {};
- private setValues(name: string, values: PD.Values<any>) {
- this.valuesCache[name] = values;
- }
- private getValues(name: string) {
- if (name in this.valuesCache) {
- return this.valuesCache[name];
- } else {
- return this.props.param.map(name).defaultValue;
- }
- }
- change(value: PD.Mapped<any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- onChangeName: ParamOnChange = e => {
- this.change({ name: e.value, params: this.getValues(e.value) });
- };
- onChangeParam: ParamOnChange = e => {
- this.setValues(this.props.value.name, e.value);
- this.change({ name: this.props.value.name, params: e.value });
- };
- toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
- areParamsEmpty(params: PD.Params) {
- for (const k of Object.keys(params)) {
- if (!params[k].isHidden) return false;
- }
- return true;
- }
- render() {
- const value: PD.Mapped<any>['defaultValue'] = this.props.value || this.props.param.defaultValue;
- const param = this.props.param.map(value.name);
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const Mapped = controlFor(param);
- const help = this.props.param.help;
- const select = help
- ? {
- ...this.props.param.select,
- help: (name: any) => help({ name, params: this.getValues(name) })
- }
- : this.props.param.select;
- const Select = <SelectControl param={select}
- isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
- name={label} value={value.name} />;
- if (!Mapped) {
- return Select;
- }
- if (param.type === 'group' && !param.isFlat) {
- if (!this.areParamsEmpty(param.params)) {
- return <div className='msp-mapped-parameter-group'>
- {Select}
- <IconButton svg={MoreHorizSvg} onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
- {this.state.isExpanded && <GroupControl inMapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />}
- </div>;
- }
- return Select;
- }
- return <>
- {Select}
- <Mapped param={param} value={value.params} name={value.name} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
- </>;
- }
- }
- type ObjectListEditorProps = { params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }
- class ObjectListEditor extends React.PureComponent<ObjectListEditorProps, { current: object }> {
- state = { current: this.props.value };
- onChangeParam: ParamOnChange = e => {
- this.setState({ current: { ...this.state.current, [e.name]: e.value } });
- };
- apply = () => {
- this.props.apply(this.state.current);
- };
- componentDidUpdate(prevProps: ObjectListEditorProps) {
- if (this.props.params !== prevProps.params || this.props.value !== prevProps.value) {
- this.setState({ current: this.props.value });
- }
- }
- render() {
- return <>
- <ParameterControls params={this.props.params} onChange={this.onChangeParam} values={this.state.current} onEnter={this.apply} isDisabled={this.props.isDisabled} />
- <button className={`msp-btn msp-btn-block msp-form-control msp-control-top-offset`} onClick={this.apply} disabled={this.props.isDisabled}>
- {this.props.isUpdate ? 'Update' : 'Add'}
- </button>
- </>;
- }
- }
- type ObjectListItemProps = { param: PD.ObjectList, value: object, index: number, actions: ObjectListControl['actions'], isDisabled?: boolean }
- class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpanded: boolean }> {
- state = { isExpanded: false };
- update = (v: object) => {
- // this.setState({ isExpanded: false }); // TODO auto update? mark changed state?
- this.props.actions.update(v, this.props.index);
- };
- moveUp = () => {
- this.props.actions.move(this.props.index, -1);
- };
- moveDown = () => {
- this.props.actions.move(this.props.index, 1);
- };
- remove = () => {
- this.setState({ isExpanded: false });
- this.props.actions.remove(this.props.index);
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- return <>
- <div className='msp-param-object-list-item'>
- <button className='msp-btn msp-btn-block msp-form-control' onClick={this.toggleExpanded}>
- <span>{`${this.props.index + 1}: `}</span>
- {this.props.param.getLabel(this.props.value)}
- </button>
- <div>
- <IconButton svg={ArrowDownwardSvg} title='Move Up' onClick={this.moveUp} small={true} />
- <IconButton svg={ArrowUpwardSvg} title='Move Down' onClick={this.moveDown} small={true} />
- <IconButton svg={DeleteOutlinedSvg} title='Remove' onClick={this.remove} small={true} />
- </div>
- </div>
- {this.state.isExpanded && <div className='msp-control-offset'>
- <ObjectListEditor params={this.props.param.element} apply={this.update} value={this.props.value} isUpdate isDisabled={this.props.isDisabled} />
- </div>}
- </>;
- }
- }
- export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
- state = { isExpanded: false };
- change(value: any) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- add = (v: object) => {
- this.change([...this.props.value, v]);
- };
- actions = {
- update: (v: object, i: number) => {
- const value = this.props.value.slice(0);
- value[i] = v;
- this.change(value);
- },
- move: (i: number, dir: -1 | 1) => {
- let xs = this.props.value;
- if (xs.length === 1) return;
- let j = (i + dir) % xs.length;
- if (j < 0) j += xs.length;
- xs = xs.slice(0);
- const t = xs[i];
- xs[i] = xs[j];
- xs[j] = t;
- this.change(xs);
- },
- remove: (i: number) => {
- const xs = this.props.value;
- const update: object[] = [];
- for (let j = 0; j < xs.length; j++) {
- if (i !== j) update.push(xs[j]);
- }
- this.change(update);
- }
- };
- toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
- this.setState({ isExpanded: !this.state.isExpanded });
- e.currentTarget.blur();
- };
- render() {
- const v = this.props.value;
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
- return <>
- <ControlRow label={label} control={<button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>} />
- {this.state.isExpanded && <div className='msp-control-offset'>
- {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
- <ControlGroup header='New Item'>
- <ObjectListEditor params={this.props.param.element} apply={this.add} value={this.props.param.ctor()} isDisabled={this.props.isDisabled} />
- </ControlGroup>
- </div>}
- </>;
- }
- }
- export class ConditionedControl extends React.PureComponent<ParamProps<PD.Conditioned<any, any, any>>> {
- change(value: PD.Conditioned<any, any, any>['defaultValue']) {
- this.props.onChange({ name: this.props.name, param: this.props.param, value });
- }
- onChangeCondition: ParamOnChange = e => {
- this.change(this.props.param.conditionedValue(this.props.value, e.value));
- };
- onChangeParam: ParamOnChange = e => {
- this.change(e.value);
- };
- render() {
- const value = this.props.value;
- const condition = this.props.param.conditionForValue(value) as string;
- const param = this.props.param.conditionParams[condition];
- const label = this.props.param.label || camelCaseToWords(this.props.name);
- const Conditioned = controlFor(param);
- const select = <SelectControl param={this.props.param.select}
- isDisabled={this.props.isDisabled} onChange={this.onChangeCondition} onEnter={this.props.onEnter}
- name={`${label} Kind`} value={condition} />;
- if (!Conditioned) {
- return select;
- }
- return <>
- {select}
- <Conditioned param={param} value={value} name={label} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
- </>;
- }
- }
- export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
- onChange: ParamOnChange = e => {
- this.props.onChange({
- name: this.props.name,
- param: this.props.param,
- value: this.props.param.toValue(e.value)
- });
- };
- render() {
- const value = this.props.param.fromValue(this.props.value);
- const Converted = controlFor(this.props.param.converted);
- if (!Converted) return null;
- return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
- }
- }
- export class ScriptControl extends React.PureComponent<ParamProps<PD.Script>> {
- onChange: ParamOnChange = ({ name, value }) => {
- const k = name as 'language' | 'expression';
- if (value !== this.props.value[k]) {
- this.props.onChange({ param: this.props.param, name: this.props.name, value: { ...this.props.value, [k]: value } });
- }
- };
- render() {
- // TODO: improve!
- const selectParam: PD.Select<PD.Script['defaultValue']['language']> = {
- defaultValue: this.props.value.language,
- options: PD.objectToOptions(Script.Info),
- type: 'select',
- };
- const select = <SelectControl param={selectParam}
- isDisabled={this.props.isDisabled} onChange={this.onChange} onEnter={this.props.onEnter}
- name='language' value={this.props.value.language} />;
- const textParam: PD.Text = {
- defaultValue: this.props.value.language,
- type: 'text',
- };
- const text = <TextControl param={textParam} isDisabled={this.props.isDisabled} onChange={this.onChange} name='expression' value={this.props.value.expression} />;
- return <>
- {select}
- {this.props.value.language !== 'mol-script' && <div className='msp-help-text' style={{ padding: '10px' }}>
- <Icon svg={WarningSvg} /> Support for PyMOL, VMD, and Jmol selections is an experimental feature and may not always work as intended.
- </div>}
- {text}
- </>;
- }
- }
|