MOD Gui support.

This commit is contained in:
Robin E. R. Davies
2025-07-10 11:37:02 -04:00
parent e7cdd38056
commit 890368352f
45 changed files with 1971 additions and 806 deletions
+114 -1
View File
@@ -1,3 +1,44 @@
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap
}
.mod-pedal {
}
@@ -40,6 +81,11 @@
margin-bottom: 20px;
}
.mod-pedal-settings .mod-control-group {
margin:0;
padding:0 18px;
}
.ppmod-dial-control-image {
width: 48px;
height: 48px;
@@ -61,4 +107,71 @@
white-space: nowrap;
overflow: hidden;
}
.mod-pedal .mod-drag-handle {
bottom:0;
left:0;
right:0;
top:0;
cursor:default; /* we dont do drag */
position:absolute;
z-index: 0;
}
.mod-pedal .mod-control-group {
z-index: 20;
}
.mod-pedal .mod-pedal-input,
.mod-pedal .mod-pedal-output {
z-index:30;
}
/* mod clearfix hack */
.mod-pedal .clearfix:before,
.mod-pedal .clearfix:after {
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.mod-pedal.clearfix:after { clear: both; }
/* enumeration list entries for embedded property browsers. */
.ppmod-dotdot-flex {
display: flex;
align-items: center;
flex-flow: row nowrap;
}
.ppmod-dotdot-text {
white-space: nowrap;
flex: 1 0 auto;
font-size: 0.8em;
padding-right: 8px;
}
.ppmod-dotdot-path {
white-space: nowrap;
font-size: 0.8em;
flex: 0 1 auto;
overflow: hidden;
}
.ppmod-dotdot-breadcrumb_link {
display: inline;
white-space: nowrap;
text-decoration: underline
}
.ppmod-dotdot-breadcrumb_current {
display: inline;
white-space: nowrap;
}
+1 -1
View File
@@ -813,7 +813,7 @@ export
{(!this.state.tinyToolBar) && !this.state.performanceView ?
(
<AppBar position="absolute" >
<Toolbar variant="dense" className={classes.toolBar} >
<Toolbar variant="dense" className={classes.toolBar} style={{overflow: "clip" }} >
<IconButtonEx tooltip="Menu"
edge="start"
aria-label="menu"
+368 -21
View File
@@ -1,23 +1,370 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
// 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.
* 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 ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import { createStyles } from './WithStyles';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import IconButtonEx from './IconButtonEx';
import { isDarkMode } from './DarkMode';
import Backdrop from '@mui/material/Backdrop';
import CloseIcon from '@mui/icons-material/Close';
import Rect from './Rect';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
const styles = (theme: Theme) => createStyles({
pgraph: {
paddingBottom: 16
}
});
export interface AutoZoomProps extends WithStyles<typeof styles> {
leftPad: number,
rightPad: number,
children?: React.ReactNode,
contentReady: boolean,
showZoomed: boolean,
setShowZoomed: (value: boolean) => void;
}
export interface AutoZoomState {
screenWidth: number,
screenHeight: number,
}
const AutoZoom = withStyles(
class extends ResizeResponsiveComponent<AutoZoomProps, AutoZoomState> {
private model: PiPedalModel;
constructor(props: AutoZoomProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
void this.model; // suppress unused variable warning
this.state = {
screenWidth: this.windowSize.width,
screenHeight: this.windowSize.height,
};
}
mounted: boolean = false;
onWindowSizeChanged(width: number, height: number): void {
this.setState({ screenWidth: width, screenHeight: height });
}
componentDidMount() {
super.componentDidMount();
this.mounted = true;
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
}
componentWillUnmount() {
this.mounted = false;
this.cancelZoomUpdate();
this.cancelMaximizedZoomUpdate();
super.componentWillUnmount();
}
componentDidUpdate(prevProps: AutoZoomProps) {
if (prevProps.contentReady !== this.props.contentReady) {
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
}
}
resizeObserver: ResizeObserver | null = null;
calculateZoomedClientRect(
clientRect: Rect,
contentWidth: number,
contentHeight: number,
buttonPadding: number
): Rect {
let rightButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, clientRect.height / contentHeight);
let topButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, (clientRect.height - buttonPadding) / contentHeight);
let effectiveClientRect = clientRect.copy();
if (rightButtonZoom > topButtonZoom) {
effectiveClientRect.width -= buttonPadding;
} else {
effectiveClientRect.y += buttonPadding;
effectiveClientRect.height -= buttonPadding;
}
let zoom = Math.min(effectiveClientRect.width / contentWidth, effectiveClientRect.height / contentHeight);
if (zoom > 1) {
zoom = 1;
}
let leftPad = (effectiveClientRect.width - contentWidth * zoom) / 2;
if (leftPad < 0) {
leftPad = 0;
}
let topPad = (effectiveClientRect.height - contentHeight * zoom) / 4;
if (topPad < 0) {
topPad = 0;
}
return new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom);
}
updateZoom(ref: HTMLDivElement) {
if (!this.mounted || !ref) {
return;
}
let content = ref.firstElementChild as HTMLElement | null;
if (content) {
const { leftPad, rightPad } = this.props;
let bounds = new Rect(leftPad, 0, ref.clientWidth - leftPad - rightPad, ref.clientHeight - 8);
let zoomedClientRect = this.calculateZoomedClientRect(
bounds, content.clientWidth, content.clientHeight, 48);
content.style.left = "0px";
content.style.top = "0px";
content.style.transformOrigin = "0 0"; // top-left corner
content.style.transform =
`translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`;
if (this.zoomButton) {
this.zoomButton.style.display = zoomedClientRect.width < content.clientWidth ? "block" : "none";
let rightSizeSpace = ref.clientWidth - zoomedClientRect.right;
if (rightSizeSpace - rightPad >= 48) {
this.zoomButton.style.top = `${zoomedClientRect.y + 24}px`;
this.zoomButton.style.left = `${zoomedClientRect.right + 4}px`;
} else {
this.zoomButton.style.top = `${zoomedClientRect.y - 48}px`;
this.zoomButton.style.left = `${zoomedClientRect.right - 64 - 12}px`;
}
}
// save the position of the Mod UI in page coordinates for later zooming.
this.zoomStartRect = zoomedClientRect.copy();
let refBounds = ref.getBoundingClientRect();
this.zoomStartRect.x += refBounds.left;
this.zoomStartRect.y += refBounds.top;
}
}
updateMaximizedZoom() {
let ref = this.maximizedRef;
if (!this.mounted || !ref) {
return;
}
let content = ref.firstElementChild as HTMLElement | null;
if (content) {
let leftPad = 16; let rightPad = 16;
let topPad = 16, bottomPad = 16;
let bounds: Rect;
bounds = new Rect(leftPad, topPad, ref.clientWidth - leftPad - rightPad, ref.clientHeight - topPad - bottomPad);
let zoomedClientRect = this.calculateZoomedClientRect(
bounds, content.clientWidth, content.clientHeight, 48);
content.style.left = "0px";
content.style.top = "0px";
content.style.transformOrigin = "0 0"; // top-left corner
content.style.transform = `translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`;
//console.log("Zoomed to: ", zoomedClientRect.toString());
}
}
private animateFrameHandle: number | null = null;
cancelZoomUpdate() {
if (this.animateFrameHandle) {
window.cancelAnimationFrame(this.animateFrameHandle);
this.animateFrameHandle = null;
}
}
requestZoomUpdate() {
if (this.animateFrameHandle) {
return; // already scheduled
}
this.animateFrameHandle = window.requestAnimationFrame(() => {
this.animateFrameHandle = null;
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
});
}
private maximizedAnimateFrameHandle: number | null = null;
cancelMaximizedZoomUpdate() {
if (this.maximizedAnimateFrameHandle) {
window.cancelAnimationFrame(this.maximizedAnimateFrameHandle);
this.maximizedAnimateFrameHandle = null;
}
}
requestMaximizedZoomUpdate() {
if (this.maximizedAnimateFrameHandle) {
return; // already scheduled
}
this.maximizedAnimateFrameHandle = window.requestAnimationFrame(() => {
this.maximizedAnimateFrameHandle = null;
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
});
}
private zoomButton: HTMLElement | null = null;
private normalRef: HTMLDivElement | null = null;
setNormalRef(ref: HTMLDivElement | null) {
this.normalRef = ref;
if (ref) {
this.zoomButton = ref.querySelector("#maximize-button") as HTMLDivElement | null;
this.resizeObserver = new ResizeObserver(() => {
this.requestZoomUpdate();
});
this.resizeObserver.observe(ref);
let child = ref.firstElementChild as HTMLElement | null;
if (child) {
this.resizeObserver.observe(child);
}
this.requestZoomUpdate();
} else {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
this.zoomButton = null;
}
this.zoomStartRect = null;
}
}
private maximizedRef: HTMLDivElement | null = null;
private maximedResizeObserver: ResizeObserver | null = null;
setMaximizedRef(ref: HTMLDivElement | null) {
this.maximizedRef = ref;
if (ref) {
this.maximedResizeObserver = new ResizeObserver(() => {
this.requestMaximizedZoomUpdate();
});
this.maximedResizeObserver.observe(ref);
let child = ref.firstElementChild as HTMLElement | null;
if (child) {
this.maximedResizeObserver.observe(child);
}
this.requestMaximizedZoomUpdate();
} else {
if (this.maximedResizeObserver) {
this.maximedResizeObserver.disconnect();
this.maximedResizeObserver = null;
}
}
}
private zoomStartRect: Rect | null = null;
startZoom() {
if (!this.zoomStartRect) {
return;
}
this.props.setShowZoomed(true);
}
render() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
return (<div style={{ overflow: "hidden", height: "100%", width: "100%" }} ref={(ref) => { this.setNormalRef(ref); }}>
<div style={{
display: "inline-block",
visibility: this.props.contentReady ? "visible" : "hidden",
position: "relative",
}}>
{!this.props.showZoomed && this.props.children}
</div>
<div id="maximize-button" style={{
display: "none", position: "absolute", left: 0, top: 0, width: 52, height: 64, zIndex: 1101,
borderRadius: "0px 26px 26px 0px",
}}
>
<IconButtonEx tooltip="Fullscreen view"
style={{ margin: 2, background: isDarkMode() ? "#444" : "#eee" }}
onClick={() => {
this.startZoom();
}}
>
<FullscreenIcon style={{ color: "white" }} />
</IconButtonEx>
</div>
{this.props.showZoomed && (
<Backdrop id="modGuiZoomBackdrop"
sx={(theme) => ({
zIndex: this.props.showZoomed ? 1101 : -1,
overflow: "clip",
background: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.9)'
})}
open={this.props.showZoomed}
>
<div ref={(ref) => { this.setMaximizedRef(ref); }}
style={{
position: "absolute", left: 0, top: 0, right: 0, bottom: 0, overflow: "hidden",
visibility: this.props.contentReady ? "visible" : "hidden"
}}>
<div style={{ display: "inline-block", position: "relative" }}>
{
this.props.showZoomed && this.props.children
}
</div>
<IconButtonEx tooltip="Exit fullscreen"
style={{
position: "absolute", right: 16, top: 16, zIndex: 1104,
}}
onClick={(e) => {
e.stopPropagation();
this.props.setShowZoomed(false);
}}
>
<CloseIcon />
</IconButtonEx>
</div>
</Backdrop>
)}
</div>);
}
},
styles);
export default AutoZoom;
-4
View File
@@ -63,10 +63,6 @@ function FontTest() {
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
</div>
);
+3
View File
@@ -110,6 +110,9 @@ const GxTunerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+146 -50
View File
@@ -20,118 +20,214 @@
// Utility class for constructing Json atoms.
class JsonAtom {
static Property(value: string): any
static _Bool(value: boolean): any
{
return {
"otype_": "Property",
"value": value
};
}
static Bool(value: boolean): any
{
return {
return new JsonAtom({
"otype_": "Bool",
"value": value
};
});
}
static Int(value: number): any
isBool(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Bool";
}
asBool(): boolean {
if (this.isBool()) {
return this.json.value as boolean;
}
throw new Error("JsonAtom is not a Bool");
}
static _Int(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Int",
"value": value
};
});
}
static Long(value: number): any
isInt(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Int";
}
asInt(): number {
if (this.isInt()) {
return this.json.value as number;;
}
throw new Error("JsonAtom is not an Int");
}
static _Long(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Long",
"value": value
};
});
}
static Float(value: number): any
isLong(): boolean
{
return value;
return this.isObject() && this.json.otype_ === "Long";
}
static Double(value: number): any
asLong(): number {
if (this.isLong()) {
return this.json.value as number;
}
throw new Error("JsonAtom is not a Long");
}
static _Float(value: number): JsonAtom
{
return {
return new JsonAtom(value);
}
static _Double(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Double",
"value": value
};
});
}
static String(value: string): any
static _String(value: string): JsonAtom
{
return value;
return new JsonAtom(value);
}
static Path(value: string): any
isString() : boolean
{
return {
return this.json && typeof this.json === 'string';
}
asString(): string {
if (this.isString()) {
return this.json as string;
}
throw new Error("JsonAtom is not a String");
}
static _Path(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "Path",
"value": value
};
});
}
static Uri(value: string): any
isPath(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Path";
}
asPath(): string {
if (this.isPath()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a Path");
}
static _Uri(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "URI",
"value": value
};
});
}
static Urid(value: string): any
isUri(): boolean
{
return {
return this.isObject() && this.json.otype_ === "URI";
}
asUri(): string {
if (this.isUri()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a URI");
}
static _Urid(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "URID",
"value": value
};
});
}
static Object(type: string): any
isUrid(): boolean
{
return {
return this.isObject() && this.json.otype_ === "URID";
}
asUrid(): string {
if (this.isUrid()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a URID");
}
static _Object(type: string): JsonAtom
{
return new JsonAtom({
"otype_": type
};
});
}
static Tuple(values: any[]): any
isObject(type?: string): boolean
{
return {
if (this.json && typeof this.json === 'object' && !Array.isArray(this.json)) {
if (!type) return true;
return type === this.json.otype_;
}
return false;
}
asObject(type?: string): Object {
if (this.isObject(type)) {
return this.json as Object;
}
throw new Error(`JsonAtom is not an Object of type ${type}`);
}
static _Tuple(values: any[]): JsonAtom
{
return new JsonAtom({
"otype_": "Tuple",
"value": values
};
});
}
static IntVector(values: []): any
static _IntVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Int",
"value": values
};
});
}
static LongVector(values: []): any
static _LongVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Long",
"value": values
};
});
}
static FloatVector(values: []): any
static _FloatVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Float",
"value": values
};
});
}
static DoubleVector(values: []): any
static _DoubleVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Float",
"value": values
};
});
}
constructor(json: any) {
this.json = json;
}
isArray(): boolean {
return this.json && Array.isArray(this.json);
}
isFloat(): boolean {
return this.json && typeof this.json === 'number';
}
isNull(): boolean {
return this.json === null;
}
asAny(): any {
return this.json;
}
private json: any;
};
export default JsonAtom;
+11
View File
@@ -1021,6 +1021,17 @@ export class UiPlugin implements Deserializable<UiPlugin> {
return null;
}
getFilePropertyByUri(patchPropertyUri: string): UiFileProperty | null {
for (let i = 0; i < this.fileProperties.length; ++i)
{
let fileProperty = this.fileProperties[i];
if (fileProperty.patchProperty === patchPropertyUri) {
return fileProperty;
}
}
return null;
}
uri: string = "";
name: string = "";
minorVersion: number = 0;
+56 -22
View File
@@ -63,6 +63,7 @@ import PipedalUiIcon from './svg/pp_ui.svg?react';
import SnapshotDialog from './SnapshotDialog';
import { css } from '@emotion/react';
import { setDefaultModGuiPreference } from './ModGuiHost';
const SPLIT_CONTROLBAR_THRESHHOLD = 750;
@@ -253,6 +254,7 @@ export const MainPage =
this.setState({
pedalboard: value,
selectedPedal: selectedItem,
showModUi: value.maybeGetItem(selectedItem)?.useModUi ?? false,
selectedSnapshot: value.selectedSnapshot
});
}
@@ -279,6 +281,14 @@ export const MainPage =
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
super.componentWillUnmount();
}
componentDidUpdate(prevProps: MainProps, prevState: MainState) {
if (prevState.selectedPedal !== this.state.selectedPedal
|| prevState.pedalboard !== this.state.pedalboard
) {
this.setState({showModUi: this.state.pedalboard.maybeGetItem(this.state.selectedPedal)?.useModUi??false});
}
}
updateResponsive() {
this.setState({
splitControlBar: this.getSplitToolbar(),
@@ -369,6 +379,15 @@ export const MainPage =
}
handleShowModUi() {
let newState = !this.state.showModUi;
this.model.setPedalboardItemUseModUi(this.state.selectedPedal, newState);
let item = this.model.pedalboard.get().maybeGetItem(this.state.selectedPedal);
if (item) {
setDefaultModGuiPreference(item.uri, newState);
}
}
onPedalboardPropertyChanged(instanceId: number, key: string, value: number) {
this.model.setPedalboardControl(instanceId, key, value);
}
@@ -380,13 +399,14 @@ export const MainPage =
if (pedalboardItem === null) return "";
return pedalboardItem.uri;
}
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode {
let title = "";
let author = "";
let infoPluginUri = "";
let presetsUri = "";
let missing = false;
let canEditTitle = false;
let modGuiButtonVisible = false;
if (pedalboardItem) {
if (pedalboardItem.isEmpty()) {
title = "";
@@ -416,6 +436,7 @@ export const MainPage =
// if (uiPlugin.description.length > 20) {
// }
infoPluginUri = uiPlugin.uri;
modGuiButtonVisible = true;
}
}
}
@@ -442,20 +463,25 @@ export const MainPage =
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
alignItems: "center"
}}>
<div style={{ position: "relative", flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<div style={{ position: "relative", flex: "0 1 auto", textAlign: "left", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
{canEditTitle ? (
<ButtonBase
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
style={{
flex: "0 1 auto", borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48,
textTransform: "none", textAlign: "left", justifyItems: "start", overflow: "clip"
}}
onClick={() => {
this.handleEditPluginDisplayName();
}}
>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
<div style={{ flex: "0 1 auto" }}>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
</div>
</ButtonBase>)
: (
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
@@ -474,22 +500,28 @@ export const MainPage =
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
{canShowModUi && (
<div style={{ flex: "0 0 auot" }}>
<IconButtonEx tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
</div>
)}
{modGuiButtonVisible && (
<div style={{ flex: "0 0 auto" }}>
<IconButtonEx
style={{ opacity: canShowModUi ? 1.0 : 0.4 }}
disabled={!canShowModUi}
tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin.
{!canShowModUi && " The current plugin does not provide a MOD user interface."}
</Typography>
</div>
)}
onClick={() => {
this.setState({ showModUi: !this.state.showModUi });
this.handleShowModUi()
}}
size="large">
{!this.state.showModUi ?
{(!this.state.showModUi || !canShowModUi) ?
(
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
) : (
@@ -498,6 +530,7 @@ export const MainPage =
}
</IconButtonEx>
</div>
)}
</div>
@@ -698,8 +731,9 @@ export const MainPage =
) :
(
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
GetControlView(pedalboardItem, this.state.showModUi && canShowModUi,
(instanceId: number, showModGui: boolean) => {
this.model.setPedalboardItemUseModUi(instanceId, showModGui)
this.setState({ showModUi: showModGui });
}
)
+448 -151
View File
@@ -21,17 +21,21 @@
* SOFTWARE.
*/
import { UiPlugin, UiControl, ControlType } from './Lv2Plugin';
import { UiPlugin, UiControl, ControlType, UiFileProperty } from './Lv2Plugin';
import React from 'react';
import CloseIcon from '@mui/icons-material/Close';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
import OkDialog from './OkDialog';
import {pathParentDirectory} from './FileUtils';
import { pathFileName, pathParentDirectory } from './FileUtils';
import JsonAtom from './JsonAtom';
import {
PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State,
ListenHandle, FileRequestResult
} from './PiPedalModel';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State,
ListenHandle, PatchPropertyListener } from './PiPedalModel';
const RANGE_SCALE = 120; // 120 pixels to move from 0 to 1.
const FINE_RANGE_SCALE = RANGE_SCALE * 10; // 1200 pixels to move from 0 to 1.
@@ -41,26 +45,50 @@ const WHEEL_RANGE_SCALE = 120 * 20;
const FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 10;
const ULTRA_FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 50;
export interface IModGuiHostSite {
monitorPort: (
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void) => MonitorPortHandle;
unmonitorPort: (handle: MonitorPortHandle) => void;
setPedalboardControl: (instanceId: number, symbol: string, value: number) => void;
monitorPatchProperty(
instanceId: number,
propertyUri: string,
onReceived: PatchPropertyListener
): ListenHandle;
export type BypassChangedCallback = (value: boolean) => void;
function HtmlEncode(value: string): string {
value = value.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return value;
cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
}
function htmlEncodeTest() {
let testString = "&<>'\"\\";
let encoded = HtmlEncode(testString);
let div = document.createElement("div");
div.innerHTML = `<div data-text='${encoded}'>${encoded}</div>`;
let innerDiv = div.children[0] as HTMLDivElement;
if (innerDiv.getAttribute("data-text") !== testString) {
throw new Error("HtmlEncode failed to encode properly: " + encoded);
}
}
htmlEncodeTest();
function PathToString(json: any): string {
let t = new JsonAtom(json);
if (t.isPath()) {
return t.asPath();
}
if (t.isString()) {
return t.asString();
}
return "";
}
function StringToPath(path: string): any {
return JsonAtom._Path(path).asAny();
}
getPatchProperty(instanceId: number, uri: string): Promise<any>;
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
};
export interface ModGuiHostProps {
instanceId: number,
@@ -69,7 +97,10 @@ export interface ModGuiHostProps {
width?: number;
height?: number;
test?: boolean;
hostSite: IModGuiHostSite;
handleFileSelect: (instanceId: number, propertyUri: string, filePath: string) => void;
onContentReady: (ready: boolean) => void;
}
export interface ModGuiJs {
set_port_value: (symbol: string, value: number) => void;
@@ -92,7 +123,10 @@ interface CustomSelectPathControlProps {
instanceId: number;
plugin: UiPlugin;
propertyUri: string,
hostSite: IModGuiHostSite;
hostSite: PiPedalModel;
handleFileSelect: (instanceId: number, propertyUri: string, filePath: string) => void;
};
@@ -107,6 +141,7 @@ class CustomSelectPathControl implements ModGuiControl {
this.props = props;
}
private isInlineList: boolean = false;
requestUpdate() {
if (!this.mounted) return;
@@ -130,23 +165,149 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
requestFileUpdate() {
private getFileProperty(): UiFileProperty | null {
for (let property of this.props.plugin.fileProperties) {
if (property.patchProperty === this.props.propertyUri) {
return property;
}
}
return null;
}
navBreadcrumbText(breadcrumbs: { pathname: string, displayName: string }[]): string {
let breadcrumbText = "";
for (let i = 0; i < breadcrumbs.length - 1; ++i) {
let breadcrumb = breadcrumbs[i];
breadcrumbText += "<div mod-role='enumeration-option' mod-filetype='directory'"
+ " mod-parameter-value='" + HtmlEncode(breadcrumb.pathname) + "'"
+ " class='ppmod-dotdot-breadcrumb_link'>"
+ HtmlEncode(breadcrumb.displayName)
+ "</div> / ";
}
let breadcrumb = breadcrumbs[breadcrumbs.length - 1];
breadcrumbText += "<div class='ppmod-dotdot-breadcrumb_current'>"
+ HtmlEncode(breadcrumb.displayName)
+ "</div>";
return breadcrumbText;
}
makeEnumeratedListItem(
type: string,
basename: string,
fullname: string,
encodebaseName?: boolean
): HTMLElement {
type = HtmlEncode(type);
fullname = HtmlEncode(fullname);
if (encodebaseName !== false) {
basename = HtmlEncode(basename);
}
let itemHtml = this.enumeratedListItemTemplate
.replace(/{{basename}}/g, basename)
.replace(/{{filetype}}/g, type)
.replace(/{{fullname}}/g, fullname);
let t = document.createElement("div");
t.innerHTML = itemHtml;
let itemElement = t.firstElementChild as HTMLElement;
return itemElement;
}
fileRequestResult: FileRequestResult | null = null;
async requestDirectoryUpdate() {
let model = PiPedalModelFactory.getInstance();
let fileProperty = this.getFileProperty();
if (!fileProperty) {
return;
}
let files = await model.requestFileList2(this.navDirectory || "", fileProperty);
this.fileRequestResult = files;
if (!this.mounted) {
return;
}
if (this.enumeratedListContainerElement) {
this.enumeratedListContainerElement.innerHTML = ""; // remove all children.
if (files.breadcrumbs.length >= 2) {
let parentDirectory = files.breadcrumbs[files.breadcrumbs.length - 2].pathname;
let dotdotHtml = "<div class='ppmod-dotdot-flex'>"
+ "<div mod-role='enumeration-option' mod-filetype='directory'"
+ " mod-parameter-value='" + HtmlEncode(parentDirectory) + "'"
+ " class='ppmod-dotdot-text'>[ ../ ]</div>"
+ "<div class='ppmod-dotdot-path'>"
+ this.navBreadcrumbText(files.breadcrumbs)
+ "</div>";
let dotdotElement = this.makeEnumeratedListItem(
"directory",
dotdotHtml,
parentDirectory,
false
);
dotdotElement.style.setProperty("border-bottom", "1px solid #888");
this.enumeratedListContainerElement.appendChild(dotdotElement);
} else {
// mod-role="enumeration-option" mod-filetype="{{filetype}}" mod-parameter-value="{{fullname}}"
let dotdotHtml = "<div class='ppmod-dotdot-flex'>"
+ "<div class='ppmod-dotdot-text'>&nbsp;</div>"
+ "<div class='ppmod-dotdot-path'>"
+ this.navBreadcrumbText(files.breadcrumbs)
+ "</div>";
let dotdotElement = this.makeEnumeratedListItem(
"directory",
dotdotHtml,
"",
false
);
dotdotElement.style.setProperty("border-bottom", "1px solid #888");
this.enumeratedListContainerElement.appendChild(dotdotElement);
}
for (let file of files.files) {
let displayName = file.displayName;
if (file.isDirectory) {
displayName = '[ ' + displayName + '/ ]';
}
let fileType = file.isDirectory ? "directory" : "file";
let itemElement = this.makeEnumeratedListItem(
fileType,
displayName,
file.pathname
);
this.enumeratedListContainerElement.appendChild(itemElement);
}
this.updateSelection();
}
}
private navDirectory: string | null = null;
setNavDirectory(navDirectory: string | null) {
if (this.navDirectory !== navDirectory) {
this.navDirectory = navDirectory;
this.requestDirectoryUpdate();
}
}
private pathValue: string | null = null;
private browsePath: string | null = null;
setPathValue(value: string) {
if (this.pathValue !== value) {
this.pathValue = value;
if (this.pathValue !== "") {
if (this.pathValue !== "" || this.navDirectory === null) {
let browsePath = pathParentDirectory(value);
if (this.browsePath !== browsePath) {
this.browsePath = browsePath;
this.requestFileUpdate();
}
this.setNavDirectory(browsePath);
}
this.requestUpdate();
}
if (this.enumeratedValueElement) {
this.enumeratedValueElement.textContent = pathFileName(this.pathValue);
}
}
private updateImage() {
@@ -175,7 +336,8 @@ class CustomSelectPathControl implements ModGuiControl {
}
handlePropertyValueChange(value: any) {
let path: string = PathToString(value);
this.setPathValue(path);
}
private mounted: boolean = false;
@@ -183,29 +345,29 @@ class CustomSelectPathControl implements ModGuiControl {
onMounted() {
this.mounted = true;
this.props.hostSite.monitorPatchProperty(this.props.instanceId, this.props.propertyUri,
(value: any) => {
(instanceId, propertyUri, value) => {
this.handlePropertyValueChange(value);
}
);
this.props.hostSite.getPatchProperty(this.props.instanceId, this.props.propertyUri)
.then((value: any) => {
this.handlePropertyValueChange(value);
}).
catch((error: any) => {
if (error instanceof Error) {
console.error((error as Error).message);
} else {
console.error(error.toString());
}
console.error(error);
});
.then((value: any) => {
this.handlePropertyValueChange(value);
}).
catch((error: any) => {
if (error instanceof Error) {
console.error((error as Error).message);
} else {
console.error(error.toString());
}
this.handlePropertyValueChange("");
});
this.requestUpdate();
}
onUnmount() {
if (this.listenHandle) {
this.props.hostSite.cancelMonitorPatchProperty(this.listenHandle);
this.listenHandle = null;
this.listenHandle = null;
}
this.mounted = false;
this.cancelRequestUpdate();
@@ -226,40 +388,92 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
handleSelected(value: string,strFileType: string)
{
this.setPathValue(value);
this.props.hostSite.setPatchProperty(this.props.instanceId, this.props.propertyUri, value);
getValueElement(): HTMLElement | null {
return null;
}
updateSelection() {
if (!this.frameElement) {
return;
}
let valueElement = this.getValueElement();
if (valueElement) {
/// xxx: update the display value.
if (this.pathValue === null || this.pathValue === "") {
valueElement.textContent = "No file selected";
return;
}
valueElement.textContent = this.pathValue;
}
if (this.isInlineList && this.enumeratedListContainerElement) {
for (let i = 0; i < this.enumeratedListContainerElement.children.length; i++) {
let child = this.enumeratedListContainerElement.children[i];
let strValue = child.getAttribute("mod-parameter-value");
let strType = child.getAttribute("mod-filetype");
if (strValue === null) return;
if (strValue === this.pathValue && strType !== "directory") {
child.classList.add("selected");
} else {
child.classList.remove("selected");
}
};
}
}
handleClick(event: MouseEvent) {
if (!this.frameElement) {
return;
}
event.preventDefault();
event.stopPropagation();
let enumeratedList = this.frameElement.querySelector(".mod-enumerated-list");
if (enumeratedList) {
let element = enumeratedList as HTMLElement;
this.toggleVisibility(element)
}
if (event.target) {
let target = event.target as HTMLElement;
let role = target.getAttribute("mod-role");
if (role === "enumeration-option") // a click in the drop-down list?
{
let strValue = target.getAttribute("mod-parameter-value");
let strFileType = target.getAttribute("mod-filetype");
if (strValue && strFileType) {
this.handleSelected(strValue,strFileType);
}
let valueElement = this.getValueElement();
if (event.target === valueElement) {
// A click on the value element, so launch the file browser. hooboy!
return;
}
if (!event.target) {
return;
}
let target = event.target as HTMLElement;
if (target.getAttribute("mod-role") === "enumeration-option") {
let strValue = target.getAttribute("mod-parameter-value");
if (strValue === null) return; let strType = target.getAttribute("mod-filetype");
if (strType === "directory") {
this.setNavDirectory(strValue);
return;
}
this.setPathValue(strValue);
this.updateSelection();
this.props.hostSite.setPatchProperty(this.props.instanceId, this.props.propertyUri, StringToPath(strValue));
return;
}
}
private enumeratedListContainerElement: HTMLElement | null = null;
private enumeratedListItemTemplate: string = "";
private enumeratedValueElement: HTMLElement | null = null;
getValueControlElement(): HTMLElement | null {
if (!this.frameElement) {
return null;
}
return this.frameElement.querySelector('[mod-role=input-parameter-value]') as HTMLElement | null;
}
async handleValueClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.props.handleFileSelect(
this.props.instanceId,
this.props.propertyUri,
this.pathValue || ""
);
}
attach(frameElement: HTMLElement) {
@@ -280,7 +494,15 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
}
void this.enumeratedListItemTemplate; // suppress unused variable warning
this.enumeratedValueElement = this.getValueControlElement();
this.isInlineList = this.enumeratedValueElement === null;
if (this.enumeratedValueElement) {
this.enumeratedValueElement.onclick = (event: MouseEvent) => {
this.handleValueClick(event);
}
}
}
@@ -362,7 +584,7 @@ class CustomSelectControl implements ModGuiControl {
this.frameElement.querySelectorAll('[mod-role=enumeration-option]')
.forEach((inputControl: Element) => {
let strValue = inputControl.getAttribute("mod-port-value");
let value = parseFloat(strValue || "");
let value = parseFloat(strValue || "");
inputControl.classList.remove("selected");
if (value === this.value) {
inputControl.classList.add("selected");
@@ -431,8 +653,7 @@ class CustomSelectControl implements ModGuiControl {
if (role === "enumeration-option") // a click in the drop-down list?
{
let strValue = target.getAttribute("mod-port-value");
if (strValue && strValue !== "")
{
if (strValue && strValue !== "") {
let value = parseFloat(strValue);
if (!isNaN(value)) {
this.setControlValue(value);
@@ -455,14 +676,7 @@ class CustomSelectControl implements ModGuiControl {
interface BypassLightControlProps {
instanceId: number;
pluginControl: UiControl;
onValueChanged: (instanceId: number, symbol: string, value: number) => void;
monitorPort: (
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void) => MonitorPortHandle;
unmonitorPort: (handle: MonitorPortHandle) => void;
model: PiPedalModel;
};
function ModMessage(message: string) {
@@ -517,23 +731,22 @@ class BypassLightControl implements ModGuiControl {
this.frameElement.classList.remove('on', 'off');
this.frameElement.classList.add(
this.value === this.props.pluginControl.min_value ?
this.value ?
'on' : 'off'
);
}
private mounted: boolean = false;
private monitorHandle: MonitorPortHandle | null = null;
private listenHandle: ListenHandle | null = null;
onMounted() {
this.mounted = true;
this.monitorHandle = this.props.monitorPort(
this.listenHandle = this.props.model.addPedalboardItemEnabledChangeListener(
this.props.instanceId,
this.props.pluginControl.symbol,
1.0 / 15,
(value: number) => {
if (value != this.value) {
this.setControlValue(value);
(instanceId: number, isEnabled: boolean) => {
let bypass = isEnabled ? 1.0: 0.0;
if (bypass !== this.value) {
this.setControlValue(bypass);
}
}
);
@@ -541,9 +754,9 @@ class BypassLightControl implements ModGuiControl {
}
onUnmount() {
if (this.monitorHandle) {
this.props.unmonitorPort(this.monitorHandle);
this.monitorHandle = null;
if (this.listenHandle) {
this.props.model.removePedalboardItemEnabledChangeListener(this.listenHandle);
this.listenHandle = null;
}
this.mounted = false;
this.cancelRequestUpdate();
@@ -647,7 +860,7 @@ class FilmstripControl implements ModGuiControl {
return;
}
let range = this.valueToRange(this.value);
this.frameElement.style.transform = `rotate(${rotation * range-rotation/2}deg)`;
this.frameElement.style.transform = `rotate(${rotation * range - rotation / 2}deg)`;
return;
}
@@ -712,9 +925,9 @@ class FilmstripControl implements ModGuiControl {
let value = this.isPointerDown ? this.pointerDownValue : this.value;
value = this.clampValue(value);
let isHorizontalStrip =
this.filmstripInfo.frameWidth/this.filmstripInfo.frameHeight
< this.filmstripInfo.width/this.filmstripInfo.height;
let isHorizontalStrip =
this.filmstripInfo.frameWidth / this.filmstripInfo.frameHeight
< this.filmstripInfo.width / this.filmstripInfo.height;
if (isHorizontalStrip) {
let range = this.valueToRange(value);
@@ -1059,15 +1272,25 @@ class FilmstripControl implements ModGuiControl {
}
};
function ModGuiHost(props: ModGuiHostProps) {
let xmodel: PiPedalModel = PiPedalModelFactory.getInstance();
let model: PiPedalModel = PiPedalModelFactory.getInstance();
const { plugin, onClose } = props;
const [hostDivRef, setHostDivRef] = React.useState<HTMLDivElement | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [contentReady, setContentReady] = React.useState<boolean|null>(null);
const [modGuiControls] = React.useState<ModGuiControl[]>([]);
const [ready, setReady] = React.useState<boolean>(xmodel.state.get() === State.Ready);
const [ready, setReady] = React.useState<boolean>(model.state.get() === State.Ready);
const [maximizedUi] = React.useState<boolean>(false);
function updateContentReady(value: boolean) {
if (value !== contentReady) {
setContentReady(value);
props.onContentReady(value);
}
}
if (!plugin.modGui) {
return (
<div>
@@ -1076,6 +1299,7 @@ function ModGuiHost(props: ModGuiHostProps) {
);
}
function addPortClass(element: Element, selector: string, className: string) {
let children = element.querySelectorAll(selector);
// call addClass to each element that matches the selector
@@ -1091,13 +1315,13 @@ function ModGuiHost(props: ModGuiHostProps) {
pluginControl: pluginControl,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort: model.monitorPort.bind(model),
unmonitorPort: model.unmonitorPort.bind(model)
}
);
customSelectControl.attach(control as HTMLElement);
@@ -1108,6 +1332,20 @@ function ModGuiHost(props: ModGuiHostProps) {
}
customSelectControl.onMounted();
}
function monitorBypassPort(
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void): MonitorPortHandle {
return model.addPedalboardItemEnabledChangeListener(
instanceId, (instanceId,value) => {
callback(value ? 1 : 0);
}
);
}
function unmonitorBypassPort(handle: MonitorPortHandle) {
model.removePedalboardItemEnabledChangeListener(handle as ListenHandle);
}
function createFootswitchControl(control: Element, symbol: string, controlType: ControlType) {
let uiControl = new UiControl();
uiControl.symbol = symbol;
@@ -1124,14 +1362,24 @@ function ModGuiHost(props: ModGuiHostProps) {
filmStrip: "/img/footswitch_strip.png",
verticalStrip: true,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
if (symbol === "_bypass") {
model.setPedalboardItemEnabled(instanceId, value !== 0);
} else {
try {
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort:
symbol === "_bypass" ?
monitorBypassPort :
model.monitorPort.bind(model),
unmonitorPort:
symbol == "_bypass" ?
unmonitorBypassPort :
model.unmonitorPort.bind(model)
}
);
filmstripControl.attach(control as HTMLElement);
@@ -1150,9 +1398,10 @@ function ModGuiHost(props: ModGuiHostProps) {
}
let selectPathControl = new CustomSelectPathControl({
instanceId: props.instanceId,
propertyUri: pathUri,
propertyUri: pathUri,
plugin: props.plugin,
hostSite: props.hostSite
hostSite: model,
handleFileSelect: props.handleFileSelect
});
selectPathControl.attach(control as HTMLElement);
@@ -1160,29 +1409,13 @@ function ModGuiHost(props: ModGuiHostProps) {
selectPathControl.onMounted();
}
function createBypassLightControl(control: Element, symbol: string) {
let uiControl = new UiControl();
uiControl.symbol = symbol;
uiControl.controlType = ControlType.BypassLight;
uiControl.min_value = 0;
uiControl.max_value = 1;
uiControl.is_logarithmic = false;
uiControl.integer_property = true;
function createBypassLightControl(control: Element) {
let bypassLightControl = new BypassLightControl(
{
instanceId: props.instanceId,
pluginControl: uiControl,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
model: model
}
);
bypassLightControl.attach(control as HTMLElement);
@@ -1211,13 +1444,13 @@ function ModGuiHost(props: ModGuiHostProps) {
verticalStrip: false,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort: model.monitorPort.bind(model),
unmonitorPort: model.unmonitorPort.bind(model)
}
);
@@ -1284,14 +1517,11 @@ function ModGuiHost(props: ModGuiHostProps) {
.forEach((control) => {
let symbol = "_bypass";
let controlType = ControlType.OnOffSwitch;
createFootswitchControl(control, symbol, controlType);
});
element.querySelectorAll('[mod-role=bypass-light]')
.forEach((control) => {
let symbol = "_bypass";
createBypassLightControl(control, symbol);
createBypassLightControl(control);
});
element.querySelectorAll('[mod-role=input-parameter]')
.forEach((control) => {
@@ -1303,6 +1533,7 @@ function ModGuiHost(props: ModGuiHostProps) {
setErrorMessage(ModMessage(message));
}
async function requestContent() {
updateContentReady(false);
try {
let modGui = plugin.modGui;
if (!modGui) {
@@ -1321,7 +1552,7 @@ function ModGuiHost(props: ModGuiHostProps) {
let encodedUri = encodeURIComponent(props.plugin.uri);
let version = props.plugin.minorVersion * 1000 + props.plugin.microVersion;
let resourceUrl = xmodel.modResourcesUrl;
let resourceUrl = model.modResourcesUrl;
let queryParams = "?ns=" + encodedUri + "&v=" + version.toString();
let templateUri = resourceUrl + "_/iconTemplate" + queryParams;
let cssUri = resourceUrl + "_/stylesheet" + queryParams;
@@ -1367,6 +1598,7 @@ function ModGuiHost(props: ModGuiHostProps) {
let height = element.clientHeight;
hostDivRef.style.width = width + "px";
hostDivRef.style.height = height + "px";
updateContentReady(true);
} catch (error) {
setModError("Error loading UI: " + (error instanceof Error ? error.message : String(error)));
}
@@ -1383,9 +1615,12 @@ function ModGuiHost(props: ModGuiHostProps) {
let stateHandler = (state: State) => {
setReady(state === State.Ready);
}
xmodel.state.addOnChangedHandler(stateHandler)
model.state.addOnChangedHandler(stateHandler)
return () => {
xmodel.state.removeOnChangedHandler(stateHandler);
model.state.removeOnChangedHandler(stateHandler);
if (tHostDivRef !== null) {
// unmount the custom content.
let children = tHostDivRef.children;
@@ -1396,6 +1631,7 @@ function ModGuiHost(props: ModGuiHostProps) {
}
}
}
updateContentReady(false);
let mc = modGuiControls;
for (let i = 0; i < mc.length; i++) {
let modGuiControl = mc[i];
@@ -1405,12 +1641,12 @@ function ModGuiHost(props: ModGuiHostProps) {
};
}, [hostDivRef, plugin, ready]);
return (
<ModGuiErrorBoundary plugin={props.plugin} onClose={() => { props.onClose(); setErrorMessage(null); }}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: "rgba(0.8,0.8,0.8, 0.8)",
display: "flex", flexFlow: "column nowrap", alignItems: "center", justifyContent: "stretch",
display: "inline-block",
paddingLeft: 20, paddingRight: 20,
overflow: "hidden",
}}
onClick={(event) => {
@@ -1418,14 +1654,14 @@ function ModGuiHost(props: ModGuiHostProps) {
event.stopPropagation();
}}
>
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
position: "absolute", top: 16, right: 16,
background: "#FFF5", zIndex: 600
}}>
<CloseIcon />
</IconButtonEx>
<div style={{ flex: "1 1 1px" }} />
{maximizedUi && (
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
position: "absolute", top: 16, right: 16,
background: "#FFF5", zIndex: 600
}}>
<CloseIcon />
</IconButtonEx>
)}
<div style={{ display: "block", position: "relative" }}>
<div ref={setHostDivRef} style={{ display: "block", position: "relative" }}
@@ -1437,17 +1673,78 @@ function ModGuiHost(props: ModGuiHostProps) {
/>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
<OkDialog title="Error" open={errorMessage !== null}
onClose={() => {
setErrorMessage(null);
onClose();
}}
text={errorMessage ?? ""}
/>
{errorMessage !== null && (
<OkDialog title="Error" open={errorMessage !== null}
onClose={() => {
setErrorMessage(null);
onClose();
}}
text={errorMessage ?? ""}
/>
)}
</ModGuiErrorBoundary>
);
}
interface ModGuiPluginPreference {
pluginUri: string;
useModGui: boolean;
}
let modGuiPluginPreferences: ModGuiPluginPreference[] | null = null;
function loadModGuiPreferences() {
if (modGuiPluginPreferences === null) {
let prefs = window.localStorage.getItem("modGuiPluginPreferences");
try {
if (prefs) {
modGuiPluginPreferences = JSON.parse(prefs);
} else {
modGuiPluginPreferences = [];
}
} catch (error) {
console.error("pipedal: Error parsing modGuiPluginPreferences from localStorage: " + String(error));
modGuiPluginPreferences = [];
}
}
}
export function getDefaultModGuiPreference(pluginUri: string) {
loadModGuiPreferences();
if (modGuiPluginPreferences === null) {
modGuiPluginPreferences = [];
}
if (modGuiPluginPreferences) {
let preference = modGuiPluginPreferences.find(p => p.pluginUri === pluginUri);
if (preference) {
return preference.useModGui;
}
}
return false;
}
export function setDefaultModGuiPreference(pluginUri: string, useModGui: boolean) {
loadModGuiPreferences();
if (modGuiPluginPreferences === null) {
modGuiPluginPreferences = [];
}
let preference = modGuiPluginPreferences.find(p => p.pluginUri === pluginUri);
if (preference) {
preference.useModGui = useModGui;
} else {
modGuiPluginPreferences.push({ pluginUri: pluginUri, useModGui });
}
try {
window.localStorage.setItem("modGuiPluginPreferences", JSON.stringify(modGuiPluginPreferences));
} catch(error) {
console.error("pipedal: Error saving modGuiPluginPreferences to localStorage: " + String(error));
}
}
export default ModGuiHost;
-346
View File
@@ -1,346 +0,0 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* 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,
* 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 React from 'react';
import Divider from '@mui/material/Divider';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
import { useTheme } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import LoadPluginDialog from './LoadPluginDialog';
import ButtonEx from './ButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { UiPlugin } from './Lv2Plugin';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
class MyPortHandle implements MonitorPortHandle {
private static nextHandle: number = 1;
constructor() {
this.handle = MyPortHandle.nextHandle++;
}
handle: number;
};
type MonitorCallback = (value: number) => void;
class ValueEntry {
constructor(defaultValue: number) {
this.value = defaultValue;
}
private listeners: { [handle: number]: MonitorCallback } = {};
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
let handle = new MyPortHandle();
this.listeners[handle.handle] = callback;
return handle;
}
unmonitor(handle: MonitorPortHandle) {
let h = (handle as MyPortHandle).handle;
// remove entry h from listeners
if (!(h in this.listeners)) {
throw new Error("Invalid handle: " + h);
}
// remove the callback from the listeners
delete this.listeners[h];
}
setValue(value: number) {
if (value !== this.value) {
this.value = value;
// notify all listeners
for (let handle in this.listeners) {
this.listeners[handle](value);
}
}
}
value: number;
};
class PatchListenerEntry {
constructor(instanceId: number, propertyUri: string) {
this.instanceId = instanceId;
this.propertyUri = propertyUri;
}
instanceId: number;
propertyUri: string;
value: any = "";
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
};
class ValueHandler {
private valueDictionary: { [symbol: string]: ValueEntry } = {};
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
constructor(instanceId: number, plugin: UiPlugin) {
for (let control of plugin.controls) {
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
}
this.valueDictionary["_bypass"] = new ValueEntry(0);
}
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
let valueEntry = this.valueDictionary[symbol];
let portHandle = valueEntry.monitor(interval, callback);
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
callback(valueEntry.value);
return portHandle;
}
unmonitorPort(handle: MonitorPortHandle): void {
let h = (handle as MyPortHandle).handle;
if (!(h in this.handleToValueEnry)) {
throw new Error("Invalid handle: " + h);
}
let valueEntry = this.handleToValueEnry[h];
valueEntry.unmonitor(handle);
delete this.handleToValueEnry[h];
}
setValue(instanceId: number, symbol: string, value: number) {
// add a delay just for funsies.
window.setTimeout(() => {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
valueEntry.setValue(value);
}, 100);
}
getValue(instanceId: number, symbol: string): number {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
return valueEntry.value;
}
private nextListenHandle: number = 1;
private patchListeners: PatchListenerEntry[] = [];
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
let h = this.nextListenHandle++;
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
// already listening to this property
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId,propertyUri,entry.value);
return { _handle: h };
}
}
let entry = new PatchListenerEntry(instanceId, propertyUri);
this.patchListeners.push(entry);
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId, propertyUri, entry.value);
return {_handle: h};
}
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
for (let i = 0; i < this.patchListeners.length; ++i) {
let entry = this.patchListeners[i];
for (let j = 0; j < entry.listeners.length; ++j) {
if (entry.listeners[j].handle === listenHandle._handle) {
// found the listener, remove it
entry.listeners.splice(j, 1);
if (entry.listeners.length === 0) {
// no more listeners, remove the entry
this.patchListeners.splice(i, 1);
}
return;
}
}
}
}
getPatchProperty(instanceId: number, uri: string): Promise<any> {
let promise = new Promise<any>((resolve, reject) => {
window.setTimeout(() => {
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
window.setTimeout(() => {
// simulate a delay for getting the property
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
}, 100);
return resolve(entry.value);
}
}
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
},50);
});
return promise;
}
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
// Implementation here
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
entry.value = value;
// notify all listeners
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
return Promise.resolve(true);
}
}
// if we reach here, the property was not found
return Promise.reject("Not found.");
}
};
function ModGuiTest() {
const [pluginUrl, setPluginUrl] = React.useState("");
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
},
unmonitorPort: (handle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.unmonitorPort(handle);
},
setPedalboardControl: (instanceId, symbol, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.setValue(instanceId, symbol, value);
},
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
},
cancelMonitorPatchProperty: (listenHandle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.cancelMonitorPatchProperty(listenHandle);
},
getPatchProperty: async (instanceId, uri) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
},
setPatchProperty: async (instanceId, uri, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
}
});
let model: PiPedalModel = PiPedalModelFactory.getInstance();
React.useEffect(() => {
const onStateChange = (state: State) => {
if (state == State.Ready) {
setLoading(false);
}
};
model.state.addOnChangedHandler(onStateChange);
return () => {
model.state.removeOnChangedHandler(onStateChange);
};
}, [])
const theme = useTheme();
return (
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
}}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0,
bottom: 0, zIndex: 1000,
display: loading ? "flex" : "none",
alignItems: "center", justifyContent: "center",
background: "rgba(0.5,0.5,0.5, 0.9)"
}}>
<CircularProgress style={{ color: theme.palette.text.secondary }} />
</div>
<div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
}}>
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
{pluginUrl ? pluginUrl : "No plugin loaded"}
</Typography>
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
Load
</ButtonEx>
</div>
<Divider />
{uiPlugin && (
<div style={{ flex: "1 1 auto", position: "relative" }}>
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
setPluginUrl("");
setUiPlugin(null);
setValueHandler(null);
}} >
<ModGuiHost instanceId={-1} plugin={uiPlugin}
onClose={() => {
setPluginUrl("");
setUiPlugin(null);
}}
hostSite={modGuiHostSite}
/>
</ModGuiErrorBoundary>
</div>
)}
{pluginDialogOpen &&
<LoadPluginDialog
open={pluginDialogOpen}
onOk={(url) => {
setPluginUrl(url as string);
let uiPlugin = model.getUiPlugin(url);
setUiPlugin(uiPlugin);
if (uiPlugin) {
setValueHandler(new ValueHandler(-1, uiPlugin));
}
setPluginDialogOpen(false);
}}
onCancel={() => setPluginDialogOpen(false)}
uri={pluginUrl}
modGuiOnly={true}
/>
}
</div>
);
}
export default ModGuiTest;
+3
View File
@@ -79,6 +79,8 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
this.lv2State = input.lv2State;
this.lilvPresetUri = input.lilvPresetUri;
this.pathProperties = input.pathProperties;
this.useModUi = input.useModUi ?? false;
return this;
}
deserialize(input: any): PedalboardItem {
@@ -212,6 +214,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
lv2State: [boolean,any] = [false,{}];
lilvPresetUri: string = "";
pathProperties: {[Name: string]: string} = {};
useModUi: boolean = false; // true if this item should use the mod-ui.
};
export class SnapshotValue {
+173 -3
View File
@@ -44,7 +44,7 @@ import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
import {getDefaultModGuiPreference} from './ModGuiHost';
export enum State {
Loading,
@@ -82,6 +82,24 @@ export enum ReconnectReason {
HotspotChanging
};
export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void;
interface PedalboardItemEnabledChangeItem {
handle: number;
instanceId: number;
currentValue: boolean;
onEnabledChanged: PedalboardItemEnabledChangeCallback;
};
export type PedalboardItemUseModUiChangeCallback = (instanceId: number, useModUi: boolean) => void;
interface PedalboardItemUseModUiChangeItem {
handle: number;
instanceId: number;
currentValue: boolean;
onUseModUiChanged: PedalboardItemUseModUiChangeCallback;
};
export type ControlValueChangedHandler = (key: string, value: number) => void;
export interface FileEntry {
@@ -146,7 +164,6 @@ export interface VuUpdateInfo {
};
export interface MonitorPortHandle {
};
export interface ControlValueChangedHandle {
_ControlValueChangedHandle: number;
@@ -574,10 +591,18 @@ export class PiPedalModel //implements PiPedalModel
return true;
}
private updateEnabledItems(pedalboard: Pedalboard)
{
for (let item of pedalboard.itemsGenerator()) {
this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled);
this.updatePedalboardItemUseModUi(item.instanceId, item.useModUi);
}
}
private setModelPedalboard(pedalboard: Pedalboard) {
this.pedalboard.set(pedalboard);
this.selectedSnapshot.set(pedalboard.selectedSnapshot);
this.updateEnabledItems(pedalboard);
}
onSocketMessage(header: PiPedalMessageHeader, body?: any) {
@@ -687,6 +712,13 @@ export class PiPedalModel //implements PiPedalModel
itemEnabledBody.enabled,
false // No server notification.
);
} else if (message == "onItemUseModUiChanged") {
let itemEnabledBody = body as PedalboardItemEnableBody;
this._setPedalboardItemUseModUi(
itemEnabledBody.instanceId,
itemEnabledBody.enabled,
false // No server notification.
);
} else if (message === "onPedalboardChanged") {
let pedalChangedBody = body as PedalboardChangedBody;
this.setModelPedalboard(new Pedalboard().deserialize(pedalChangedBody.pedalboard));
@@ -1563,6 +1595,42 @@ export class PiPedalModel //implements PiPedalModel
setPedalboardItemEnabled(instanceId: number, value: boolean): void {
this._setPedalboardItemEnabled(instanceId, value, true);
}
setPedalboardItemUseModUi(instanceId: number, useModUi: boolean): void {
this._setPedalboardItemUseModUi(instanceId, useModUi, true);
}
getPedalboardItemUseModUi(instanceId: number): boolean {
if (!this.pedalboard.get().hasItem(instanceId)) return false;
let item = this.pedalboard.get().getItem(instanceId);
return item.useModUi;
}
_setPedalboardItemUseModUi(instanceId: number, useModUi: boolean, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
let item = newPedalboard.getItem(instanceId);
let changed = useModUi !== item.useModUi;
if (changed) {
item.useModUi = useModUi;
this.setModelPedalboard(newPedalboard);
if (notifyServer) {
let body = {
clientId: this.clientId,
instanceId: instanceId,
useModUi: useModUi
};
this.webSocket?.send("setPedalboardItemUseModUi", body);
}
}
}
getPedalboardItemEnabled(instanceId: number): boolean {
if (!this.pedalboard.get().hasItem(instanceId)) return false;
let item = this.pedalboard.get().getItem(instanceId);
return item.isEnabled;
}
private _setPedalboardItemEnabled(instanceId: number, value: boolean, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
@@ -1625,6 +1693,7 @@ export class PiPedalModel //implements PiPedalModel
item.lv2State = [false, {}];
item.vstState = "";
item.pathProperties = {};
item.useModUi = getDefaultModGuiPreference(selectedUri);
for (let fileProperty of plugin.fileProperties) {
// stringized json for an atom. see AtomConverter.hpp.
// null -> we've never seen a value.
@@ -1845,6 +1914,7 @@ export class PiPedalModel //implements PiPedalModel
}
newPedalboard.setItemEmpty(item);
this.setModelPedalboard(newPedalboard);
this.updateServerPedalboard();
return item.instanceId;
@@ -2210,6 +2280,18 @@ export class PiPedalModel //implements PiPedalModel
}
getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type> {
let result = new Promise<Type>((resolve, reject) => {
let pedalboard = this.pedalboard.get();
if (pedalboard) {
let item = pedalboard.getItem(instanceId);
if (item) {
if (item.pathProperties.hasOwnProperty(uri)) {
let value = item.pathProperties[uri];
let jsonValue = JSON.parse(value);
resolve(jsonValue as Type);
}
}
}
if (!this.webSocket) {
reject("Socket closed.");
} else {
@@ -3233,6 +3315,94 @@ export class PiPedalModel //implements PiPedalModel
throw new Error("No connection.");
}
private pedalboardItemEnabledChangeListeners: PedalboardItemEnabledChangeItem[] = [];
private pedalboardItemUseModUiChangeListeners: PedalboardItemUseModUiChangeItem[] = [];
private updatePedalboardItemEnabled(instanceId: number, enabled: boolean) {
for (let listener of this.pedalboardItemEnabledChangeListeners) {
if (listener.instanceId === instanceId) {
if (listener.currentValue !== enabled) {
listener.currentValue = enabled;
listener.onEnabledChanged(instanceId, enabled);
}
}
}
}
private updatePedalboardItemUseModUi(instanceId: number, useModUi: boolean) {
for (let listener of this.pedalboardItemUseModUiChangeListeners) {
if (listener.instanceId === instanceId) {
if (listener.currentValue !== useModUi) {
listener.currentValue = useModUi;
listener.onUseModUiChanged(instanceId, useModUi);
}
}
}
}
addPedalboardItemEnabledChangeListener(
instanceId: number,
onEnabledChanged: PedalboardItemEnabledChangeCallback
) : ListenHandle{
let handle = ++this.nextListenHandle;
let currentValue = this.getPedalboardItemEnabled(instanceId);
let entry: PedalboardItemEnabledChangeItem = {
instanceId: instanceId,
handle: handle,
currentValue: currentValue,
onEnabledChanged: onEnabledChanged
};
this.pedalboardItemEnabledChangeListeners.push( entry);
onEnabledChanged(instanceId, currentValue);
return { _handle: handle };
}
addPedalboardItemUseModUiChangeListener(
instanceId: number,
onUseModUiChanged : PedalboardItemUseModUiChangeCallback
) : ListenHandle{
let handle = ++this.nextListenHandle;
let currentValue = this.getPedalboardItemUseModUi(instanceId);
let entry: PedalboardItemUseModUiChangeItem = {
instanceId: instanceId,
handle: handle,
currentValue: currentValue,
onUseModUiChanged: onUseModUiChanged
};
this.pedalboardItemUseModUiChangeListeners.push( entry);
onUseModUiChanged(instanceId, currentValue);
return { _handle: handle };
}
removePedalboardItemEnabledChangeListener(listenHandle: ListenHandle): boolean {
for (let i = 0; i < this.pedalboardItemEnabledChangeListeners.length; ++i) {
if (this.pedalboardItemEnabledChangeListeners[i].handle === listenHandle._handle) {
this.pedalboardItemEnabledChangeListeners.splice(i, 1);
return true;
}
}
return false;
}
removePedalboardItemUseModUiChangeListener(listenHandle: ListenHandle): boolean {
for (let i = 0; i < this.pedalboardItemUseModUiChangeListeners.length; ++i) {
if (this.pedalboardItemUseModUiChangeListeners[i].handle === listenHandle._handle) {
this.pedalboardItemUseModUiChangeListeners.splice(i, 1);
return true;
}
}
return false;
}
};
let instance: PiPedalModel | undefined = undefined;
+81 -45
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// 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
@@ -23,7 +23,9 @@ import WithStyles, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import AutoZoom from './AutoZoom';
import ModGuiHost from './ModGuiHost';
import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
@@ -353,7 +355,9 @@ type PluginControlViewState = {
imeInitialHeight: number;
showFileDialog: boolean,
dialogFileProperty: UiFileProperty,
dialogFileValue: string
dialogFileValue: string,
modGuiContentReady: boolean,
showModGuiZoomed: boolean,
};
const PluginControlView =
@@ -372,7 +376,9 @@ const PluginControlView =
imeInitialHeight: 0,
showFileDialog: false,
dialogFileProperty: new UiFileProperty(),
dialogFileValue: ""
dialogFileValue: "",
modGuiContentReady: false,
showModGuiZoomed: false
}
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
@@ -774,36 +780,64 @@ const PluginControlView =
}
return false;
}
makeHostSite(): IModGuiHostSite {
// Yuck. :-( This worked out badly.
let model = this.model;
return {
monitorPort: this.model.monitorPort.bind(model),
unmonitorPort: this.model.unmonitorPort.bind(model),
setPedalboardControl: this.model.setPedalboardControl.bind(model),
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
getPatchProperty: this.model.getPatchProperty.bind(model),
setPatchProperty: this.model.setPatchProperty.bind(model),
};
}
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 (
<ModGuiHost
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
<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 }); }
}
}}
hostSite={this.makeHostSite()}
/>
>
<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>
)
}
@@ -859,22 +893,22 @@ const PluginControlView =
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 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>
);
@@ -895,7 +929,7 @@ const PluginControlView =
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let frameClass = classes.frame;
if (this.fullScreen()) {
if (this.fullScreen() || this.props.showModGui) {
frameClass = classes.noScrollFrame;
}
@@ -920,13 +954,15 @@ const PluginControlView =
instanceId={this.props.instanceId}
selectedFile={this.state.dialogFileValue}
onCancel={() => {
this.setState({ showFileDialog: false });
this.setState({
showFileDialog: false
});
}}
onApply={(fileProperty, selectedFile) => {
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
@@ -941,7 +977,7 @@ const PluginControlView =
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
+7 -1
View File
@@ -31,7 +31,7 @@ class Rect {
this.x = x; this.y = y; this.width = width; this.height = height;
}
toString() { return '{' + this.x + "," + this.y + " " + this.width + "," + this.height +"}"; }
toString() { return '{' + this.x + "," + this.y + " - " + this.width + "," + this.height +"}"; }
copy(): Rect {
let result = new Rect();
result.x = this.x;
@@ -84,6 +84,12 @@ class Rect {
this.x += xOffset;
this.y += yOffset;
}
equals(other: Rect): boolean {
return this.x === other.x &&
this.y === other.y &&
this.width === other.width &&
this.height === other.height;
}
}
export default Rect;
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// 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
+3
View File
@@ -77,6 +77,9 @@ const ToobCabSimView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+9 -8
View File
@@ -21,14 +21,14 @@ 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 IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
//import ToobFrequencyResponseView from './ToobFrequencyResponseView';
@@ -47,12 +47,11 @@ interface ToobInputStageState {
const ToobInputStageView =
withStyles(
class extends React.Component<ToobInputStageProps, ToobInputStageState>
implements ControlViewCustomization
{
class extends React.Component<ToobInputStageProps, ToobInputStageState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobInputStageProps) {
super(props);
@@ -64,8 +63,7 @@ const ToobInputStageView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
return controls;
// let group = controls[1] as ControlGroup;
// group.controls.splice(0,0,
@@ -79,6 +77,9 @@ const ToobInputStageView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+10 -9
View File
@@ -21,7 +21,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";
@@ -48,7 +48,7 @@ const ToobMLView =
class extends React.Component<ToobMLProps, ToobMLState>
implements ControlViewCustomization {
model: PiPedalModel;
gainRef: React.RefObject<HTMLDivElement|null>;
gainRef: React.RefObject<HTMLDivElement | null>;
customizationId: number = 1;
@@ -119,19 +119,17 @@ const ToobMLView =
// }
let controlIndex: number = -1;
let controlValues = this.props.item.controlValues
for (let i:number = 0; i < controlValues.length; ++i)
{
for (let i: number = 0; i < controlValues.length; ++i) {
let ctl: any = controls[i];
let key: any = ctl?.props?.uiControl?.symbol??"";
if (key === "gain")
{
let key: any = ctl?.props?.uiControl?.symbol ?? "";
if (key === "gain") {
controlIndex = i;
break;
}
}
if (controlIndex !== -1 ) {
if (controlIndex !== -1) {
let gainControl: React.ReactElement = controls[controlIndex] as React.ReactElement;
controls[controlIndex] = (<div ref={this.gainRef}> {gainControl} </div>);
@@ -144,6 +142,9 @@ const ToobMLView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+4 -4
View File
@@ -525,7 +525,7 @@ export default function ToobPlayerControl(
function onNextTrack() {
model.getNextAudioFile(audioFile)
.then((file) => {
let json = JsonAtom.Path(file);
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
@@ -538,7 +538,7 @@ export default function ToobPlayerControl(
function onPreviousTrack() {
model.getPreviousAudioFile(audioFile)
.then((file) => {
let json = JsonAtom.Path(file);
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
@@ -873,7 +873,7 @@ export default function ToobPlayerControl(
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
@@ -888,7 +888,7 @@ export default function ToobPlayerControl(
model.setPatchProperty(
props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
+6 -3
View File
@@ -110,14 +110,14 @@ const ToobPlayerView =
for (let mixControl of mixPanel.controls) {
extraControls.push(
(
<div key={"kExtra"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
<div key={"kExtra" + iKey++} style={{ flex: "0 0 auto", position: "relative", height: "100%" }}>
{mixControl}
</div>
));
}
let panel = (
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
);
let result: (React.ReactNode | ControlGroup)[] = [];
@@ -133,6 +133,9 @@ const ToobPlayerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>
);
}
@@ -146,7 +149,7 @@ class ToobPlayerViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-player";
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
return (<ToobPlayerView key={"ppmv"+pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
return (<ToobPlayerView key={"ppmv" + pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
}
+2
View File
@@ -179,6 +179,8 @@ const ToobPowerstage2View =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+12 -11
View File
@@ -21,14 +21,14 @@ 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 IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
import ToobSpectrumResponseView from './ToobSpectrumResponseView';
@@ -48,12 +48,11 @@ interface ToobSpectrumAnalyzerState {
const ToobSpectrumAnalyzerView =
withStyles(
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
implements ControlViewCustomization
{
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobSpectrumAnalyzerProps) {
super(props);
@@ -65,11 +64,10 @@ const ToobSpectrumAnalyzerView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
controls.splice(0,0,
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
);
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
controls.splice(0, 0,
(<ToobSpectrumResponseView instanceId={this.props.instanceId} />)
);
return controls;
}
render() {
@@ -78,6 +76,9 @@ const ToobSpectrumAnalyzerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+24 -27
View File
@@ -21,14 +21,14 @@ 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 IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel,ControlValueChangedHandle } from "./PiPedalModel";
import { PiPedalModelFactory, PiPedalModel, ControlValueChangedHandle } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
@@ -47,12 +47,11 @@ interface ToobToneStackState {
const ToobToneStackView =
withStyles(
class extends React.Component<ToobToneStackProps, ToobToneStackState>
implements ControlViewCustomization
{
class extends React.Component<ToobToneStackProps, ToobToneStackState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobToneStackProps) {
super(props);
@@ -61,27 +60,24 @@ const ToobToneStackView =
isBaxandall: this.IsBaxandall()
}
}
IsBaxandall() : boolean {
IsBaxandall(): boolean {
return this.props.item.getControl("ampmodel").value === 2.0;
}
controlValueChangedHandle?: ControlValueChangedHandle;
componentDidMount()
{
componentDidMount() {
this.controlValueChangedHandle = this.model.addControlValueChangeListener(
this.props.instanceId,
(key,value) => {
if (key === "ampmodel")
{
this.setState({isBaxandall: value === 2.0});
(key, value) => {
if (key === "ampmodel") {
this.setState({ isBaxandall: value === 2.0 });
}
}
);
}
componentWillUnmount() {
if (this.controlValueChangedHandle)
{
if (this.controlValueChangedHandle) {
this.model.removeControlValueChangeListener(this.controlValueChangedHandle);
this.controlValueChangedHandle = undefined;
}
@@ -90,18 +86,16 @@ const ToobToneStackView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
if (this.state.isBaxandall)
{
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId}
/>)
);
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
if (this.state.isBaxandall) {
controls.splice(0, 0,
(<ToobFrequencyResponseView instanceId={this.props.instanceId}
/>)
);
} else {
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
controls.splice(0, 0,
(<ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
}
return controls;
}
@@ -111,6 +105,9 @@ const ToobToneStackView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},