ModGUI Sidechain select

This commit is contained in:
Robin E. R. Davies
2025-10-01 21:47:13 -04:00
parent aa26fb2fea
commit fb092016b1
9 changed files with 286 additions and 143 deletions
+147 -123
View File
@@ -24,17 +24,40 @@ 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
import Rect from './Rect';
import { css } from '@emotion/react';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
const styles = (theme: Theme) => createStyles({
pgraph: {
pgraph: css({
paddingBottom: 16
}
}),
backdrop: css({
zIndex: 1101,
overflow: "clip",
background: theme.palette.background.default,
}),
noToolbar: css({
display: "none"
}),
topToolBar: css({
position: "absolute",
zIndex: 1101,
top: 0,
display: "flex", flexFlow: "row nowrap", justifyContent: "start", columnGap: 8,
}),
rightToolbar: css({
position: "absolute",
zIndex: 1101,
top: 24,
display: "flex", flexFlow: "column nowrap", rowGap: 8
})
});
@@ -42,6 +65,7 @@ export interface AutoZoomProps extends WithStyles<typeof styles> {
leftPad: number,
rightPad: number,
children?: React.ReactNode,
toolbarChildren?: React.ReactNode[],
contentReady: boolean,
showZoomed: boolean,
setShowZoomed: (value: boolean) => void;
@@ -53,6 +77,15 @@ export interface AutoZoomState {
}
// non-react zoom state.
class UnzoomedLayout {
horizontalToolbar = false;
effectiveClientRect: Rect = new Rect();
showToolbar = false;
zoom: number = 1;
showZoomButton = false;
contentRect: Rect = new Rect();
}
const AutoZoom = withStyles(
class extends ResizeResponsiveComponent<AutoZoomProps, AutoZoomState> {
@@ -97,38 +130,43 @@ const AutoZoom = withStyles(
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
}
}
resizeObserver: ResizeObserver | null = null;
calculateZoomedClientRect(
calculateUnzoomedLayout(
clientRect: Rect,
contentWidth: number,
contentHeight: number,
buttonPadding: number
): Rect {
): UnzoomedLayout {
let result = new UnzoomedLayout();
console.log("Zoom: " + clientRect.toString() + " content: " + contentWidth + "x" + contentHeight + " buttonPadding: " + buttonPadding);
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) {
result.horizontalToolbar = false;
effectiveClientRect.width -= buttonPadding;
} else {
result.horizontalToolbar = true;
effectiveClientRect.y += buttonPadding;
effectiveClientRect.height -= buttonPadding;
}
result.effectiveClientRect = effectiveClientRect;
let zoom = Math.min(effectiveClientRect.width / contentWidth, effectiveClientRect.height / contentHeight);
if (zoom > 1) {
zoom = 1;
}
result.zoom = zoom;
result.showZoomButton = zoom !== 1;
result.showToolbar = result.showZoomButton || !!this.props.toolbarChildren;
let leftPad = (effectiveClientRect.width - contentWidth * zoom) / 2;
if (leftPad < 0) {
leftPad = 0;
@@ -137,7 +175,8 @@ const AutoZoom = withStyles(
if (topPad < 0) {
topPad = 0;
}
return new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom);
result.contentRect = new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom);
return result;
}
updateZoom(ref: HTMLDivElement) {
@@ -145,59 +184,58 @@ const AutoZoom = withStyles(
return;
}
let content = ref.firstElementChild as HTMLElement | null;
const classes = withStyles.getClasses(this.props);
if (content) {
const { leftPad, rightPad } = this.props;
let bounds = new Rect(leftPad, 0, ref.clientWidth - leftPad - rightPad, ref.clientHeight - 8);
let zoomedClientRect = this.calculateZoomedClientRect(
let layout = this.calculateUnzoomedLayout(
bounds, content.clientWidth, content.clientHeight, 48);
let contentRect = layout.contentRect;
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})`;
`translate(${contentRect.x}px, ${contentRect.y}px) scale(${layout.zoom}, ${layout.zoom})`;
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`;
if (this.zoomToolbar) {
if (!layout.showToolbar) {
this.zoomToolbar.className = classes.noToolbar;
} else if (layout.horizontalToolbar) {
this.zoomToolbar.className = classes.topToolBar;
this.zoomToolbar.style.left = `${contentRect.x +16}px`
this.zoomToolbar.style.top = `${contentRect.y - 48}px`;
this.zoomToolbar.style.width = `${contentRect.width}px`;
} else {
this.zoomButton.style.top = `${zoomedClientRect.y - 48}px`;
this.zoomButton.style.left = `${zoomedClientRect.right - 64 - 12}px`;
this.zoomToolbar.className = classes.rightToolbar;
this.zoomToolbar.style.left = "";
this.zoomToolbar.style.top = `${contentRect.y}px`;
this.zoomToolbar.style.left = `${contentRect.right + 4}px`;
this.zoomToolbar.style.width = "";
}
}
if (this.zoomButton) {
this.zoomButton.style.display = layout.showZoomButton ? "block" : "none";
}
// save the position of the Mod UI in page coordinates for later zooming.
this.zoomStartRect = zoomedClientRect.copy();
this.zoomStartRect = layout.contentRect.copy();
let refBounds = ref.getBoundingClientRect();
this.zoomStartRect.x += refBounds.left;
this.zoomStartRect.y += refBounds.top;
} else {
if (this.zoomToolbar) {
this.zoomToolbar.className = classes.noToolbar;
}
}
}
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());
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
}
@@ -234,20 +272,20 @@ const AutoZoom = withStyles(
}
this.maximizedAnimateFrameHandle = window.requestAnimationFrame(() => {
this.maximizedAnimateFrameHandle = null;
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
this.updateMaximizedZoom();
});
}
private zoomToolbar: HTMLElement | null = null;
private zoomButton: HTMLElement | null = null;
private normalRef: HTMLDivElement | null = null;
setNormalRef(ref: HTMLDivElement | null) {
setContentRef(ref: HTMLDivElement | null) {
this.normalRef = ref;
if (ref) {
this.zoomButton = ref.querySelector("#maximize-button") as HTMLDivElement | null;
this.zoomToolbar = ref.querySelector("#zoom-toolbar") as HTMLDivElement | null;
this.zoomButton = ref.querySelector("#zoom-button") as HTMLDivElement | null;
this.resizeObserver = new ResizeObserver(() => {
this.requestZoomUpdate();
});
@@ -261,36 +299,15 @@ const AutoZoom = withStyles(
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
this.zoomButton = null;
this.zoomToolbar = null;
}
this.zoomStartRect = null;
this.zoomToolbar = null;
this.zoomButton = 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() {
@@ -300,70 +317,77 @@ const AutoZoom = withStyles(
this.props.setShowZoomed(true);
}
render() {
content() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
const landscape = this.state.screenWidth > this.state.screenHeight;
return (<div style={{ overflow: "hidden", height: "100%", width: "100%" }} ref={(ref) => { this.setNormalRef(ref); }}>
<div style={{
display: "inline-block",
position: "absolute",
visibility: this.props.contentReady ? "visible" : "hidden",
}}>
{!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",
}}
return (
<div ref={(ref) => { this.setContentRef(ref); }}
style={{ width: "100%", height: "100%" }}
>
<IconButtonEx tooltip="Fullscreen view"
style={{ margin: 2, background: isDarkMode() ? "#444" : "#eee" }}
onClick={() => {
this.startZoom();
<div style={{
display: "inline-block",
position: "absolute",
visibility: this.props.contentReady ? "visible" : "hidden"
}}>
{this.props.children}
</div>
<div id="zoom-toolbar" className={classes.noToolbar}
style={{
visibility: this.props.contentReady ? "visible" : "hidden"
}}
>
<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: landscape? 80: 16, // avoid potential cutout in landscape mode
top: 16, zIndex: 1104,
{!this.props.showZoomed ? (
<IconButtonEx id="zoom-button" tooltip="Fullscreen view"
onClick={() => {
this.startZoom();
}}
>
<FullscreenIcon style={{ opacity: 0.6 }}
/>
</IconButtonEx>
) : (
<IconButtonEx tooltip="Exit fullscreen"
style={{}}
onClick={(e) => {
e.stopPropagation();
this.props.setShowZoomed(false);
}}
>
<CloseIcon />
<ArrowBackIcon style={{
opacity: 0.6,
}} />
</IconButtonEx>
</div>
)}
{this.props.toolbarChildren && this.props.toolbarChildren.map((child, index) => (
<div key={index} style={{ display: "inline-block" }}>
{child}
</div>
))}
</div>
</div>
);
}
render() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
return (<div style={{ overflow: "hidden", height: "100%", width: "100%" }} >
{!this.props.showZoomed && this.content()}
{this.props.showZoomed && (
<Backdrop id="modGuiZoomBackdrop"
className={classes.backdrop}
open={this.props.showZoomed}
>
{this.content()}
</Backdrop>
)}
</div>);
+44 -6
View File
@@ -22,6 +22,9 @@ import { Theme } from '@mui/material/styles';
import WithStyles, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import IconButtonEx from './IconButtonEx';
import SidechainConnectIcon from './svg/ic_sidechain_open_48.svg?react';
import AutoZoom from './AutoZoom';
@@ -37,7 +40,7 @@ import {
} from './Pedalboard';
import PluginControl from './PluginControl';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import SideChainSelectControl from './SideChainSelectControl';
import SideChainSelectControl, {SideChainSelectDialog} from './SideChainSelectControl';
import VuMeter from './VuMeter';
import { nullCast } from './Utility'
import { PiPedalStateError } from './PiPedalError';
@@ -373,11 +376,12 @@ type PluginControlViewState = {
dialogFileValue: string,
modGuiContentReady: boolean,
showModGuiZoomed: boolean,
sidechainDialogOpen: boolean
};
const PluginControlView =
withTheme(withStyles(
class extends ResizeResponsiveComponent<PluginControlViewProps, PluginControlViewState> {
class extends ResizeResponsiveComponent<PluginControlViewProps, PluginControlViewState> {
model: PiPedalModel;
constructor(props: PluginControlViewProps) {
@@ -393,7 +397,8 @@ const PluginControlView =
dialogFileProperty: new UiFileProperty(),
dialogFileValue: "",
modGuiContentReady: false,
showModGuiZoomed: false
showModGuiZoomed: false,
sidechainDialogOpen: false
}
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
@@ -608,15 +613,15 @@ const PluginControlView =
let title = uiPlugin.audio_side_chain_title ? uiPlugin.audio_side_chain_title : "Side chain";
return (
<SideChainSelectControl
<SideChainSelectControl
title={title}
selectItems={items}
selectedInstanceId={selectedInstanceId}
onChanged={(instanceId: number) => {
this.model.setPedalboardSideChainInput(this.props.item.instanceId, instanceId);
}}
/>
/>
);
}
@@ -876,6 +881,9 @@ const PluginControlView =
return false;
}
handleSelectSidechainInput() {
this.setState({sidechainDialogOpen: true});
}
handleModGuiFileProperty(instanceId: number, filePropertyUri: string, selectedFile: string) {
let plugin = this.model.getUiPlugin(this.props.item.uri);
@@ -900,10 +908,30 @@ const PluginControlView =
if (!uiPlugin) {
return (<div />);
}
let toolBarChildren: ReactNode[] = [];
if (uiPlugin.audio_side_chain_inputs > 0) {
toolBarChildren.push(
<IconButtonEx tooltip="Select sidechain input"
onClick={() => {
this.handleSelectSidechainInput();
}}
>
<SidechainConnectIcon
style={{
opacity: 0.6,
width: 24,
height: 24,
fill: this.props.theme.palette.text.primary
}}
/>
</IconButtonEx>
);
}
return (
<div style={{ width: "100%", height: "100%", }}>
<AutoZoom leftPad={36} rightPad={52} contentReady={this.state.modGuiContentReady}
showZoomed={this.state.showModGuiZoomed}
toolbarChildren={toolBarChildren}
setShowZoomed={
(value) => { this.setState({ showModGuiZoomed: value }); }
}
@@ -1085,6 +1113,16 @@ const PluginControlView =
}
/>
)}
{this.state.sidechainDialogOpen && (
<SideChainSelectDialog open={this.state.sidechainDialogOpen}
onClose={()=>{ this.setState({sidechainDialogOpen: false});}}
selectItems={this.getSidechainSelectItems()}
selectedInstanceId={this.props.item.sideChainInputId}
onChanged={(instanceId: number) => {
this.model.setPedalboardSideChainInput(this.props.item.instanceId, instanceId);
}}
/>
)}
{/* xxx: I don't think we need this anyore. */}
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
+58 -1
View File
@@ -26,6 +26,12 @@ import { Component } from 'react';
import { Theme } from '@mui/material/styles';
import { css } from '@emotion/react';
import DialogEx from './DialogEx';
import DialogTitle from '@mui/material/DialogTitle';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import Toolbar from '@mui/material/Toolbar';
import DialogContent from '@mui/material/DialogContent';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
import Select from '@mui/material/Select';
@@ -163,4 +169,55 @@ const SideChainSelectControl =
, styles)
)
;
export default SideChainSelectControl;
export default SideChainSelectControl;
export interface SideChainSelectDialogProps {
open: boolean,
onClose: () => void,
selectItems: { instanceId: number, title: string }[];
selectedInstanceId: number;
onChanged(instanceId: number): void;
}
export function SideChainSelectDialog(props: SideChainSelectDialogProps) {
let { open, onClose, selectItems, selectedInstanceId, onChanged } = props;
return (
<DialogEx tag="nameDialog" open={open} fullWidth maxWidth="xs" onClose={onClose} aria-labelledby="Rename-dialog-title"
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<DialogTitle id="alert-dialog-title" style={{ userSelect: "none", padding: 0, margin: 0 }}>
<Toolbar style={{ paddingLeft: 24, paddingRight: 24, margin: 0 }} >
<IconButtonEx
tooltip="Back" edge="start" color="inherit" onClick={() => { onClose(); }} aria-label="back"
>
<ArrowBackIcon style={{opacity: 0.6}} />
</IconButtonEx>
<Typography noWrap variant="body1" style={{ flex: "1 1 auto" }}>Sidechain Input</Typography>
</Toolbar>
</DialogTitle>
<DialogContent >
<Select
variant="standard"
onChange={(event) => {
onChanged(event.target.value as number);
}}
value={selectedInstanceId}
style={{ width: "100%" }}
>
{selectItems.map((item) => {
return (
<MenuItem key={item.instanceId} value={item.instanceId}
selected={item.instanceId === selectedInstanceId}>
{item.title}
</MenuItem>
);
})}
</Select>
</DialogContent>
</DialogEx>
);
}
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.35 6.35"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
id="path4452"
d="M 3.1501953 1.1797729 C 2.3438819 1.1900494 1.6500313 1.6826477 1.3451375 2.38125 L 1.786971 2.38125 C 2.0624356 1.9000342 2.5807156 1.5761312 3.1760335 1.5761312 C 4.0608593 1.5761312 4.7733521 2.2912075 4.7733521 3.1760335 C 4.7733521 4.0608594 4.0608593 4.7733521 3.1760335 4.7733521 C 2.580068 4.7733521 2.0611725 4.450254 1.7859375 3.96875 L 1.3446208 3.96875 C 1.6524938 4.6748172 2.3583381 5.1697103 3.1760335 5.1697103 C 4.2753461 5.1697103 5.1697103 4.2753461 5.1697103 3.1760335 C 5.1697103 2.0767208 4.2753461 1.1797729 3.1760335 1.1797729 C 3.1674451 1.1797729 3.1587586 1.1796638 3.1501953 1.1797729 z " />
<path
id="ellipse4674"
d="M 3.1760335 2.3300903 C 2.7944551 2.3300903 2.4622893 2.5720679 2.3455933 2.9098999 L 0.64078776 2.9098999 L 0.64078776 3.4395833 L 2.3450765 3.4395833 C 2.4612025 3.7778767 2.7937654 4.019393 3.1760335 4.0193929 C 3.651938 4.0193929 4.0488484 3.6453545 4.0488485 3.1760335 C 4.0488485 2.7067124 3.6519381 2.3300904 3.1760335 2.3300903 z M 3.1760335 2.7264486 C 3.4466224 2.7264486 3.6524902 2.9307799 3.6524902 3.1760335 C 3.6524901 3.4212871 3.4466224 3.6230347 3.1760335 3.6230347 C 2.9054446 3.6230347 2.6975099 3.4212871 2.6975098 3.1760335 C 2.6975098 2.9307799 2.9054446 2.7264486 3.1760335 2.7264486 z " />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB