MOD Gui support.
This commit is contained in:
@@ -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
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -110,6 +110,9 @@ const GxTunerView =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
+146
-50
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
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'> </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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -77,6 +77,9 @@ const ToobCabSimView =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
|
||||
@@ -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} />);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ const ToobPowerstage2View =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user