groups in MIDI bindings and info dialog. Momentary swtiches, Progress ctl,, TooB Looper and Record plugins.
This commit is contained in:
@@ -44,7 +44,7 @@ import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { type OnChangedHandler } from './ObservableProperty';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/Error';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Button from '@mui/material/Button';
|
||||
import PresetSelector from './PresetSelector';
|
||||
|
||||
@@ -197,7 +197,7 @@ const GxTunerControl =
|
||||
|
||||
let nameIndex = noteNumber -octave* tet;
|
||||
if (nameIndex < 0) nameIndex += tet;
|
||||
name = names[ nameIndex ] + (octave-2);
|
||||
name = names[ nameIndex ] + (octave-1);
|
||||
valid = true;
|
||||
fraction = note-noteNumber;
|
||||
if (fraction >= 0) {
|
||||
|
||||
@@ -451,9 +451,12 @@ export enum ControlType {
|
||||
ABSwitch,
|
||||
Select,
|
||||
Trigger,
|
||||
Momentary,
|
||||
MomentaryOnByDefault,
|
||||
|
||||
Tuner,
|
||||
Vu,
|
||||
Progress,
|
||||
DbVu,
|
||||
OutputText
|
||||
}
|
||||
@@ -494,6 +497,8 @@ deserialize(input: any): UiControl {
|
||||
this.display_priority = input.display_priority;
|
||||
this.range_steps = input.range_steps;
|
||||
this.integer_property = input.integer_property;
|
||||
this.mod_momentaryOffByDefault = input.mod_momentaryOffByDefault;
|
||||
this.mod_momentaryOnByDefault = input.mod_momentaryOnByDefault;
|
||||
this.enumeration_property = input.enumeration_property;
|
||||
this.toggled_property = input.toggled_property;
|
||||
this.trigger_property = input.trigger_property;
|
||||
@@ -522,6 +527,8 @@ deserialize(input: any): UiControl {
|
||||
|
||||
} else if (this.units === Units.db) {
|
||||
this.controlType = ControlType.DbVu;
|
||||
} else if (this.units === Units.pc) {
|
||||
this.controlType = ControlType.Progress;
|
||||
} else if (this.enumeration_property) {
|
||||
this.controlType = ControlType.OutputText;
|
||||
} else if (displayUnitAsText(this.units))
|
||||
@@ -533,7 +540,7 @@ deserialize(input: any): UiControl {
|
||||
}
|
||||
if (this.isValidEnumeration()) {
|
||||
this.controlType = ControlType.Select;
|
||||
if (this.scale_points.length === 2) {
|
||||
if (this.scale_points.length === 2 && this.min_value === 0 && this.max_value === 1) {
|
||||
this.controlType = ControlType.ABSwitch;
|
||||
}
|
||||
} else {
|
||||
@@ -541,9 +548,15 @@ deserialize(input: any): UiControl {
|
||||
this.controlType = ControlType.OnOffSwitch;
|
||||
}
|
||||
}
|
||||
if (this.is_input && this.trigger_property)
|
||||
if (this.is_input)
|
||||
{
|
||||
this.controlType = ControlType.Trigger;
|
||||
if (this.mod_momentaryOnByDefault) {
|
||||
this.controlType = ControlType.MomentaryOnByDefault;
|
||||
} else if (this.mod_momentaryOffByDefault) {
|
||||
this.controlType = ControlType.Momentary;
|
||||
} else if (this.trigger_property) {
|
||||
this.controlType = ControlType.Trigger;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -591,6 +604,8 @@ deserialize(input: any): UiControl {
|
||||
display_priority: number = -1;
|
||||
range_steps: number = 0;
|
||||
integer_property: boolean = false;
|
||||
mod_momentaryOffByDefault: boolean = false;
|
||||
mod_momentaryOnByDefault: boolean = false;
|
||||
enumeration_property: boolean = false;
|
||||
trigger_property: boolean = false;
|
||||
not_on_gui: boolean = false;
|
||||
@@ -640,6 +655,14 @@ deserialize(input: any): UiControl {
|
||||
isTrigger(): boolean {
|
||||
return this.controlType === ControlType.Trigger;
|
||||
}
|
||||
isMomentary(): boolean {
|
||||
return this.controlType === ControlType.Momentary;
|
||||
}
|
||||
isMomentaryOnByDefault(): boolean {
|
||||
return this.controlType === ControlType.MomentaryOnByDefault;
|
||||
}
|
||||
|
||||
|
||||
isOutputText(): boolean {
|
||||
return !this.is_input && this.controlType === ControlType.OutputText;
|
||||
}
|
||||
@@ -662,6 +685,9 @@ deserialize(input: any): UiControl {
|
||||
isDbVu(): boolean {
|
||||
return this.controlType === ControlType.DbVu;
|
||||
}
|
||||
isProgress(): boolean {
|
||||
return this.controlType === ControlType.Progress;
|
||||
}
|
||||
|
||||
valueToRange(value: number): number {
|
||||
if (this.toggled_property) return value === 0 ? 0 : 1;
|
||||
|
||||
@@ -48,10 +48,11 @@ const styles = (theme: Theme) => createStyles({
|
||||
|
||||
enum MidiControlType {
|
||||
None,
|
||||
Select,
|
||||
Dial,
|
||||
Toggle,
|
||||
Trigger,
|
||||
Dial,
|
||||
Select,
|
||||
MomentarySwitch
|
||||
}
|
||||
|
||||
interface MidiBindingViewProps extends WithStyles<typeof styles> {
|
||||
@@ -149,7 +150,7 @@ const MidiBindingView =
|
||||
|
||||
for (let i = 0; i < 127; ++i) {
|
||||
result.push(
|
||||
<MenuItem value={i}>{Utility.midiNoteName(i)}</MenuItem>
|
||||
<MenuItem key={"k" + i} value={i}>{Utility.midiNoteName(i)}</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,6 +189,12 @@ const MidiBindingView =
|
||||
let port = this.props.uiPlugin.getControl(this.props.midiBinding.symbol);
|
||||
|
||||
if (!port) return MidiControlType.None;
|
||||
if (port.mod_momentaryOffByDefault || port.mod_momentaryOnByDefault) {
|
||||
return MidiControlType.MomentarySwitch;
|
||||
}
|
||||
if (port.trigger_property) {
|
||||
return MidiControlType.Trigger;
|
||||
}
|
||||
if (port.trigger_property) {
|
||||
return MidiControlType.Trigger;
|
||||
}
|
||||
@@ -230,7 +237,9 @@ const MidiBindingView =
|
||||
value={midiBinding.bindingType}
|
||||
>
|
||||
<MenuItem value={0}>None</MenuItem>
|
||||
{(controlType === MidiControlType.Toggle || controlType === MidiControlType.Trigger) && (
|
||||
{(controlType === MidiControlType.Toggle
|
||||
|| controlType === MidiControlType.Trigger
|
||||
|| controlType === MidiControlType.MomentarySwitch) && (
|
||||
<MenuItem value={1}>Note</MenuItem>
|
||||
)}
|
||||
<MenuItem value={2}>Control</MenuItem>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
@@ -34,7 +34,7 @@ import IconButton from '@mui/material/IconButton';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import MidiBindingView from './MidiBindingView';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import { UiPlugin,makeSplitUiPlugin } from './Lv2Plugin';
|
||||
import { PortGroup, UiPlugin, makeSplitUiPlugin } from './Lv2Plugin';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
@@ -54,6 +54,11 @@ const styles = (theme: Theme) => createStyles({
|
||||
pluginHead: css({
|
||||
borderTop: "1pt #DDD solid", paddingLeft: 22, paddingRight: 22
|
||||
}),
|
||||
groupHead: css({
|
||||
borderBottom: "1px solid " + theme.palette.divider,
|
||||
marginLeft: 0,
|
||||
marginBottom: 0
|
||||
}),
|
||||
bindingTd: css({
|
||||
verticalAlign: "top",
|
||||
paddingLeft: 12, paddingBottom: 8
|
||||
@@ -65,7 +70,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
}),
|
||||
});
|
||||
|
||||
function not_null<T>(value: T | null) {
|
||||
function not_null<T>(value: T | null) {
|
||||
if (!value) throw Error("Unexpected null value");
|
||||
return value;
|
||||
}
|
||||
@@ -116,8 +121,7 @@ export const MidiBindingDialog =
|
||||
clearTimeout(this.listenTimeoutHandle);
|
||||
this.listenTimeoutHandle = undefined;
|
||||
}
|
||||
if (this.listenHandle)
|
||||
{
|
||||
if (this.listenHandle) {
|
||||
this.model.cancelListenForMidiEvent(this.listenHandle)
|
||||
this.listenHandle = undefined;
|
||||
}
|
||||
@@ -126,10 +130,9 @@ export const MidiBindingDialog =
|
||||
|
||||
}
|
||||
|
||||
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number)
|
||||
{
|
||||
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) {
|
||||
this.cancelListenForControl();
|
||||
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let item = pedalboard.getItem(instanceId);
|
||||
if (!item) return;
|
||||
@@ -144,7 +147,7 @@ export const MidiBindingDialog =
|
||||
newBinding.control = noteOrControl;
|
||||
}
|
||||
|
||||
this.model.setMidiBinding(instanceId,newBinding);
|
||||
this.model.setMidiBinding(instanceId, newBinding);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,9 +160,9 @@ export const MidiBindingDialog =
|
||||
|
||||
this.listenHandle = this.model.listenForMidiEvent(listenForControl,
|
||||
(isNote: boolean, noteOrControl: number) => {
|
||||
this.handleListenSucceeded(instanceId,symbol,isNote, noteOrControl);
|
||||
this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -190,6 +193,7 @@ export const MidiBindingDialog =
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let iter = pedalboard.itemsGenerator();
|
||||
let keyIx = 1;
|
||||
while (true) {
|
||||
let v = iter.next();
|
||||
if (v.done) break;
|
||||
@@ -197,45 +201,43 @@ export const MidiBindingDialog =
|
||||
|
||||
|
||||
let isSplit = item.uri === "uri://two-play/pipedal/pedalboard#Split";
|
||||
let plugin : UiPlugin | null = this.model.getUiPlugin(item.uri);
|
||||
if (plugin === null && isSplit)
|
||||
{
|
||||
let plugin: UiPlugin | null = this.model.getUiPlugin(item.uri);
|
||||
if (plugin === null && isSplit) {
|
||||
plugin = makeSplitUiPlugin();
|
||||
let splitType = item.getControlValue("splitType");
|
||||
not_null(plugin.getControl("splitType")).not_on_gui = true;
|
||||
switch (splitType)
|
||||
{
|
||||
case 0: // A/B
|
||||
not_null(plugin.getControl("select")).not_on_gui = false;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 1: //mixer
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 2: // L/R
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = false;
|
||||
break;
|
||||
switch (splitType) {
|
||||
case 0: // A/B
|
||||
not_null(plugin.getControl("select")).not_on_gui = false;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 1: //mixer
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 2: // L/R
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// xxx
|
||||
}
|
||||
if (plugin) {
|
||||
result.push(
|
||||
<tr>
|
||||
<tr key={"k" + keyIx++}>
|
||||
<td colSpan={2} className={classes.pluginHead}>
|
||||
<Typography variant="caption" color="textSecondary" noWrap>
|
||||
{plugin.name}
|
||||
@@ -243,10 +245,9 @@ export const MidiBindingDialog =
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
if (!isSplit)
|
||||
{
|
||||
if (!isSplit) {
|
||||
result.push(
|
||||
<tr>
|
||||
<tr key={"k" + keyIx++}>
|
||||
<td className={classes.nameTd}>
|
||||
<Typography noWrap style={{ verticalAlign: "center", height: 48 }}>
|
||||
Bypass
|
||||
@@ -256,8 +257,7 @@ export const MidiBindingDialog =
|
||||
<MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")}
|
||||
uiPlugin={plugin}
|
||||
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
|
||||
if (instanceId === -2)
|
||||
{
|
||||
if (instanceId === -2) {
|
||||
this.cancelListenForControl();
|
||||
} else {
|
||||
this.handleListenForControl(instanceId, symbol, listenForControl);
|
||||
@@ -271,21 +271,35 @@ export const MidiBindingDialog =
|
||||
);
|
||||
}
|
||||
|
||||
let lastPortGroup: PortGroup | null = null;
|
||||
for (let i = 0; i < plugin.controls.length; ++i) {
|
||||
let control = plugin.controls[i];
|
||||
if (control.isHidden())
|
||||
{
|
||||
if (!control.is_input) {
|
||||
continue;
|
||||
}
|
||||
if (!control.is_input)
|
||||
{
|
||||
continue;
|
||||
let portGroup = plugin.getPortGroupBySymbol(control.port_group);
|
||||
if (portGroup != lastPortGroup) {
|
||||
lastPortGroup = portGroup;
|
||||
if (portGroup != null) {
|
||||
result.push(
|
||||
<tr key={"k" + keyIx++}>
|
||||
<td colSpan={2} className={classes.nameTd} >
|
||||
<div className={classes.groupHead}>
|
||||
<Typography variant="caption" color="textSecondary" noWrap>
|
||||
{portGroup?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let symbol = control.symbol;
|
||||
result.push(
|
||||
<tr>
|
||||
<tr key={"k" + keyIx++}>
|
||||
<td className={classes.nameTd}>
|
||||
<Typography noWrap style={{ verticalAlign: "middle", maxWidth: 120 }} >
|
||||
<Typography noWrap style={{ verticalAlign: "middle", maxWidth: 120, marginLeft: portGroup ? 24:0 }} >
|
||||
{control.name}
|
||||
</Typography>
|
||||
</td>
|
||||
@@ -319,66 +333,66 @@ export const MidiBindingDialog =
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open} = props;
|
||||
let { open } = props;
|
||||
const classes = withStyles.getClasses(props);
|
||||
if (!open) {
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogEx tag="midiBindings" open={open} fullWidth onClose={this.handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={true}
|
||||
style={{userSelect: "none"}}
|
||||
onEnterKey={()=>{}}
|
||||
<DialogEx tag="midiBindings" open={open} fullWidth onClose={this.handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={true}
|
||||
style={{ userSelect: "none" }}
|
||||
onEnterKey={() => { }}
|
||||
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} >
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={this.handleClose}
|
||||
aria-label="back"
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
Preset MIDI Bindings
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ overflow: "auto", flex: "1 1 auto", width: "100%" }}>
|
||||
<table className={classes.pluginTable} >
|
||||
<colgroup>
|
||||
<col style={{ width: "auto" }} />
|
||||
<col style={{ width: "100%" }} />
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
{this.generateTable()}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} >
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={this.handleClose}
|
||||
aria-label="back"
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
Preset MIDI Bindings
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.listenSnackbarOpen}
|
||||
autoHideDuration={1500}
|
||||
onClose={() => this.setState({ listenSnackbarOpen: false })}
|
||||
message="Listening for MIDI input"
|
||||
/>
|
||||
<div style={{ overflow: "auto", flex: "1 1 auto", width: "100%" }}>
|
||||
<table className={classes.pluginTable} >
|
||||
<colgroup>
|
||||
<col style={{ width: "auto" }} />
|
||||
<col style={{ width: "100%" }} />
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
{this.generateTable()}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.listenSnackbarOpen}
|
||||
autoHideDuration={1500}
|
||||
onClose={() => this.setState({ listenSnackbarOpen: false })}
|
||||
message="Listening for MIDI input"
|
||||
/>
|
||||
</DialogEx >
|
||||
);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
styles);
|
||||
|
||||
|
||||
export default MidiBindingDialog;
|
||||
@@ -2297,12 +2297,12 @@ export class PiPedalModel //implements PiPedalModel
|
||||
private monitorPatchPropertyListeners: PatchPropertyListenerItem[] = [];
|
||||
|
||||
nextListenHandle = 1;
|
||||
listenForMidiEvent(listenForControlsOnly: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle {
|
||||
listenForMidiEvent(listenForControl: boolean, onComplete: (isNote: boolean, noteOrControl: number) => void): ListenHandle {
|
||||
let handle = this.nextListenHandle++;
|
||||
|
||||
this.midiListeners.push(new MidiEventListener(handle, onComplete));
|
||||
|
||||
this.webSocket?.send("listenForMidiEvent", { listenForControlsOnly: listenForControlsOnly, handle: handle });
|
||||
this.webSocket?.send("listenForMidiEvent", { listenForControls: listenForControl, handle: handle });
|
||||
return {
|
||||
_handle: handle
|
||||
};
|
||||
|
||||
@@ -21,8 +21,8 @@ import React, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent }
|
||||
import { css } from '@emotion/react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
import WithStyles, { withTheme } from './WithStyles';
|
||||
import { createStyles } from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { UiControl, ScalePoint } from './Lv2Plugin';
|
||||
@@ -37,11 +37,17 @@ import DialIcon from './svg/fx_dial.svg?react';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import ControlTooltip from './ControlTooltip';
|
||||
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import StopIcon from '@mui/icons-material/Stop';
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
||||
|
||||
const MIN_ANGLE = -135;
|
||||
const MAX_ANGLE = 135;
|
||||
const FONT_SIZE = "0.8em";
|
||||
|
||||
|
||||
enum ButtonStyle { None, Trigger, Momentary, MomentaryOnByDefault }
|
||||
|
||||
|
||||
const SELECTED_OPACITY = 0.8;
|
||||
const DEFAULT_OPACITY = 0.6;
|
||||
@@ -53,6 +59,20 @@ const ULTRA_FINE_RANGE_SCALE = RANGE_SCALE * 50; // 12000 pixels to move from 0
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
|
||||
|
||||
function androidEmoji(text: string) {
|
||||
// android chrome doesn't support these characters properly.
|
||||
if (text === "⏹") {
|
||||
return (<StopIcon fontSize={"medium"} />);
|
||||
}
|
||||
if (text === "⏺") {
|
||||
return (<FiberManualRecordIcon fontSize={"medium"} />)
|
||||
}
|
||||
if (text === "⏵")
|
||||
{
|
||||
return (<PlayArrowIcon fontSize={"medium"}/>);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export const pluginControlStyles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
@@ -115,12 +135,12 @@ const PluginControl =
|
||||
withTheme(withStyles(
|
||||
class extends Component<PluginControlProps, PluginControlState> {
|
||||
|
||||
frameRef: React.RefObject<HTMLDivElement|null>;
|
||||
imgRef: React.RefObject<SVGSVGElement|null>;
|
||||
inputRef: React.RefObject<HTMLInputElement|null>;
|
||||
selectRef: React.RefObject<HTMLSelectElement|null>;
|
||||
frameRef: React.RefObject<HTMLDivElement | null>;
|
||||
imgRef: React.RefObject<SVGSVGElement | null>;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
selectRef: React.RefObject<HTMLSelectElement | null>;
|
||||
|
||||
displayValueRef: React.RefObject<HTMLDivElement|null>;
|
||||
displayValueRef: React.RefObject<HTMLDivElement | null>;
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
@@ -498,19 +518,50 @@ const PluginControl =
|
||||
this.isTap = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
handleTriggerMouseDown() {
|
||||
handleButtonMouseLeave(buttonStyle: ButtonStyle) {
|
||||
}
|
||||
handleButtonMouseDown(buttonStyle: ButtonStyle) {
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
let value = uiControl.max_value;
|
||||
if (uiControl.max_value === uiControl.default_value) {
|
||||
value = uiControl.min_value;
|
||||
switch (buttonStyle) {
|
||||
case ButtonStyle.Momentary:
|
||||
this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.max_value);
|
||||
break;
|
||||
case ButtonStyle.MomentaryOnByDefault:
|
||||
this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.min_value);
|
||||
break;
|
||||
case ButtonStyle.Trigger:
|
||||
{
|
||||
let value = uiControl.max_value;
|
||||
if (uiControl.max_value === uiControl.default_value) {
|
||||
value = uiControl.min_value;
|
||||
}
|
||||
this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value);
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value);
|
||||
}
|
||||
}
|
||||
handleTriggerMouseUp() {
|
||||
// triggers are reset on the audio thread.
|
||||
handleButtonMouseUp(buttonStyle: ButtonStyle) {
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
switch (buttonStyle) {
|
||||
case ButtonStyle.Momentary:
|
||||
this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.min_value);
|
||||
break;
|
||||
case ButtonStyle.MomentaryOnByDefault:
|
||||
this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol,uiControl.max_value);
|
||||
break;
|
||||
case ButtonStyle.Trigger:
|
||||
{
|
||||
// trigger values are reset automatically.
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
previewInputValue(value: number, commitValue: boolean) {
|
||||
let range = this.valueToRange(value);
|
||||
@@ -657,8 +708,7 @@ const PluginControl =
|
||||
}
|
||||
range = Math.log(value / minValue) / Math.log(uiControl.max_value / minValue);
|
||||
if (!isFinite(range)) {
|
||||
if (range < 0)
|
||||
{
|
||||
if (range < 0) {
|
||||
range = 0;
|
||||
} else {
|
||||
range = 1.0;
|
||||
@@ -672,8 +722,7 @@ const PluginControl =
|
||||
} else {
|
||||
range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value);
|
||||
if (!isFinite(range)) {
|
||||
if (range < 0)
|
||||
{
|
||||
if (range < 0) {
|
||||
range = 0;
|
||||
} else {
|
||||
range = 1.0;
|
||||
@@ -760,14 +809,28 @@ const PluginControl =
|
||||
let isSelect = control.isSelect();
|
||||
let isAbSwitch = control.isAbToggle();
|
||||
let isOnOffSwitch = control.isOnOffSwitch();
|
||||
let isTrigger = control.isTrigger();
|
||||
|
||||
let isButton = false;
|
||||
let buttonStyle: ButtonStyle = ButtonStyle.None;
|
||||
if (control.isTrigger()) {
|
||||
isButton = true;
|
||||
buttonStyle = ButtonStyle.Trigger;
|
||||
}
|
||||
if (control.isMomentary()) {
|
||||
isButton = true;
|
||||
buttonStyle = ButtonStyle.Momentary;
|
||||
}
|
||||
if (control.isMomentaryOnByDefault()) {
|
||||
isButton = true;
|
||||
buttonStyle = ButtonStyle.MomentaryOnByDefault;
|
||||
}
|
||||
|
||||
if (isAbSwitch) {
|
||||
switchText = control.scale_points[0].value === value ? control.scale_points[0].label : control.scale_points[1].label;
|
||||
}
|
||||
|
||||
let item_width: number | undefined = isSelect ? 160 : 80;
|
||||
if (isTrigger) {
|
||||
if (isButton) {
|
||||
item_width = undefined;
|
||||
}
|
||||
|
||||
@@ -788,24 +851,38 @@ const PluginControl =
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: isSelect ? "left" : "center"
|
||||
}}> {isTrigger ? "\u00A0" : control.name}</Typography>
|
||||
}}> {isButton ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}>
|
||||
|
||||
{isTrigger ?
|
||||
{isButton ?
|
||||
(
|
||||
control.name.length !== 1 ? (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleTriggerMouseDown(); }
|
||||
onMouseDown={
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
onTouchStart={
|
||||
(evt) => { evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleTriggerMouseUp(); }
|
||||
}
|
||||
style={{
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8, minWidth: 60,
|
||||
@@ -820,11 +897,25 @@ const PluginControl =
|
||||
) : (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleTriggerMouseDown(); }
|
||||
(evt) => { this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleTriggerMouseUp(); }
|
||||
(evt) => { this.handleButtonMouseUp(buttonStyle); }
|
||||
}
|
||||
onTouchStart={
|
||||
(evt) => { evt.preventDefault();
|
||||
this.handleButtonMouseDown(buttonStyle); }
|
||||
}
|
||||
onTouchEnd={
|
||||
(evt) => {
|
||||
evt.preventDefault();
|
||||
this.handleButtonMouseUp(buttonStyle);
|
||||
}
|
||||
}
|
||||
onMouseLeave={(
|
||||
(evet) => { this.handleButtonMouseLeave(buttonStyle); }
|
||||
)}
|
||||
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
@@ -839,7 +930,7 @@ const PluginControl =
|
||||
}}
|
||||
|
||||
>
|
||||
{control.name}
|
||||
{androidEmoji(control.name)}
|
||||
</Button>
|
||||
)
|
||||
)
|
||||
@@ -865,7 +956,7 @@ const PluginControl =
|
||||
|
||||
{/* LABEL/EDIT SECTION*/}
|
||||
<div className={classes.editSection} >
|
||||
{(!(isSelect || isOnOffSwitch || isTrigger)) &&
|
||||
{(!(isSelect || isOnOffSwitch || isButton)) &&
|
||||
(
|
||||
(isAbSwitch) ? (
|
||||
<Typography variant="caption" display="block" textAlign="center" noWrap style={{
|
||||
|
||||
@@ -155,6 +155,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
paddingTop: 8,
|
||||
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start",
|
||||
@@ -170,6 +171,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
// 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",
|
||||
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
flex: "0 0 auto",
|
||||
@@ -208,7 +210,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
paddingLeft: 3,
|
||||
paddingRight: 0,
|
||||
paddingRight: 8,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
@@ -216,6 +218,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
elevation: 12,
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
|
||||
flex: "0 1 auto",
|
||||
}),
|
||||
portGroupLandscape: css({
|
||||
@@ -226,6 +229,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
position: "relative",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
@@ -253,6 +257,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
paddingTop: 6,
|
||||
rowGap: 16,
|
||||
paddingBottom: 8
|
||||
}),
|
||||
portGroupControlsLandscape: css({
|
||||
@@ -260,6 +265,7 @@ const styles = (theme: Theme) => createStyles({
|
||||
flexFlow: "row nowrap",
|
||||
width: "fit-content",
|
||||
paddingTop: 6,
|
||||
rowGap: 16,
|
||||
paddingBottom: 8
|
||||
})
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
import React from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { createStyles } from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
@@ -34,7 +33,7 @@ import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import { UiPlugin, UiControl } from './Lv2Plugin';
|
||||
import { UiPlugin, PortGroup } from './Lv2Plugin';
|
||||
import PluginIcon from './PluginIcon';
|
||||
import { Remark } from 'react-remark';
|
||||
import { css } from '@emotion/react';
|
||||
@@ -56,6 +55,9 @@ const styles = (theme: Theme) => {
|
||||
icon: {
|
||||
fill: theme.palette.text.primary,
|
||||
opacity: 0.6
|
||||
},
|
||||
controlHeader: {
|
||||
borderBottom: "1px solid " + theme.palette.divider,
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -88,7 +90,7 @@ const PluginInfoDialogContent = withStyles(
|
||||
(theme: Theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -146,7 +148,8 @@ function ioDescription(plugin: UiPlugin): string {
|
||||
|
||||
// );
|
||||
// }
|
||||
function makeControls(controls: UiControl[]) {
|
||||
function makeControls(plugin: UiPlugin, controlHeadeClass: string) {
|
||||
let controls = plugin.controls;
|
||||
let hasComments = false;
|
||||
|
||||
for (let i = 0; i < controls.length; ++i) {
|
||||
@@ -156,14 +159,32 @@ function makeControls(controls: UiControl[]) {
|
||||
}
|
||||
}
|
||||
hasComments = true;
|
||||
let lastPortGroup: PortGroup | null = null;
|
||||
|
||||
if (hasComments) {
|
||||
let trs: React.ReactElement[] = [];
|
||||
for (let i = 0; i < controls.length; ++i) {
|
||||
let control = controls[i];
|
||||
if (!(control.not_on_gui) && control.is_input)
|
||||
if (!(control.not_on_gui) && control.is_input) {
|
||||
let portGroup = plugin.getPortGroupBySymbol(control.port_group);
|
||||
if (portGroup !== lastPortGroup) {
|
||||
if (portGroup !== null)
|
||||
{
|
||||
trs.push((
|
||||
<tr>
|
||||
<td className={controlHeadeClass} style={{ verticalAlign: "top", paddingTop: 8 }} colSpan={2}>
|
||||
<Typography variant="body2" style={{ whiteSpace: "nowrap" }}>
|
||||
{portGroup?.name ?? ""}
|
||||
</Typography>
|
||||
</td>
|
||||
</tr >
|
||||
));
|
||||
}
|
||||
lastPortGroup = portGroup;
|
||||
}
|
||||
trs.push((
|
||||
<tr>
|
||||
<td style={{ verticalAlign: "top" }}>
|
||||
<td style={{ verticalAlign: "top", paddingLeft: portGroup ? 24 : 0 }}>
|
||||
<Typography variant="body2" style={{ whiteSpace: "nowrap" }}>
|
||||
{control.name}
|
||||
</Typography>
|
||||
@@ -175,14 +196,14 @@ function makeControls(controls: UiControl[]) {
|
||||
</td>
|
||||
</tr >
|
||||
));
|
||||
|
||||
}
|
||||
}
|
||||
return (
|
||||
<table style={{ paddingLeft: "24px", verticalAlign: "top" }}>
|
||||
<table style={{ paddingLeft: "24px", verticalAlign: "top" }}><tbody>
|
||||
{
|
||||
trs
|
||||
}
|
||||
</table>
|
||||
</tbody></table>
|
||||
);
|
||||
|
||||
} else {
|
||||
@@ -290,7 +311,7 @@ const PluginInfoDialog = withStyles((props: PluginInfoProps) => {
|
||||
Controls:
|
||||
</Typography>
|
||||
{
|
||||
makeControls(plugin.controls)
|
||||
makeControls(plugin, classes.controlHeader)
|
||||
}
|
||||
{plugin.description.length > 0 && (
|
||||
<div>
|
||||
@@ -338,6 +359,6 @@ const PluginInfoDialog = withStyles((props: PluginInfoProps) => {
|
||||
</div >
|
||||
);
|
||||
},
|
||||
styles);
|
||||
styles);
|
||||
|
||||
export default PluginInfoDialog;
|
||||
|
||||
@@ -130,6 +130,7 @@ const PluginOutputControl =
|
||||
|
||||
private model: PiPedalModel;
|
||||
private vuRef: React.RefObject<HTMLDivElement|null>;
|
||||
private progressRef: React.RefObject<HTMLDivElement|null>;
|
||||
private dbVuRef: React.RefObject<HTMLDivElement|null>;
|
||||
private dbVuTelltaleRef: React.RefObject<HTMLDivElement|null>;
|
||||
private lampRef: React.RefObject<HTMLDivElement|null>;
|
||||
@@ -138,6 +139,7 @@ const PluginOutputControl =
|
||||
constructor(props: PluginOutputControlProps) {
|
||||
super(props);
|
||||
this.vuRef = React.createRef<HTMLDivElement>();
|
||||
this.progressRef = React.createRef<HTMLDivElement>();
|
||||
this.dbVuRef = React.createRef<HTMLDivElement>();
|
||||
this.dbVuTelltaleRef = React.createRef<HTMLDivElement>();
|
||||
this.lampRef = React.createRef<HTMLDivElement>();
|
||||
@@ -182,6 +184,7 @@ const PluginOutputControl =
|
||||
}
|
||||
}
|
||||
|
||||
private PROGRESS_WIDTH = 40-4;
|
||||
private VU_HEIGHT = 60 - 4;
|
||||
private DB_VU_HEIGHT = 60 - 4;
|
||||
private animationHandle: number | undefined = undefined;
|
||||
@@ -257,7 +260,23 @@ const PluginOutputControl =
|
||||
} else if (this.dbVuRef.current) {
|
||||
this.dbVuValue = value;
|
||||
this.requestDbVuAnimation();
|
||||
}
|
||||
} else if (this.progressRef.current) {
|
||||
let control = this.props.uiControl;
|
||||
let range = (value - control.min_value) / (control.max_value - control.min_value);
|
||||
let w = this.PROGRESS_WIDTH * range;
|
||||
|
||||
if (!this.animationHandle) {
|
||||
this.animationHandle = requestAnimationFrame(
|
||||
() => {
|
||||
if (this.progressRef.current) {
|
||||
this.progressRef.current.style.width = w + "px";
|
||||
}
|
||||
this.animationHandle = undefined;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
else if (this.vuRef.current) {
|
||||
let control = this.props.uiControl;
|
||||
let range = (value - control.min_value) / (control.max_value - control.min_value);
|
||||
@@ -364,6 +383,43 @@ const PluginOutputControl =
|
||||
</div >
|
||||
|
||||
);
|
||||
} else if (control.isProgress()) {
|
||||
item_width = undefined;
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{ width: item_width }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ width: "100%" }}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> {control.name === "" ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}
|
||||
style={{ flex: "1 1 1", display: "flex", justifyContent: "center", alignItems: "start", flexFlow: "row nowrap" }}>
|
||||
<div style={{ width: this.PROGRESS_WIDTH+2, height: 12, marginLeft: 8, marginRight: 8, background: "#181818", }}>
|
||||
<div style={{ height: 8, width: this.PROGRESS_WIDTH, overflow: "hidden", position: "absolute",
|
||||
margin: "1px 1px 1px 1px", background: "#282828" }}>
|
||||
<div ref={this.progressRef} style={{ height: 10, width: this.PROGRESS_WIDTH, position: "absolute", marginTop: 0, background: "#0C0" }} />
|
||||
<div style={{ height: 10, width: this.PROGRESS_WIDTH, position: "absolute",
|
||||
boxShadow: "inset 0px 2px 4px #000D", background: "transparent"
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={classes.editSectionNoContent}>
|
||||
|
||||
</div>
|
||||
|
||||
</div >
|
||||
|
||||
);
|
||||
|
||||
} else if (control.isDbVu()) {
|
||||
item_width = undefined;
|
||||
@@ -407,7 +463,6 @@ const PluginOutputControl =
|
||||
);
|
||||
}
|
||||
else if (control.isVu()) {
|
||||
// yyx: convert this to a horizontal progress bar.
|
||||
item_width = undefined;
|
||||
return (
|
||||
<div className={classes.controlFrame} style={{ width: item_width}}>
|
||||
|
||||
Reference in New Issue
Block a user