// Copyright (c) 2022 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import { ReactNode } from 'react'; import { Theme } from '@mui/material/styles'; import { WithStyles } from '@mui/styles'; import createStyles from '@mui/styles/createStyles'; import withStyles from '@mui/styles/withStyles'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { UiPlugin, UiControl } from './Lv2Plugin'; import { PedalBoard, PedalBoardItem, ControlValue, } from './PedalBoard'; import PluginControl from './PluginControl'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import VuMeter from './VuMeter'; import { nullCast } from './Utility' import { PiPedalStateError } from './PiPedalError'; import Typography from '@mui/material/Typography'; import FullScreenIME from './FullScreenIME'; export const StandardItemSize = { width: 80, height: 110 }; const LANDSCAPE_HEIGHT_BREAK = 500; const styles = (theme: Theme) => createStyles({ frame: { display: "block", position: "relative", flexDirection: "row", flexWrap: "nowrap", paddingTop: "8px", paddingBottom: "0px", height: "100%", overflowX: "auto", overflowY: "auto" }, vuMeterL: { position: "fixed", paddingLeft: 12, paddingRight: 4, paddingBottom: 24, left: 0, background: "white", zIndex: 3 }, vuMeterR: { position: "fixed", right: 0, marginRight: 22, paddingLeft: 4, paddingBottom: 24, background: "white", zIndex: 3 }, vuMeterRLandscape: { position: "fixed", right: 0, paddingRight: 22, paddingLeft: 12, paddingBottom: 24, background: "white", zIndex: 3 }, normalGrid: { position: "relative", paddingLeft: 22, paddingRight: 34, flex: "1 1 auto", display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "flex-start", alignItems: "flex_start", }, landscapeGrid: { paddingLeft: 40, // marginRight: 40, : bug in chrome layout engine wrt/ right margin/padding. See the spacer div added after all controls in render() with provides the same effect. display: "flex", flexDirection: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "flex_start", flex: "0 0 auto" }, portgroupControlPadding: { flex: "0 0 auto", marginTop: 12, marginBottom: 8 }, controlPadding: { flex: "0 0 auto", marginTop: 0, marginBottom: 0 }, portGroup: { marginLeft: 8, marginTop: 0, marginRight: 8, marginBottom: 12, position: "relative", paddingLeft: 3, paddingRight: 0, paddingTop: 0, paddingBottom: 0, border: "2pt #AAA solid", borderRadius: 8, elevation: 12, display: "flex", flexDirection: "row", flexWrap: "wrap", flex: "0 1 auto", }, portGroupLandscape: { marginLeft: 8, marginTop: 0, marginRight: 8, marginBottom: 12, position: "relative", paddingLeft: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 0, border: "2pt #AAA solid", borderRadius: 8, elevation: 12, display: "flex", flexDirection: "row", flexWrap: "nowrap", flex: "0 0 auto" }, portGroupTitle: { position: "absolute", top: -15, background: "white", marginLeft: 20, paddingLeft: 8, paddingRight: 8 }, portGroupControls: { display: "flex", flexDirection: "row", flexWrap: "wrap", paddingTop: 6, paddingBottom: 8 } }); export class ControlGroup { constructor(name: string, controls: ReactNode[]) { this.name = name; this.controls = controls; } name: string; controls: React.ReactNode[]; } export type ControlNodes = (ReactNode | ControlGroup)[]; export interface ControlViewCustomization { ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[]; } export interface PluginControlViewProps extends WithStyles { theme: Theme; instanceId: number; item: PedalBoardItem; customization?: ControlViewCustomization; customizationId?: number; } type PluginControlViewState = { landscapeGrid: boolean; imeUiControl?: UiControl; imeValue: number; imeCaption: string; imeInitialHeight: number; }; const PluginControlView = withStyles(styles, { withTheme: true })( class extends ResizeResponsiveComponent { model: PiPedalModel; constructor(props: PluginControlViewProps) { super(props); this.model = PiPedalModelFactory.getInstance(); this.state = { landscapeGrid: false, imeUiControl: undefined, imeValue: 0, imeCaption: "", imeInitialHeight: 0 } this.onPedalBoardChanged = this.onPedalBoardChanged.bind(this); this.onValueChanged = this.onValueChanged.bind(this); this.onPreviewChange = this.onPreviewChange.bind(this); } onPreviewChange(key: string, value: number): void { this.model.previewPedalBoardValue(this.props.instanceId, key, value); } onValueChanged(key: string, value: number): void { this.model.setPedalBoardControlValue(this.props.instanceId, key, value); } onPedalBoardChanged(value?: PedalBoard) { //let item = this.model.pedalBoard.get().maybeGetItem(this.props.instanceId); //this.setState({ pedalBoardItem: item }); } componentDidMount() { super.componentDidMount(); this.model.pedalBoard.addOnChangedHandler(this.onPedalBoardChanged); } componentWillUnmount() { this.model.pedalBoard.removeOnChangedHandler(this.onPedalBoardChanged); super.componentWillUnmount(); } onWindowSizeChanged(width: number, height: number): void { this.setState({ landscapeGrid: height < LANDSCAPE_HEIGHT_BREAK }); } filterNotOnGui(controlValues: ControlValue[], uiPlugin: UiPlugin): ControlValue[] { let result: ControlValue[] = []; for (let i = 0; i < controlValues.length; ++i) { let controlValue = controlValues[i]; let control = uiPlugin.getControl(controlValue.key); if (control && !control.not_on_gui) { result.push(controlValue); } } return result; } requestImeEdit(uiControl: UiControl, value: number) { // eslint-disable-next-line no-restricted-globals this.setState({ imeUiControl: uiControl, imeValue: value, imeCaption: uiControl.name, imeInitialHeight: window.innerHeight }); } makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode { let symbol = uiControl.symbol; let controlValue: ControlValue | undefined = undefined; for (let i = 0; i < controlValues.length; ++i) { if (controlValues[i].key === symbol) { controlValue = controlValues[i]; break; } } if (!controlValue) { throw new PiPedalStateError("Missing control value."); } return (( { this.onValueChanged(controlValue!.key, value) }} onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }} requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)} /> )); } getStandardControlNodes(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes { let result: ControlNodes = []; for (let i = 0; i < plugin.controls.length; ++i) { let pluginControl = plugin.controls[i]; if (!pluginControl.not_on_gui) { if (pluginControl.port_group !== "") { let portGroup = pluginControl.port_group; let groupControls: ReactNode[] = []; groupControls.push( this.makeStandardControl(pluginControl, controlValues) ); while (i + 1 < plugin.controls.length && plugin.controls[i + 1].port_group === portGroup) { ++i; pluginControl = plugin.controls[i]; if (!pluginControl.not_on_gui) { groupControls.push( this.makeStandardControl(pluginControl, controlValues) ) } } result.push( new ControlGroup(plugin.getPortGroup(portGroup).name, groupControls) ) } else { result.push( this.makeStandardControl(pluginControl, controlValues) ); } } } return result; } getControl(controlValues: ControlValue[], key: string) { for (let i = 0; i < controlValues.length; ++i) { if (controlValues[i].key === key) { return controlValues[i]; } } throw new Error("Not found."); } onImeValueChange(key: string, value: number) { this.model.setPedalBoardControlValue(this.props.instanceId, key, value); this.onImeClose(); } onImeClose() { this.setState({ imeUiControl: undefined }); } hasGroups(nodes: (ReactNode | ControlGroup)[]): boolean { for (let i = 0; i < nodes.length; ++i) { let node = nodes[i]; if (node instanceof ControlGroup) return true; } return false; } controlNodesToNodes(nodes: (ReactNode | ControlGroup)[]): ReactNode[] { let classes = this.props.classes; let isLandscapeGrid = this.state.landscapeGrid; let hasGroups = this.hasGroups(nodes); let result: ReactNode[] = []; for (let i = 0; i < nodes.length; ++i) { let node = nodes[i]; if (node instanceof ControlGroup) { let controlGroup = node as ControlGroup; let controls: ReactNode[] = []; for (let j = 0; j < controlGroup.controls.length; ++j) { let item = controlGroup.controls[j]; controls.push( (
{ item }
) ); } result.push((
{controlGroup.name}
{ controls }
)); } else { result.push((
{node as ReactNode}
)); } } return result; } render(): ReactNode { let classes = this.props.classes; let pedalBoardItem = this.model.pedalBoard.get().getItem(this.props.instanceId); if (!pedalBoardItem) return (
); let controlValues = pedalBoardItem.controlValues; let plugin: UiPlugin = nullCast(this.model.getUiPlugin(pedalBoardItem.uri)); controlValues = this.filterNotOnGui(controlValues, plugin); let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid; let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR; let controlNodes: ControlNodes; controlNodes = this.getStandardControlNodes(plugin, controlValues); if (this.props.customization) { // allow wrapper class to insert/remove/rebuild controls. controlNodes = this.props.customization.ModifyControls(controlNodes); } let nodes = this.controlNodesToNodes(controlNodes); return (
{ nodes }
{ (!this.state.landscapeGrid) && (
) }
this.onImeValueChange(key, value)} initialHeight={ this.state.imeInitialHeight} caption={this.state.imeCaption} onClose={() => this.onImeClose()} />
); } } ); export default PluginControlView;