Files
op-pedal/vite/src/pipedal/PluginControlView.tsx
T
2025-07-20 11:52:26 -04:00

1015 lines
40 KiB
TypeScript

// Copyright (c) 2025 Robin E. R. 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, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import AutoZoom from './AutoZoom';
import ModGuiHost from './ModGuiHost';
import {midiChannelBindingControlFeatureEnabled} from './MidiChannelBinding';
import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } 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';
import FilePropertyControl from './FilePropertyControl';
import FilePropertyDialog from './FilePropertyDialog';
import JsonAtom from './JsonAtom';
import PluginOutputControl from './PluginOutputControl';
import Units from './Units';
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
import ToolTipEx from './ToolTipEx';
import MidiChannelBindingControl from './MidiChannelBindingControl';
import MidiChannelBinding from './MidiChannelBinding';
export const StandardItemSize = { width: 80, height: 110 };
const LANDSCAPE_HEIGHT_BREAK = 500;
function makeIoPluginInfo(name: string, uri: string): UiPlugin {
let result = new UiPlugin();
result.name = name;
result.uri = uri;
let volumeControl = new UiControl();
volumeControl.name = "Volume";
volumeControl.symbol = "volume_db";
volumeControl.index = 0;
volumeControl.is_input = true;
volumeControl.min_value = -60;
volumeControl.max_value = 30;
volumeControl.default_value = 0;
volumeControl.units = Units.db;
volumeControl.scale_points = [
new ScalePoint().deserialize({ label: "-INF", value: -60 })
];
result.controls = [
volumeControl
];
return result;
}
export const startPluginInfo: UiPlugin =
makeIoPluginInfo("Input", Pedalboard.START_PEDALBOARD_ITEM_URI);
export const endPluginInfo: UiPlugin =
makeIoPluginInfo("Output", Pedalboard.END_PEDALBOARD_ITEM_URI);
const styles = (theme: Theme) => createStyles({
frame: css({
display: "block",
position: "relative",
flexDirection: "row",
flexWrap: "nowrap",
paddingTop: "8px",
paddingBottom: "0px",
height: "100%",
overflowX: "hidden",
overflowY: "hidden"
}),
noScrollFrame: css({
display: "block",
position: "relative",
width: "100%",
height: "100%",
left: 0, top: 0,
flexDirection: "row",
flexWrap: "nowrap",
paddingTop: "0px",
paddingBottom: "0px",
overflowX: "hidden",
overflowY: "hidden"
}),
frameScrollNone: css({
display: "block",
position: "relative",
left: 0, top: 0,
width: "100%", height: "100%",
flexDirection: "row",
flexWrap: "nowrap",
paddingTop: "0px",
paddingBottom: "0px",
overflowX: "auto",
overflowY: "hidden"
}),
frameScrollLandscape: css({
display: "block",
position: "absolute",
left: 0, top: 0, right: 0, bottom: 0,
flexDirection: "row",
flexWrap: "nowrap",
paddingTop: "0px",
paddingBottom: "0px",
overflowX: "auto",
overflowY: "hidden"
}),
frameScrollPortrait: css({
display: "block",
position: "absolute",
left: 0, top: 0, right: 0, bottom: 0,
flexDirection: "row",
flexWrap: "nowrap",
paddingTop: "0px",
paddingBottom: "96px",
overflowX: "hidden",
overflowY: "auto",
}),
vuMeterL: css({
position: "absolute",
left: 0, top: 0,
paddingLeft: 6,
paddingRight: 4,
paddingBottom: 12, // cover bottom line of a portgroup in landscape.
background: theme.mainBackground,
zIndex: 3
}),
vuMeterR: css({
position: "absolute",
right: 0, top: 0,
marginRight: 20, // has to potentially clear a scrollbar.
paddingLeft: 4,
paddingBottom: 12,
background: theme.mainBackground,
zIndex: 3
}),
vuMeterRLandscape: css({
position: "absolute",
right: 0, top: 0,
paddingRight: 6,
paddingLeft: 12,
paddingBottom: 13, // cover bottom line of a portgroup in landscape.
background: theme.mainBackground,
zIndex: 3
}),
noScrollGrid: css({
position: "relative",
left: 0,
top: 0,
bottom: 0,
width: "100%",
height: "100%",
paddingLeft: 30,
paddingRight: 45,
paddingTop: 0,
flex: "1 1 auto",
}),
normalGrid: css({
position: "relative",
paddingLeft: 30,
paddingRight: 45,
paddingTop: 8,
flex: "1 1 auto",
display: "flex", flexDirection: "row", flexWrap: "wrap",
justifyContent: "flex-start", alignItems: "flex_start",
rowGap: 14,
height: "fit-content"
}),
landscapeGrid: css({
paddingLeft: 40, paddingRight: 40, paddingTop: 8,
// 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",
position: "relative",
overflowX: "hidden",
overflowY: "hidden",
flex: "0 0 auto",
width: "fit-content"
}),
portgroupControlPadding: css({
flex: "0 0 auto",
marginTop: 12,
marginBottom: 8
}),
controlPadding: css({
flex: "0 0 auto",
marginTop: 0,
marginBottom: 0,
height: 116
}),
controlPair: css({
display: "flex", flexFlow: "row nowrap",
flex: "0 0 auto",
height: 116
}),
controlSpacer: css({
display: "none",
minWidth: 0,
width: 0,
height: 116
}),
portGroup: css({
marginLeft: 8,
marginTop: 0,
marginRight: 8,
marginBottom: 12,
position: "relative",
paddingLeft: 3,
paddingRight: 8,
paddingTop: 0,
paddingBottom: 0,
border: "2pt #AAA solid",
borderRadius: 8,
elevation: 12,
display: "flex",
flexDirection: "row", flexWrap: "wrap",
flex: "0 1 auto",
}),
portGroupLandscape: css({
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: "inline-flex",
textOverflow: "ellipsis",
flexDirection: "row", flexWrap: "nowrap",
flex: "0 0 auto",
width: "fit-content",
minWidth: "max-content"
}),
portGroupTitle: css({
position: "absolute",
top: -15,
background: theme.mainBackground,
textOverflow: "ellipsis",
minWidth: 0,
marginLeft: 20,
paddingLeft: 8,
paddingRight: 8,
margin_right: 28
}),
portGroupControls: css({
display: "flex",
flexDirection: "row", flexWrap: "wrap",
paddingTop: 6,
rowGap: 16,
paddingBottom: 8
}),
portGroupControlsLandscape: css({
display: "flex",
flexFlow: "row nowrap",
width: "fit-content",
paddingTop: 6,
rowGap: 16,
paddingBottom: 8
})
});
export class ControlGroup {
constructor(name: string, indexes: number[], controls: ReactNode[]) {
this.name = name;
this.indexes = indexes;
this.controls = controls;
}
name: string;
indexes: number[];
controls: React.ReactNode[];
}
export type ControlNodes = (ReactNode | ControlGroup)[];
export interface ControlViewCustomization {
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
fullScreen(): boolean;
}
export interface PluginControlViewProps extends WithStyles<typeof styles> {
theme: Theme;
instanceId: number;
item: PedalboardItem;
customization?: ControlViewCustomization;
customizationId?: number;
showModGui: boolean;
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
}
type PluginControlViewState = {
landscapeGrid: boolean;
imeUiControl?: UiControl;
imeValue: number;
imeCaption: string;
imeInitialHeight: number;
showFileDialog: boolean,
dialogFileProperty: UiFileProperty,
dialogFileValue: string,
modGuiContentReady: boolean,
showModGuiZoomed: boolean,
};
const PluginControlView =
withTheme(withStyles(
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,
showFileDialog: false,
dialogFileProperty: new UiFileProperty(),
dialogFileValue: "",
modGuiContentReady: false,
showModGuiZoomed: false
}
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
this.onControlValueChanged = this.onControlValueChanged.bind(this);
this.onPreviewChange = this.onPreviewChange.bind(this);
}
// unmonitorPort: (handle: MonitorPortHandle) => void;
// onValueChanged: (instanceId: number, symbol: string, value: number) => void;
// monitorPatchProperty(
// instanceId: number,
// propertyUri: string,
// onReceived: PatchPropertyListener
// ): ListenHandle;
// cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
// getPatchProperty(instanceId: number, uri: string): Promise<any>;
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
onPreviewChange(key: string, value: number): void {
this.model.previewPedalboardValue(this.props.instanceId, key, value);
}
onControlValueChanged(key: string, value: number): void {
this.model.setPedalboardControl(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.isHidden()) {
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: document.documentElement.clientHeight
});
}
makeFilePropertyUI(fileProperty: UiFileProperty): ReactNode {
return ((
<FilePropertyControl pedalboardItem={this.props.item}
fileProperty={fileProperty}
onFileClick={(fileProperty, selectedFile) => {
this.setState({ showFileDialog: true, dialogFileProperty: fileProperty, dialogFileValue: selectedFile });
}}
/>
));
}
makeFrequencyPlotUI(frequencyPlot: UiFrequencyPlot): ReactNode {
return ((
<ToobFrequencyResponseView instanceId={this.props.instanceId}
propertyName={frequencyPlot.patchProperty}
width={frequencyPlot.width}
/>
));
}
private ixKey: number = 1;
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
let symbol = uiControl.symbol;
if (!uiControl.is_input) {
return (
<PluginOutputControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} />
);
}
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 key={"ppc" + uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
/>
));
// return ((
// <PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
// onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
// onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
// requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
// />
// ));
}
push_control(controls: ControlNodes, pluginControl: UiControl, controlValues: ControlValue[]) {
// combine lamps with their previous control
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0) {
const classes = withStyles.getClasses(this.props);
let newControl = this.makeStandardControl(pluginControl, controlValues);
let previousControl = controls[controls.length - 1];
if (!(previousControl instanceof ControlGroup)) {
let pair = (
<div key={"k" + this.ixKey++} className={classes.controlPair}>
{previousControl as ReactNode}
{newControl}
</div>
);
controls[controls.length - 1] = pair;
// push a spacer control in order to make placing of extended controls predictable.
// (e.g.. inserting at position 4 still places the extended control after four previous controls
controls.push((
<div key={"k" + this.ixKey++} className={classes.controlSpacer} />
));
} else {
controls.push(newControl);
}
} else {
controls.push(
this.makeStandardControl(pluginControl, controlValues)
)
}
}
getStandardControlNodes(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes {
let result: ControlNodes = [];
let portGroupMap: { [id: string]: ControlGroup } = {};
for (let i = 0; i < plugin.controls.length; ++i) {
let pluginControl = plugin.controls[i];
if (!pluginControl.isHidden()) {
if (pluginControl.port_group !== "" && plugin.getPortGroupBySymbol(pluginControl.port_group)) {
let portGroup = nullCast(plugin.getPortGroupBySymbol(pluginControl.port_group));
let groupControls: ReactNode[] = [];
let indexes: number[] = [];
groupControls.push(
this.makeStandardControl(pluginControl, controlValues)
);
indexes.push(pluginControl.index);
while (i + 1 < plugin.controls.length && plugin.controls[i + 1].port_group === pluginControl.port_group) {
++i;
pluginControl = plugin.controls[i];
if (!pluginControl.isHidden()) {
this.push_control(groupControls, pluginControl, controlValues);
indexes.push(pluginControl.index);
}
}
let controlGroup = new ControlGroup(portGroup.name, indexes, groupControls);
result.push(
controlGroup
)
portGroupMap[pluginControl.port_group] = controlGroup;
} else {
this.push_control(result, pluginControl, controlValues);
}
}
}
for (let i = 0; i < plugin.fileProperties.length; ++i) {
let fileProperty = plugin.fileProperties[i];
let filePropertyUi = this.makeFilePropertyUI(fileProperty);
if (fileProperty.portGroup !== "" && plugin.getPortGroupByUri(fileProperty.portGroup)) {
let portGroup = nullCast(plugin.getPortGroupByUri(fileProperty.portGroup));
let controlGroup = portGroupMap[portGroup.symbol];
if (controlGroup) {
let insertPosition = controlGroup.indexes.length;
if (fileProperty.index !== -1) {
for (let i = 0; i < controlGroup.controls.length; ++i) {
if (controlGroup.indexes[i] >= fileProperty.index) {
insertPosition = i;
break;
}
}
}
let index = fileProperty.index !== -1 ? fileProperty.index : 100;
controlGroup.controls.splice(insertPosition, 0, filePropertyUi);
controlGroup.indexes.splice(insertPosition, 0, index);
} else {
let index = fileProperty.index !== -1 ? fileProperty.index : 100;
let controlGroup = new ControlGroup(
portGroup.name,
[index],
[filePropertyUi]);
result.push(
controlGroup
);
portGroupMap[portGroup.symbol] = controlGroup;
}
} else if (fileProperty.index !== -1) {
result.splice(fileProperty.index, 0, filePropertyUi);
} else {
result.push(
filePropertyUi
);
}
}
for (let i = 0; i < plugin.frequencyPlots.length; ++i) {
let frequencyPlot = plugin.frequencyPlots[i];
let frequencyPlotUi = this.makeFrequencyPlotUI(frequencyPlot);
if (frequencyPlot.portGroup !== "" && plugin.getPortGroupByUri(frequencyPlot.portGroup)) {
let portGroup = nullCast(plugin.getPortGroupByUri(frequencyPlot.portGroup));
let controlGroup = portGroupMap[portGroup.symbol];
if (controlGroup) {
let insertPosition = controlGroup.indexes.length;
if (frequencyPlot.index !== -1) {
for (let i = 0; i < controlGroup.controls.length; ++i) {
if (controlGroup.indexes[i] >= frequencyPlot.index) {
insertPosition = i;
break;
}
}
}
let index = frequencyPlot.index !== -1 ? frequencyPlot.index : 100;
controlGroup.controls.splice(insertPosition, 0, frequencyPlotUi);
controlGroup.indexes.splice(insertPosition, 0, index);
} else {
let index = frequencyPlot.index !== -1 ? frequencyPlot.index : 100;
let controlGroup = new ControlGroup(
portGroup.name,
[index],
[frequencyPlotUi]);
result.push(
controlGroup
);
portGroupMap[portGroup.symbol] = controlGroup;
}
} else if (frequencyPlot.index !== -1) {
result.splice(frequencyPlot.index, 0, frequencyPlotUi);
} else {
result.push(
frequencyPlotUi
);
}
}
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.setPedalboardControl(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;
}
controlKeyIndex: number = 0;
controlNodesToNodes(nodes: (ReactNode | ControlGroup)[]): ReactNode[] {
const classes = withStyles.getClasses(this.props);
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 key={"ctlx" + (this.controlKeyIndex++)} className={classes.controlPadding}>
{item}
</div>
)
);
}
result.push((
<div key={"ctlx" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div className={classes.portGroupTitle}>
<ToolTipEx title={controlGroup.name}
>
<Typography noWrap variant="caption" >{controlGroup.name}</Typography>
</ToolTipEx>
</div>
<div className={
this.state.landscapeGrid ? classes.portGroupControlsLandscape : classes.portGroupControls} >
{
controls
}
</div>
</div>
));
} else {
if (this.fullScreen()) {
result.push(
<div key={"fullScreen" + this.props.instanceId} style={{ position: "relative", width: "100%", height: "100%" }}
>
{node as ReactNode}
</div>
);
} else {
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
{node as ReactNode}
</div>
));
}
}
}
return result;
}
static startPluginInfo: UiPlugin =
makeIoPluginInfo("Input", Pedalboard.START_PEDALBOARD_ITEM_URI);
static endPluginInfo: UiPlugin =
makeIoPluginInfo("Output", Pedalboard.END_PEDALBOARD_ITEM_URI);
midiBindingControl(pedalboardItem: PedalboardItem): ReactNode {
if ((!pedalboardItem.midiChannelBinding) || (midiChannelBindingControlFeatureEnabled === false)) {
return false;
}
return (
<MidiChannelBindingControl key="channelBindingCtl" midiChannelBinding={pedalboardItem.midiChannelBinding}
onChange={(result) => {
}}
/>
)
}
fullScreen() {
if (this.props.customization) {
return this.props.customization.fullScreen();
}
return false;
}
handleModGuiFileProperty(instanceId: number, filePropertyUri: string, selectedFile: string) {
let plugin = this.model.getUiPlugin(this.props.item.uri);
if (!plugin) {
throw (new Error("Plugin not found."));
}
let fileProperty = plugin.getFilePropertyByUri(filePropertyUri);
if (!fileProperty) {
throw (new Error("File property not found."));
}
this.setState({
showFileDialog: true,
dialogFileProperty: fileProperty,
dialogFileValue: selectedFile
});
}
renderModGui(): ReactNode {
const classes = withStyles.getClasses(this.props);
void classes;
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
if (!uiPlugin) {
return (<div />);
}
return (
<div style={{ width: "100%", height: "100%", }}>
<AutoZoom leftPad={36} rightPad={52} contentReady={this.state.modGuiContentReady}
showZoomed={this.state.showModGuiZoomed}
setShowZoomed={
(value) => { this.setState({ showModGuiZoomed: value }); }
}
>
<ModGuiHost
key={this.props.instanceId.toString()}
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
}
this.setState({showModGuiZoomed: false})
}}
handleFileSelect={(instanceId, fileProperty, selectedFile) => {
this.handleModGuiFileProperty(
instanceId,
fileProperty,
selectedFile);
}}
onContentReady={(value) => {
this.setState({ modGuiContentReady: value });
}}
/>
</AutoZoom>
</div>
)
}
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
this.controlKeyIndex = 0;
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
if (!pedalboardItem)
return (<div className={classes.frame} ></div>);
let controlValues = pedalboardItem.controlValues;
let plugin: UiPlugin;
if (pedalboardItem.isStart()) {
plugin = startPluginInfo;
controlValues = [new ControlValue("volume_db", pedalboard.input_volume_db)];
} else if (pedalboardItem.isEnd()) {
plugin = endPluginInfo;
controlValues = [new ControlValue("volume_db", pedalboard.output_volume_db)];
} else {
plugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri));
}
controlValues = this.filterNotOnGui(controlValues, plugin);
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
if (this.fullScreen()) {
gridClass = classes.noScrollGrid;
scrollClass = classes.frameScrollNone;
}
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);
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding) {
pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue();
}
if (pedalboardItem.midiChannelBinding && midiChannelBindingControlFeatureEnabled) {
nodes.push(this.midiBindingControl(pedalboardItem));
}
return (
<div className={scrollClass}>
<div className={gridClass} >
{
nodes
}
{/* Extra space to allow scrolling right to the end in lascape especially */}
{!this.fullScreen() && (
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
)}
{
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
)
}
</div>
</div>
);
}
render(): ReactNode {
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
let pedalboardItem: PedalboardItem;
if (this.props.instanceId === Pedalboard.START_CONTROL) {
pedalboardItem = pedalboard.makeStartItem();
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
pedalboardItem = pedalboard.makeEndItem();
} else {
pedalboardItem = pedalboard.getItem(this.props.instanceId);
}
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let frameClass = classes.frame;
if (this.fullScreen() || this.props.showModGui) {
frameClass = classes.noScrollFrame;
}
return (
<div className={frameClass}>
<div className={classes.vuMeterL}>
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
</div>
{
this.props.showModGui && this.renderModGui()
|| this.renderPiPedalControl(pedalboardItem)
}
{this.state.showFileDialog && (
<FilePropertyDialog open={this.state.showFileDialog}
fileProperty={this.state.dialogFileProperty}
instanceId={this.props.instanceId}
selectedFile={this.state.dialogFileValue}
onCancel={() => {
this.setState({
showFileDialog: false
});
}}
onApply={(fileProperty, selectedFile) => {
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
})
.catch((error) => {
this.model.showAlert("Unable to complete the operation. " + error);
});
}}
onOk={(fileProperty, selectedFile) => {
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
})
.catch((error) => {
this.model.showAlert("Unable to complete the operation. " + error);
});
this.setState({ showFileDialog: false });
}
}
/>
)}
{/* xxx: I don't think we need this anyore. */}
<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 >
);
}
},
styles
));
export default PluginControlView;