Files
op-pedal/react/src/PluginControlView.tsx
T
2022-07-12 21:10:49 -04:00

482 lines
17 KiB
TypeScript

// 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<typeof styles> {
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<PluginControlViewProps, PluginControlViewState>
{
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 ((
<PluginControl instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { 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(
(
<div className={classes.controlPadding}>
{ item }
</div>
)
);
}
result.push((
<div className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div className={classes.portGroupTitle}>
<Typography variant="caption">{controlGroup.name}</Typography>
</div>
<div className={classes.portGroupControls} >
{
controls
}
</div>
</div>
));
} else {
result.push((
<div className={ hasGroups ? classes.portgroupControlPadding: classes.controlPadding } >
{node as ReactNode}
</div>
));
}
}
return result;
}
render(): ReactNode {
let classes = this.props.classes;
let pedalBoardItem = this.model.pedalBoard.get().getItem(this.props.instanceId);
if (!pedalBoardItem)
return (<div className={classes.frame} ></div>);
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 (
<div className={classes.frame}>
<div className={classes.vuMeterL}>
<VuMeter display="input" instanceId={pedalBoardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter display="output" instanceId={pedalBoardItem.instanceId} />
</div>
<div className={gridClass} >
{
nodes
}
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
{
(!this.state.landscapeGrid) && (
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
)
}
</div>
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
onChange={(key, value) => this.onImeValueChange(key, value)}
initialHeight={
this.state.imeInitialHeight}
caption={this.state.imeCaption}
onClose={() => this.onImeClose()} />
</div >
);
}
}
);
export default PluginControlView;