Port from CRT to Vite
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react'
|
||||
import ReactLogo from './assets/react.svg?react'
|
||||
//import ViteLogo from '/vite.svg?react'
|
||||
import './App.css'
|
||||
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
{/* <ViteLogo/> */}
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<ReactLogo style={{background: "blue", padding: 96}} />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './pipedal/App.tsx'
|
||||
|
||||
// createRoot(document.getElementById('root')!).render(
|
||||
// <StrictMode>
|
||||
// <App />
|
||||
// </StrictMode>,
|
||||
// )
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
)
|
||||
@@ -0,0 +1,240 @@
|
||||
|
||||
import React, { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import JackHostStatus from './JackHostStatus';
|
||||
import { PiPedalError } from './PiPedalError';
|
||||
import DialogEx from './DialogEx';
|
||||
|
||||
|
||||
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
|
||||
interface AboutDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
interface AboutDialogState {
|
||||
jackStatus?: JackHostStatus;
|
||||
|
||||
openSourceNotices: string;
|
||||
};
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: SlideProps, ref: React.Ref<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
|
||||
const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
refNotices: React.RefObject<HTMLDivElement | null >;
|
||||
|
||||
|
||||
constructor(props: AboutDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.refNotices = React.createRef();
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.state = {
|
||||
jackStatus: undefined,
|
||||
openSourceNotices: ""
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
mounted: boolean = false;
|
||||
subscribed: boolean = false;
|
||||
|
||||
tick() {
|
||||
if (this.model.state.get() === State.Ready) {
|
||||
this.model.getJackStatus()
|
||||
.then(jackStatus => {
|
||||
this.setState({ jackStatus: jackStatus });
|
||||
})
|
||||
.catch(_error => { /* ignore*/ });
|
||||
}
|
||||
}
|
||||
|
||||
timerHandle?: number;
|
||||
|
||||
updateNotifications() {
|
||||
let subscribed = this.mounted && this.props.open;
|
||||
if (subscribed !== this.subscribed) {
|
||||
if (subscribed) {
|
||||
this.timerHandle = setInterval(() => this.tick(), 1000);
|
||||
} else {
|
||||
if (this.timerHandle) {
|
||||
clearInterval(this.timerHandle);
|
||||
this.timerHandle = undefined;
|
||||
}
|
||||
}
|
||||
this.subscribed = subscribed;
|
||||
}
|
||||
}
|
||||
|
||||
startFossRequest() {
|
||||
if (this.state.openSourceNotices === "") {
|
||||
fetch("var/notices.txt")
|
||||
.then((request) => {
|
||||
if (!request.ok) {
|
||||
throw new PiPedalError("notices request failed.");
|
||||
}
|
||||
return request.text();
|
||||
})
|
||||
.then((text) => {
|
||||
if (this.mounted) {
|
||||
this.setState({ openSourceNotices: text });
|
||||
}
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
// ok in debug builds. File doesn't get placed until install time.
|
||||
console.log("Failed to fetch open-source notices. " + err.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount?.();
|
||||
this.mounted = true;
|
||||
this.updateNotifications();
|
||||
this.startFossRequest();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount?.();
|
||||
this.mounted = false;
|
||||
this.updateNotifications();
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<AboutDialogProps>, prevState: Readonly<AboutDialogState>, snapshot: any): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
this.updateNotifications();
|
||||
}
|
||||
|
||||
handleDialogClose(_e: SyntheticEvent) {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let addressKey = 0;
|
||||
let serverVersion = this.model.serverVersion?.serverVersion ?? "";
|
||||
let nPos = serverVersion.indexOf(' ');
|
||||
if (nPos !== -1) {
|
||||
serverVersion = serverVersion.substring(nPos + 1);
|
||||
}
|
||||
return (
|
||||
<DialogEx tag="about" fullScreen open={this.props.open}
|
||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
||||
onEnterKey={() => { this.props.onClose() }}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar style={{
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}} >
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6"
|
||||
sx={{ maringLeft: 2, flex: 1 }}
|
||||
>
|
||||
About
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: "1 1 auto", position: "relative", overflow: "hidden",
|
||||
overflowX: "hidden", overflowY: "auto", userSelect: "text"
|
||||
}}
|
||||
>
|
||||
<div id="debug_info" style={{
|
||||
userSelect: "text",
|
||||
cursor: "text",margin: 24
|
||||
}}>
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap" }}>
|
||||
<Typography noWrap display="block" variant="h6" color="textPrimary" style={{ flexGrow: 1, flexShrink: 1 }}>
|
||||
PiPedal <span style={{ fontSize: "0.7em" }}>
|
||||
{serverVersion
|
||||
+ (this.model.serverVersion?.debug ? " (Debug)" : "")}
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
</div>
|
||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
||||
Copyright © 2022-2024 Robin Davies.
|
||||
</Typography>
|
||||
{this.model.isAndroidHosted() && (
|
||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 0 }} >
|
||||
{this.model.getAndroidHostVersion()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{this.model.isAndroidHosted() && (
|
||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
||||
Copyright © 2022-2024 Robin Davies.
|
||||
</Typography>
|
||||
)}
|
||||
<Divider />
|
||||
<Typography noWrap display="block" variant="caption" >
|
||||
ADDRESSES
|
||||
</Typography>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{
|
||||
this.model.serverVersion?.webAddresses.map((address) =>
|
||||
(
|
||||
<Typography key={addressKey++} display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||
{address}
|
||||
</Typography>
|
||||
))
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<Typography noWrap display="block" variant="caption" >
|
||||
SERVER OS
|
||||
</Typography>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||
{this.model.serverVersion?.osVersion ?? ""}
|
||||
</Typography>
|
||||
</div>
|
||||
</div><div>
|
||||
|
||||
<Divider />
|
||||
<Typography display="block" variant="caption" >
|
||||
LEGAL NOTICES
|
||||
</Typography>
|
||||
<div dangerouslySetInnerHTML={{ __html: this.state.openSourceNotices }} style={{ fontSize: "0.8em", maxWidth: 400 }}>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogEx >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
export default AboutDialog;
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
|
||||
export default class AlsaDeviceInfo {
|
||||
deserialize(input: any): AlsaDeviceInfo {
|
||||
this.cardId = input.cardId;
|
||||
this.id = input.id;
|
||||
this.name = input.name;
|
||||
this.longName = input.longName;
|
||||
this.sampleRates = input.sampleRates as number[];
|
||||
this.minBufferSize = input.minBufferSize;
|
||||
this.maxBufferSize = input.maxBufferSize;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): AlsaDeviceInfo[]
|
||||
{
|
||||
let result: AlsaDeviceInfo[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new AlsaDeviceInfo().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
closestSampleRate(sr: number): number
|
||||
{
|
||||
let result = 48000;
|
||||
let bestError = 1E36;
|
||||
for (let rate of this.sampleRates)
|
||||
{
|
||||
let error = (sr-rate)*(sr-rate);
|
||||
if (error < bestError)
|
||||
{
|
||||
bestError = error;
|
||||
result = rate;
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
closestBufferSize(buffSize: number): number
|
||||
{
|
||||
let result = 64;
|
||||
let bestError = 1E36;
|
||||
|
||||
for (let sz = 2; sz <= this.maxBufferSize; sz *= 2)
|
||||
{
|
||||
if (sz >= this.minBufferSize)
|
||||
{
|
||||
let error = (sz-buffSize)*(sz-buffSize);
|
||||
if (error < bestError)
|
||||
{
|
||||
bestError = error;
|
||||
result = sz;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
cardId: number = -1;
|
||||
id: string = "";
|
||||
name: string = "";
|
||||
longName: string = "";
|
||||
sampleRates: number[] = [];
|
||||
minBufferSize: number = 0;
|
||||
maxBufferSize: number = 0;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
export class AlsaMidiDeviceInfo {
|
||||
deserialize(input: any) : AlsaMidiDeviceInfo{
|
||||
this.name = input.name;
|
||||
this.description = input.description;
|
||||
return this;
|
||||
}
|
||||
static deserializeArray(input: any[]): AlsaMidiDeviceInfo[] {
|
||||
let result: AlsaMidiDeviceInfo[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new AlsaMidiDeviceInfo().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
name: string = "";
|
||||
description: string = "";
|
||||
|
||||
equals(other: AlsaMidiDeviceInfo) : boolean {
|
||||
return this.name === other.name && this.description === other.description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
export interface AndroidHostInterface {
|
||||
showSponsorship() : void;
|
||||
isAndroidHosted(): boolean;
|
||||
getHostVersion() : string;
|
||||
chooseNewDevice() : void;
|
||||
setDisconnected(isDisconnected: boolean): void;
|
||||
launchExternalUrl(url:string): boolean;
|
||||
setThemePreference(theme: number): void;
|
||||
getThemePreference(): number;
|
||||
isDarkTheme?: ()=> boolean;
|
||||
};
|
||||
|
||||
export class FakeAndroidHost implements AndroidHostInterface
|
||||
{
|
||||
isAndroidHosted(): boolean {
|
||||
return true;
|
||||
}
|
||||
getHostVersion(): string {
|
||||
return "Fake Android 1.0";
|
||||
}
|
||||
chooseNewDevice(): void {
|
||||
|
||||
}
|
||||
isDarkTheme(): boolean {
|
||||
return true;
|
||||
}
|
||||
setDisconnected(_isDisconnected: boolean): void {
|
||||
|
||||
}
|
||||
showSponsorship() : void { }
|
||||
|
||||
launchExternalUrl(_url:string): boolean
|
||||
{
|
||||
return false;
|
||||
}
|
||||
private theme = 1;
|
||||
setThemePreference(theme: number): void{
|
||||
this.theme = theme;
|
||||
}
|
||||
getThemePreference(): number {
|
||||
return this.theme;
|
||||
}
|
||||
}
|
||||
export function isAndroidHosted(): boolean {
|
||||
return ((window as any).AndroidHost as AndroidHostInterface) !== undefined;
|
||||
}
|
||||
|
||||
export function getAndroidHost(): AndroidHostInterface | undefined {
|
||||
return ((window as any).AndroidHost as AndroidHostInterface);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
|
||||
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
|
||||
import AppThemed from "./AppThemed";
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
interface Theme {
|
||||
mainBackground: React.CSSProperties['color'];
|
||||
toolbarColor: React.CSSProperties['color'];
|
||||
}
|
||||
interface ThemeOptions {
|
||||
mainBackground?: React.CSSProperties['color'];
|
||||
toolbarColor?: React.CSSProperties['color'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
interface ButtonPropsVariantOverrides {
|
||||
dialogPrimary: true;
|
||||
dialogSecondary: true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// declare module '@mui/styles/defaultTheme' {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
// interface DefaultTheme extends Theme { }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
const theme = createTheme(
|
||||
isDarkMode() ?
|
||||
{
|
||||
cssVariables: true,
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
containedPrimary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
textTransform: "none"
|
||||
},
|
||||
containedSecondary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
textTransform: "none"
|
||||
}
|
||||
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'dialogPrimary' },
|
||||
style: {
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
color: "rgb(255,255,255,0.7)"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#A770E4'// #6750A4" // #5B5690 #60529A #5C5694
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
}
|
||||
},
|
||||
mainBackground: "#222",
|
||||
toolbarColor: '#222'
|
||||
}
|
||||
:
|
||||
{
|
||||
cssVariables: true,
|
||||
components: {
|
||||
/* make the selection state for MuiListItemButtons a smidgen darker (light theme only) */
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.25)', // Slightly darker on hover
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
containedPrimary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
textTransform: "none"
|
||||
},
|
||||
containedSecondary: {
|
||||
borderRadius: '9999px',
|
||||
paddingLeft: "16px", paddingRight: "16px",
|
||||
textTransform: "none"
|
||||
}
|
||||
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'dialogPrimary' },
|
||||
style: {
|
||||
color: "rgb(0,0,0,0.87)"
|
||||
}
|
||||
},
|
||||
{
|
||||
props: { variant: 'dialogSecondary', },
|
||||
style: {
|
||||
color: "rgb(0,0,0,0.6)"
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#6750A4" // #5B5690 #60529A #5C5694
|
||||
},
|
||||
secondary: {
|
||||
main: "#FF6060"
|
||||
}
|
||||
|
||||
},
|
||||
mainBackground: "#FFFFFF",
|
||||
toolbarColor: '#FFFFFF'
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
type AppThemeProps = {
|
||||
|
||||
};
|
||||
|
||||
|
||||
const App = (class extends React.Component {
|
||||
// Before the component mounts, we initialise our state
|
||||
|
||||
constructor(props: AppThemeProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
if (!App.virtualKeyboardHandler)
|
||||
{
|
||||
App.virtualKeyboardHandler = new VirtualKeyboardHandler();
|
||||
}
|
||||
}
|
||||
|
||||
static virtualKeyboardHandler?: VirtualKeyboardHandler;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
|
||||
<AppThemed />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,19 @@
|
||||
.fossCopyrights P {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.fossCopyrights {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.fossLicense {
|
||||
padding-left: 36px;
|
||||
border-bottom: 1px #CCC solid
|
||||
}
|
||||
div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,581 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { BankIndexEntry, BankIndex } from './Banks';
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonBase from "@mui/material/ButtonBase";
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import WithStyles from './WithStyles';
|
||||
|
||||
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
|
||||
import Fade from '@mui/material/Fade';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import RenameDialog from './RenameDialog';
|
||||
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DownloadIcon from './svg/file_download_black_24dp.svg?react';
|
||||
import UploadIcon from './svg/file_upload_black_24dp.svg?react';
|
||||
import BankIcon from './svg/ic_bank.svg?react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
//import PublishIcon from '@mui/icons-material/Publish';
|
||||
// import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
// import AppsIcon from '@mui/icons-material/Apps';
|
||||
|
||||
interface BankDialogProps extends WithStyles<typeof styles> {
|
||||
show: boolean;
|
||||
isEditDialog: boolean;
|
||||
onDialogClose: () => void;
|
||||
|
||||
};
|
||||
|
||||
interface BankDialogState {
|
||||
banks: BankIndex;
|
||||
|
||||
showActionBar: boolean;
|
||||
|
||||
selectedItem: number;
|
||||
|
||||
filenameDialogOpen: boolean;
|
||||
filenameSaveAs: boolean;
|
||||
moreMenuAnchorEl: HTMLElement | null;
|
||||
showDeletePrompt: boolean;
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
listIcon: css({
|
||||
width: 24, height: 24,
|
||||
opacity: 0.6, fill: theme.palette.text.primary
|
||||
}),
|
||||
dialogAppBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}),
|
||||
dialogActionBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0,
|
||||
background: "black"
|
||||
}),
|
||||
dialogTitle: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
}),
|
||||
itemFrame: css({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
height: "56px",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 8
|
||||
}),
|
||||
iconFrame: css({
|
||||
flex: "0 0 auto",
|
||||
|
||||
}),
|
||||
itemIcon: css({
|
||||
width: 24, height: 24, margin: 12, opacity: 0.6
|
||||
}),
|
||||
itemLabel: css({
|
||||
flex: "1 1 1px",
|
||||
marginLeft: 8
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: SlideProps, ref: React.Ref<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
|
||||
const BankDialog = withStyles(
|
||||
class extends Component<BankDialogProps, BankDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
refUpload: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: BankDialogProps) {
|
||||
super(props);
|
||||
|
||||
this.refUpload = React.createRef();
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
let banks = this.model.banks.get();
|
||||
this.state = {
|
||||
banks: banks,
|
||||
showActionBar: false,
|
||||
selectedItem: banks.selectedBank,
|
||||
filenameDialogOpen: false,
|
||||
filenameSaveAs: false,
|
||||
moreMenuAnchorEl: null,
|
||||
showDeletePrompt: false
|
||||
};
|
||||
this.handleBanksChanged = this.handleBanksChanged.bind(this);
|
||||
|
||||
}
|
||||
onMoreClick(e: React.SyntheticEvent): void {
|
||||
this.setState({ moreMenuAnchorEl: e.currentTarget as HTMLElement })
|
||||
}
|
||||
|
||||
handleDownloadBank() {
|
||||
this.handleMoreClose();
|
||||
this.model.download("downloadBank", this.state.selectedItem);
|
||||
}
|
||||
|
||||
async uploadFiles(fileList: FileList): Promise<number> {
|
||||
let uploadAfter = this.state.selectedItem;
|
||||
try {
|
||||
for (let i = 0; i < fileList.length; ++i) {
|
||||
uploadAfter = await this.model.uploadBank(fileList[i], uploadAfter);
|
||||
}
|
||||
} catch (error) {
|
||||
this.model.showAlert(error + "");
|
||||
};
|
||||
return uploadAfter;
|
||||
}
|
||||
|
||||
handleUpload(e: any) {
|
||||
if (!e.target.files) return;
|
||||
if (e.target.files.length === 0) return;
|
||||
var fileList = e.target.files;
|
||||
this.uploadFiles(fileList)
|
||||
.then((newSelection) => {
|
||||
e.target.value = ""; // clear the file list so we can get another change notice.
|
||||
this.setState({ selectedItem: newSelection });
|
||||
})
|
||||
.catch(err => {
|
||||
e.target.value = ""; // clear the file list so we can get another change notice.
|
||||
this.model.showAlert(err.toString());
|
||||
});
|
||||
}
|
||||
handleUploadBank() {
|
||||
if (this.refUpload.current) {
|
||||
this.refUpload.current.click();
|
||||
}
|
||||
this.handleMoreClose();
|
||||
|
||||
}
|
||||
handleMoreClose(): void {
|
||||
this.setState({ moreMenuAnchorEl: null });
|
||||
|
||||
}
|
||||
|
||||
|
||||
selectItemAtIndex(index: number) {
|
||||
let instanceId = this.state.banks.entries[index].instanceId;
|
||||
this.setState({ selectedItem: instanceId });
|
||||
}
|
||||
isEditMode() {
|
||||
return this.state.showActionBar || this.props.isEditDialog;
|
||||
}
|
||||
|
||||
handleBanksChanged() {
|
||||
let banks = this.model.banks.get();
|
||||
|
||||
if (!banks.areEqual(this.state.banks, false)) // avoid a bunch of peculiar effects if we update while a drag is in progress
|
||||
{
|
||||
// if we don't have a valid selection, then use the current bank.
|
||||
if (this.state.banks.getEntry(this.state.selectedItem) == null) {
|
||||
this.setState({ banks: banks, selectedItem: banks.selectedBank });
|
||||
} else {
|
||||
this.setState({ banks: banks });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
componentDidMount() {
|
||||
this.model.banks.addOnChangedHandler(this.handleBanksChanged);
|
||||
this.handleBanksChanged();
|
||||
// scroll selected item into view.
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.banks.removeOnChangedHandler(this.handleBanksChanged);
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
getSelectedIndex() {
|
||||
let instanceId = this.isEditMode() ? this.state.selectedItem : this.state.banks.selectedBank;
|
||||
let banks = this.state.banks;
|
||||
for (let i = 0; i < banks.entries.length; ++i) {
|
||||
if (banks.entries[i].instanceId === instanceId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
handleDeleteClick() {
|
||||
if (!this.state.selectedItem) return;
|
||||
this.setState({ showDeletePrompt: true });
|
||||
}
|
||||
handleDeletePromptClose() {
|
||||
this.setState({ showDeletePrompt: false });
|
||||
}
|
||||
handleDeletePromptOk() {
|
||||
this.handleDeletePromptClose();
|
||||
|
||||
let selectedItem = this.state.selectedItem;
|
||||
if (selectedItem !== -1) {
|
||||
this.model.deleteBankItem(selectedItem)
|
||||
.then((newSelection: number) => {
|
||||
this.setState({
|
||||
selectedItem: newSelection
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
handleDialogClose() {
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
|
||||
handleItemClick(instanceId: number): void {
|
||||
if (this.isEditMode()) {
|
||||
this.setState({ selectedItem: instanceId });
|
||||
} else {
|
||||
this.model.openBank(instanceId);
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
}
|
||||
showActionBar(show: boolean): void {
|
||||
this.setState({ showActionBar: show });
|
||||
|
||||
}
|
||||
|
||||
|
||||
mapElement(el: any): React.ReactNode {
|
||||
let bankEntry = el as BankIndexEntry;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.banks.selectedBank;
|
||||
return (
|
||||
<div key={bankEntry.instanceId} >
|
||||
|
||||
<ButtonBase style={{ width: "100%", height: 48 }}
|
||||
onClick={() => this.handleItemClick(bankEntry.instanceId)}
|
||||
>
|
||||
<SelectHoverBackground selected={bankEntry.instanceId === selectedItem} showHover={true} />
|
||||
<div className={classes.itemFrame}>
|
||||
<div className={classes.listIcon}>
|
||||
<BankIcon />
|
||||
</div>
|
||||
<div className={classes.itemLabel}>
|
||||
<Typography noWrap variant="body2" color="textPrimary">
|
||||
{bankEntry.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
moveElement(from: number, to: number): void {
|
||||
let newBanks = this.state.banks.clone();
|
||||
newBanks.moveBank(from, to);
|
||||
this.setState({
|
||||
banks: newBanks,
|
||||
selectedItem: newBanks.entries[to].instanceId
|
||||
});
|
||||
this.model.moveBank(from, to)
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedName(): string {
|
||||
let item = this.state.banks.getEntry(this.state.selectedItem);
|
||||
if (item) return item.name;
|
||||
return "";
|
||||
}
|
||||
|
||||
handleRenameClick() {
|
||||
let item = this.state.banks.getEntry(this.state.selectedItem);
|
||||
if (item) {
|
||||
this.setState({ filenameDialogOpen: true, filenameSaveAs: false });
|
||||
}
|
||||
}
|
||||
handleRenameOk(text: string) {
|
||||
let item = this.state.banks.getEntry(this.state.selectedItem);
|
||||
if (!item) return;
|
||||
if (item.name !== text) {
|
||||
if (this.state.banks.hasName(text)) {
|
||||
this.model.showAlert("A bank with that name already exists.");
|
||||
}
|
||||
this.model.renameBank(this.state.selectedItem, text)
|
||||
.catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ filenameDialogOpen: false });
|
||||
}
|
||||
handleSaveAsOk(text: string) {
|
||||
let item = this.state.banks.getEntry(this.state.selectedItem);
|
||||
if (item) {
|
||||
if (item.name !== text) {
|
||||
if (this.state.banks.hasName(text)) {
|
||||
this.model.showAlert("A bank with that name already exists.");
|
||||
return;
|
||||
}
|
||||
this.model.saveBankAs(this.state.selectedItem, text)
|
||||
.then((newSelection) => {
|
||||
this.setState({ selectedItem: newSelection });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.setState({ filenameDialogOpen: false });
|
||||
}
|
||||
|
||||
handleCopy() {
|
||||
let item = this.state.banks.getEntry(this.state.selectedItem);
|
||||
if (item) {
|
||||
this.setState({ filenameDialogOpen: true, filenameSaveAs: true });
|
||||
}
|
||||
}
|
||||
|
||||
onError(error: string): void {
|
||||
this.model?.showAlert(error);
|
||||
}
|
||||
|
||||
getSelectedBankName() {
|
||||
try {
|
||||
return this.model.banks.get()?.getEntry(this.state.selectedItem)?.name??"";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
|
||||
let defaultSelectedIndex = this.getSelectedIndex();
|
||||
|
||||
return (
|
||||
<DialogEx tag="bank" fullScreen open={this.props.show}
|
||||
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
|
||||
onEnterKey={()=>{}}
|
||||
style={{ userSelect: "none" }}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
disabled={this.isEditMode()}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
Banks
|
||||
</Typography>
|
||||
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
|
||||
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
|
||||
>
|
||||
<Toolbar>
|
||||
{(!this.props.isEditDialog) ? (
|
||||
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
)}
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
Banks
|
||||
</Typography>
|
||||
{(this.state.banks.getEntry(this.state.selectedItem) != null)
|
||||
&& (
|
||||
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Button color="inherit" onClick={(e) => this.handleCopy()}>
|
||||
Copy
|
||||
</Button>
|
||||
<Button color="inherit" onClick={() => this.handleRenameClick()}>
|
||||
Rename
|
||||
</Button>
|
||||
<RenameDialog
|
||||
open={this.state.filenameDialogOpen}
|
||||
defaultName={this.getSelectedName()}
|
||||
acceptActionName={this.state.filenameSaveAs ? "SAVE AS" : "RENAME"}
|
||||
onClose={() => { this.setState({ filenameDialogOpen: false }) }}
|
||||
onOk={(text: string) => {
|
||||
if (this.state.filenameSaveAs) {
|
||||
this.handleSaveAsOk(text);
|
||||
} else {
|
||||
this.handleRenameOk(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
|
||||
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="more-menu"
|
||||
anchorEl={this.state.moreMenuAnchorEl}
|
||||
keepMounted
|
||||
open={Boolean(this.state.moreMenuAnchorEl)}
|
||||
onClose={() => this.handleMoreClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => { this.handleDownloadBank(); }} >
|
||||
<ListItemIcon>
|
||||
<DownloadIcon className={classes.listIcon}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Download bank
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
<label htmlFor="bankDialog_UploadInput">
|
||||
<MenuItem onClick={(e) => this.handleUploadBank()}>
|
||||
|
||||
|
||||
<ListItemIcon >
|
||||
<UploadIcon className={classes.listIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Upload bank
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
</label>
|
||||
</Menu>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Toolbar>
|
||||
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
|
||||
<DraggableGrid
|
||||
onLongPress={(item) => this.showActionBar(true)}
|
||||
canDrag={this.isEditMode()}
|
||||
onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }}
|
||||
moveElement={(from, to) => { this.moveElement(from, to); }}
|
||||
scroll={ScrollDirection.Y}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
>
|
||||
{
|
||||
this.state.banks.entries.map((element) => {
|
||||
return this.mapElement(element);
|
||||
})
|
||||
}
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
id="bankDialog_UploadInput"
|
||||
ref={this.refUpload}
|
||||
type="file"
|
||||
accept=".piBank"
|
||||
multiple
|
||||
onChange={(e) => this.handleUpload(e)}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
<DialogEx tag="deletePrompt" open={this.state.showDeletePrompt} onClose={() => this.handleDeletePromptClose()}
|
||||
style={{ userSelect: "none" }}
|
||||
onEnterKey={()=>{ this.handleDeletePromptOk() }}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography>Are you sure you want to delete bank '{this.getSelectedBankName()}'?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={() => this.handleDeletePromptClose()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => this.handleDeletePromptOk()} color="secondary" >
|
||||
DELETE
|
||||
</Button>
|
||||
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
|
||||
</DialogEx >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default BankDialog;
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
export class BankIndexEntry {
|
||||
deserialize(input: any) : BankIndexEntry {
|
||||
this.instanceId = input.instanceId;
|
||||
this.name = input.name;
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
areEqual(other: BankIndexEntry) : boolean {
|
||||
return (this.instanceId === other.instanceId)
|
||||
&& (this.name === other.name);
|
||||
}
|
||||
static deserialize_array(input: any): BankIndexEntry[] {
|
||||
let result: BankIndexEntry[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new BankIndexEntry().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
instanceId: number = -1;
|
||||
name: string = "";
|
||||
}
|
||||
|
||||
export class BankIndex {
|
||||
deserialize(input: any) : BankIndex {
|
||||
this.selectedBank = input.selectedBank;
|
||||
this.entries = BankIndexEntry.deserialize_array(input.entries);
|
||||
return this;
|
||||
}
|
||||
selectedBank: number = -1;
|
||||
entries: BankIndexEntry[] = [];
|
||||
|
||||
areEqual(other: BankIndex, includeSelection: boolean): boolean {
|
||||
if (includeSelection && this.selectedBank !== other.selectedBank) return false;
|
||||
|
||||
if (this.entries.length !== other.entries.length) return false;
|
||||
|
||||
for (let i = 0; i < this.entries.length; ++i)
|
||||
{
|
||||
if (!this.entries[i].areEqual(other.entries[i])) return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
moveBank(from: number, to: number)
|
||||
{
|
||||
let t = this.entries[from];
|
||||
this.entries.splice(from,1);
|
||||
this.entries.splice(to,0,t);
|
||||
|
||||
}
|
||||
|
||||
clone(): BankIndex {
|
||||
return new BankIndex().deserialize(this);
|
||||
}
|
||||
|
||||
hasName(name: string): boolean {
|
||||
for (let i = 0; i < this.entries.length; ++i)
|
||||
{
|
||||
let entry = this.entries[i];
|
||||
if (entry.name === name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
getEntry(instanceId: number): BankIndexEntry | null {
|
||||
for (let i = 0; i < this.entries.length; ++i)
|
||||
{
|
||||
let entry = this.entries[i];
|
||||
if (entry.instanceId === instanceId) return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getSelectedEntryName(): string {
|
||||
let entry = this.getEntry(this.selectedBank);
|
||||
if (!entry) return "";
|
||||
return entry.name;
|
||||
}
|
||||
nameExists(name: string): boolean {
|
||||
for (let i = 0; i < this.entries.length; ++i)
|
||||
{
|
||||
let entry = this.entries[i];
|
||||
if (entry.name === name) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Divider from '@mui/material/Divider';
|
||||
//import TextFieldEx from './TextFieldEx';
|
||||
|
||||
|
||||
export interface ChannelBindingHelpDialogProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
};
|
||||
|
||||
export interface ChannelBindingHelpDialogState {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export default class ChannelBindingHelpDialog extends ResizeResponsiveComponent<ChannelBindingHelpDialogProps, ChannelBindingHelpDialogState> {
|
||||
|
||||
refText: React.RefObject<HTMLInputElement | null>;
|
||||
|
||||
constructor(props: ChannelBindingHelpDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
};
|
||||
this.refText = React.createRef<HTMLInputElement>();
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open, onClose } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogEx tag="bindingHelp" open={open} fullWidth maxWidth="sm"
|
||||
onClose={handleClose}
|
||||
aria-labelledby="Rename-dialog-title"
|
||||
style={{ userSelect: "none" }}
|
||||
fullwidth
|
||||
onEnterKey={() => { }}
|
||||
>
|
||||
<DialogContent style={{ minHeight: 96 }}>
|
||||
<Typography variant="h5" style={{ marginBottom: 8 }}>
|
||||
MIDI Control Binding Priority
|
||||
</Typography>
|
||||
<div style={{ fontSize: "0.9em", lineHeight: "18pt" }}>
|
||||
<p style={{ lineHeight: "1.4em" }}>MIDI Channel Filters determine which MIDI messages get sent to a plugin that accepts MIDI messages. Note that the
|
||||
MIDI Channel Filters do NOT affect system MIDI bindings or control bindings.
|
||||
</p>
|
||||
<p>
|
||||
MIDI message processing occurs in the following order
|
||||
</p>
|
||||
<ul>
|
||||
<li>If the message is a Program Change message, the message is first offered to any MIDI plugins in the currently loaded preset (Message Filters permitting).
|
||||
The message is sent to each MIDI plugin in the current preset that wants it. If any MIDI plugin accepts the program change,
|
||||
no futher processing occurs. Specifically, the program change will not change the currently selected PiPedal preset.
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
If the message is a Program Change message, and no MIDI plugin has accepted the message, PiPedal selects the PiPedal preset that corresponds
|
||||
to the requested program.
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
The message is then checked to see if it has been bound to a Pipedal feature using the System Midi Bindings settings. If it has,
|
||||
Pipedal processes the message, and it is not forwarded to MIDI plugins.
|
||||
<br />
|
||||
</li>
|
||||
<li>
|
||||
Otherwise, the message is sent to each MIDI plugin in the currently loaded Pipedal preset (channel filters permitting).)
|
||||
<br />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions style={{ flexShrink: 1 }}>
|
||||
<Button onClick={handleClose} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { colorKeys, getBackgroundColor } from './MaterialColors';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
const colors: string[] = colorKeys;
|
||||
|
||||
|
||||
export enum DropdownAlignment {
|
||||
SE, SW
|
||||
}
|
||||
|
||||
interface ColorDropdownButtonProps {
|
||||
currentColor?: string;
|
||||
onColorChange: (color: string) => void;
|
||||
dropdownAlignment: DropdownAlignment;
|
||||
}
|
||||
|
||||
const ColorDropdownButton: React.FC<ColorDropdownButtonProps> = ({
|
||||
currentColor = colors[0],
|
||||
onColorChange,
|
||||
dropdownAlignment = DropdownAlignment.SE
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedColor, setSelectedColor] = useState<string>(currentColor);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedColor(currentColor);
|
||||
}, [currentColor]);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleColorSelect = (color: string) => {
|
||||
handleClose();
|
||||
setSelectedColor(color);
|
||||
onColorChange(color);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ButtonBase
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: 48, height: 48, padding: 12, borderRadius: 18
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 24, height: 24,
|
||||
background: getBackgroundColor(selectedColor),
|
||||
borderRadius: 6,
|
||||
borderColor: theme.palette.text.secondary,
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid"
|
||||
}} />
|
||||
|
||||
</ButtonBase>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={anchorEl !== null}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left')
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left'),
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexFlow: "row wrap", width: 56 * 4 + 2 }}>
|
||||
{colors.map((color) => {
|
||||
let selected = color === selectedColor;
|
||||
return (
|
||||
<MenuItem key={color} onClick={() => handleColorSelect(color)}
|
||||
style={{
|
||||
width: 48, height: 48, borderRadius: 6, margin: 4,
|
||||
borderStyle: "solid",
|
||||
borderWidth: selected ? 2 : 0.25,
|
||||
borderColor: color === selectedColor ?
|
||||
theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
backgroundColor: getBackgroundColor(color)
|
||||
}}
|
||||
></MenuItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorDropdownButton;
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import Tooltip from "@mui/material/Tooltip"
|
||||
import { UiControl } from './Lv2Plugin';
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
|
||||
interface ControlTooltipProps {
|
||||
children: ReactElement,
|
||||
uiControl: UiControl
|
||||
}
|
||||
|
||||
|
||||
export default function ControlTooltip(props: ControlTooltipProps) {
|
||||
let { children, uiControl } = props;
|
||||
if (uiControl.comment && (uiControl.comment !== uiControl.name)) {
|
||||
return (
|
||||
<Tooltip title={(
|
||||
<React.Fragment>
|
||||
<Typography variant="caption">{uiControl.name}</Typography>
|
||||
<Divider />
|
||||
<Typography variant="caption">{uiControl.comment}</Typography>
|
||||
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={uiControl.name}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
|
||||
import { PedalboardItem, PedalboardSplitItem } from './Pedalboard';
|
||||
import PluginControlView from './PluginControlView';
|
||||
import SplitControlView from './SplitControlView';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import { GxTunerViewFactory } from './GxTunerView';
|
||||
import ToobPowerstage2ViewFactory from './ToobPowerStage2View';
|
||||
import ToobSpectrumAnalyzerViewFactory from './ToobSpectrumAnalyzerView';
|
||||
import ToobMLViewFactory from './ToobMLView';
|
||||
|
||||
|
||||
let pluginFactories: IControlViewFactory[] = [
|
||||
new GxTunerViewFactory(),
|
||||
new ToobPowerstage2ViewFactory(),
|
||||
new ToobSpectrumAnalyzerViewFactory(),
|
||||
new ToobMLViewFactory()
|
||||
];
|
||||
|
||||
|
||||
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
if (!pedalboardItem) {
|
||||
return (<div />);
|
||||
}
|
||||
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
);
|
||||
}
|
||||
if (pedalboardItem.isSplit()) {
|
||||
return (
|
||||
<SplitControlView item={pedalboardItem as PedalboardSplitItem} instanceId={pedalboardItem!.instanceId}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
for (let i = 0; i < pluginFactories.length; ++i) {
|
||||
let factory = pluginFactories[i];
|
||||
if (factory.uri === pedalboardItem.uri) {
|
||||
return factory.Create(model, pedalboardItem);
|
||||
}
|
||||
}
|
||||
let uiPlugin = model.getUiPlugin(pedalboardItem.uri);
|
||||
if (!uiPlugin) {
|
||||
<div style={{ paddingLeft: 40, paddingRight: 40 }}>
|
||||
<Typography color="error" variant="h6" >Missing plugin.</Typography>
|
||||
<Typography>The plugin '{pedalboardItem.pluginName}' ({pedalboardItem.uri}) is not currently installed.</Typography>
|
||||
</div>
|
||||
} else {
|
||||
return (
|
||||
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { AndroidHostInterface, getAndroidHost } from "./AndroidHost";
|
||||
|
||||
export enum ColorTheme {
|
||||
Light,
|
||||
Dark,
|
||||
System
|
||||
};
|
||||
|
||||
export function getEffectiveColorScheme(): ColorTheme {
|
||||
let androidHost = (window as any).AndroidHost as AndroidHostInterface;
|
||||
if (androidHost) {
|
||||
if (androidHost.isDarkTheme)
|
||||
return androidHost.isDarkTheme() ? ColorTheme.Dark: ColorTheme.Light;
|
||||
return ColorTheme.Dark;
|
||||
}
|
||||
return getColorScheme();
|
||||
|
||||
}
|
||||
|
||||
export function getColorScheme(): ColorTheme {
|
||||
let androidHost = (window as any).AndroidHost as AndroidHostInterface;
|
||||
if (androidHost) {
|
||||
switch (androidHost.getThemePreference() as number)
|
||||
{
|
||||
case 0: return ColorTheme.Light;
|
||||
case 1: return ColorTheme.Dark;
|
||||
default:
|
||||
case 2: return ColorTheme.System;
|
||||
}
|
||||
}
|
||||
switch (localStorage.getItem('colorScheme')) {
|
||||
case null:
|
||||
default:
|
||||
return ColorTheme.Light;
|
||||
case "Light":
|
||||
return ColorTheme.Light;
|
||||
case "Dark":
|
||||
return ColorTheme.Dark;
|
||||
case "System":
|
||||
return ColorTheme.System;
|
||||
}
|
||||
}
|
||||
export function setColorScheme(value: ColorTheme): void {
|
||||
let androidHost = (window as any).AndroidHost as AndroidHostInterface;
|
||||
if (androidHost) {
|
||||
switch (value) {
|
||||
case ColorTheme.Light:
|
||||
androidHost.setThemePreference(0);
|
||||
break;
|
||||
case ColorTheme.Dark:
|
||||
androidHost.setThemePreference(1);
|
||||
break;
|
||||
default:
|
||||
case ColorTheme.System:
|
||||
androidHost.setThemePreference(2);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var storageValue;
|
||||
switch (value) {
|
||||
default:
|
||||
case ColorTheme.Light:
|
||||
storageValue = "Light";
|
||||
break;
|
||||
case ColorTheme.Dark:
|
||||
storageValue = "Dark";
|
||||
break;
|
||||
|
||||
case ColorTheme.System:
|
||||
storageValue = "System";
|
||||
break;
|
||||
}
|
||||
localStorage.setItem("colorScheme", storageValue);
|
||||
}
|
||||
|
||||
var gIsDarkTheme: boolean| undefined = undefined;
|
||||
|
||||
export function isDarkMode(): boolean {
|
||||
if (gIsDarkTheme !== undefined)
|
||||
{
|
||||
return gIsDarkTheme;
|
||||
}
|
||||
var colorTheme = getColorScheme();
|
||||
if (colorTheme === ColorTheme.System) {
|
||||
let androidHost = getAndroidHost();
|
||||
if (androidHost)
|
||||
{
|
||||
if (androidHost.isDarkTheme)
|
||||
{
|
||||
return gIsDarkTheme = androidHost.isDarkTheme();
|
||||
}
|
||||
return gIsDarkTheme = true;
|
||||
}
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return gIsDarkTheme = true;
|
||||
}
|
||||
}
|
||||
return gIsDarkTheme = (colorTheme === ColorTheme.Dark);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { IDialogStackable, pushDialogStack, popDialogStack } from './DialogStack';
|
||||
import Dialog, { DialogProps } from '@mui/material/Dialog';
|
||||
|
||||
|
||||
interface DialogExState {
|
||||
}
|
||||
|
||||
|
||||
interface DialogExProps extends DialogProps {
|
||||
tag: string;
|
||||
fullwidth?: boolean;
|
||||
onEnterKey: () => void;
|
||||
}
|
||||
|
||||
class DialogEx extends React.Component<DialogExProps, DialogExState> implements IDialogStackable {
|
||||
constructor(props: DialogExProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
stateWasPopped: boolean = false;
|
||||
|
||||
getTag() {
|
||||
return this.props.tag;
|
||||
}
|
||||
isOpen(): boolean {
|
||||
return this.props.open;
|
||||
}
|
||||
onDialogStackClose() {
|
||||
this.props.onClose?.({}, "backdropClick");
|
||||
}
|
||||
updateHooks(): void {
|
||||
let wantHooks = this.mounted && this.props.open;
|
||||
if (wantHooks !== this.hasHooks) {
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks) {
|
||||
this.stateWasPopped = false;
|
||||
pushDialogStack(this);
|
||||
} else {
|
||||
popDialogStack(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount?.();
|
||||
|
||||
this.mounted = true;
|
||||
this.updateHooks();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount?.();
|
||||
this.mounted = false;
|
||||
this.updateHooks();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateHooks();
|
||||
}
|
||||
myOnClose(event: {}, reason: "backdropClick" | "escapeKeyDown") {
|
||||
if (this.props.onClose) this.props.onClose(event, reason);
|
||||
}
|
||||
|
||||
handleEnterKey() {
|
||||
if (this.props.onEnterKey) {
|
||||
this.props.onEnterKey();
|
||||
}
|
||||
}
|
||||
onKeyDown(evt: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (evt.key === 'Enter') {
|
||||
this.handleEnterKey();
|
||||
}
|
||||
evt.stopPropagation();
|
||||
}
|
||||
render() {
|
||||
let { tag, onClose, fullWidth, onEnterKey, ...extra } = this.props;
|
||||
return (
|
||||
<Dialog fullWidth={fullWidth ?? false}
|
||||
maxWidth={fullWidth ? false : undefined} {...extra}
|
||||
onClose={(event, reason) => { this.myOnClose(event, reason); }}
|
||||
onKeyDown={(evt) => { this.onKeyDown(evt); }}
|
||||
>
|
||||
{this.props.children}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default DialogEx;
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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,
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
export interface IDialogStackable {
|
||||
getTag: () => string;
|
||||
isOpen: () => boolean;
|
||||
onDialogStackClose(): void;
|
||||
}
|
||||
|
||||
interface DialogStackEntry {
|
||||
dialog: IDialogStackable | null;
|
||||
historyState: DialogStackHistoryState
|
||||
};
|
||||
|
||||
let dialogStack: DialogStackEntry[] = [];
|
||||
|
||||
|
||||
const DLG_STATE_MAGIC = "DlgState438327"
|
||||
export interface DialogStackHistoryState {
|
||||
magic: string;
|
||||
topmost: boolean;
|
||||
stackDepth: number;
|
||||
tag: string;
|
||||
id: string;
|
||||
previousState: DialogStackHistoryState | null;
|
||||
};
|
||||
|
||||
export function pushDialogStack(dialog: IDialogStackable): void {
|
||||
let stackId = Math.random().toString(36).substring(7);
|
||||
let dialogStackState: DialogStackHistoryState = {
|
||||
magic: DLG_STATE_MAGIC,
|
||||
topmost: false,
|
||||
stackDepth: dialogStack.length + 1,
|
||||
tag: dialog.getTag(),
|
||||
id: stackId, previousState:
|
||||
window.history.state as DialogStackHistoryState | null
|
||||
};
|
||||
window.history.replaceState(
|
||||
dialogStackState,
|
||||
"",
|
||||
"#" + dialog.getTag());
|
||||
|
||||
// refresh reloads the page.
|
||||
let topState: DialogStackHistoryState = {
|
||||
topmost: true,
|
||||
magic: DLG_STATE_MAGIC,
|
||||
previousState: dialogStackState,
|
||||
tag: dialog.getTag(),
|
||||
id: stackId,
|
||||
stackDepth: dialogStack.length + 1
|
||||
};
|
||||
|
||||
dialogStack.push(
|
||||
{
|
||||
dialog: dialog,
|
||||
historyState: dialogStackState
|
||||
});
|
||||
|
||||
console.log("pushDialogStack " + stackId + " #" + dialog.getTag());
|
||||
window.history.pushState(
|
||||
topState,
|
||||
"",
|
||||
"#" + dialog.getTag());
|
||||
|
||||
if (window.history.state.id != stackId)
|
||||
{
|
||||
console.log("pushDialogStack: nav deferred.");
|
||||
}
|
||||
if (!gWatchingHistory) {
|
||||
gWatchingHistory = true;
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
}
|
||||
}
|
||||
|
||||
let gWatchingHistory = false;
|
||||
let gPopFromBack = true;
|
||||
|
||||
let handlePopState = (ev: PopStateEvent) => {
|
||||
let evTag: DialogStackHistoryState | null = ev.state as DialogStackHistoryState | null;
|
||||
if (!evTag) return;
|
||||
if (evTag.magic !== DLG_STATE_MAGIC) return;
|
||||
|
||||
ev.stopPropagation();
|
||||
|
||||
console.log("handlePopState: " + evTag?.tag + " " + evTag?.id + " " + gPopFromBack);
|
||||
let id = evTag.id;
|
||||
let ix = dialogStack.length-1;
|
||||
while (ix >= 0) {
|
||||
let entry = dialogStack[ix];
|
||||
if (entry.historyState.id === id) {
|
||||
break;
|
||||
}
|
||||
ix--;
|
||||
}
|
||||
if (ix < 0) {
|
||||
// user must have navigated directly.
|
||||
// so just reload the page.
|
||||
dialogStack = [];
|
||||
window.location.reload();
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
while (dialogStack.length > ix) {
|
||||
let topEntry = dialogStack[dialogStack.length - 1];
|
||||
if (topEntry.dialog) {
|
||||
try {
|
||||
|
||||
if (topEntry.dialog.isOpen()) {
|
||||
topEntry.dialog.onDialogStackClose();
|
||||
}
|
||||
} catch (_e) {
|
||||
|
||||
}
|
||||
topEntry.dialog = null;
|
||||
}
|
||||
dialogStack.pop();
|
||||
}
|
||||
let previousState = evTag.previousState;
|
||||
if (previousState)
|
||||
{
|
||||
let newState = {...previousState, topmost: true};
|
||||
window.history.replaceState(newState,"","#"+previousState.tag);
|
||||
} else {
|
||||
window.history.replaceState(null,"",window.location.pathname);
|
||||
}
|
||||
|
||||
if (dialogStack.length != 0 && gPopFromBack) {
|
||||
if (dialogStack[dialogStack.length -1].dialog === null)
|
||||
{
|
||||
if (window.history.state != null)
|
||||
{
|
||||
console.log("handlePopState: nav back");
|
||||
window.setTimeout(() => { window.history.back(); }, 0);
|
||||
console.log("handlePopState: id= "+ (window.history?.state?.id??"null"));
|
||||
} else {
|
||||
dialogStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
|
||||
export function popDialogStack(dialog: IDialogStackable): void {
|
||||
let ix = dialogStack.length-1;
|
||||
|
||||
while (ix >= 0) {
|
||||
let entry = dialogStack[ix];
|
||||
if (entry.dialog === dialog) {
|
||||
console.log("popDialogStack " + entry.historyState.id + " #" + dialog.getTag());
|
||||
break;
|
||||
}
|
||||
ix--;
|
||||
}
|
||||
if (ix < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
dialogStack[ix].dialog = null;
|
||||
if (dialogStack.length != 0 && dialogStack[dialogStack.length - 1].dialog === null)
|
||||
{
|
||||
console.log("popDialogStack nav back: " + dialogStack[ix].historyState.id );
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Close all dialogs higher in the dialog stack than "tag".
|
||||
export function removeDialogStackEntriesAbove(tag: string) {
|
||||
while (dialogStack.length > 0) {
|
||||
let entry = dialogStack[dialogStack.length - 1];
|
||||
if (entry.dialog && entry.dialog.getTag() === tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialogStack.pop();
|
||||
|
||||
if (entry.dialog && entry.dialog.isOpen()) {
|
||||
entry.dialog.onDialogStackClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { MouseEvent, PointerEvent, ReactNode, Component } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
const SELECT_SCALE = 1.5;
|
||||
|
||||
const AUTOSCROLL_TICK_DELAY = 30;
|
||||
const AUTOSCROLL_THRESHOLD = 48;
|
||||
const AUTOSCROLL_SCROLL_PER_SECOND = 100;
|
||||
const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000;
|
||||
|
||||
const LONG_PRESS_TIME_MS = 250;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative"
|
||||
})
|
||||
});
|
||||
|
||||
type Point = { x: number, y: number };
|
||||
|
||||
export interface DraggableProps extends WithStyles<typeof styles> {
|
||||
onDragStart?: (clientX: number, clientY: number) => void;
|
||||
onDragMove?: (clientX: number, clientY: number) => void;
|
||||
onDragEnd?: (clientX: number, clientY: number) => void;
|
||||
onDragCancel?: (clientX: number, clientY: number) => void;
|
||||
getScrollContainer: () => HTMLDivElement | null | undefined;
|
||||
|
||||
children?: ReactNode | ReactNode[];
|
||||
draggable?: boolean;
|
||||
|
||||
|
||||
}
|
||||
type DraggableState = {
|
||||
};
|
||||
|
||||
|
||||
const Draggable =
|
||||
withStyles(
|
||||
class extends Component<DraggableProps, DraggableState>
|
||||
{
|
||||
|
||||
constructor(props: DraggableProps) {
|
||||
super(props);
|
||||
this.onPointerDownCapture = this.onPointerDownCapture.bind(this);
|
||||
this.onPointerDown = this.onPointerDown.bind(this);
|
||||
this.onPointerCancel = this.onPointerCancel.bind(this);
|
||||
this.onPointerMove = this.onPointerMove.bind(this);
|
||||
this.onPointerUp = this.onPointerUp.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.autoScrollTick = this.autoScrollTick.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
||||
this.handleTouchStart = this.handleTouchStart.bind(this);
|
||||
|
||||
}
|
||||
handleTouchStart(e: any) {
|
||||
if (this.dragStarted) {
|
||||
//e.preventDefault();
|
||||
}
|
||||
}
|
||||
handleTouchMove(e: any) {
|
||||
if (this.dragStarted) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
handleTouchEnd(e: any) {
|
||||
if (this.dragStarted) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
isValidPointer(e: PointerEvent<HTMLDivElement>): boolean {
|
||||
if (e.pointerType === "mouse") {
|
||||
return e.button === 0;
|
||||
} else if (e.pointerType === "pen") {
|
||||
return true;
|
||||
} else if (e.pointerType === "touch") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mouseDown: boolean = false;
|
||||
pointerId: number = 0;
|
||||
pointerType: string = "";
|
||||
|
||||
lastOnDragTime: number = 0;
|
||||
|
||||
onClick(e: MouseEvent<HTMLDivElement>) {
|
||||
// if the click event immediately follows drag end, suppress it.
|
||||
let dt = new Date().getTime() - this.lastOnDragTime;
|
||||
if (dt < 150) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
|
||||
}
|
||||
isCapturedPointer(e: PointerEvent<HTMLDivElement>): boolean {
|
||||
return this.mouseDown
|
||||
&& e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
|
||||
}
|
||||
|
||||
startX: number = 0;
|
||||
startY: number = 0;
|
||||
startClientX: number = 0;
|
||||
startClientY: number = 0;
|
||||
dragStarted: boolean = false;
|
||||
savedIndex: string = "";
|
||||
lastPointerDown: number = 0;
|
||||
originalBounds?: DOMRect;
|
||||
captureElement?: HTMLDivElement;
|
||||
pointerDownTime: number = 0;
|
||||
|
||||
onPointerDownCapture(e: any) {
|
||||
// a new pointer down of any type cancels capture
|
||||
this.cancelDrag();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
longPressTimer?: number = undefined;
|
||||
|
||||
longPressTimerTick() {
|
||||
if (this.pointerType === "touch") {
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([10]);
|
||||
}
|
||||
if (!this.dragStarted) {
|
||||
this.dragTarget!.style.transform = "scale(" + SELECT_SCALE + ")";
|
||||
this.dragTarget!.style.zIndex = "3";
|
||||
}
|
||||
|
||||
this.startDrag();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cancelDrag() {
|
||||
window.removeEventListener("touchmove", this.handleTouchMove);
|
||||
window.removeEventListener("touchend", this.handleTouchEnd);
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = undefined;
|
||||
}
|
||||
|
||||
this.stopAutoScroll();
|
||||
if (this.dragStarted && this.captureElement) {
|
||||
try {
|
||||
this.captureElement.releasePointerCapture(this.pointerId);
|
||||
} catch (error)
|
||||
{
|
||||
// throws if we've already lost it.
|
||||
}
|
||||
|
||||
}
|
||||
this.dragStarted = false;
|
||||
if (this.captureElement) {
|
||||
this.clearDragTransform(this.captureElement);
|
||||
this.captureElement = undefined;
|
||||
}
|
||||
this.mouseDown = false;
|
||||
|
||||
document.body.removeEventListener("pointerdown", this.onPointerDownCapture, true);
|
||||
this.dragTarget = undefined;
|
||||
}
|
||||
|
||||
|
||||
dragTarget?: HTMLDivElement;
|
||||
|
||||
onPointerDown(e: PointerEvent<HTMLDivElement>): void {
|
||||
|
||||
if (this.props.draggable !== undefined && !this.props.draggable) return;
|
||||
|
||||
// any new pointer down cancles a drag in progress.
|
||||
this.cancelDrag();
|
||||
|
||||
// avoid 2nd click of a double click (which is difficult to cancel)
|
||||
let timeMs = new Date().getTime();
|
||||
if (timeMs - this.lastPointerDown < 300) {
|
||||
this.lastPointerDown = timeMs;
|
||||
return;
|
||||
}
|
||||
this.lastPointerDown = timeMs;
|
||||
|
||||
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
|
||||
this.longPressTimer = setTimeout(() => this.longPressTimerTick(), LONG_PRESS_TIME_MS);
|
||||
|
||||
this.dragTarget = e.currentTarget as HTMLDivElement;
|
||||
document.body.addEventListener("pointerdown", this.onPointerDownCapture, true);
|
||||
this.captureElement = e.currentTarget;
|
||||
// e.currentTarget.setPointerCapture(e.pointerId);
|
||||
|
||||
this.mouseDown = true;
|
||||
this.dragStarted = false;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startClientX = e.clientX;
|
||||
this.startClientY = e.clientY;
|
||||
|
||||
this.pointerId = e.pointerId;
|
||||
this.pointerType = e.pointerType;
|
||||
this.savedIndex = e.currentTarget.style.zIndex;
|
||||
if (this.pointerType !== "touch") {
|
||||
this.dragTarget.style.transform = "scale(" + SELECT_SCALE + ")";
|
||||
this.dragTarget.style.zIndex = "3";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearDragTransform(element: HTMLDivElement): void {
|
||||
element.style.transform = "";
|
||||
element.style.zIndex = this.savedIndex;
|
||||
}
|
||||
|
||||
onPointerCancel(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
this.cancelDrag();
|
||||
if (this.props.onDragCancel) {
|
||||
this.props.onDragCancel(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPointerUp(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.dragStarted) {
|
||||
this.lastOnDragTime = new Date().getTime();
|
||||
if (this.props.onDragEnd) {
|
||||
this.props.onDragEnd(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
this.cancelDrag();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dragThresholdExceeded(e: PointerEvent<HTMLDivElement>) {
|
||||
let dx = e.clientX - this.startX;
|
||||
let dy = e.clientY - this.startY;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (this.pointerType === "touch") {
|
||||
return false; // wait for long press.
|
||||
}
|
||||
|
||||
let DRAG_THRESHOLD = 5;
|
||||
if (this.pointerType === "touch") {
|
||||
DRAG_THRESHOLD = 15;
|
||||
}
|
||||
let result = (d2 > DRAG_THRESHOLD * DRAG_THRESHOLD);
|
||||
return result;
|
||||
}
|
||||
|
||||
lastX: number = 0;
|
||||
lastY: number = 0;
|
||||
|
||||
translateViewPortPoint(fromElement: HTMLElement, toElement: HTMLElement, pt: Point): Point {
|
||||
let rcFrom = fromElement.getBoundingClientRect();
|
||||
let rcTo = toElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: pt.x + rcFrom.left + fromElement.scrollLeft - rcTo.left - toElement.scrollLeft,
|
||||
y: pt.y + rcFrom.top + fromElement.scrollTop - rcTo.top - toElement.scrollTop
|
||||
|
||||
};
|
||||
}
|
||||
translateViewPortRect(fromElement: HTMLElement, toElement: HTMLElement, rect: DOMRect): DOMRect {
|
||||
let rcFrom = fromElement.getBoundingClientRect();
|
||||
let rcTo = toElement.getBoundingClientRect();
|
||||
|
||||
return new DOMRect(
|
||||
rect.x + rcFrom.left - fromElement.scrollLeft - rcTo.left + toElement.scrollLeft,
|
||||
rect.y + rcFrom.top - fromElement.scrollTop - rcTo.top + toElement.scrollTop,
|
||||
rect.width,
|
||||
rect.height);
|
||||
}
|
||||
|
||||
|
||||
makeTransform(targetElement: HTMLElement, clientX: number, clientY: number): string {
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (scrollContainer && this.originalBounds) {
|
||||
// the bounds of the translated element must COMPLETELY fit within the scroll range of the container.
|
||||
let newLocation = new DOMRect(
|
||||
this.originalBounds.x + clientX - this.startX,
|
||||
this.originalBounds.y + clientY - this.startY,
|
||||
this.originalBounds.width,
|
||||
this.originalBounds.height);
|
||||
|
||||
|
||||
if (newLocation.x < 0) {
|
||||
clientX -= newLocation.x;
|
||||
} else if (newLocation.right >= scrollContainer.scrollWidth) {
|
||||
clientX -= newLocation.right - scrollContainer.scrollWidth;
|
||||
}
|
||||
if (newLocation.top < 0) {
|
||||
clientY -= newLocation.top;
|
||||
} else if (newLocation.bottom > scrollContainer.scrollHeight) {
|
||||
clientY -= newLocation.bottom - scrollContainer.scrollHeight;
|
||||
}
|
||||
|
||||
}
|
||||
return "translate(" + (clientX - this.startX) + "px," + (clientY - this.startY) + "px) scale(" + SELECT_SCALE + ")";
|
||||
|
||||
}
|
||||
|
||||
createOriginalBounds() {
|
||||
// bounds of the target element, in ScrollContainer coordinates.
|
||||
let rcTarget = this.dragTarget!.getBoundingClientRect();
|
||||
rcTarget.x = 0;
|
||||
rcTarget.y = 0;
|
||||
|
||||
let scrollContainer = this.props.getScrollContainer();
|
||||
if (!scrollContainer) {
|
||||
throw new PiPedalStateError("ScrollContainer reference not valid.");
|
||||
}
|
||||
return this.translateViewPortRect(this.dragTarget!, scrollContainer, rcTarget);
|
||||
}
|
||||
|
||||
startDrag() {
|
||||
window.addEventListener("touchmove", this.handleTouchMove, { passive: false });
|
||||
window.addEventListener("touchend", this.handleTouchEnd, { passive: false });
|
||||
|
||||
|
||||
this.dragStarted = true;
|
||||
if (this.props.onDragStart) {
|
||||
this.props.onDragStart(this.startX, this.startY);
|
||||
}
|
||||
this.dragTarget!.setPointerCapture(this.pointerId);
|
||||
this.originalBounds = this.createOriginalBounds();
|
||||
|
||||
}
|
||||
|
||||
onPointerMove(e: PointerEvent<HTMLDivElement>): void {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
if (!this.dragStarted && this.dragThresholdExceeded(e)) {
|
||||
this.startDrag();
|
||||
|
||||
|
||||
}
|
||||
if (this.dragStarted) {
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
e.currentTarget.style.transform = this.makeTransform(e.currentTarget, e.clientX, e.clientY);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onDragMove) {
|
||||
this.props.onDragMove(e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
this.checkForAutoScroll(e.currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
autoScrollTimer?: number;
|
||||
|
||||
autoScrollTick(currentTarget: HTMLElement, dx: number, dy: number) {
|
||||
if (!this.mouseDown) return;
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (scrollContainer) {
|
||||
let tx = scrollContainer.scrollLeft;
|
||||
let ty = scrollContainer.scrollTop;
|
||||
scrollContainer.scrollBy(dx, dy);
|
||||
this.startX -= (scrollContainer.scrollLeft - tx);
|
||||
this.startY -= (scrollContainer.scrollTop - ty);
|
||||
|
||||
currentTarget.style.transform = this.makeTransform(currentTarget, this.lastX, this.lastY);
|
||||
|
||||
this.checkForAutoScroll(currentTarget);
|
||||
}
|
||||
}
|
||||
stopAutoScroll() {
|
||||
if (this.autoScrollTimer) {
|
||||
cancelAnimationFrame(this.autoScrollTimer);
|
||||
this.autoScrollTimer = undefined;
|
||||
}
|
||||
}
|
||||
startAutoScroll(scrollContainer: HTMLElement, dx: number, dy: number) {
|
||||
this.stopAutoScroll();
|
||||
|
||||
this.autoScrollTimer = requestAnimationFrame(
|
||||
() => { this.autoScrollTick(scrollContainer, dx, dy) });
|
||||
}
|
||||
|
||||
checkForAutoScroll(currentTarget: HTMLElement) {
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (!scrollContainer) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
|
||||
let dy: number = 0;
|
||||
let dx: number = 0;
|
||||
|
||||
if (this.lastX < scrollContainerRect.x + AUTOSCROLL_THRESHOLD) {
|
||||
dx = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dx < -scrollContainer.scrollLeft) {
|
||||
dx = -scrollContainer.scrollLeft;
|
||||
}
|
||||
} else if (this.lastX >= scrollContainerRect.right - AUTOSCROLL_THRESHOLD) {
|
||||
dx = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollWidth - scrollContainer.clientWidth, 0);
|
||||
if (dx > maxScroll) dx = maxScroll;
|
||||
}
|
||||
if (this.lastY < scrollContainerRect.top) {
|
||||
dy = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dy < -scrollContainer.scrollTop) dy = -scrollContainer.scrollTop;
|
||||
} else if (this.lastY >= scrollContainerRect.top - AUTOSCROLL_THRESHOLD) {
|
||||
dy = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
||||
if (dy > maxScroll) dy = maxScroll;
|
||||
}
|
||||
|
||||
if (dx === 0 && dy === 0) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
} else {
|
||||
this.startAutoScroll(currentTarget, dx, dy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
getScrollContainer(): HTMLDivElement | null {
|
||||
if (this.props.getScrollContainer) {
|
||||
let t = this.props.getScrollContainer();
|
||||
if (!t) return null;
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
return (
|
||||
<div className={classes.frame} style={{ transform: "" }}
|
||||
onPointerDown={this.onPointerDown}
|
||||
onPointerMove={this.onPointerMove}
|
||||
onPointerCancel={this.onPointerCancel}
|
||||
onPointerCancelCapture={this.onPointerCancel}
|
||||
onPointerUp={this.onPointerUp}
|
||||
onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default Draggable;
|
||||
@@ -0,0 +1,932 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
import React, { MouseEvent, PointerEvent, Component } from 'react';
|
||||
import { Property } from "csstype";
|
||||
import { nullCast} from './Utility'
|
||||
import {isDarkMode} from './DarkMode';
|
||||
|
||||
const SELECT_SCALE = 1.0;
|
||||
|
||||
const AUTOSCROLL_TICK_DELAY = 30;
|
||||
const AUTOSCROLL_THRESHOLD = 48;
|
||||
const AUTOSCROLL_SCROLL_PER_SECOND = 300;
|
||||
const AUTOSCROLL_SCROLL_RATE = AUTOSCROLL_SCROLL_PER_SECOND * AUTOSCROLL_TICK_DELAY / 1000;
|
||||
|
||||
const LONG_PRESS_TIME_MS = 250;
|
||||
|
||||
|
||||
const ANIMATION_DURATION_MS = 100;
|
||||
|
||||
|
||||
|
||||
class AnimationData {
|
||||
constructor(element: HTMLElement, originalPosition: DOMRect) {
|
||||
this.element = element;
|
||||
this.originalPosition = originalPosition;
|
||||
this.fromY = this.toY = this.currentY = this.originalPosition.y;
|
||||
this.key = (element as any)["key"];
|
||||
}
|
||||
key: any;
|
||||
element: HTMLElement;
|
||||
originalPosition: DOMRect;
|
||||
fromY: number;
|
||||
toY: number;
|
||||
currentY: number;
|
||||
|
||||
animating: boolean = false;
|
||||
|
||||
setPosition(y: number)
|
||||
{
|
||||
this.fromY = this.toY;
|
||||
this.toY = y;
|
||||
this.animating = true;
|
||||
}
|
||||
snapToPosition() {
|
||||
this.fromY = this.toY;
|
||||
this.currentY = this.toY;
|
||||
this.animating = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
type Point = { x: number, y: number };
|
||||
|
||||
export enum ScrollDirection {
|
||||
None, X, Y
|
||||
}
|
||||
export interface DraggableGridProps
|
||||
{
|
||||
fullWidth?: boolean; // defaults to true
|
||||
fullHeight?: boolean; // defaults to true.
|
||||
onDragStart?: (itemIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragMove?: (currentItemIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragEnd?: (fromIndex: number, toIndex: number, clientX: number, clientY: number) => void;
|
||||
onDragCancel?: (clientX: number, clientY: number) => void;
|
||||
|
||||
defaultSelectedIndex?: number;
|
||||
|
||||
onLongPress?: (itemIndex: number) => void;
|
||||
|
||||
canDrag?: boolean;
|
||||
|
||||
children: React.ReactNode[];
|
||||
|
||||
moveElement: (from: number, to: number) => void;
|
||||
scroll: ScrollDirection;
|
||||
|
||||
}
|
||||
|
||||
type DraggableGridState = {
|
||||
indexToSelect?: number;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const DraggableGrid =
|
||||
(
|
||||
class extends Component<DraggableGridProps, DraggableGridState>
|
||||
{
|
||||
refScrollContainer: React.RefObject<HTMLDivElement|null>;
|
||||
refGrid: React.RefObject<HTMLDivElement|null>;
|
||||
|
||||
constructor(props: DraggableGridProps) {
|
||||
super(props);
|
||||
this.refScrollContainer = React.createRef();
|
||||
this.refGrid = React.createRef();
|
||||
|
||||
this.state = {
|
||||
indexToSelect: props.defaultSelectedIndex
|
||||
};
|
||||
|
||||
this.handlePointerDownCapture = this.handlePointerDownCapture.bind(this);
|
||||
this.handlePointerDown = this.handlePointerDown.bind(this);
|
||||
this.handlePointerCancel = this.handlePointerCancel.bind(this);
|
||||
this.handlePointerMove = this.handlePointerMove.bind(this);
|
||||
this.handlePointerUp = this.handlePointerUp.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.handleAutoScrollTick = this.handleAutoScrollTick.bind(this);
|
||||
this.animationTick = this.animationTick.bind(this);
|
||||
this.longPressTimerTick = this.longPressTimerTick.bind(this);
|
||||
this.handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
||||
}
|
||||
|
||||
animationData: AnimationData[] = [];
|
||||
|
||||
|
||||
handleTouchMove(e: any)
|
||||
{
|
||||
if (this.dragStarted)
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
handleTouchEnd(e: any)
|
||||
{
|
||||
if (this.dragStarted)
|
||||
{
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
animationStartTime: number = 0;
|
||||
timerHandle: number| null = null;
|
||||
|
||||
animationTick() {
|
||||
let t = (new Date().getTime() - this.animationStartTime) / ANIMATION_DURATION_MS;
|
||||
if (t > 1) t = 1;
|
||||
|
||||
// easing function.
|
||||
//t = 1-(1-t)*(1-t);
|
||||
let didAnimate: boolean = false;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
let animationData = this.animationData[i];
|
||||
if (animationData.animating)
|
||||
{
|
||||
if (i !== this.startIndex) {
|
||||
//if ((animationData.xFrom !== animationData.xTo) || (animationData.yFrom !== animationData.yTo))
|
||||
let y = t * (animationData.toY - animationData.fromY) + animationData.fromY;
|
||||
let style = animationData.element.style;
|
||||
style.left = "0px";
|
||||
style.top = y-animationData.originalPosition.y + "px";
|
||||
animationData.currentY = y;
|
||||
animationData.animating = animationData.toY !== animationData.fromY;
|
||||
if (animationData.animating) didAnimate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t < 1 && didAnimate) {
|
||||
this.timerHandle = requestAnimationFrame(this.animationTick)
|
||||
} else {
|
||||
this.timerHandle = null;
|
||||
}
|
||||
|
||||
}
|
||||
startAnimation() {
|
||||
this.animationStartTime = new Date().getTime();
|
||||
|
||||
if (this.timerHandle === null) {
|
||||
|
||||
this.timerHandle = requestAnimationFrame(this.animationTick);
|
||||
this.animationTick();
|
||||
}
|
||||
}
|
||||
stopAnimation() {
|
||||
if (this.timerHandle !== null) {
|
||||
cancelAnimationFrame(this.timerHandle);
|
||||
}
|
||||
this.timerHandle = null;
|
||||
}
|
||||
|
||||
isValidPointer(e: PointerEvent<HTMLDivElement>): boolean
|
||||
{
|
||||
if (e.pointerType === "mouse") {
|
||||
return e.button === 0;
|
||||
} else if (e.pointerType === "pen") {
|
||||
return true;
|
||||
} else if (e.pointerType === "touch") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bringSelectedItemIntoView() {
|
||||
if (this.state.indexToSelect && this.state.indexToSelect >= 0)
|
||||
{
|
||||
let grid = this.refGrid.current;
|
||||
if (grid)
|
||||
{
|
||||
if (this.state.indexToSelect >= 0 && this.state.indexToSelect < grid.childNodes.length)
|
||||
{
|
||||
let element = grid.childNodes[this.state.indexToSelect] as HTMLElement;
|
||||
element.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.bringSelectedItemIntoView();
|
||||
// this.refGrid.current!.addEventListener("touchstart",(e)=> this.handleTouchStart(e),{passive: false});
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
//this.refGrid.current!.removeEventListener("touchstart",(e) => this.handleTouchStart(e));
|
||||
this.stopAnimation();
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.mouseDown)
|
||||
{
|
||||
this.bringSelectedItemIntoView();
|
||||
}
|
||||
if (this.mouseDown)
|
||||
{
|
||||
this.reprepareChildren();
|
||||
this.reflowChildren();
|
||||
this.stopAnimation();
|
||||
this.animationTick();
|
||||
}
|
||||
}
|
||||
|
||||
mouseDown: boolean = false;
|
||||
pointerId: number = 0;
|
||||
pointerType: string = "";
|
||||
|
||||
lastOnDragTime: number = 0;
|
||||
|
||||
onClick(e: MouseEvent<HTMLDivElement>) {
|
||||
// if the click event immediately follows drag end, suppress it.
|
||||
let dt = new Date().getTime() - this.lastOnDragTime;
|
||||
if (dt < 150) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
}
|
||||
hasClass(element: HTMLElement, className: string) {
|
||||
let classes = element.classList;
|
||||
for (let i = 0; i < classes.length; ++i) {
|
||||
if (classes[i] === className) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
findGridItem(element: any | null): HTMLDivElement | null {
|
||||
let grid = this.refGrid.current;
|
||||
if(!grid) return null;
|
||||
while (element !== null && element !== grid) {
|
||||
if (element.parentElement === grid) {
|
||||
return element as HTMLDivElement;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
isCapturedPointer(e: PointerEvent<HTMLDivElement>): boolean {
|
||||
return this.mouseDown
|
||||
&& e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
|
||||
}
|
||||
|
||||
startX: number = 0;
|
||||
startY: number = 0;
|
||||
startClientX: number = 0;
|
||||
startClientY: number = 0;
|
||||
dragStarted: boolean = false;
|
||||
savedIndex: string = "";
|
||||
savedBackground: string = "";
|
||||
lastPointerDown: number = 0;
|
||||
|
||||
ptItemStart: DOMPoint = new DOMPoint(0, 0);
|
||||
captureElement?: HTMLDivElement;
|
||||
|
||||
handlePointerDownCapture(e: any) {
|
||||
// a new pointer down of any type cancels capture
|
||||
this.cancelDrag();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
|
||||
|
||||
cancelDrag() {
|
||||
//window.removeEventListener("touchstart",this.handleTouchStart);
|
||||
window.removeEventListener("touchmove",this.handleTouchMove);
|
||||
window.removeEventListener("touchend",this.handleTouchEnd);
|
||||
|
||||
if (this.originalOverscrollBehaviourY)
|
||||
{
|
||||
this.getScrollContainer()!.style.setProperty("overscroll-behaviour-y",this.originalOverscrollBehaviourY);
|
||||
this.originalOverscrollBehaviourY = undefined;
|
||||
}
|
||||
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = undefined;
|
||||
}
|
||||
this.stopAnimation();
|
||||
this.stopAutoScroll();
|
||||
if (this.dragStarted && this.captureElement) {
|
||||
try {
|
||||
this.captureElement.releasePointerCapture(this.pointerId);
|
||||
} catch (error)
|
||||
{
|
||||
// throws if we've already lost it.
|
||||
}
|
||||
|
||||
}
|
||||
this.dragStarted = false;
|
||||
if (this.dragTarget) {
|
||||
this.clearDragTransform(this.dragTarget);
|
||||
this.dragTarget = null;
|
||||
}
|
||||
this.captureElement = undefined;
|
||||
this.mouseDown = false;
|
||||
this.unprepareChildren();
|
||||
|
||||
document.body.removeEventListener("pointerdown", this.handlePointerDownCapture, true);
|
||||
}
|
||||
|
||||
|
||||
prepareChildren() {
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
let childRects = [];
|
||||
|
||||
let gridClientRect = grid.getBoundingClientRect();
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
let rect = child.getBoundingClientRect();
|
||||
rect.x -= gridClientRect.left;
|
||||
rect.y -= gridClientRect.top;
|
||||
childRects[i] = rect;
|
||||
}
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "relative";
|
||||
child.style.left = "0px";
|
||||
child.style.top = "0px";
|
||||
}
|
||||
this.animationData = [];
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
this.animationData[i] = new AnimationData(nodes[i] as HTMLElement, childRects[i]);
|
||||
}
|
||||
grid.style.height = this.gridBounds.height + "px";
|
||||
grid.style.width = this.gridBounds.width + "px";
|
||||
|
||||
}
|
||||
reprepareChildren()
|
||||
{
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "relative";
|
||||
child.style.left = "0px";
|
||||
child.style.top = "0px";
|
||||
}
|
||||
if (nodes.length !== this.animationData.length)
|
||||
{
|
||||
this.cancelDrag();
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let key: any = (nodes[i] as any).key;
|
||||
if (nodes[i] as HTMLElement !== this.animationData[i].element) {
|
||||
console.log("Animation failed: Element " + i + " changed.");
|
||||
}
|
||||
if (key !== this.animationData[i].key) {
|
||||
console.log("Animation failed: Element " + i + " key has changed.");
|
||||
}
|
||||
this.animationData[i].element = nodes[i] as HTMLElement;
|
||||
if (i !== this.startIndex)
|
||||
{
|
||||
this.animationData[i].animating = true;
|
||||
}
|
||||
}
|
||||
grid.style.height = this.gridBounds.height + "px";
|
||||
grid.style.width = this.gridBounds.width + "px";
|
||||
this.animationTick();
|
||||
|
||||
|
||||
}
|
||||
|
||||
getIndexFromPosition(clientX: number, clientY: number)
|
||||
{
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
let gridBounds = grid.getBoundingClientRect();
|
||||
clientX -= gridBounds.left;
|
||||
clientY -= gridBounds.top;
|
||||
|
||||
if (clientY < 0) return -1;
|
||||
for (let i = 0; i < this.animationData.length; ++i)
|
||||
{
|
||||
let item = this.animationData[i];
|
||||
if (clientY >= item.originalPosition.x && clientY < item.originalPosition.bottom)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
|
||||
}
|
||||
getDragIndexFromPosition(clientX: number, clientY: number): number {
|
||||
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
|
||||
let originalBounds = this.animationData[this.startIndex].originalPosition;
|
||||
// Base test on the CENTER point of the currently dragged item.
|
||||
clientX += originalBounds.width / 2 - this.ptItemStart.x;
|
||||
clientY += originalBounds.height / 2 - this.ptItemStart.y;
|
||||
|
||||
let gridBounds = grid.getBoundingClientRect();
|
||||
|
||||
clientX -= gridBounds.x;
|
||||
clientY -= gridBounds.top;
|
||||
|
||||
// clientX/Y is now the position of the center of the currently-dragged item, in the coordinates of the contenst of the scroll view (scroll applied)
|
||||
|
||||
|
||||
if (clientY < 0) return 0;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
let rect = this.animationData[i].originalPosition;
|
||||
|
||||
if (clientY >= rect.top && clientY < rect.bottom)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return this.animationData.length-1;
|
||||
}
|
||||
|
||||
|
||||
unprepareChildren() {
|
||||
if (this.refGrid.current === null) return;
|
||||
let grid = this.refGrid.current;
|
||||
let nodes = grid.childNodes;
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let child = nodes[i] as HTMLDivElement;
|
||||
child.style.position = "";
|
||||
child.style.top = ""
|
||||
child.style.left = "";
|
||||
child.style.width = "";
|
||||
child.style.height = "";
|
||||
}
|
||||
|
||||
grid.style.height = "";
|
||||
grid.style.width = "100%";
|
||||
|
||||
// releast refs and storage.
|
||||
this.dragTarget = null;
|
||||
this.animationData = [];
|
||||
|
||||
}
|
||||
|
||||
dragTarget: HTMLDivElement | null = null;
|
||||
|
||||
elementKeys: any[] = [];
|
||||
|
||||
|
||||
|
||||
clearDragTransform(element: HTMLDivElement): void {
|
||||
element.style.transform = "";
|
||||
element.style.zIndex = this.savedIndex;
|
||||
element.style.background = this.savedBackground;
|
||||
element.style.opacity = "1";
|
||||
|
||||
}
|
||||
|
||||
handlePointerCancel(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
this.cancelDrag();
|
||||
if (this.props.onDragCancel) {
|
||||
this.props.onDragCancel(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerUp(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
|
||||
this.assertKeysNotChanged(this.elementKeys); // make sure that a late render hasn't swapped elements while we're dragging.
|
||||
|
||||
this.handlePointerMove(e);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.dragStarted) {
|
||||
this.lastOnDragTime = new Date().getTime();
|
||||
if (this.props.onDragEnd) {
|
||||
this.props.onDragEnd(
|
||||
this.startIndex, this.currentIndex,
|
||||
e.clientX, e.clientY);
|
||||
}
|
||||
if (this.startIndex !== this.currentIndex) {
|
||||
this.props.moveElement(this.startIndex, this.currentIndex);
|
||||
}
|
||||
}
|
||||
this.cancelDrag();
|
||||
}
|
||||
}
|
||||
|
||||
dragThresholdExceeded(e: PointerEvent<HTMLDivElement>) {
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
let timeMs = new Date().getTime();
|
||||
let dt = timeMs -this.lastPointerDown;
|
||||
if (dt < LONG_PRESS_TIME_MS) return false;
|
||||
}
|
||||
|
||||
let dx = e.clientX - this.startX;
|
||||
let dy = e.clientY - this.startY;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
|
||||
let DRAG_THRESHOLD = 5;
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
DRAG_THRESHOLD = 15;
|
||||
}
|
||||
return (d2 > DRAG_THRESHOLD * DRAG_THRESHOLD);
|
||||
}
|
||||
|
||||
lastX: number = 0;
|
||||
lastY: number = 0;
|
||||
|
||||
translateViewPortPoint(fromElement: HTMLElement, toElement: HTMLElement, pt: Point): Point {
|
||||
let rcFrom = fromElement.getBoundingClientRect();
|
||||
let rcTo = toElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: pt.x + rcFrom.left + fromElement.scrollLeft - rcTo.left - toElement.scrollLeft,
|
||||
y: pt.y + rcFrom.top + fromElement.scrollTop - rcTo.top - toElement.scrollTop
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
makeTransform(targetElement: HTMLElement, clientX: number, clientY: number): string {
|
||||
|
||||
// the bounds of the translated element must COMPLETELY fit within the scroll range of the container.
|
||||
let originalBounds = this.animationData[this.startIndex].originalPosition;
|
||||
|
||||
let newLocation = new DOMRect(
|
||||
originalBounds.x + clientX - this.startX,
|
||||
originalBounds.y + clientY - this.startY,
|
||||
originalBounds.width,
|
||||
originalBounds.height);
|
||||
|
||||
|
||||
if (newLocation.x < 0) {
|
||||
newLocation.x = 0;
|
||||
} else if (newLocation.right >= this.gridBounds.width) {
|
||||
newLocation.x -= newLocation.right - this.gridBounds.width;
|
||||
}
|
||||
|
||||
// chrome: inconsistent scroll height calculations.
|
||||
let maxY = this.gridBounds.height;
|
||||
if (newLocation.top < 0) {
|
||||
newLocation.y = 0;
|
||||
} else if (newLocation.bottom > maxY) {
|
||||
newLocation.y -= newLocation.bottom - maxY;
|
||||
}
|
||||
|
||||
return "translate(" + (newLocation.x - originalBounds.x) + "px,"
|
||||
+ (newLocation.y - originalBounds.y) + "px) scale(" + SELECT_SCALE + ")";
|
||||
}
|
||||
|
||||
createOriginalBounds(): DOMRect {
|
||||
// bounds of the target element, in ScrollContainer coordinates.
|
||||
let rcTarget = nullCast(this.dragTarget).getBoundingClientRect();
|
||||
|
||||
let rcScroll = nullCast(this.getScrollContainer()).getBoundingClientRect();
|
||||
rcTarget.x -= rcScroll.x;
|
||||
rcTarget.y -= rcScroll.y;
|
||||
return rcTarget;
|
||||
}
|
||||
|
||||
startIndex: number = 0;
|
||||
currentIndex: number = 0;
|
||||
|
||||
|
||||
reflowChildren(): void {
|
||||
|
||||
let y = 0;
|
||||
for (let i = 0; i < this.animationData.length; ++i) {
|
||||
if (i === this.startIndex) {
|
||||
if (i === this.currentIndex) {
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (i === this.currentIndex && this.currentIndex < this.startIndex)
|
||||
{
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
let animationItem = this.animationData[i];
|
||||
animationItem.setPosition(y);
|
||||
y += animationItem.originalPosition.height;
|
||||
if (i === this.currentIndex && this.currentIndex > this.startIndex)
|
||||
{
|
||||
y += this.animationData[this.startIndex].originalPosition.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stopAnimation();
|
||||
this.animationStartTime = new Date().getTime();
|
||||
this.animationTick();
|
||||
}
|
||||
|
||||
itemOffsetX: number = 0;
|
||||
itemOffsetY: number = 0;
|
||||
longPressTimer?: number = undefined;
|
||||
|
||||
|
||||
longPressTimerTick() {
|
||||
if (navigator.vibrate)
|
||||
{
|
||||
navigator.vibrate([5]);
|
||||
}
|
||||
if (!this.dragStarted)
|
||||
{
|
||||
this.startDrag();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handlePointerDown(e: PointerEvent<HTMLDivElement>): void {
|
||||
|
||||
if (this.props.canDrag !== undefined && !this.props.canDrag) return;
|
||||
|
||||
// any new pointer down cancels a drag in progress.
|
||||
this.cancelDrag();
|
||||
|
||||
// avoid 2nd click of a double click (which is difficult to cancel)
|
||||
let timeMs = new Date().getTime();
|
||||
if (timeMs - this.lastPointerDown < 300) {
|
||||
this.lastPointerDown = timeMs;
|
||||
return;
|
||||
}
|
||||
this.lastPointerDown = timeMs;
|
||||
|
||||
|
||||
let gridElement = this.findGridItem(e.target);
|
||||
if (gridElement === null) return;
|
||||
|
||||
|
||||
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
|
||||
this.dragTarget = gridElement;
|
||||
|
||||
|
||||
document.body.addEventListener("pointerdown", this.handlePointerDownCapture, true);
|
||||
this.captureElement = e.currentTarget;
|
||||
|
||||
this.elementKeys = this.getElementKeys();
|
||||
this.longPressTimer = setTimeout(this.longPressTimerTick,LONG_PRESS_TIME_MS);
|
||||
this.mouseDown = true;
|
||||
|
||||
///window.addEventListener("touchstart",this.handleTouchStart, {passive: false});
|
||||
|
||||
this.dragStarted = false;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.startClientX = e.clientX;
|
||||
this.startClientY = e.clientY;
|
||||
|
||||
this.pointerId = e.pointerId;
|
||||
this.pointerType = e.pointerType;
|
||||
|
||||
this.savedIndex = gridElement.style.zIndex;
|
||||
gridElement.style.zIndex = "3";
|
||||
this.savedBackground = gridElement.style.background;
|
||||
gridElement.style.background = isDarkMode()? "333": "#EEE";
|
||||
gridElement.style.opacity = "0.8";
|
||||
}
|
||||
}
|
||||
|
||||
originalOverscrollBehaviourY?: string;
|
||||
|
||||
gridBounds: DOMRect = new DOMRect();
|
||||
|
||||
startDrag() {
|
||||
this.dragStarted = true;
|
||||
let grid = this.refGrid.current!;
|
||||
this.gridBounds = grid.getBoundingClientRect();
|
||||
|
||||
window.addEventListener("touchmove",this.handleTouchMove, {passive: false});
|
||||
window.addEventListener("touchend",this.handleTouchEnd, {passive: false});
|
||||
|
||||
this.prepareChildren();
|
||||
|
||||
this.startIndex = this.getIndexFromPosition(this.startX, this.startY);
|
||||
if (this.startIndex === -1)
|
||||
{
|
||||
this.cancelDrag();
|
||||
return;
|
||||
}
|
||||
let t = grid.childNodes[this.startIndex];
|
||||
if (t !== this.dragTarget) {
|
||||
console.count("dragTarget mismatch!");
|
||||
this.startIndex = this.getIndexFromPosition(this.startX,this.startY);
|
||||
this.dragTarget = grid.childNodes[this.startIndex] as HTMLDivElement;
|
||||
}
|
||||
this.currentIndex = this.startIndex;
|
||||
if (this.props.onDragStart) {
|
||||
this.props.onDragStart(this.startIndex, this.startX, this.startY);
|
||||
}
|
||||
this.dragTarget!.setPointerCapture(this.pointerId);
|
||||
|
||||
let rcItem = this.dragTarget!.getBoundingClientRect();
|
||||
this.ptItemStart = new DOMPoint(this.startX - rcItem.x, this.startY - rcItem.y);
|
||||
|
||||
this.originalOverscrollBehaviourY = this.getScrollContainer()!.style.getPropertyValue("overscroll-behaviour-y");
|
||||
this.getScrollContainer()!.style.setProperty("overscroll-behaviour-y","none");
|
||||
|
||||
if (this.pointerType === "touch")
|
||||
{
|
||||
this.startY -= 4; // causes the selected item to "pop"
|
||||
}
|
||||
}
|
||||
|
||||
handlePointerMove(e: PointerEvent<HTMLDivElement>): void {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
if (!this.dragStarted && this.dragThresholdExceeded(e)) {
|
||||
this.startDrag();
|
||||
}
|
||||
if (this.dragStarted) {
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
if (this.dragTarget !== null) {
|
||||
this.dragTarget.style.transform = this.makeTransform(this.dragTarget, e.clientX, e.clientY);
|
||||
|
||||
let index = this.getDragIndexFromPosition(e.clientX, e.clientY);
|
||||
if (index !== this.currentIndex) {
|
||||
this.currentIndex = index;
|
||||
this.reflowChildren();
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.onDragMove) {
|
||||
this.props.onDragMove(this.currentIndex, e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
this.checkForAutoScroll(e.currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
autoScrollTimer?: number;
|
||||
|
||||
|
||||
getElementKeys(): any[] {
|
||||
let grid = nullCast(this.refGrid.current);
|
||||
let children = grid.childNodes;
|
||||
let result: any[] = [];
|
||||
for (let i = 0; i < children.length; ++i)
|
||||
{
|
||||
result[i] = (children[i] as any).key;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
assertKeysNotChanged(oldKeys: any[])
|
||||
{
|
||||
let newKeys = this.getElementKeys();
|
||||
for (let i = 0; i < newKeys.length; ++i)
|
||||
{
|
||||
if (newKeys[i] !== oldKeys[i])
|
||||
{
|
||||
throw new Error("Keys have changed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleAutoScrollTick(currentTarget: HTMLElement, dx: number, dy: number) {
|
||||
if (!this.mouseDown) return;
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (scrollContainer) {
|
||||
let tx = scrollContainer.scrollLeft;
|
||||
let ty = scrollContainer.scrollTop;
|
||||
|
||||
let x = scrollContainer.scrollLeft + dx;
|
||||
let y = scrollContainer.scrollTop + dy;
|
||||
x = Math.min(x,scrollContainer.scrollWidth-scrollContainer.clientWidth);
|
||||
y = Math.min(y,scrollContainer.scrollHeight-scrollContainer.clientHeight);
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
scrollContainer.scrollTo(x, y);
|
||||
this.startX -= (scrollContainer.scrollLeft - tx);
|
||||
this.startY -= (scrollContainer.scrollTop - ty);
|
||||
|
||||
if (this.dragTarget) {
|
||||
this.dragTarget.style.transform = this.makeTransform(this.dragTarget, this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
this.checkForAutoScroll(currentTarget);
|
||||
}
|
||||
}
|
||||
stopAutoScroll() {
|
||||
if (this.autoScrollTimer !== undefined) {
|
||||
clearTimeout(this.autoScrollTimer);
|
||||
this.autoScrollTimer = undefined;
|
||||
}
|
||||
}
|
||||
startAutoScroll(scrollContainer: HTMLElement, dx: number, dy: number) {
|
||||
this.stopAutoScroll();
|
||||
|
||||
this.autoScrollTimer = setTimeout(
|
||||
() => { this.handleAutoScrollTick(scrollContainer, dx, dy) }, AUTOSCROLL_TICK_DELAY);
|
||||
}
|
||||
|
||||
checkForAutoScroll(currentTarget: HTMLElement) {
|
||||
let scrollContainer = this.getScrollContainer();
|
||||
if (!scrollContainer) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let scrollContainerRect = scrollContainer.getBoundingClientRect();
|
||||
|
||||
let dy: number = 0;
|
||||
let dx: number = 0;
|
||||
|
||||
if (this.lastX < scrollContainerRect.x + AUTOSCROLL_THRESHOLD) {
|
||||
dx = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dx < -scrollContainer.scrollLeft) {
|
||||
dx = -scrollContainer.scrollLeft;
|
||||
}
|
||||
} else if (this.lastX >= scrollContainerRect.right - AUTOSCROLL_THRESHOLD) {
|
||||
dx = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollWidth - scrollContainer.clientWidth, 0);
|
||||
if (dx > maxScroll) dx = maxScroll;
|
||||
}
|
||||
if (this.lastY < scrollContainerRect.top) {
|
||||
dy = -AUTOSCROLL_SCROLL_RATE;
|
||||
if (dy < -scrollContainer.scrollTop) dy = -scrollContainer.scrollTop;
|
||||
} else if (this.lastY >= scrollContainerRect.bottom - AUTOSCROLL_THRESHOLD) {
|
||||
dy = AUTOSCROLL_SCROLL_RATE;
|
||||
let maxScroll = Math.max(scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
||||
if (dy > maxScroll) dy = maxScroll;
|
||||
}
|
||||
|
||||
if (dx === 0 && dy === 0) {
|
||||
this.stopAutoScroll();
|
||||
return;
|
||||
} else {
|
||||
this.startAutoScroll(currentTarget, dx, dy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getScrollContainer(): HTMLDivElement | null {
|
||||
return this.refScrollContainer.current;
|
||||
}
|
||||
|
||||
overflowX(): Property.OverflowX {
|
||||
return this.props.scroll === ScrollDirection.X ? "auto" : "hidden";
|
||||
}
|
||||
overflowY(): Property.OverflowX {
|
||||
return this.props.scroll === ScrollDirection.Y ? "auto" : "hidden";
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: "",
|
||||
overflowX: this.overflowX(),
|
||||
overflowY: this.overflowY(),
|
||||
width: this.props.fullWidth??true? "100%": "auto",
|
||||
height: this.props.fullHeight??true? "100%": "auto",
|
||||
}}
|
||||
onPointerDown={this.handlePointerDown}
|
||||
onPointerMove={this.handlePointerMove}
|
||||
onPointerCancel={this.handlePointerCancel}
|
||||
onPointerCancelCapture={this.handlePointerCancel}
|
||||
onPointerUp={this.handlePointerUp}
|
||||
onDragStart={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onClick={this.onClick}
|
||||
ref={this.refScrollContainer}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", position: "relative" }}
|
||||
ref={this.refGrid}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default DraggableGrid;
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2023 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Component, SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle, State } from './PiPedalModel';
|
||||
import ButtonBase from '@mui/material/ButtonBase'
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import { PedalboardItem } from './Pedalboard';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Tooltip from "@mui/material/Tooltip"
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import WithStyles from './WithStyles';
|
||||
import { withTheme } from './WithStyles';
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
frame: css({
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
}),
|
||||
switchTrack: css({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: 14 / 2,
|
||||
zIndex: -1,
|
||||
transition: theme.transitions.create(['opacity', 'background-color'], {
|
||||
duration: theme.transitions.duration.shortest
|
||||
}),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
|
||||
}),
|
||||
controlFrame: css({
|
||||
display: "flex", flexDirection: "column", alignItems: "start", justifyContent: "space-between", height: 116
|
||||
}),
|
||||
displayValue: css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: theme.mainBackground,
|
||||
color: theme.palette.text.secondary,
|
||||
// zIndex: -1,
|
||||
}),
|
||||
titleSection: css({
|
||||
flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 8, marginRight: 0
|
||||
}),
|
||||
midSection: css({
|
||||
flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center"
|
||||
}),
|
||||
editSection: css({
|
||||
flex: "0 0 0", position: "relative", width: 60, height: 28, minHeight: 28
|
||||
})
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
|
||||
export interface FilePropertyControlProps extends WithStyles<typeof styles> {
|
||||
fileProperty: UiFileProperty;
|
||||
pedalboardItem: PedalboardItem;
|
||||
onFileClick: (fileProperty: UiFileProperty, value: string) => void;
|
||||
theme: Theme;
|
||||
}
|
||||
type FilePropertyControlState = {
|
||||
error: boolean;
|
||||
hasValue: boolean;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const FilePropertyControl =
|
||||
withTheme(withStyles(
|
||||
class extends Component<FilePropertyControlProps, FilePropertyControlState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
constructor(props: FilePropertyControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: false,
|
||||
hasValue: false,
|
||||
value: "",
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.onStateChanged = this.onStateChanged.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onStateChanged(state: State) {
|
||||
if (this.mounted) {
|
||||
if (state === State.Ready) {
|
||||
this.subscribeToPatchProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
// private propertyGetHandle?: ListenHandle;
|
||||
|
||||
monitorPropertyHandle?: ListenHandle;
|
||||
|
||||
subscribeToPatchProperty() {
|
||||
// this.propertyGetHandle = this.model.listenForAtomOutput(this.props.instanceId,(instanceId,atomOutput)=>{
|
||||
// // PARSE THE OBJECT!
|
||||
// // this.setState({value: atomOutput?.value as string});
|
||||
// });
|
||||
this.unsubscribeToPatchProperty();
|
||||
this.monitorPropertyHandle = this.model.monitorPatchProperty(this.props.pedalboardItem.instanceId,
|
||||
this.props.fileProperty.patchProperty,
|
||||
(instanceId, property, propertyValue) => {
|
||||
if (propertyValue.otype_ === "Path") {
|
||||
this.setState({ value: propertyValue.value as string, hasValue: true });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
unsubscribeToPatchProperty() {
|
||||
if (this.monitorPropertyHandle !== undefined) {
|
||||
this.model.cancelMonitorPatchProperty(this.monitorPropertyHandle);
|
||||
this.monitorPropertyHandle = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
private mounted: boolean = false;
|
||||
componentDidMount() {
|
||||
this.model.state.addOnChangedHandler(this.onStateChanged);
|
||||
this.mounted = true;
|
||||
this.subscribeToPatchProperty();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.unsubscribeToPatchProperty();
|
||||
this.mounted = false;
|
||||
this.model.state.removeOnChangedHandler(this.onStateChanged);
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyControlProps>, prevState: Readonly<FilePropertyControlState>, snapshot?: any): void {
|
||||
if (prevProps.fileProperty.patchProperty !== this.props.fileProperty.patchProperty
|
||||
|| prevProps.pedalboardItem.instanceId !== this.props.pedalboardItem.instanceId
|
||||
|| prevProps.pedalboardItem.stateUpdateCount !== this.props.pedalboardItem.stateUpdateCount
|
||||
|
||||
) {
|
||||
this.unsubscribeToPatchProperty();
|
||||
this.subscribeToPatchProperty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
inputChanged: boolean = false;
|
||||
|
||||
onDrag(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
onFileClick() {
|
||||
this.props.onFileClick(this.props.fileProperty, this.state.value);
|
||||
}
|
||||
private fileNameOnly(path: string): string {
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
}
|
||||
let extPos = path.lastIndexOf('.');
|
||||
if (extPos < 0 || extPos < slashPos) {
|
||||
extPos = path.length;
|
||||
}
|
||||
|
||||
return path.substring(slashPos, extPos);
|
||||
|
||||
}
|
||||
render() {
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
let fileProperty = this.props.fileProperty;
|
||||
|
||||
let value = "\u00A0";
|
||||
if (this.state.hasValue) {
|
||||
if (this.state.value.length === 0) {
|
||||
value = "<none>";
|
||||
} else {
|
||||
value = this.fileNameOnly(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
let item_width = 264;
|
||||
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{ width: item_width }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
>
|
||||
<Tooltip title={fileProperty.label} placement="top-start" arrow
|
||||
enterDelay={1000} enterNextDelay={1000}
|
||||
>
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: "start"
|
||||
}}> {fileProperty.label}</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection} style={{ width: "100%", paddingLeft: 8 }}>
|
||||
|
||||
<ButtonBase style={{ width: "100%", borderRadius: "4px 4px 0px 0px", overflow: "hidden" }} onClick={() => { this.onFileClick() }} >
|
||||
<div style={{
|
||||
width: "100%", background:
|
||||
isDarkMode() ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.07)",
|
||||
borderRadius: "4px 4px 0px 0px"
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<Typography noWrap={true} style={{ flex: "1 1 100%", textAlign: "start", verticalAlign: "center", paddingTop: 4, paddingBottom: 4, paddingLeft: 4 }}
|
||||
variant='caption'
|
||||
>{value}</Typography>
|
||||
<MoreHorizIcon style={{ flex: "0 0 auto", width: "16px", height: "16px", verticalAlign: "center", opacity: 0.5, marginRight: 4 }} />
|
||||
</div>
|
||||
<div style={{ height: "1px", width: "100%", background: this.props.theme.palette.text.secondary }}> </div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*/}
|
||||
<div className={classes.editSection} >
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
,styles)
|
||||
)
|
||||
;
|
||||
export default FilePropertyControl;
|
||||
@@ -0,0 +1,914 @@
|
||||
// Copyright (c) 2023 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
|
||||
import RenameDialog from './RenameDialog';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MoreIcon from '@mui/icons-material/MoreVert';
|
||||
import { PiPedalModel, PiPedalModelFactory, FileEntry, BreadcrumbEntry, FileRequestResult } from './PiPedalModel';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Button from '@mui/material/Button';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import AudioFileIcon from '@mui/icons-material/AudioFile';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import OldDeleteIcon from './OldDeleteIcon';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
|
||||
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import UploadFileDialog from './UploadFileDialog';
|
||||
import OkCancelDialog from './OkCancelDialog';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
secondaryText: {
|
||||
color: theme.palette.text.secondary
|
||||
},
|
||||
});
|
||||
|
||||
const audioFileExtensions: { [name: string]: boolean } = {
|
||||
".wav": true,
|
||||
".flac": true,
|
||||
".ogg": true,
|
||||
".aac": true,
|
||||
".au": true,
|
||||
".snd": true,
|
||||
".mid": true,
|
||||
".rmi": true,
|
||||
".mp3": true,
|
||||
".mp4": true,
|
||||
".aif": true,
|
||||
".aifc": true,
|
||||
".aiff": true,
|
||||
".ra": true
|
||||
};
|
||||
|
||||
function isAudioFile(filename: string) {
|
||||
let npos = filename.lastIndexOf('.');
|
||||
let nposSlash = filename.lastIndexOf('/');
|
||||
if (nposSlash >= npos) {
|
||||
return false;
|
||||
}
|
||||
let extension = filename.substring(npos);
|
||||
return audioFileExtensions[extension] !== undefined;
|
||||
}
|
||||
|
||||
export interface FilePropertyDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
fileProperty: UiFileProperty,
|
||||
instanceId: number,
|
||||
selectedFile: string,
|
||||
onOk: (fileProperty: UiFileProperty, selectedItem: string) => void,
|
||||
onCancel: () => void
|
||||
};
|
||||
export interface FilePropertyDialogState {
|
||||
fullScreen: boolean;
|
||||
selectedFile: string;
|
||||
selectedFileIsDirectory: boolean;
|
||||
selectedFileProtected: boolean;
|
||||
hasSelection: boolean;
|
||||
canDelete: boolean;
|
||||
fileResult: FileRequestResult;
|
||||
navDirectory: string;
|
||||
uploadDirectory: string;
|
||||
isProtectedDirectory: boolean;
|
||||
columns: number;
|
||||
columnWidth: number;
|
||||
openUploadFileDialog: boolean;
|
||||
openConfirmDeleteDialog: boolean;
|
||||
menuAnchorEl: null | HTMLElement;
|
||||
newFolderDialogOpen: boolean;
|
||||
renameDialogOpen: boolean;
|
||||
moveDialogOpen: boolean;
|
||||
};
|
||||
|
||||
function pathExtension(path: string) {
|
||||
let dotPos = path.lastIndexOf('.');
|
||||
if (dotPos === -1) return "";
|
||||
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos !== -1) {
|
||||
if (dotPos <= slashPos + 1) return "";
|
||||
}
|
||||
return path.substring(dotPos); // include the '.'.
|
||||
|
||||
}
|
||||
function pathParentDirectory(path: string) {
|
||||
let npos = path.lastIndexOf('/');
|
||||
if (npos === -1) return "";
|
||||
return path.substring(0, npos);
|
||||
}
|
||||
function pathConcat(left: string, right: string) {
|
||||
if (left === "") return right;
|
||||
if (right === "") return left;
|
||||
if (left.endsWith('/')) {
|
||||
left = left.substring(0, left.length - 1);
|
||||
}
|
||||
if (right.startsWith("/")) {
|
||||
right = right.substring(1);
|
||||
}
|
||||
return left + "/" + right;
|
||||
}
|
||||
export function pathFileNameOnly(path: string): string {
|
||||
if (path === "..") return path;
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
}
|
||||
let extPos = path.lastIndexOf('.');
|
||||
if (extPos < 0 || extPos < slashPos) {
|
||||
extPos = path.length;
|
||||
}
|
||||
|
||||
return path.substring(slashPos, extPos);
|
||||
}
|
||||
|
||||
export function pathFileName(path: string): string {
|
||||
if (path === "..") return path;
|
||||
let slashPos = path.lastIndexOf('/');
|
||||
if (slashPos < 0) {
|
||||
slashPos = 0;
|
||||
} else {
|
||||
++slashPos;
|
||||
}
|
||||
return path.substring(slashPos);
|
||||
}
|
||||
|
||||
|
||||
export default withStyles(
|
||||
class FilePropertyDialog extends ResizeResponsiveComponent<FilePropertyDialogProps, FilePropertyDialogState> {
|
||||
|
||||
getNavDirectoryFromFile(selectedFile: string, fileProperty: UiFileProperty): string {
|
||||
if (selectedFile === "") {
|
||||
return "";
|
||||
}
|
||||
return pathParentDirectory(selectedFile);
|
||||
}
|
||||
|
||||
constructor(props: FilePropertyDialogProps) {
|
||||
super(props);
|
||||
|
||||
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
let selectedFile = props.selectedFile;
|
||||
this.state = {
|
||||
fullScreen: this.getFullScreen(),
|
||||
selectedFile: selectedFile,
|
||||
selectedFileProtected: true,
|
||||
selectedFileIsDirectory: false,
|
||||
navDirectory: this.getNavDirectoryFromFile(selectedFile, props.fileProperty),
|
||||
uploadDirectory: "",
|
||||
hasSelection: false,
|
||||
canDelete: false,
|
||||
columns: 0,
|
||||
columnWidth: 1,
|
||||
fileResult: new FileRequestResult(),
|
||||
isProtectedDirectory: true,
|
||||
openUploadFileDialog: false,
|
||||
openConfirmDeleteDialog: false,
|
||||
menuAnchorEl: null,
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false
|
||||
};
|
||||
this.requestScroll = true;
|
||||
}
|
||||
getFullScreen() {
|
||||
return window.innerWidth < 450 || window.innerHeight < 450;
|
||||
}
|
||||
|
||||
private scrollRef: HTMLDivElement | null = null;
|
||||
|
||||
onScrollRef(element: HTMLDivElement | null) {
|
||||
this.scrollRef = element;
|
||||
this.maybeScrollIntoView();
|
||||
}
|
||||
private maybeScrollIntoView() {
|
||||
if (this.scrollRef !== null && this.state.fileResult.files.length !== 0 && this.mounted && this.requestScroll) {
|
||||
this.requestScroll = false;
|
||||
let options: ScrollIntoViewOptions = { block: "nearest" };
|
||||
options.block = "nearest";
|
||||
|
||||
this.scrollRef.scrollIntoView(options);
|
||||
}
|
||||
}
|
||||
private mounted: boolean = false;
|
||||
private model: PiPedalModel;
|
||||
|
||||
private requestFiles(navPath: string) {
|
||||
if (!this.props.open) {
|
||||
return;
|
||||
}
|
||||
if (this.props.fileProperty.directory === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.requestFileList2(navPath, this.props.fileProperty)
|
||||
.then((filesResult) => {
|
||||
if (this.mounted) {
|
||||
// let insertionPoint = files.length;
|
||||
// for (let i = 0; i < files.length; ++i) {
|
||||
// if (!files[i].isDirectory) {
|
||||
// insertionPoint = i;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
filesResult.files.splice(0, 0, { pathname: "", displayName: "<none>", isDirectory: false, isProtected: true });
|
||||
|
||||
let fileEntry = this.getFileEntry(filesResult.files, this.state.selectedFile);
|
||||
this.setState({
|
||||
isProtectedDirectory: filesResult.isProtected,
|
||||
fileResult: filesResult,
|
||||
hasSelection: !!fileEntry,
|
||||
selectedFileProtected: fileEntry ? fileEntry.isProtected: true,
|
||||
navDirectory: navPath,
|
||||
uploadDirectory: filesResult.currentDirectory
|
||||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (!this.mounted) return;
|
||||
if (navPath !== "") // deleted sample directory maybe?
|
||||
{
|
||||
this.navigate("");
|
||||
} else {
|
||||
this.model.showAlert(error.toString())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private lastDivRef: HTMLDivElement | null = null;
|
||||
|
||||
onMeasureRef(div: HTMLDivElement | null) {
|
||||
this.lastDivRef = div;
|
||||
if (div) {
|
||||
let width = div.offsetWidth;
|
||||
if (width === 0) return;
|
||||
let columns = 1;
|
||||
let columnWidth = (width - 40) / columns;
|
||||
if (columns !== this.state.columns || columnWidth !== this.state.columnWidth) {
|
||||
this.setState({ columns: columns, columnWidth: columnWidth });
|
||||
}
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
|
||||
this.setState({
|
||||
fullScreen: this.getFullScreen()
|
||||
})
|
||||
if (this.lastDivRef !== null) {
|
||||
this.onMeasureRef(this.lastDivRef);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private requestScroll: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.requestFiles(this.state.navDirectory)
|
||||
this.requestScroll = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
this.lastDivRef = null;
|
||||
}
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDialogProps>, prevState: Readonly<FilePropertyDialogState>, snapshot?: any): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
if (prevProps.open !== this.props.open || prevProps.fileProperty !== this.props.fileProperty || prevProps.selectedFile !== this.props.selectedFile) {
|
||||
if (this.props.open) {
|
||||
let selectedFile = this.props.selectedFile;
|
||||
let navDirectory = this.getNavDirectoryFromFile(selectedFile, this.props.fileProperty);
|
||||
this.setState({
|
||||
selectedFile: selectedFile,
|
||||
selectedFileIsDirectory: false,
|
||||
selectedFileProtected: true,
|
||||
newFolderDialogOpen: false,
|
||||
renameDialogOpen: false,
|
||||
moveDialogOpen: false,
|
||||
navDirectory: navDirectory
|
||||
});
|
||||
this.requestFiles(navDirectory)
|
||||
this.requestScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private isDirectory(path: string): boolean {
|
||||
for (var fileEntry of this.state.fileResult.files) {
|
||||
if (fileEntry.pathname === path) {
|
||||
return fileEntry.isDirectory;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private isFileInList(files: FileEntry[], file: string) {
|
||||
|
||||
let hasSelection = false;
|
||||
if (file === "") return true;
|
||||
for (var listFile of files) {
|
||||
if (listFile.pathname === file) {
|
||||
hasSelection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasSelection;
|
||||
}
|
||||
private getFileEntry(files: FileEntry[], file: string): FileEntry | undefined {
|
||||
|
||||
if (file === "") return undefined;
|
||||
for (var listFile of files) {
|
||||
if (listFile.pathname === file) {
|
||||
return listFile;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onSelectValue(fileEntry: FileEntry) {
|
||||
this.requestScroll = true;
|
||||
this.setState({
|
||||
selectedFile: fileEntry.pathname,
|
||||
selectedFileIsDirectory: fileEntry.isDirectory,
|
||||
selectedFileProtected: fileEntry.isProtected,
|
||||
hasSelection: this.isFileInList(this.state.fileResult.files, fileEntry.pathname)
|
||||
})
|
||||
}
|
||||
onDoubleClickValue(selectedFile: string) {
|
||||
this.openSelectedFile();
|
||||
}
|
||||
|
||||
handleMenuOpen(event: React.MouseEvent<HTMLElement>) {
|
||||
this.setState({ menuAnchorEl: event.currentTarget });
|
||||
|
||||
}
|
||||
handleMenuClose() {
|
||||
this.setState({ menuAnchorEl: null });
|
||||
}
|
||||
handleDelete() {
|
||||
if (this.state.selectedFileProtected) {
|
||||
return;
|
||||
}
|
||||
this.setState({ openConfirmDeleteDialog: true });
|
||||
}
|
||||
handleConfirmDelete() {
|
||||
if (this.state.selectedFileProtected) {
|
||||
return;
|
||||
}
|
||||
this.setState({ openConfirmDeleteDialog: false });
|
||||
if (this.state.hasSelection) {
|
||||
let selectedFile = this.state.selectedFile;
|
||||
let position = -1;
|
||||
for (let i = 0; i < this.state.fileResult.files.length; ++i) {
|
||||
let file = this.state.fileResult.files[i];
|
||||
if (file.pathname === selectedFile) {
|
||||
position = i;
|
||||
}
|
||||
}
|
||||
let newSelection: FileEntry | undefined = undefined;
|
||||
if (position >= 0 && position < this.state.fileResult.files.length - 1) {
|
||||
newSelection = this.state.fileResult.files[position + 1];
|
||||
} else if (position === this.state.fileResult.files.length - 1) {
|
||||
if (position !== 0) {
|
||||
newSelection = this.state.fileResult.files[position - 1];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.model.deleteUserFile(this.state.selectedFile)
|
||||
.then(
|
||||
() => {
|
||||
if (newSelection)
|
||||
{
|
||||
this.setState({
|
||||
selectedFile: newSelection.pathname,
|
||||
selectedFileIsDirectory: newSelection.isDirectory,
|
||||
selectedFileProtected: newSelection.isProtected,
|
||||
hasSelection: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedFile: "",
|
||||
selectedFileIsDirectory: false,
|
||||
selectedFileProtected: true,
|
||||
hasSelection: false
|
||||
});
|
||||
|
||||
}
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}
|
||||
).catch(
|
||||
(e: any) => {
|
||||
this.model.showAlert(e.toString());
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
navigate(relativeDirectory: string) {
|
||||
this.requestFiles(relativeDirectory);
|
||||
this.setState({ navDirectory: relativeDirectory });
|
||||
|
||||
}
|
||||
renderBreadcrumbs() {
|
||||
if (this.state.navDirectory === "") {
|
||||
return (<Divider />);
|
||||
}
|
||||
let breadcrumbs: React.ReactElement[] = [(
|
||||
<Button variant="text"
|
||||
color="inherit"
|
||||
key="h" onClick={() => { this.navigate(""); }}
|
||||
style={{ textTransform: "none", flexShrink: 0 }}
|
||||
startIcon={(<HomeIcon sx={{ mr: 0.6 }} fontSize="inherit" />)}
|
||||
>
|
||||
<Typography style={{}} variant="body2" noWrap>Home</Typography>
|
||||
</Button>
|
||||
)
|
||||
];
|
||||
|
||||
for (let i = 1; i < this.state.fileResult.breadcrumbs.length - 1; ++i) {
|
||||
let breadcrumb: BreadcrumbEntry = this.state.fileResult.breadcrumbs[i];
|
||||
|
||||
breadcrumbs.push((
|
||||
<Typography key={"x" + (i)} variant="body2" style={{ marginTop: 6, marginBottom: 6 }} noWrap>/</Typography>
|
||||
|
||||
)
|
||||
);
|
||||
breadcrumbs.push((
|
||||
<Button variant="text"
|
||||
key={"bc" + i}
|
||||
color="inherit"
|
||||
onClick={() => { this.navigate(breadcrumb.pathname) }}
|
||||
style={{ minWidth: 0, textTransform: "none", flexShrink: 0 }}
|
||||
>
|
||||
<Typography variant="body2" style={{}} noWrap>{breadcrumb.displayName}</Typography>
|
||||
|
||||
|
||||
</Button>
|
||||
));
|
||||
}
|
||||
if (this.state.fileResult.breadcrumbs.length > 1) {
|
||||
let lastdirectory = this.state.fileResult.breadcrumbs[this.state.fileResult.breadcrumbs.length - 1];
|
||||
breadcrumbs.push((
|
||||
<Typography key={"xLast"} variant="body2" style={{ userSelect: "none", marginTop: 6, marginBottom: 6 }} noWrap>/</Typography>
|
||||
|
||||
)
|
||||
);
|
||||
breadcrumbs.push((
|
||||
<div key={"bc-last"} style={{ flexShrink: 0, paddingLeft: 8, paddingRight: 8, paddingTop: 6, paddingBottom: 6, overflowX: "clip" }}>
|
||||
<Typography noWrap
|
||||
style={{}} variant="body2"
|
||||
color="text.secondary"> {lastdirectory.displayName}</Typography>
|
||||
</div>
|
||||
));
|
||||
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div aria-label="breadcrumb"
|
||||
style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}>
|
||||
{breadcrumbs}
|
||||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultPath(): string {
|
||||
try {
|
||||
let storage = window.localStorage;
|
||||
let result = storage.getItem("fpDefaultPath");
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return this.state.navDirectory;
|
||||
}
|
||||
setDefaultPath(path: string) {
|
||||
try {
|
||||
let storage = window.localStorage;
|
||||
storage.setItem("fpDefaultPath", path);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
hasSelectedFileOrFolder(): boolean {
|
||||
return this.state.hasSelection && this.state.selectedFile !== "";
|
||||
}
|
||||
getFileExtensionList(uiFileProperty: UiFileProperty): string {
|
||||
let result = "";
|
||||
for (var fileType of uiFileProperty.fileTypes) {
|
||||
if (fileType.fileExtension !== "" && fileType.fileExtension !== ".zip") {
|
||||
if (result !== "") result = result + ",";
|
||||
result += fileType.fileExtension;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private getIcon(fileEntry: FileEntry) {
|
||||
if (fileEntry.pathname === "") {
|
||||
return (<InsertDriveFileOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.5, marginRight: 8, marginLeft: 8 }} />);
|
||||
}
|
||||
if (fileEntry.isDirectory) {
|
||||
return (
|
||||
<FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
|
||||
);
|
||||
}
|
||||
if (isAudioFile(fileEntry.pathname)) {
|
||||
return (<AudioFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />);
|
||||
}
|
||||
return (<InsertDriveFileIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />);
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let columnWidth = this.state.columnWidth;
|
||||
let okButtonText = "Select";
|
||||
if (this.state.hasSelection && this.state.selectedFileIsDirectory) {
|
||||
okButtonText = "Open";
|
||||
}
|
||||
let protectedDirectory = this.state.fileResult.isProtected;
|
||||
let protectedItem = true;
|
||||
if (this.state.hasSelection)
|
||||
{
|
||||
protectedItem = this.state.selectedFileProtected;
|
||||
}
|
||||
let canMoveOrRename = this.hasSelectedFileOrFolder() && !protectedItem;
|
||||
|
||||
return this.props.open &&
|
||||
(
|
||||
<DialogEx
|
||||
fullScreen={this.state.fullScreen}
|
||||
onClose={() => {
|
||||
this.props.onCancel();
|
||||
}}
|
||||
onEnterKey={() => {
|
||||
this.openSelectedFile();
|
||||
}}
|
||||
open={this.props.open} tag="fileProperty"
|
||||
fullWidth maxWidth="md"
|
||||
PaperProps={
|
||||
this.state.fullScreen ?
|
||||
{}
|
||||
:
|
||||
{
|
||||
style: {
|
||||
minHeight: "90%",
|
||||
maxHeight: "90%",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{ paddingBottom: 0 }} >
|
||||
<Toolbar style={{ padding: 0 }}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="back"
|
||||
style={{ opacity: 0.6 }}
|
||||
onClick={() => { this.props.onCancel(); }}
|
||||
>
|
||||
<ArrowBackIcon style={{ width: 24, height: 24 }} />
|
||||
</IconButton>
|
||||
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||
{this.props.fileProperty.label}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
aria-label="new folder"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
style={{ opacity: 0.6, marginRight: 8 }}
|
||||
onClick={(ev) => { this.onNewFolder(); }}
|
||||
disabled={protectedDirectory}
|
||||
>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
|
||||
|
||||
<IconButton
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
style={{ opacity: 0.6 }}
|
||||
onClick={(ev) => { this.handleMenuOpen(ev); }}
|
||||
disabled={protectedDirectory}
|
||||
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={this.state.menuAnchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={this.state.menuAnchorEl !== null}
|
||||
onClose={() => { this.handleMenuClose(); }}
|
||||
>
|
||||
<MenuItem onClick={() => { this.handleMenuClose(); this.onNewFolder(); }}>New folder</MenuItem>
|
||||
{canMoveOrRename && (<Divider />)}
|
||||
{canMoveOrRename && (<MenuItem onClick={() => { this.handleMenuClose(); this.onMove(); }}>Move</MenuItem>)}
|
||||
{canMoveOrRename && this.hasSelectedFileOrFolder() && !protectedItem && (<MenuItem onClick={() => { this.handleMenuClose(); this.onRename(); }}>Rename</MenuItem>)}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
<div style={{ flex: "0 0 auto", marginLeft: 0, marginRight: 0, overflowX: "clip" }}>
|
||||
{this.renderBreadcrumbs()}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ paddingLeft: 0, paddingRight: 0, paddingTop: 0, colorScheme: (isDarkMode() ? "dark" : "light") }}>
|
||||
|
||||
<div
|
||||
ref={(element) => this.onMeasureRef(element)}
|
||||
style={{
|
||||
flex: "1 1 100%", display: "flex", flexFlow: "row wrap",
|
||||
justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16
|
||||
}}>
|
||||
{
|
||||
(this.state.columns !== 0) && // don't render until we have number of columns derived from layout.
|
||||
this.state.fileResult.files.map(
|
||||
(value: FileEntry, index: number) => {
|
||||
let displayValue = value.displayName;
|
||||
if (displayValue === "") {
|
||||
displayValue = "<none>";
|
||||
}
|
||||
let selected = value.pathname === this.state.selectedFile;
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
if (isDarkMode()) {
|
||||
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
|
||||
}
|
||||
let scrollRef: ((element: HTMLDivElement | null) => void) | undefined = undefined;
|
||||
if (selected) {
|
||||
scrollRef = (element) => { this.onScrollRef(element) };
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonBase key={value.pathname}
|
||||
|
||||
style={{ width: columnWidth, flex: "0 0 auto", height: 48, position: "relative" }}
|
||||
onClick={() => this.onSelectValue(value)}
|
||||
onDoubleClick={() => { this.onDoubleClickValue(value.pathname); }}
|
||||
>
|
||||
<div ref={scrollRef} style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", width: "100%", height: "100%" }}>
|
||||
{this.getIcon(value)}
|
||||
<Typography noWrap className={classes.secondaryText} variant="body2" style={{ flex: "1 1 auto", textAlign: "left" }}>{displayValue}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions style={{ justifyContent: "stretch" }}>
|
||||
<div style={{ display: "flex", width: "100%", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<IconButton style={{ visibility: (this.state.hasSelection ? "visible" : "hidden") }} aria-label="delete" component="label" color="primary"
|
||||
disabled={!this.state.hasSelection || this.state.selectedFile === "" || protectedItem}
|
||||
onClick={() => this.handleDelete()} >
|
||||
<OldDeleteIcon fontSize='small' />
|
||||
</IconButton>
|
||||
|
||||
<Button style={{ flex: "0 0 auto" }} aria-label="upload" variant="text" startIcon={<FileUploadIcon />}
|
||||
onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory}
|
||||
>
|
||||
<div>Upload</div>
|
||||
</Button>
|
||||
<div style={{ flex: "1 1 auto" }}> </div>
|
||||
|
||||
<Button variant="dialogSecondary" onClick={() => { this.props.onCancel(); }} aria-label="cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" style={{ flex: "0 0 auto" }}
|
||||
onClick={() => { this.openSelectedFile(); }}
|
||||
|
||||
disabled={(!this.state.hasSelection) && this.state.selectedFile !== ""} aria-label="select"
|
||||
>
|
||||
{okButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
<UploadFileDialog
|
||||
open={this.state.openUploadFileDialog}
|
||||
onClose=
|
||||
{
|
||||
() => {
|
||||
this.setState({ openUploadFileDialog: false });
|
||||
}
|
||||
}
|
||||
uploadPage={
|
||||
"uploadUserFile?directory=" + encodeURIComponent(this.state.uploadDirectory)
|
||||
+ "&id=" + this.props.instanceId.toString()
|
||||
+ "&property=" + encodeURIComponent(this.props.fileProperty.patchProperty)
|
||||
+ "&ext="
|
||||
+ encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
|
||||
}
|
||||
onUploaded={() => { this.requestFiles(this.state.navDirectory); }}
|
||||
fileProperty={this.props.fileProperty}
|
||||
|
||||
|
||||
/>
|
||||
<OkCancelDialog open={this.state.openConfirmDeleteDialog}
|
||||
text={
|
||||
(this.isDirectory(this.state.selectedFile))
|
||||
? "Are you sure you want to delete the selected directory and all its contents?"
|
||||
: "Are you sure you want to delete the selected file?"}
|
||||
okButtonText="Delete"
|
||||
onOk={() => this.handleConfirmDelete()}
|
||||
onClose={() => {
|
||||
this.setState({ openConfirmDeleteDialog: false });
|
||||
}}
|
||||
|
||||
/>
|
||||
{
|
||||
this.state.newFolderDialogOpen && (
|
||||
<RenameDialog open={this.state.newFolderDialogOpen} defaultName=""
|
||||
onOk={(newName) => { this.setState({ newFolderDialogOpen: false }); this.onExecuteNewFolder(newName) }}
|
||||
onClose={() => { this.setState({ newFolderDialogOpen: false }); }}
|
||||
acceptActionName="OK"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.renameDialogOpen && (
|
||||
<RenameDialog open={this.state.renameDialogOpen} defaultName={this.renameDefaultName()}
|
||||
onOk={(newName) => { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }}
|
||||
onClose={() => { this.setState({ renameDialogOpen: false }); }}
|
||||
acceptActionName="OK"
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.moveDialogOpen && (
|
||||
(
|
||||
<FilePropertyDirectorySelectDialog
|
||||
open={this.state.moveDialogOpen}
|
||||
dialogTitle="Move to"
|
||||
uiFileProperty={this.props.fileProperty}
|
||||
defaultPath={this.getDefaultPath()}
|
||||
selectedFile={this.state.selectedFile}
|
||||
excludeDirectory={
|
||||
this.isDirectory(this.state.selectedFile)
|
||||
? this.state.selectedFile : ""}
|
||||
onClose={() => { this.setState({ moveDialogOpen: false }); }}
|
||||
onOk={
|
||||
(path) => {
|
||||
this.setState({ moveDialogOpen: false });
|
||||
this.setDefaultPath(path);
|
||||
this.onExecuteMove(path);
|
||||
}
|
||||
}
|
||||
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
openSelectedFile(): void {
|
||||
if (this.isDirectory(this.state.selectedFile)) {
|
||||
this.requestFiles(this.state.selectedFile);
|
||||
this.setState({ navDirectory: this.state.selectedFile });
|
||||
} else {
|
||||
this.props.onOk(this.props.fileProperty, this.state.selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
private renameDefaultName(): string {
|
||||
let name = this.state.selectedFile;
|
||||
if (name === "") return "";
|
||||
if (this.isDirectory(name)) {
|
||||
return pathFileName(name);
|
||||
} else {
|
||||
return pathFileNameOnly(name);
|
||||
}
|
||||
}
|
||||
private onMove(): void {
|
||||
this.setState({ moveDialogOpen: true });
|
||||
}
|
||||
private onExecuteMove(newDirectory: string) {
|
||||
let fileName = pathFileName(this.state.selectedFile);
|
||||
let oldFilePath = pathConcat(this.state.navDirectory, fileName);
|
||||
let newFilePath = pathConcat(newDirectory, fileName);
|
||||
|
||||
this.model.renameFilePropertyFile(oldFilePath, newFilePath, this.props.fileProperty)
|
||||
.then(() => {
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private onRename(): void {
|
||||
this.setState({ renameDialogOpen: true });
|
||||
}
|
||||
private onExecuteRename(newName: string) {
|
||||
let newPath: string = "";
|
||||
let oldPath: string = "";
|
||||
if (this.isDirectory(this.state.selectedFile)) {
|
||||
let oldName = pathFileName(this.state.selectedFile);
|
||||
if (oldName === newName) return;
|
||||
oldPath = pathConcat(this.state.navDirectory, oldName);
|
||||
newPath = pathConcat(this.state.navDirectory, newName);;
|
||||
} else {
|
||||
let oldName = pathFileNameOnly(this.state.selectedFile);
|
||||
if (oldName === newName) return;
|
||||
let extension = pathExtension(this.state.selectedFile);
|
||||
oldPath = pathConcat(this.state.navDirectory, oldName + extension);
|
||||
newPath = pathConcat(this.state.navDirectory, newName + extension);
|
||||
}
|
||||
this.model.renameFilePropertyFile(oldPath, newPath, this.props.fileProperty)
|
||||
.then((newPath) => {
|
||||
this.setState({ selectedFile: newPath });
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
this.requestScroll = true
|
||||
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private onNewFolder(): void {
|
||||
this.setState({ newFolderDialogOpen: true });
|
||||
}
|
||||
|
||||
private onExecuteNewFolder(newName: string) {
|
||||
this.model.createNewSampleDirectory(pathConcat(this.state.navDirectory, newName), this.props.fileProperty)
|
||||
.then((newPath) => {
|
||||
this.setState({
|
||||
selectedFile: newPath,
|
||||
selectedFileIsDirectory:
|
||||
true,selectedFileProtected: false
|
||||
});
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
this.requestScroll = true
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
@@ -0,0 +1,352 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiFileProperty } from './Lv2Plugin';
|
||||
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Divider from '@mui/material/Divider';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
|
||||
class DirectoryTree {
|
||||
name: string = "";
|
||||
path: string = "";
|
||||
expanded: boolean = false;
|
||||
isProtected: boolean = true;
|
||||
children: DirectoryTree[] = [];
|
||||
|
||||
constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) {
|
||||
this.name = tree.directoryName;
|
||||
this.path = tree.directoryName;
|
||||
this.name = tree.displayName;
|
||||
this.isProtected = tree.isProtected;
|
||||
for (var treeChild of tree.children) {
|
||||
this.children.push(new DirectoryTree(treeChild, this));
|
||||
}
|
||||
}
|
||||
canExpand(): boolean {
|
||||
return this.children.length !== 0;
|
||||
}
|
||||
expand(path: string): void {
|
||||
this.expanded = true;
|
||||
|
||||
let currentNode: DirectoryTree = this;
|
||||
for (var segment of path.split('/')) {
|
||||
let found = false;
|
||||
for (var child of currentNode.children) {
|
||||
if (child.name === segment) {
|
||||
found = true;
|
||||
if (child.children.length !== 0) {
|
||||
child.expanded = true;
|
||||
}
|
||||
currentNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static remove_(directoryTree: DirectoryTree, path: string): boolean
|
||||
{
|
||||
for (let i = 0; i < directoryTree.children.length; ++i)
|
||||
{
|
||||
let child = directoryTree.children[i];
|
||||
if (child.path === path)
|
||||
{
|
||||
directoryTree.children.splice(i,1)
|
||||
return true;
|
||||
}
|
||||
if (DirectoryTree.remove_(child,path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
remove(path: string): boolean
|
||||
{
|
||||
return DirectoryTree.remove_(this,path);
|
||||
}
|
||||
find(path: string): DirectoryTree | null {
|
||||
let currentNode: DirectoryTree = this;
|
||||
for (var segment of path.split('/')) {
|
||||
let found = false;
|
||||
for (var child of currentNode.children) {
|
||||
if (child.name === segment) {
|
||||
found = true;
|
||||
currentNode = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return currentNode;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface FilePropertyDirectorySelectDialogProps {
|
||||
open: boolean,
|
||||
dialogTitle: string,
|
||||
uiFileProperty: UiFileProperty;
|
||||
defaultPath: string,
|
||||
excludeDirectory: string,
|
||||
selectedFile: string,
|
||||
onOk: (path: string) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface FilePropertyDirectorySelectDialogState {
|
||||
fullScreen: boolean;
|
||||
selectedPath: string;
|
||||
directoryTree?: DirectoryTree;
|
||||
hasSelection: boolean;
|
||||
isProtected: boolean;
|
||||
directoryTreeInvalidatecount: number;
|
||||
|
||||
};
|
||||
|
||||
export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveComponent<FilePropertyDirectorySelectDialogProps, FilePropertyDirectorySelectDialogState> {
|
||||
|
||||
private model: PiPedalModel;
|
||||
|
||||
constructor(props: FilePropertyDirectorySelectDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
fullScreen: false,
|
||||
selectedPath: props.defaultPath,
|
||||
directoryTree: undefined,
|
||||
hasSelection: false,
|
||||
isProtected: true,
|
||||
directoryTreeInvalidatecount: 0
|
||||
|
||||
};
|
||||
|
||||
this.requestDirectoryTree();
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
private requestScroll = true;
|
||||
requestDirectoryTree() {
|
||||
if (!this.props.open) return;
|
||||
|
||||
this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty,this.props.selectedFile)
|
||||
.then((filePropertyDirectoryTree) => {
|
||||
let myTree = new DirectoryTree(filePropertyDirectoryTree);
|
||||
if (this.props.excludeDirectory.length !== 0)
|
||||
{
|
||||
myTree.remove(this.props.excludeDirectory);
|
||||
}
|
||||
myTree.expand(this.state.selectedPath);
|
||||
|
||||
let selection = myTree.find(this.state.selectedPath);
|
||||
let hasSelection = !!selection;
|
||||
this.setState({
|
||||
directoryTree: myTree,
|
||||
hasSelection: hasSelection,
|
||||
isProtected: selection ? selection.isProtected : false
|
||||
});
|
||||
this.requestScroll = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.model.showAlert(e.toString());
|
||||
this.setState({ hasSelection: false });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<FilePropertyDirectorySelectDialogProps>, prevState: Readonly<FilePropertyDirectorySelectDialogState>, snapshot: any): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState, snapshot);
|
||||
|
||||
if (prevProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
this.setState({ selectedPath: this.props.defaultPath, hasSelection: false });
|
||||
this.requestDirectoryTree();
|
||||
this.requestScroll = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onOK() {
|
||||
if (this.state.isProtected) {
|
||||
return;
|
||||
}
|
||||
if (this.state.hasSelection) {
|
||||
this.props.onOk(this.state.selectedPath);
|
||||
}
|
||||
}
|
||||
onClose() {
|
||||
this.props.onClose();
|
||||
}
|
||||
private onToggleExpand(directoryTree: DirectoryTree)
|
||||
{
|
||||
directoryTree.expanded = !directoryTree.expanded;
|
||||
this.setState({directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount+1});
|
||||
}
|
||||
private onTreeClick(directoryTree: DirectoryTree) {
|
||||
if (!this.state.directoryTree) return;
|
||||
this.state.directoryTree.expand(directoryTree.path);
|
||||
this.setState({
|
||||
selectedPath: directoryTree.path,
|
||||
hasSelection: true,
|
||||
isProtected: directoryTree.isProtected,
|
||||
directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 });
|
||||
}
|
||||
private renderTree_(directoryTree: DirectoryTree) {
|
||||
let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined;
|
||||
let selected = directoryTree.path === this.state.selectedPath;
|
||||
if (selected) {
|
||||
ref = (element) => {
|
||||
if (this.requestScroll) {
|
||||
this.requestScroll = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
let selectBg = selected ? "rgba(0,0,0,0.15)" : "rgba(0,0,0,0.0)";
|
||||
if (isDarkMode()) {
|
||||
selectBg = selected ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.0)";
|
||||
}
|
||||
let showToggle = directoryTree.canExpand() && directoryTree.path.length !== 0;
|
||||
return (
|
||||
<div key={directoryTree.path} style={{flexGrow: 1}}>
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "center" }}>
|
||||
<IconButton onClick={()=>{ this.onToggleExpand(directoryTree); }}
|
||||
style={{opacity: showToggle ? 1: 0}}
|
||||
disabled={!showToggle}
|
||||
>
|
||||
{
|
||||
directoryTree.expanded &&
|
||||
(
|
||||
<ArrowDropDownIcon />
|
||||
)
|
||||
}
|
||||
{
|
||||
!directoryTree.expanded &&
|
||||
(
|
||||
<ArrowRightIcon/>
|
||||
)
|
||||
}
|
||||
</IconButton>
|
||||
<ButtonBase ref={ref} style={{ flexGrow: 1, flexShrink: 1, position: "relative" }} onClick={() => { this.onTreeClick(directoryTree); }}>
|
||||
<div style={{ position: "absolute", background: selectBg, width: "100%", height: "100%", borderRadius: 4 }} />
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", flexWrap: "nowrap", justifyContent: "flex-start", alignItems: "center", paddingLeft: 8 }}>
|
||||
{
|
||||
directoryTree.path === "" ? (
|
||||
<HomeIcon fontSize="small" />
|
||||
) : (
|
||||
<FolderIcon />
|
||||
)
|
||||
}
|
||||
|
||||
<Typography noWrap style={{ marginLeft: 8, marginTop: 8, marginBottom: 8, textAlign: "left", flexGrow: 1, flexShrink: 1 }} variant="body2">{directoryTree.name === "" ? "Home" : directoryTree.name}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
{directoryTree.expanded
|
||||
&& directoryTree.canExpand()
|
||||
&& (
|
||||
<div style={{ marginLeft: 32 }}>
|
||||
{
|
||||
directoryTree.children.map(
|
||||
(child, index) => {
|
||||
return this.renderTree_(child);
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTree() {
|
||||
if (!this.state.directoryTree) { return undefined; }
|
||||
return this.renderTree_(this.state.directoryTree);
|
||||
}
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="fpDirectorySelect" open={this.props.open} fullWidth onClose={() => { this.onClose(); }} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{ userSelect: "none" }}
|
||||
onEnterKey={()=>{ this.onOK(); }}
|
||||
PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="body1">{this.props.dialogTitle}</Typography>
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent style={{marginLeft: 0, paddingLeft: 0}} >
|
||||
<div style={{ display: "flex", flexGrow: 1, flexShrink: 1, overflowY: "auto" }} >
|
||||
{
|
||||
this.renderTree()
|
||||
}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<Divider />
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={() => { this.onClose(); }} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => { this.onOK(); }} color="secondary"
|
||||
disabled={(!this.state.hasSelection) || this.state.isProtected} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2024 Robin 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.
|
||||
|
||||
|
||||
export default class FilePropertyDirectoryTree {
|
||||
deserialize(input: any) : FilePropertyDirectoryTree {
|
||||
this.directoryName = input.directoryName;
|
||||
this.displayName = input.displayName;
|
||||
this.isProtected = input.isProtected;
|
||||
this.children = FilePropertyDirectoryTree.deserialize_array(input.children);
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): FilePropertyDirectoryTree[] {
|
||||
let result: FilePropertyDirectoryTree[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new FilePropertyDirectoryTree().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
directoryName: string = "";
|
||||
displayName: string = "";
|
||||
isProtected: boolean = false;
|
||||
children: FilePropertyDirectoryTree[] = [];
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { UiControl } from './Lv2Plugin';
|
||||
import DialogEx from './DialogEx';
|
||||
import Input from '@mui/material/Input';
|
||||
import { nullCast } from './Utility';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
display: "block",
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "0px",
|
||||
height: "100%",
|
||||
overflowX: "auto",
|
||||
overflowY: "auto"
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
interface FullScreenIMEProps extends WithStyles<typeof styles> {
|
||||
theme: Theme;
|
||||
uiControl?: UiControl;
|
||||
caption: string;
|
||||
value: number;
|
||||
onChange: (key: string, value: number) => void;
|
||||
onClose: () => void;
|
||||
initialHeight: number;
|
||||
}
|
||||
type FullScreenIMEState = {
|
||||
error: boolean;
|
||||
};
|
||||
|
||||
const FullScreenIME =
|
||||
withTheme(withStyles(
|
||||
class extends ResizeResponsiveComponent<FullScreenIMEProps, FullScreenIMEState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
inputRef: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: FullScreenIMEProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.inputRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
error: false
|
||||
|
||||
}
|
||||
this.onValueChanged = this.onValueChanged.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onInputKeyPress = this.onInputKeyPress.bind(this);
|
||||
this.onInputLostFocus = this.onInputLostFocus.bind(this);
|
||||
}
|
||||
|
||||
onValueChanged(key: string, value: number): void {
|
||||
this.props.onChange(key, value);
|
||||
}
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
|
||||
if (this.props.uiControl) {
|
||||
// detecting a keyboard IME is painfully difficult.
|
||||
// we do it by comparing the current window height against the window height
|
||||
// when the IME was requested.
|
||||
|
||||
// a width change indicates a screen flip. Cancel the fullscreen ime in that case
|
||||
// because we can't determine the initial window heigth anymore.
|
||||
if (this.currentWidth === undefined)
|
||||
{
|
||||
this.currentWidth = window.innerWidth;
|
||||
}
|
||||
if (this.currentWidth !== window.innerWidth)
|
||||
{
|
||||
this.validateInput(true);
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (window.innerHeight >= this.props.initialHeight) {
|
||||
this.validateInput(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleClose(e: {}, reason: string): void {
|
||||
this.props.onClose();
|
||||
}
|
||||
onInputLostFocus() {
|
||||
this.validateInput(true);
|
||||
|
||||
}
|
||||
|
||||
inputChanged: boolean = false;
|
||||
onInputChange() {
|
||||
|
||||
this.inputChanged = true;
|
||||
this.validateInput(false);
|
||||
}
|
||||
|
||||
currentValue: number = 0;
|
||||
|
||||
validateInput(commitValue: boolean) {
|
||||
if (!this.inputRef.current) return;
|
||||
|
||||
let text = this.inputRef.current.value;
|
||||
let valid = false;
|
||||
let result: number = this.currentValue;
|
||||
try {
|
||||
if (text.length === 0) {
|
||||
valid = false;
|
||||
} else {
|
||||
let v = Number(text);
|
||||
if (isNaN(v)) {
|
||||
valid = false;
|
||||
} else {
|
||||
valid = true;
|
||||
result = v;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (commitValue) {
|
||||
this.setState({ error: false });
|
||||
if (!valid) {
|
||||
result = this.currentValue; // reset the value!
|
||||
}
|
||||
// clamp and quantize.
|
||||
let uiControl = nullCast(this.uiControl);
|
||||
|
||||
// quantize value.
|
||||
result = uiControl.clampValue(result);
|
||||
|
||||
this.props.onChange(this.props.uiControl!.symbol, result);
|
||||
}
|
||||
}
|
||||
|
||||
onOk() {
|
||||
if (this.inputChanged) {
|
||||
this.inputChanged = false;
|
||||
this.validateInput(true);
|
||||
}
|
||||
else {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
onInputKeyPress(e: any): void {
|
||||
if (e.charCode === 13) {
|
||||
this.onOk();
|
||||
}
|
||||
}
|
||||
|
||||
uiControl?: UiControl;
|
||||
|
||||
currentWidth?: number;
|
||||
|
||||
render(): ReactNode {
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
let control = this.props.uiControl;
|
||||
if (!control) {
|
||||
this.currentWidth = undefined;
|
||||
return (<div/>);
|
||||
}
|
||||
this.currentWidth = window.innerWidth;
|
||||
|
||||
this.uiControl = control;
|
||||
|
||||
let value = this.props.value;
|
||||
|
||||
return (
|
||||
<DialogEx tag="ime" fullScreen open={!!(this.props.uiControl)} onClose={(e, r) => this.handleClose(e, r)}
|
||||
onEnterKey={()=>{ /*nothing*/}}
|
||||
>
|
||||
<div style={{
|
||||
width: "100%", height: "100%", position: "relative",
|
||||
display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
justifyContent: "start", alignItems: "center", marginTop:12
|
||||
}}
|
||||
>
|
||||
<Typography noWrap variant="body2">{this.props.caption}</Typography>
|
||||
<Input key={value}
|
||||
type="number"
|
||||
defaultValue={control.formatShortValue(value)}
|
||||
error={this.state.error}
|
||||
autoFocus
|
||||
inputProps={{
|
||||
'aria-label':
|
||||
control.symbol + " value",
|
||||
style: { textAlign: "center" },
|
||||
width: 80
|
||||
}}
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
|
||||
</div>
|
||||
|
||||
</DialogEx>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
));
|
||||
|
||||
export default FullScreenIME;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export default class GovernorSettings {
|
||||
deserialize(input: any) : GovernorSettings{
|
||||
this.hasGovernor = input.hasGovernor;
|
||||
this.governors = input.governors;
|
||||
this.governor = input.governor;
|
||||
return this;
|
||||
}
|
||||
clone(): GovernorSettings
|
||||
{
|
||||
return new GovernorSettings().deserialize(this);
|
||||
}
|
||||
hasGovernor: boolean = true;
|
||||
governors: string[] = ["ondemand","performance"];
|
||||
governor: string = "performance";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { MonitorPortHandle, PiPedalModel, State, PiPedalModelFactory } from "./PiPedalModel";
|
||||
import SvgPathBuilder from './SvgPathBuilder'
|
||||
import {isDarkMode} from './DarkMode';
|
||||
//const char* model[] = {"12-TET","19-TET","24-TET", "31-TET", "53-TET"};
|
||||
// set_adjustment(ui->widget[2]->adj,440.0, 440.0, 427.0, 453.0, 0.1, CL_CONTINUOS);
|
||||
|
||||
const DEFAULT_FREQUENCY_PORT_NAME = "FREQ";
|
||||
|
||||
const DIAL_WIDTH= 220;
|
||||
const DIAL_HEIGHT = 100;
|
||||
const NEEDLE_CY = 320;
|
||||
const CENTS_PER_MARK = 5;
|
||||
const NEEDLE_OUTER_RADIUS = 0.98;
|
||||
const TICK_OUTER_RADIUS = 0.96;
|
||||
const TICK_INNER_RADIUS = 0.85;
|
||||
const TICK_INNER_ZERO_RADIUS = 0.82;
|
||||
const DIAL_ANGLE_RADIANS = 20*Math.PI/180;
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
icon: {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6"
|
||||
}
|
||||
});
|
||||
|
||||
interface PitchInfo {
|
||||
valid: boolean;
|
||||
name: string;
|
||||
fractionText: string;
|
||||
fraction: number;
|
||||
|
||||
semitoneCents: number
|
||||
}
|
||||
|
||||
const FLAT = "\u{266d}";
|
||||
const SHARP = "#";
|
||||
// const DOUBLE_FLAT = "\uD834\uDD2B";
|
||||
// const DOUBLE_SHARP = "\uD834\uDD2A";
|
||||
const HALF_FLAT = "\uD834\uDD33";
|
||||
const HALF_SHARP = "\uD834\uDD32";
|
||||
|
||||
|
||||
const NOTES_12TET: string[] = [
|
||||
"C", "C" + SHARP, "D", "E"+FLAT,"E", "F", "F" + SHARP, "G", "A"+FLAT,"A","B"+FLAT,"B"
|
||||
];
|
||||
const NOTES_19TET: string[] = [
|
||||
"C", "C" + SHARP, "D"+FLAT, "D", "D" + SHARP,"E"+FLAT,"E", "E" + SHARP, "F", "F" + SHARP, "G"+FLAT,"G","G" + SHARP,"A"+FLAT,"A","A" + SHARP,"B"+FLAT,"B","C"+FLAT
|
||||
];
|
||||
const NOTES_24TET: string[] = [
|
||||
"C","C"+HALF_SHARP, "C" + SHARP,"D"+HALF_FLAT, "D", "D"+HALF_SHARP, "E"+FLAT, "E"+HALF_FLAT, "E","E"+HALF_SHARP,"F", "F"+HALF_SHARP, "F" + SHARP, "G"+HALF_FLAT,"G","G"+HALF_SHARP, "A"+FLAT,"A"+HALF_FLAT,"A","A"+HALF_SHARP,"B"+FLAT,"B"+HALF_FLAT,"B","C"+HALF_FLAT
|
||||
];
|
||||
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const NOTES_31TET: string[] = [
|
||||
// "C", "D"+DOUBLE_FLAT, "C" + SHARP,"Db","C"+DOUBLE_SHARP,"D","E"+DOUBLE_FLAT,"D" + SHARP,"Eb","D"+DOUBLE_SHARP,"E",'Fb","E" + SHARP,"F","G"+DOUBLE_FLAT,"F" + SHARP,"Gb""Fx","G","A"+DOUBLE_FLAT,"G"+DOUBLE_SHARP,"A"'
|
||||
// ];
|
||||
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const NOTES_53TET = ["la","laa","lo","law","ta","teh","te","tu","tuh","ti","tih","to","taw","da","do","di","daw","ro","rih","ra","ru","ruh","reh","re ","ri","raw","ma","meh","me","mu","muh","mi","maa","mo","maw","fe","fa","fih","fu","fuh","fi","se","suh","su","sih","sol","si","saw","lo","leh","le","lu","luh"];
|
||||
|
||||
interface GxTunerControlProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
valueIsMidi: boolean;
|
||||
symbolName?: string;
|
||||
}
|
||||
type GxTunerControlState = {
|
||||
pitchInfo: PitchInfo
|
||||
tet: number;
|
||||
refFrequency: number;
|
||||
animationTime: number;
|
||||
};
|
||||
|
||||
const GxTunerControl =
|
||||
withStyles(
|
||||
class extends Component<GxTunerControlProps, GxTunerControlState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
refRoot: React.RefObject<HTMLDivElement|null>;
|
||||
|
||||
constructor(props: GxTunerControlProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.refRoot = React.createRef();
|
||||
|
||||
this.state = {
|
||||
pitchInfo: {
|
||||
valid: false,
|
||||
name: "",
|
||||
fractionText: "",
|
||||
fraction: 0,
|
||||
semitoneCents: 100},
|
||||
tet: 12,
|
||||
animationTime: 0,
|
||||
refFrequency: 440
|
||||
|
||||
};
|
||||
this.onFrequencyUpdated = this.onFrequencyUpdated.bind(this);
|
||||
this.onStateChanged = this.onStateChanged.bind(this);
|
||||
}
|
||||
|
||||
frequencyPortName() : string {
|
||||
if (this.props.symbolName) { return this.props.symbolName;}
|
||||
return DEFAULT_FREQUENCY_PORT_NAME;
|
||||
}
|
||||
|
||||
noteToPitchInfo(value: number) : PitchInfo
|
||||
{
|
||||
let aOffset = 69;
|
||||
|
||||
let tet = this.state.tet;
|
||||
let refFrequency = this.state.refFrequency;
|
||||
let names: string[];
|
||||
let semitoneCents = 100;
|
||||
let valid = false;
|
||||
|
||||
let hz = -1;
|
||||
let midiNote = -1;
|
||||
|
||||
|
||||
switch (tet)
|
||||
{
|
||||
case 12:
|
||||
default:
|
||||
aOffset = 69;
|
||||
names = NOTES_12TET;
|
||||
semitoneCents = 100;
|
||||
break;
|
||||
case 19:
|
||||
aOffset = 4*19+13;
|
||||
names = NOTES_19TET;
|
||||
semitoneCents = 100*12.0/19;
|
||||
break;
|
||||
case 24:
|
||||
aOffset = 4*24+18;
|
||||
semitoneCents = 50;
|
||||
names = NOTES_24TET;
|
||||
break;
|
||||
}
|
||||
let name = "";
|
||||
let fractionText = "";
|
||||
let fraction = 0;
|
||||
|
||||
if (this.props.valueIsMidi)
|
||||
{
|
||||
midiNote = value;
|
||||
} else {
|
||||
hz = value;
|
||||
if (hz < 65)
|
||||
{
|
||||
hz = -1;
|
||||
midiNote = -1;
|
||||
} else {
|
||||
midiNote = Math.log2(hz/refFrequency)*tet + aOffset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (midiNote > 0)
|
||||
{
|
||||
|
||||
let note = midiNote;
|
||||
let noteNumber = Math.round(note);
|
||||
|
||||
let octave = Math.floor((noteNumber)/tet);
|
||||
|
||||
let nameIndex = noteNumber -octave* tet;
|
||||
if (nameIndex < 0) nameIndex += tet;
|
||||
name = names[ nameIndex ] + (octave-2);
|
||||
valid = true;
|
||||
fraction = note-noteNumber;
|
||||
if (fraction >= 0) {
|
||||
fractionText = "+" + fraction.toFixed(2).substr(1);
|
||||
} else {
|
||||
fractionText = "\u2212" + fraction.toFixed(2).substr(2);
|
||||
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: valid,
|
||||
name: name,
|
||||
fraction: fraction,
|
||||
fractionText: fractionText,
|
||||
semitoneCents: semitoneCents
|
||||
};
|
||||
}
|
||||
|
||||
lastValue: number = -1;
|
||||
onFrequencyUpdated(value: number): void
|
||||
{
|
||||
// suppress repeated zeros.
|
||||
if (value === this.lastValue && value <= 0 && this.animationIdle) return;
|
||||
|
||||
this.lastValue = value;
|
||||
|
||||
let pitchInfo = this.noteToPitchInfo(value);
|
||||
this.setState({
|
||||
pitchInfo: pitchInfo
|
||||
});
|
||||
if (value <= 0)
|
||||
{
|
||||
this.startAnimationTimer();
|
||||
} else {
|
||||
this.cancelAnimationTimer();
|
||||
}
|
||||
}
|
||||
|
||||
animationTimer?: number = undefined;
|
||||
|
||||
cancelAnimationTimer(): void {
|
||||
if (this.animationTimer)
|
||||
{
|
||||
clearTimeout(this.animationTimer);
|
||||
this.animationTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
startAnimationTimer(): void {
|
||||
if (!this.animationTimer)
|
||||
{
|
||||
this.animationTimer = setInterval(
|
||||
() => {
|
||||
this.setState({
|
||||
animationTime: new Date().getTime()
|
||||
});
|
||||
},
|
||||
1000/30
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
subscribedInstanceId: number = -1;
|
||||
|
||||
monitorHandle?: MonitorPortHandle;
|
||||
|
||||
addSubscription() {
|
||||
this.subscribedInstanceId = this.props.instanceId;
|
||||
this.monitorHandle = this.model.monitorPort(this.props.instanceId,this.frequencyPortName(),1.0/30,this.onFrequencyUpdated);
|
||||
}
|
||||
removeSubscription() {
|
||||
this.subscribedInstanceId = -1;
|
||||
if (this.monitorHandle) {
|
||||
this.model.unmonitorPort(this.monitorHandle);
|
||||
this.monitorHandle = undefined;
|
||||
}
|
||||
}
|
||||
updateSubscription() {
|
||||
if (this.subscribedInstanceId !== this.props.instanceId)
|
||||
{
|
||||
this.removeSubscription();
|
||||
this.addSubscription();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
onStateChanged(state: State)
|
||||
{
|
||||
// initial or reconnect
|
||||
if (state === State.Ready)
|
||||
{
|
||||
this.removeSubscription();
|
||||
this.updateSubscription();
|
||||
}
|
||||
}
|
||||
componentDidMount()
|
||||
{
|
||||
this.model.state.addOnChangedHandler(this.onStateChanged);
|
||||
this.addSubscription();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.updateSubscription();
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
this.model.state.removeOnChangedHandler(this.onStateChanged);
|
||||
this.removeSubscription();
|
||||
this.cancelAnimationTimer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
makeDialTick(pitchInfo: PitchInfo, cents: number): React.ReactNode {
|
||||
let isZeroTick = Math.abs(cents) < 0.001;
|
||||
let r0 = TICK_OUTER_RADIUS;
|
||||
let r1 = (isZeroTick? TICK_INNER_ZERO_RADIUS: TICK_INNER_RADIUS);
|
||||
let width = isZeroTick? 3: 2;
|
||||
return this.makeTick(pitchInfo,cents,r0,r1,"#666", width);
|
||||
}
|
||||
|
||||
nextKey() { return "key" + (this.keyCounter++)}
|
||||
makeTick(pitchInfo: PitchInfo, cents: number, r0: number, r1: number, stroke: string,width: number): React.ReactNode
|
||||
{
|
||||
let range = pitchInfo.semitoneCents;
|
||||
if (range > 50) {
|
||||
range = 50;
|
||||
}
|
||||
r0 *= NEEDLE_CY;
|
||||
r1 *= NEEDLE_CY;
|
||||
let angle = DIAL_ANGLE_RADIANS * (cents*2/(range));
|
||||
|
||||
let sin_ = Math.sin(angle);
|
||||
let cos_ = -Math.cos(angle);
|
||||
let cx = DIAL_WIDTH/2;
|
||||
let cy = NEEDLE_CY;
|
||||
let path = new SvgPathBuilder().moveTo(r0*sin_+cx,r0*cos_+cy).lineTo(r1*sin_+cx,r1*cos_+cy).toString();
|
||||
|
||||
return (<path key={this.nextKey()} d={path} stroke={stroke} strokeWidth={width+""} />);
|
||||
}
|
||||
|
||||
lastValidTime: number = 0;
|
||||
lastValidCents: number = -100;
|
||||
animationIdle: boolean = true;
|
||||
|
||||
makeNeedle(pitchInfo: PitchInfo)
|
||||
{
|
||||
let maxCents = pitchInfo.semitoneCents/2;
|
||||
if (maxCents > 25) {
|
||||
maxCents = 25;
|
||||
}
|
||||
let cents: number;
|
||||
if (!pitchInfo.valid)
|
||||
{
|
||||
const NEEDLE_DECAY_RATE_PER_MS = -50*7/1000;
|
||||
|
||||
// animate to zero position.
|
||||
let time = new Date().getTime();
|
||||
let dt = time-this.lastValidTime;
|
||||
|
||||
cents = this.lastValidCents+(dt*NEEDLE_DECAY_RATE_PER_MS);
|
||||
if (cents <= -maxCents)
|
||||
{
|
||||
cents = -maxCents;
|
||||
this.animationIdle = true;
|
||||
this.cancelAnimationTimer();
|
||||
} else {
|
||||
this.animationIdle = false;
|
||||
}
|
||||
} else {
|
||||
cents = pitchInfo.semitoneCents*pitchInfo.fraction;
|
||||
if (cents > maxCents) {
|
||||
cents = maxCents;
|
||||
}
|
||||
if (cents < -maxCents)
|
||||
{
|
||||
cents = -maxCents;
|
||||
}
|
||||
this.lastValidCents = cents;
|
||||
this.lastValidTime = new Date().getTime();
|
||||
}
|
||||
return this.makeTick(pitchInfo,cents,NEEDLE_OUTER_RADIUS,0.1,"#800",3);
|
||||
}
|
||||
|
||||
renderDial(pitchInfo: PitchInfo)
|
||||
{
|
||||
|
||||
let bounds = "0,0," + DIAL_WIDTH+ "," +DIAL_HEIGHT;
|
||||
|
||||
let content: React.ReactNode[] = [];
|
||||
|
||||
let maxCents = Math.floor(pitchInfo.semitoneCents/2 / CENTS_PER_MARK)*CENTS_PER_MARK;
|
||||
|
||||
for (let cents = -maxCents; cents <= maxCents+ 0.01; cents += CENTS_PER_MARK) {
|
||||
content.push(this.makeDialTick(pitchInfo,cents));
|
||||
}
|
||||
|
||||
content.push(
|
||||
this.makeNeedle(pitchInfo)
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<svg viewBox={bounds} width={DIAL_WIDTH} height={DIAL_HEIGHT} style={{position: "absolute"}} >
|
||||
{ content }
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
keyCounter: number = 0;
|
||||
|
||||
render() {
|
||||
this.keyCounter = 0;
|
||||
let textColor = isDarkMode() ? "#999": "#444";
|
||||
return (<div ref={this.refRoot} style={{width: DIAL_WIDTH, height: DIAL_HEIGHT, fontSize: "2em", fontWeight: 700, position: "relative",
|
||||
boxShadow: isDarkMode() ?
|
||||
"5px 5px 6px rgba(0,0,0,0.8) inset":
|
||||
"1px 5px 6px #888 inset",
|
||||
background: isDarkMode()? "rgba(255,255,255,0.07)" : "",
|
||||
|
||||
fontFamily: "arial,roboto,helvetica,sans"}}>
|
||||
<div style={{position: "absolute", left: 0, bottom: 5, width: "50%",textAlign: "right"}}>
|
||||
<span style={{ marginRight: 20, color: textColor, textAlign: "right" }}>{this.state.pitchInfo.name}</span>
|
||||
</div>
|
||||
<div style={{position: "absolute", right: 0, bottom: 5, width: "50%",textAlign: "left"}}>
|
||||
<span style={{ marginLeft: 20, color: textColor, textAlign: "left" }}>{this.state.pitchInfo.fractionText}</span>
|
||||
</div>
|
||||
|
||||
{ this.renderDial(this.state.pitchInfo) }
|
||||
</div>);
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
export default GxTunerControl;
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
import WithStyles 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 GxTunerControl from './GxTunerControl';
|
||||
|
||||
const GXTUNER_URI = "http://guitarix.sourceforge.net/plugins/gxtuner#tuner";
|
||||
const TOOBTUNER_URI = "http://two-play.com/plugins/toob-tuner";
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface GxTunerProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalboardItem;
|
||||
isToobTuner: boolean;
|
||||
|
||||
}
|
||||
interface GxTunerState {
|
||||
|
||||
}
|
||||
|
||||
const GxTunerView =
|
||||
withStyles(
|
||||
class extends React.Component<GxTunerProps, GxTunerState>
|
||||
implements ControlViewCustomization
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
customizationId: number = 1;
|
||||
|
||||
constructor(props: GxTunerProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getControlIndex(key: string): number {
|
||||
let item = this.props.item;
|
||||
for (let i = 0; i < item.controlValues.length; ++i)
|
||||
{
|
||||
if (item.controlValues[i].key === key)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
throw new Error("GxTuner: Control '" + key + "' not found.");
|
||||
}
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
let refFreqIndex = this.getControlIndex("REFFREQ");
|
||||
let thresholdIndex = this.getControlIndex("THRESHOLD");
|
||||
let muteIndex = -1;
|
||||
if (this.props.isToobTuner)
|
||||
{
|
||||
muteIndex = this.getControlIndex("MUTE")
|
||||
}
|
||||
|
||||
let result: (React.ReactNode | ControlGroup)[] = [];
|
||||
|
||||
let tunerControl = (<GxTunerControl instanceId={this.props.instanceId} valueIsMidi={false} />);
|
||||
result.push(tunerControl);
|
||||
|
||||
result.push(controls[refFreqIndex]);
|
||||
result.push(controls[thresholdIndex]);
|
||||
if (muteIndex !== -1)
|
||||
{
|
||||
result.push(controls[muteIndex]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
render() {
|
||||
return (<PluginControlView
|
||||
instanceId={this.props.instanceId}
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
/>);
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
|
||||
|
||||
export class GxTunerViewFactory implements IControlViewFactory {
|
||||
uri: string = GXTUNER_URI;
|
||||
|
||||
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
|
||||
return (<GxTunerView isToobTuner={false} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ToobTuner uses the same controls and control semantics.
|
||||
export class ToobTunerViewFactory implements IControlViewFactory {
|
||||
uri: string = TOOBTUNER_URI;
|
||||
|
||||
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
|
||||
return (<GxTunerView isToobTuner={true} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { PiPedalModel } from "./PiPedalModel";
|
||||
import { PedalboardItem } from './Pedalboard';
|
||||
|
||||
|
||||
|
||||
interface IControlViewFactory {
|
||||
uri: string;
|
||||
Create(model: PiPedalModel,pedalboardItem: PedalboardItem): React.ReactNode;
|
||||
|
||||
}
|
||||
|
||||
export default IControlViewFactory;
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
|
||||
|
||||
function getChannelNumber(channelId: string): number {
|
||||
let pos = channelId.indexOf("_");
|
||||
let i = Number(channelId.substring(pos+1));
|
||||
return i;
|
||||
}
|
||||
|
||||
export class JackChannelSelection {
|
||||
deserialize(input: any): JackChannelSelection
|
||||
{
|
||||
this.inputAudioPorts = input.inputAudioPorts.slice();
|
||||
this.outputAudioPorts = input.outputAudioPorts.slice();
|
||||
this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
|
||||
this.sampleRate = input.sampleRate;
|
||||
this.bufferSize = input.bufferSize;
|
||||
this.numberOfBuffers = input.numberOfBuffers;
|
||||
return this;
|
||||
}
|
||||
clone() : JackChannelSelection {
|
||||
return new JackChannelSelection().deserialize(this);
|
||||
}
|
||||
inputAudioPorts: string[] = [];
|
||||
outputAudioPorts: string[] = [];
|
||||
inputMidiDevices: AlsaMidiDeviceInfo[] = [];
|
||||
|
||||
sampleRate: number = 48000;
|
||||
bufferSize: number = 64;
|
||||
numberOfBuffers: number = 3;
|
||||
|
||||
static makeDefault(jackConfiguration: JackConfiguration): JackChannelSelection {
|
||||
let result = new JackChannelSelection();
|
||||
result.inputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
|
||||
result.outputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
|
||||
result.inputMidiDevices = [];
|
||||
return result;
|
||||
}
|
||||
getChannelDisplayValue(selectedChannels: string[], availableChannels: string[],isConfigValid: boolean): string
|
||||
{
|
||||
if (!isConfigValid)
|
||||
{
|
||||
if (selectedChannels.length === 2) {
|
||||
return "Stereo";
|
||||
}
|
||||
if (selectedChannels.length === 1) return "Mono";
|
||||
|
||||
return "\u00A0"; // nbsp
|
||||
|
||||
}
|
||||
if (selectedChannels.length === 0) return "Invalid selection";
|
||||
if (availableChannels.length === 1) return "Mono";
|
||||
if (availableChannels.length === 2) {
|
||||
if (selectedChannels.length === 2) return "Stereo";
|
||||
if (selectedChannels[0] === availableChannels[0]) {
|
||||
return "Mono (left channel only)";
|
||||
} else if (selectedChannels[0] === availableChannels[1]) {
|
||||
return "Mono (right channel only)";
|
||||
} else {
|
||||
return "\u00A0"; // nbsp
|
||||
}
|
||||
} else {
|
||||
if (selectedChannels.length === 2)
|
||||
{
|
||||
let result: string = "Stereo (" + (getChannelNumber(selectedChannels[0])+1) + "," + (getChannelNumber(selectedChannels[1])+1) + ")";
|
||||
return result;
|
||||
}
|
||||
if (selectedChannels.length === 1)
|
||||
{
|
||||
let result: string = "Mono (" + (getChannelNumber(selectedChannels[0])+1) +")";
|
||||
return result;
|
||||
|
||||
}
|
||||
return "\u00A0"; // nbsp
|
||||
}
|
||||
}
|
||||
getAudioInputDisplayValue(jackConfiguration: JackConfiguration): string
|
||||
{
|
||||
return this.getChannelDisplayValue(this.inputAudioPorts,jackConfiguration.inputAudioPorts, jackConfiguration.isValid);
|
||||
}
|
||||
getAudioOutputDisplayValue(jackConfiguration: JackConfiguration): string
|
||||
{
|
||||
return this.getChannelDisplayValue(this.outputAudioPorts,jackConfiguration.outputAudioPorts, jackConfiguration.isValid);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export class JackConfiguration {
|
||||
deserialize(input: any): JackConfiguration {
|
||||
this.isValid = input.isValid;
|
||||
this.isRestarting = input.isRestarting;
|
||||
this.errorState = input.errorState;
|
||||
this.sampleRate = input.sampleRate;
|
||||
this.blockLength = input.blockLength;
|
||||
this.midiBufferSize = input.midiBufferSize;
|
||||
this.maxAllowedMidiDelta = input.maxAllowedMidiDelta;
|
||||
this.inputAudioPorts = input.inputAudioPorts;
|
||||
this.outputAudioPorts = input.outputAudioPorts;
|
||||
this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
|
||||
return this;
|
||||
}
|
||||
isValid: boolean = false;
|
||||
isRestarting: boolean = false;
|
||||
errorState: string = "Not loaded.";
|
||||
sampleRate: number = 0;
|
||||
blockLength: number = 0;
|
||||
midiBufferSize: number = 0;
|
||||
maxAllowedMidiDelta: number = 0;
|
||||
|
||||
inputAudioPorts: string[] = [];
|
||||
outputAudioPorts: string[] = [];
|
||||
inputMidiDevices: AlsaMidiDeviceInfo[] = [];
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default JackConfiguration;
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
const RED_COLOR = isDarkMode() ? "#F88" : "#C00";
|
||||
const GREEN_COLOR = isDarkMode() ? "rgba(255,255,255,0.7)" : "#666";
|
||||
|
||||
|
||||
|
||||
function tempDisplay(mC: number): string {
|
||||
return (mC / 1000).toFixed(1) + "\u00B0C"; // degrees C.
|
||||
}
|
||||
function cpuDisplay(cpu: number): string {
|
||||
return cpu.toFixed(1) + "%";
|
||||
}
|
||||
|
||||
function fmtCpuFreq(freq: number): string {
|
||||
if (freq >= 100000000) {
|
||||
return (freq / 1000000).toFixed(1) + " GHz";
|
||||
}
|
||||
if (freq >= 10000000) {
|
||||
return (freq / 1000000).toFixed(2) + " GHz";
|
||||
}
|
||||
if (freq >= 1000000) {
|
||||
return (freq / 1000000).toFixed(3) + " GHz";
|
||||
}
|
||||
if (freq >= 1000) {
|
||||
return (freq / 1000).toFixed(3) + " MHz";
|
||||
}
|
||||
return freq + " KHz";
|
||||
}
|
||||
|
||||
function areSameFrequency(max: number, min: number): boolean {
|
||||
return true;
|
||||
// // x86 cpus can show incredibly minor frequency variations between CPUs
|
||||
// if (min === 0) return false;
|
||||
// let ratio = max/min;
|
||||
// if (ratio < 1.2)
|
||||
// {
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
}
|
||||
|
||||
export default class JackHostStatus {
|
||||
deserialize(input: any): JackHostStatus {
|
||||
this.active = input.active;
|
||||
this.restarting = input.restarting;
|
||||
this.errorMessage = input.errorMessage;
|
||||
this.underruns = input.underruns;
|
||||
this.cpuUsage = input.cpuUsage;
|
||||
this.msSinceLastUnderrun = input.msSinceLastUnderrun;
|
||||
this.temperaturemC = input.temperaturemC;
|
||||
this.cpuFreqMax = input.cpuFreqMax;
|
||||
this.cpuFreqMin = input.cpuFreqMin;
|
||||
this.hasCpuGovernor = input.hasCpuGovernor;
|
||||
this.governor = input.governor;
|
||||
return this;
|
||||
}
|
||||
hasTemperature(): boolean {
|
||||
return this.temperaturemC >= -100000;
|
||||
}
|
||||
active: boolean = false;
|
||||
errorMessage: string = "";
|
||||
restarting: boolean = false;
|
||||
underruns: number = 0;
|
||||
cpuUsage: number = 0;
|
||||
msSinceLastUnderrun: number = -5000 * 1000;
|
||||
temperaturemC: number = -1000000;
|
||||
cpuFreqMax: number = 0;
|
||||
cpuFreqMin: number = 0;
|
||||
hasCpuGovernor: boolean = false;
|
||||
governor: string = "";
|
||||
|
||||
static getCpuInfo(label: string, status?: JackHostStatus): React.ReactNode {
|
||||
if (!status) {
|
||||
return (<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
<Typography variant="caption"> </Typography>
|
||||
</div>);
|
||||
}
|
||||
return (<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
{(status.cpuFreqMin !== 0 || status.cpuFreqMax !== 0) &&
|
||||
(
|
||||
<Typography variant="caption" color="inherit">
|
||||
{
|
||||
(areSameFrequency(status.cpuFreqMax,status.cpuFreqMin)) ?
|
||||
(
|
||||
<span> {status.governor} {fmtCpuFreq(status.cpuFreqMax)} </span>
|
||||
)
|
||||
: (
|
||||
<span> {status.governor} {fmtCpuFreq(status.cpuFreqMin)}-{fmtCpuFreq(status.cpuFreqMax)} </span>
|
||||
|
||||
)
|
||||
}
|
||||
</Typography>
|
||||
)}
|
||||
</div>);
|
||||
|
||||
|
||||
}
|
||||
static getDisplayView(label: string, status?: JackHostStatus): React.ReactNode {
|
||||
if (!status) {
|
||||
return (<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
<Typography variant="caption"> </Typography>
|
||||
</div>);
|
||||
}
|
||||
if (status.restarting) {
|
||||
return (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
<span style={{ color: RED_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">Restarting </Typography>
|
||||
</span>
|
||||
{
|
||||
status.temperaturemC > -100000 &&
|
||||
(
|
||||
<span style={{ color: status.temperaturemC > 75000 ? RED_COLOR : GREEN_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">{tempDisplay(status.temperaturemC)}</Typography>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
} else if (!status.active) {
|
||||
return (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
|
||||
<span style={{ color: RED_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage} </Typography>
|
||||
</span>
|
||||
{
|
||||
status.temperaturemC > -100000 &&
|
||||
(
|
||||
<span style={{ color: status.temperaturemC > 75000 ? RED_COLOR : GREEN_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">{tempDisplay(status.temperaturemC)}</Typography>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let underrunError = status.msSinceLastUnderrun < 15 * 1000;
|
||||
return (
|
||||
<div style={{ whiteSpace: "nowrap" }}>
|
||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||
<span style={{ color: underrunError ? RED_COLOR : GREEN_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">
|
||||
XRuns: {status.underruns + ""}
|
||||
</Typography>
|
||||
</span>
|
||||
<span style={{ color: underrunError ? RED_COLOR : GREEN_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">
|
||||
CPU: {cpuDisplay(status.cpuUsage)}
|
||||
</Typography>
|
||||
</span>
|
||||
|
||||
<span style={{ color: GREEN_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">{tempDisplay(status.temperaturemC)}</Typography>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export default class JackServerSettings {
|
||||
deserialize(input: any) : JackServerSettings{
|
||||
this.valid = input.valid;
|
||||
this.isOnboarding = input.isOnboarding;
|
||||
this.isJackAudio = input.isJackAudio;
|
||||
this.rebootRequired = input.rebootRequired;
|
||||
this.alsaDevice = input.alsaDevice?? "";
|
||||
this.sampleRate = input.sampleRate;
|
||||
this.bufferSize = input.bufferSize;
|
||||
this.numberOfBuffers = input.numberOfBuffers;
|
||||
return this;
|
||||
}
|
||||
// constructor(alsaDevice: string, sampleRate?: number, bufferSize?: number, numberOfBuffers?: number)
|
||||
// {
|
||||
// if (sampleRate) this.sampleRate = sampleRate;
|
||||
// if (bufferSize) this.bufferSize = bufferSize;
|
||||
// if (numberOfBuffers) this.numberOfBuffers = numberOfBuffers;
|
||||
// if (numberOfBuffers) {
|
||||
// this.valid = true;
|
||||
// }
|
||||
// }
|
||||
clone(): JackServerSettings
|
||||
{
|
||||
return new JackServerSettings().deserialize(this);
|
||||
}
|
||||
valid: boolean = false;
|
||||
isOnboarding: boolean = true;
|
||||
rebootRequired = false;
|
||||
isJackAudio = false;
|
||||
alsaDevice: string = "";
|
||||
sampleRate = 48000;
|
||||
bufferSize = 64;
|
||||
numberOfBuffers = 3;
|
||||
|
||||
getSummaryText() {
|
||||
if (this.valid) {
|
||||
let device = this.alsaDevice;
|
||||
if (device.startsWith("hw:")) device = device.substring(3);
|
||||
return device + " - Rate: " + this.sampleRate + " BufferSize: " + this.bufferSize + " Buffers: " + this.numberOfBuffers;
|
||||
} else {
|
||||
return "Not configured";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogEx from './DialogEx';
|
||||
import JackServerSettings from './JackServerSettings';
|
||||
|
||||
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Select from '@mui/material/Select';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
|
||||
import AlsaDeviceInfo from './AlsaDeviceInfo';
|
||||
|
||||
const MIN_BUFFER_SIZE = 16;
|
||||
const MAX_BUFFER_SIZE = 2048;
|
||||
|
||||
|
||||
interface BufferSetting {
|
||||
bufferSize: number;
|
||||
numberOfBuffers: number;
|
||||
};
|
||||
|
||||
const INVALID_DEVICE_ID = "_invalid_";
|
||||
interface JackServerSettingsDialogState {
|
||||
latencyText: string;
|
||||
jackServerSettings: JackServerSettings;
|
||||
alsaDevices?: AlsaDeviceInfo[];
|
||||
okEnabled: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
formControl: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 120,
|
||||
},
|
||||
selectEmpty: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
});
|
||||
export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean;
|
||||
jackServerSettings: JackServerSettings;
|
||||
onClose: () => void;
|
||||
onApply: (jackServerSettings: JackServerSettings) => void;
|
||||
}
|
||||
|
||||
function getLatencyText(settings?: JackServerSettings ): string {
|
||||
if (!settings)
|
||||
{
|
||||
return "\u00A0";
|
||||
}
|
||||
|
||||
if (!settings.valid) return "\u00A0";
|
||||
let ms = settings.bufferSize * settings.numberOfBuffers / settings.sampleRate * 1000;
|
||||
return ms.toFixed(1) + "ms";
|
||||
}
|
||||
|
||||
function getValidBufferCounts(bufferSize: number, alsaDeviceInfo?: AlsaDeviceInfo): number[]
|
||||
{
|
||||
if (!alsaDeviceInfo)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
let result: number[] = [];
|
||||
if (bufferSize * 2 >= alsaDeviceInfo.minBufferSize)
|
||||
{
|
||||
result = [2,3,4];
|
||||
} else {
|
||||
let minBuffers = Math.ceil(alsaDeviceInfo.minBufferSize/bufferSize/2-0.0001);
|
||||
result = [minBuffers*2,minBuffers*3, minBuffers*4];
|
||||
}
|
||||
for (let i = 0; i < result.length; ++i)
|
||||
{
|
||||
let selectedSize = result[i]*bufferSize;
|
||||
if (selectedSize < alsaDeviceInfo.minBufferSize || selectedSize > alsaDeviceInfo.maxBufferSize)
|
||||
{
|
||||
result.splice(i,1);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
function getValidBufferSizes(alsaDeviceInfo? : AlsaDeviceInfo): number[]
|
||||
{
|
||||
if (!alsaDeviceInfo )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
let result: number[] = [];
|
||||
for (let i = MIN_BUFFER_SIZE; i <= MAX_BUFFER_SIZE; i *= 2)
|
||||
{
|
||||
if (getValidBufferCounts(i,alsaDeviceInfo).length !== 0)
|
||||
{
|
||||
result.push(i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function getBestBuffers(
|
||||
alsaDeviceInfo: AlsaDeviceInfo | undefined,
|
||||
bufferSize: number,
|
||||
numberOfBuffers: number,
|
||||
bufferSizeChanging: boolean = false
|
||||
) : BufferSetting {
|
||||
if (!alsaDeviceInfo)
|
||||
{
|
||||
return { bufferSize:bufferSize, numberOfBuffers: numberOfBuffers};
|
||||
}
|
||||
|
||||
// If the numberOfbuffers is fine as is, don't change the number of Buffers.
|
||||
// set default values. Otherwise, choose the best buffer count from available buffer counts.
|
||||
let validBuffercounts = getValidBufferCounts(bufferSize,alsaDeviceInfo);
|
||||
let ix = validBuffercounts.indexOf(numberOfBuffers);
|
||||
if (ix !== -1) {
|
||||
if (bufferSize*numberOfBuffers <= alsaDeviceInfo.maxBufferSize
|
||||
&& bufferSize*numberOfBuffers >= alsaDeviceInfo.minBufferSize)
|
||||
{
|
||||
return {bufferSize: bufferSize, numberOfBuffers: numberOfBuffers};
|
||||
}
|
||||
}
|
||||
// if numberOfBuffers is not valid, and the user has just selected a new buffers size,
|
||||
// then choose from available buffer counts.
|
||||
if (bufferSizeChanging)
|
||||
{
|
||||
if (validBuffercounts.length !== 0)
|
||||
{
|
||||
// prefer the one thats divisible by 3.
|
||||
for (let i = 0; i < validBuffercounts.length; ++i)
|
||||
{
|
||||
if (validBuffercounts[i] % 3 === 0)
|
||||
{
|
||||
numberOfBuffers = validBuffercounts[i];
|
||||
return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers};
|
||||
}
|
||||
}
|
||||
numberOfBuffers = validBuffercounts[0];
|
||||
return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers};
|
||||
}
|
||||
}
|
||||
// otherwise select a sensible starting value.
|
||||
|
||||
// favored default: 64x3.
|
||||
if (64*3 >= alsaDeviceInfo.minBufferSize && 64*3 <= alsaDeviceInfo.maxBufferSize)
|
||||
{
|
||||
return { bufferSize: 64, numberOfBuffers: 3};
|
||||
}
|
||||
// if that isn't possible then minBufferSize/2 x 4.
|
||||
bufferSize = alsaDeviceInfo.minBufferSize/2;
|
||||
numberOfBuffers = 4;
|
||||
// otherwise, minBufferSize/2 x 2.
|
||||
if (bufferSize*numberOfBuffers > alsaDeviceInfo.maxBufferSize)
|
||||
{
|
||||
numberOfBuffers = 2;
|
||||
}
|
||||
return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers};
|
||||
|
||||
};
|
||||
|
||||
function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[])
|
||||
{
|
||||
if (!jackServerSettings.valid) return false;
|
||||
if (!alsaDevices) return false;
|
||||
let alsaDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
for (let i = 0; i < alsaDevices.length; ++i)
|
||||
{
|
||||
if (alsaDevices[i].id === jackServerSettings.alsaDevice)
|
||||
{
|
||||
alsaDevice = alsaDevices[i];
|
||||
}
|
||||
}
|
||||
if (!alsaDevice)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let deviceBufferSize = jackServerSettings.bufferSize * jackServerSettings.numberOfBuffers;
|
||||
if (deviceBufferSize < alsaDevice.minBufferSize || deviceBufferSize > alsaDevice.maxBufferSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let validBufferCounts = getValidBufferCounts(jackServerSettings.bufferSize, alsaDevice);
|
||||
let ix = validBufferCounts.indexOf(jackServerSettings.numberOfBuffers);
|
||||
if (ix === -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const JackServerSettingsDialog = withStyles(
|
||||
class extends Component<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: JackServerSettingsDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
|
||||
this.state = {
|
||||
latencyText: getLatencyText(props.jackServerSettings),
|
||||
jackServerSettings: props.jackServerSettings.clone(), // invalid, but not nullish
|
||||
alsaDevices: undefined,
|
||||
okEnabled: false
|
||||
};
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
requestAlsaInfo() {
|
||||
this.model.getAlsaDevices()
|
||||
.then((devices) => {
|
||||
if (this.mounted) {
|
||||
if (this.props.open) {
|
||||
let settings = this.applyAlsaDevices(this.props.jackServerSettings, devices);
|
||||
this.setState({
|
||||
alsaDevices: devices,
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,devices)
|
||||
});
|
||||
} else {
|
||||
this.setState({ alsaDevices: devices });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) {
|
||||
let result = jackServerSettings.clone();
|
||||
if (!alsaDevices) {
|
||||
return result;
|
||||
}
|
||||
result.valid = false;
|
||||
if (alsaDevices.length === 0) {
|
||||
result.alsaDevice = INVALID_DEVICE_ID;
|
||||
return result;
|
||||
}
|
||||
|
||||
let selectedDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
for (let i = 0; i < alsaDevices.length; ++i) {
|
||||
if (alsaDevices[i].id === result.alsaDevice) {
|
||||
selectedDevice = alsaDevices[i]
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!selectedDevice) {
|
||||
selectedDevice = alsaDevices[0];
|
||||
result.alsaDevice = selectedDevice.id;
|
||||
|
||||
}
|
||||
if (result.sampleRate === 0) result.sampleRate = 48000;
|
||||
result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate);
|
||||
|
||||
let bestBuffers = getBestBuffers(selectedDevice,result.bufferSize,result.numberOfBuffers);
|
||||
result.bufferSize = bestBuffers.bufferSize;
|
||||
result.numberOfBuffers = bestBuffers.numberOfBuffers;
|
||||
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
if (this.props.open) {
|
||||
this.requestAlsaInfo();
|
||||
}
|
||||
|
||||
}
|
||||
componentDidUpdate(oldProps: JackServerSettingsDialogProps) {
|
||||
if ((this.props.open && !oldProps.open) && this.mounted) {
|
||||
let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices);
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
if (!this.state.alsaDevices) {
|
||||
this.requestAlsaInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
|
||||
}
|
||||
getSelectedDevice(deviceId: string): AlsaDeviceInfo | null {
|
||||
if (!this.state.alsaDevices) return null;
|
||||
for (let i = 0; i < this.state.alsaDevices.length; ++i) {
|
||||
if (this.state.alsaDevices[i].id === deviceId) {
|
||||
return this.state.alsaDevices[i];
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handleDeviceChanged(e: any) {
|
||||
let device = e.target.value as string;
|
||||
|
||||
let selectedDevice = this.getSelectedDevice(device);
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.alsaDevice = device;
|
||||
settings = this.applyAlsaDevices(settings,this.state.alsaDevices);
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
handleRateChanged(e: any) {
|
||||
let rate = e.target.value as number;
|
||||
console.log("New rate: " + rate);
|
||||
let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice);
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.sampleRate = rate;
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
handleSizeChanged(e: any) {
|
||||
let size = e.target.value as number;
|
||||
|
||||
let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice);
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.bufferSize = size;
|
||||
let bestBufferSetting = getBestBuffers(selectedDevice,settings.bufferSize,settings.numberOfBuffers,true);
|
||||
settings.bufferSize = bestBufferSetting.bufferSize;
|
||||
settings.numberOfBuffers = bestBufferSetting.numberOfBuffers;
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
handleNumberOfBuffersChanged(e: any) {
|
||||
let bufferCount = e.target.value as number;
|
||||
|
||||
let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice);
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.numberOfBuffers = bufferCount;
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
if (this.state.okEnabled)
|
||||
{
|
||||
this.props.onApply(this.state.jackServerSettings.clone());
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
const { onClose, open } = this.props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
let waitingForDevices = !this.state.alsaDevices
|
||||
let noDevices = this.state.alsaDevices && this.state.alsaDevices.length === 0;
|
||||
let selectedDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
if (this.state.alsaDevices) {
|
||||
for (let device of this.state.alsaDevices) {
|
||||
if (device.id === this.state.jackServerSettings.alsaDevice) {
|
||||
selectedDevice = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let bufferSizes: number[] = getValidBufferSizes(selectedDevice);
|
||||
let bufferCounts = getValidBufferCounts(this.state.jackServerSettings.bufferSize,selectedDevice);
|
||||
let bufferSizeDisabled = !selectedDevice;
|
||||
let bufferCountDisabled = !selectedDevice;
|
||||
|
||||
return (
|
||||
<DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
||||
onEnterKey={() => {
|
||||
this.handleApply();
|
||||
}}
|
||||
|
||||
>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="jsd_device">Device</InputLabel>
|
||||
<Select variant="standard" onChange={(e) => this.handleDeviceChanged(e)}
|
||||
value={this.state.jackServerSettings.alsaDevice}
|
||||
style={{width: 220}}
|
||||
inputProps={{
|
||||
name: "Device",
|
||||
id: "jsd_device",
|
||||
}}
|
||||
>
|
||||
{(noDevices && !waitingForDevices) &&
|
||||
(
|
||||
<MenuItem value={INVALID_DEVICE_ID}>No suitable devices.</MenuItem>
|
||||
)
|
||||
}
|
||||
{((!noDevices) && !waitingForDevices) && (
|
||||
this.state.alsaDevices!.map((device) =>
|
||||
(
|
||||
<MenuItem key={device.id} value={device.id}>{device.name}</MenuItem>
|
||||
)
|
||||
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div><div>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="jsd_sampleRate">Sample rate</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleRateChanged(e)}
|
||||
value={this.state.jackServerSettings.sampleRate}
|
||||
disabled={!selectedDevice}
|
||||
inputProps={{
|
||||
id: 'jsd_sampleRate',
|
||||
style: {
|
||||
textAlign: "right"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDevice &&
|
||||
selectedDevice.sampleRates.map((sr) => {
|
||||
return ( <MenuItem value={sr}>{sr}</MenuItem> );
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div style={{ display: "inline", whiteSpace: "nowrap" }}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleSizeChanged(e)}
|
||||
value={this.state.jackServerSettings.bufferSize}
|
||||
disabled={bufferSizeDisabled}
|
||||
inputProps={{
|
||||
name: 'Buffer size',
|
||||
id: 'jsd_bufferSize',
|
||||
}}
|
||||
>
|
||||
{bufferSizes.map((buffSize) =>
|
||||
(
|
||||
<MenuItem key={"b"+buffSize} value={buffSize}>{buffSize}</MenuItem>
|
||||
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleNumberOfBuffersChanged(e)}
|
||||
value={this.state.jackServerSettings.numberOfBuffers}
|
||||
disabled={bufferCountDisabled}
|
||||
inputProps={{
|
||||
name: 'Number of buffers',
|
||||
id: 'jsd_bufferCount',
|
||||
}}
|
||||
>
|
||||
{
|
||||
bufferCounts.map((bufferCount)=>
|
||||
{
|
||||
return (
|
||||
<MenuItem key={bufferCount} value={bufferCount}>{bufferCount.toString()}</MenuItem>
|
||||
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<Typography display="block" variant="caption" style={{ textAlign: "left", marginTop: 8, marginLeft: 24 }}
|
||||
color="textSecondary">
|
||||
Latency: {this.state.latencyText}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => this.handleApply()}
|
||||
disabled={!this.state.okEnabled}>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default JackServerSettingsDialog;
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory,State } from './PiPedalModel';
|
||||
import JackHostStatus from './JackHostStatus';
|
||||
|
||||
|
||||
|
||||
|
||||
interface JackStatusViewProps {
|
||||
|
||||
};
|
||||
|
||||
interface JackStatusViewState {
|
||||
jackStatus?: JackHostStatus;
|
||||
}
|
||||
|
||||
|
||||
export default class JackStatusView extends React.Component<JackStatusViewProps, JackStatusViewState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: JackStatusViewProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
jackStatus: undefined
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.tick = this.tick.bind(this);
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.model.state.get() === State.Ready) {
|
||||
this.model.getJackStatus()
|
||||
.then(jackStatus => {
|
||||
this.setState({jackStatus: jackStatus});
|
||||
})
|
||||
.catch(error => { /* ignore*/ });
|
||||
}
|
||||
}
|
||||
|
||||
timerHandle?: number;
|
||||
componentDidMount() {
|
||||
this.timerHandle = setInterval(this.tick, 1000);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (this.timerHandle) {
|
||||
clearTimeout(this.timerHandle);
|
||||
this.timerHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute", right: 30, bottom: 0, height: 30, width: 200,
|
||||
paddingRight: 20, paddingBottom: 6,
|
||||
textAlign: "right", opacity: 0.7, whiteSpace: "nowrap", fontSize: 12, zIndex: 10, fontWeight: 900
|
||||
}}>
|
||||
{JackHostStatus.getDisplayView("",this.state.jackStatus) }
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2023 Robin 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.
|
||||
|
||||
|
||||
// Utility class for constructing Json atoms.
|
||||
class JsonAtom {
|
||||
static Property(value: string): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Property",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Bool(value: boolean): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Bool",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Int(value: number): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Int",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Long(value: number): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Long",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Float(value: number): any
|
||||
{
|
||||
return value;
|
||||
}
|
||||
static Double(value: number): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Double",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static String(value: string): any
|
||||
{
|
||||
return value;
|
||||
}
|
||||
static Path(value: string): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Path",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Uri(value: string): any
|
||||
{
|
||||
return {
|
||||
"otype_": "URI",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Urid(value: string): any
|
||||
{
|
||||
return {
|
||||
"otype_": "URID",
|
||||
"value": value
|
||||
};
|
||||
}
|
||||
static Object(type: string): any
|
||||
{
|
||||
return {
|
||||
"otype_": type
|
||||
};
|
||||
}
|
||||
static Tuple(values: any[]): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Tuple",
|
||||
"value": values
|
||||
};
|
||||
}
|
||||
static IntVector(values: []): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Vector",
|
||||
"vtype_": "Int",
|
||||
"value": values
|
||||
};
|
||||
}
|
||||
static LongVector(values: []): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Vector",
|
||||
"vtype_": "Long",
|
||||
"value": values
|
||||
};
|
||||
}
|
||||
|
||||
static FloatVector(values: []): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Vector",
|
||||
"vtype_": "Float",
|
||||
"value": values
|
||||
};
|
||||
}
|
||||
static DoubleVector(values: []): any
|
||||
{
|
||||
return {
|
||||
"otype_": "Vector",
|
||||
"vtype_": "Float",
|
||||
"value": values
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default JsonAtom;
|
||||
@@ -0,0 +1,918 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { ReactNode, SyntheticEvent, CSSProperties, Fragment, ReactElement } from 'react';
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory, FavoritesList } from './PiPedalModel';
|
||||
import { UiPlugin, PluginType } from './Lv2Plugin';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import Button from '@mui/material/Button';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import PluginInfoDialog from './PluginInfoDialog'
|
||||
import PluginIcon from './PluginIcon'
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PluginClass from './PluginClass'
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import SearchControl from './SearchControl';
|
||||
import SearchFilter from './SearchFilter';
|
||||
import { FixedSizeGrid } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import FilterListOffIcon from '@mui/icons-material/FilterListOff';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
export type CloseEventHandler = () => void;
|
||||
export type OkEventHandler = (pluginUri: string) => void;
|
||||
|
||||
const NARROW_DISPLAY_THRESHOLD = 600;
|
||||
const FILTER_STORAGE_KEY = "com.twoplay.pipedal.load_dlg.filter";
|
||||
|
||||
|
||||
|
||||
const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}),
|
||||
|
||||
top: css({
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
left: "0px",
|
||||
bottom: "64px",
|
||||
position: "relative",
|
||||
flexGrow: 1,
|
||||
overflowX: "hidden",
|
||||
overflowY: "visible"
|
||||
}),
|
||||
bottom: css({
|
||||
position: "relative",
|
||||
bottom: "0px",
|
||||
height: "64px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: "24px",
|
||||
paddingRight: "48px",
|
||||
|
||||
}),
|
||||
paper: css({
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
height: "56px",
|
||||
width: "100%",
|
||||
background: theme.palette.background.paper
|
||||
}),
|
||||
buttonBase: css({
|
||||
|
||||
width: "100%",
|
||||
height: "100%'"
|
||||
}),
|
||||
content: css({
|
||||
marginTop: "4px",
|
||||
marginBottom: "4px",
|
||||
marginLeft: "8px",
|
||||
marginRight: "8px",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
height: 48,
|
||||
alignItems: "start",
|
||||
flexWrap: "nowrap"
|
||||
}),
|
||||
content2: css({
|
||||
display: "flex", flexDirection: "column", flex: "1 1 auto", width: "100%",
|
||||
paddingLeft: 12, whiteSpace: "nowrap", textOverflow: "ellipsis"
|
||||
|
||||
}),
|
||||
favoriteDecoration: css({
|
||||
flex: "0 0 auto"
|
||||
}),
|
||||
table: css({
|
||||
borderCollapse: "collapse",
|
||||
}),
|
||||
icon: css({
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6"
|
||||
}),
|
||||
iconBorder: css({
|
||||
flex: "0 0 auto",
|
||||
paddingTop: "4px"
|
||||
}),
|
||||
label: css({
|
||||
width: "100%"
|
||||
}),
|
||||
label2: css({
|
||||
marginTop: 8,
|
||||
color: theme.palette.text.secondary
|
||||
}),
|
||||
control: css({
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
tdText: css({
|
||||
padding: "0px"
|
||||
|
||||
})
|
||||
})
|
||||
;
|
||||
|
||||
interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
|
||||
onOk: OkEventHandler;
|
||||
onCancel: CloseEventHandler;
|
||||
uri?: string;
|
||||
minimumItemWidth?: number;
|
||||
theme: Theme;
|
||||
open: boolean
|
||||
};
|
||||
|
||||
type PluginGridState = {
|
||||
selected_uri?: string,
|
||||
search_string: string;
|
||||
search_collapsed: boolean;
|
||||
filterType: PluginType,
|
||||
client_width: number,
|
||||
client_height: number,
|
||||
grid_cell_width: number,
|
||||
grid_cell_columns: number,
|
||||
minimumItemWidth: number,
|
||||
favoritesList: FavoritesList,
|
||||
uiPlugins: UiPlugin[]
|
||||
//gridItems: UiPlugin[];
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const LoadPluginDialog =
|
||||
withTheme(
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<PluginGridProps, PluginGridState> {
|
||||
model: PiPedalModel;
|
||||
searchInputRef: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: PluginGridProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.searchInputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
let filterType_ = PluginType.Plugin; // i.e. "Any".
|
||||
let persistedFilter = window.localStorage.getItem(FILTER_STORAGE_KEY);
|
||||
if (persistedFilter) {
|
||||
filterType_ = persistedFilter as PluginType;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selected_uri: this.props.uri,
|
||||
search_string: "",
|
||||
search_collapsed: true,
|
||||
filterType: filterType_,
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight,
|
||||
grid_cell_width: this.getCellWidth(window.innerWidth),
|
||||
grid_cell_columns: this.getCellColumns(window.innerWidth),
|
||||
minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220,
|
||||
favoritesList: this.model.favorites.get(),
|
||||
uiPlugins: this.model.ui_plugins.get()
|
||||
|
||||
};
|
||||
|
||||
this.updateWindowSize = this.updateWindowSize.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.handleOk = this.handleOk.bind(this);
|
||||
this.handleSearchStringReady = this.handleSearchStringReady.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this);
|
||||
this.handlePluginsChanged = this.handlePluginsChanged.bind(this);
|
||||
|
||||
this.requestScrollTo();
|
||||
}
|
||||
|
||||
nominal_column_width: number = 350;
|
||||
margin_reserve: number = 30;
|
||||
|
||||
getCellColumns(width: number) {
|
||||
let gridWidth = width - this.margin_reserve;
|
||||
let columns = Math.floor((gridWidth) / this.nominal_column_width);
|
||||
if (columns < 1) columns = 1;
|
||||
if (columns > 3) columns = 3;
|
||||
return columns;
|
||||
}
|
||||
|
||||
|
||||
getCellWidth(width: number) {
|
||||
return Math.floor(width / this.getCellColumns(width));
|
||||
}
|
||||
|
||||
handleKeyPress(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
let searchInput = this.searchInputRef.current;
|
||||
if (searchInput && e.target !== searchInput) // if the search input doesn't have focus.
|
||||
{
|
||||
if (this.searchInputRef.current) // we do have one, right?
|
||||
{
|
||||
if (e.key.length === 1) {
|
||||
if (/[a-zA-Z0-9]/.exec(e.key)) // if it's alpha-numeric.
|
||||
{
|
||||
if (this.state.search_collapsed) // and search is collapsed.
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
let newValue = searchInput.value + e.key;
|
||||
searchInput.value = newValue; // add the key. to the value.
|
||||
this.handleSearchStringChanged(newValue);
|
||||
searchInput.focus();
|
||||
this.setState({
|
||||
search_collapsed: false
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateWindowSize() {
|
||||
this.setState({
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight,
|
||||
grid_cell_width: this.getCellWidth(window.innerWidth),
|
||||
grid_cell_columns: this.getCellColumns(window.innerWidth)
|
||||
});
|
||||
}
|
||||
|
||||
handleFavoritesChanged() {
|
||||
let favorites = this.model.favorites.get();
|
||||
this.requestScrollTo();
|
||||
this.setState(
|
||||
{
|
||||
favoritesList: favorites,
|
||||
});
|
||||
}
|
||||
handlePluginsChanged(plugins: UiPlugin[]) {
|
||||
this.setState(
|
||||
{
|
||||
uiPlugins: plugins
|
||||
}
|
||||
);
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.updateWindowSize();
|
||||
window.addEventListener('resize', this.updateWindowSize);
|
||||
|
||||
this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged);
|
||||
this.model.ui_plugins.addOnChangedHandler(this.handlePluginsChanged);
|
||||
|
||||
this.setState({
|
||||
favoritesList: this.model.favorites.get(),
|
||||
uiPlugins: this.model.ui_plugins.get()
|
||||
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.cancelSearchTimeout();
|
||||
this.model.ui_plugins.removeOnChangedHandler(this.handlePluginsChanged);
|
||||
this.model.favorites.removeOnChangedHandler(this.handleFavoritesChanged);
|
||||
window.removeEventListener('resize', this.updateWindowSize);
|
||||
this.mounted = false;
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: PluginGridProps) {
|
||||
if (oldProps.open !== this.props.open) {
|
||||
if (this.props.open) {
|
||||
// reset state now.
|
||||
if (this.state.search_string !== "" || !this.state.search_collapsed) {
|
||||
this.setState({
|
||||
search_string: "",
|
||||
search_collapsed: true,
|
||||
});
|
||||
}
|
||||
this.requestScrollTo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width, height);
|
||||
|
||||
}
|
||||
|
||||
onFilterChange(e: any) {
|
||||
let filterValue = e.target.value as PluginType;
|
||||
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, filterValue as string);
|
||||
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
filterType: filterValue,
|
||||
});
|
||||
}
|
||||
onClearFilter(): void {
|
||||
let value = PluginType.Plugin;
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
if (this.state.filterType !== value) {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
filterType: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectItem(item: number): void {
|
||||
let uri: string = "";
|
||||
let plugins = this.model.ui_plugins.get();
|
||||
if (item >= 0 && item < plugins.length) {
|
||||
uri = plugins[item].uri;
|
||||
}
|
||||
if (uri !== this.state.selected_uri) {
|
||||
this.setState({ selected_uri: uri });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
handleCancel(e: SyntheticEvent): void {
|
||||
this.cancel();
|
||||
}
|
||||
handleOk(e?: SyntheticEvent): void {
|
||||
|
||||
let selectedPlugin: UiPlugin | undefined = undefined;
|
||||
if (this.state.selected_uri) {
|
||||
let t = this.model.getUiPlugin(this.state.selected_uri);
|
||||
if (t) selectedPlugin = t;
|
||||
}
|
||||
if (!selectedPlugin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.state.selected_uri) {
|
||||
this.props.onOk(this.state.selected_uri);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick(e: SyntheticEvent, uri: string): void {
|
||||
// handled with onClick e.detail which provides better behaviour on Chrome.
|
||||
// this.props.onOk(uri);
|
||||
}
|
||||
|
||||
|
||||
cancel(): void {
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
fireSelected(item?: UiPlugin) {
|
||||
if (item) {
|
||||
this.setState({ selected_uri: item.uri });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onClick(e: React.MouseEvent<HTMLDivElement>, uri: string): void {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
this.setState({ selected_uri: uri });
|
||||
|
||||
// a better doubleclick: tracks how many recent clicks in the same area.
|
||||
// regardless of whether we re-rendered in the interrim.
|
||||
let isDoubleClick = e.detail === 2;
|
||||
// we have to synthesize double clicks because
|
||||
// DOM rewrites interfere with natural double click.
|
||||
if (isDoubleClick) {
|
||||
this.props.onOk(uri);
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseEnter(e: SyntheticEvent, uri: string): void {
|
||||
// this.setHoverUri(uri);
|
||||
|
||||
}
|
||||
handleMouseLeave(e: SyntheticEvent, uri: string): void {
|
||||
// this.setHoverUri("");
|
||||
|
||||
}
|
||||
onInfoClicked(): void {
|
||||
// let selectedUri = this.state.selected_uri;
|
||||
}
|
||||
|
||||
vst3_indicator(uiPlugin?: UiPlugin) {
|
||||
if (uiPlugin?.is_vst3) {
|
||||
return (<img alt="vst" src="/img/vst.svg" style={{ marginLeft: 8, height: 22, opacity: 0.6, position: "relative", top: -1 }} />)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
stereo_indicator(uiPlugin?: UiPlugin): string {
|
||||
if (!uiPlugin) return "";
|
||||
if (uiPlugin.audio_inputs === 2 || uiPlugin.audio_outputs === 2) {
|
||||
return " (Stereo)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
info_string(uiPlugin?: UiPlugin): ReactElement {
|
||||
if (!uiPlugin) {
|
||||
return (<Fragment />);
|
||||
} else {
|
||||
let stereoIndicator = "\u00A0" + this.stereo_indicator(uiPlugin)
|
||||
if (uiPlugin.author_name !== "") {
|
||||
if (uiPlugin.author_homepage !== "") {
|
||||
return (<Fragment>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ",\u00A0"}</Typography>
|
||||
</div>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<a href={uiPlugin.author_homepage} target="_blank" rel="noopener noreferrer" >
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.author_name}</Typography>
|
||||
</a>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{stereoIndicator}</Typography>
|
||||
</div>
|
||||
</Fragment>);
|
||||
|
||||
} else {
|
||||
return (<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ", " + uiPlugin.author_name + stereoIndicator}</Typography>
|
||||
</Fragment>);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin?.name ?? "" + stereoIndicator}</Typography>
|
||||
</Fragment>);
|
||||
|
||||
}
|
||||
}
|
||||
createFilterChildren(result: ReactNode[], classNode: PluginClass, level: number): void {
|
||||
for (let i = 0; i < classNode.children.length; ++i) {
|
||||
let child = classNode.children[i];
|
||||
let name = "\u00A0".repeat(level * 3 + 1) + child.display_name;
|
||||
result.push((<MenuItem key={child.plugin_type} value={child.plugin_type}>{name}</MenuItem>));
|
||||
if (child.children.length !== 0) {
|
||||
this.createFilterChildren(result, child, level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
createFilterOptions(): ReactNode[] {
|
||||
let classes = this.model.plugin_classes.get();
|
||||
let result: ReactNode[] = [];
|
||||
|
||||
result.push((<MenuItem key={PluginType.Plugin} value={PluginType.Plugin}> All</MenuItem>));
|
||||
this.createFilterChildren(result, classes, 1);
|
||||
return result;
|
||||
|
||||
}
|
||||
getFilteredPlugins(plugins: UiPlugin[], searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] {
|
||||
try {
|
||||
if (searchString === null) {
|
||||
searchString = this.state.search_string;
|
||||
}
|
||||
if (filterType === null) {
|
||||
filterType = this.state.filterType;
|
||||
}
|
||||
if (favoritesList === null) {
|
||||
favoritesList = this.state.favoritesList;
|
||||
}
|
||||
|
||||
let results: { score: number; plugin: UiPlugin }[] = [];
|
||||
let searchFilter = new SearchFilter(searchString);
|
||||
let rootClass = this.model.plugin_classes.get();
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let plugin = plugins[i];
|
||||
try {
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
|
||||
let score: number = 0;
|
||||
if (plugin.is_vst3) {
|
||||
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name, "vst3");
|
||||
} else {
|
||||
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name);
|
||||
}
|
||||
|
||||
if (score !== 0) {
|
||||
if (favoritesList[plugin.uri]) {
|
||||
score += 32768;
|
||||
}
|
||||
results.push({ score: score, plugin: plugin });
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("Bad plugin:" + (plugin?.name ?? "null") + " plugin type: " + (plugin?.plugin_type ?? "null"
|
||||
+ " " + e.toString()));
|
||||
}
|
||||
}
|
||||
results.sort((left: { score: number; plugin: UiPlugin }, right: { score: number; plugin: UiPlugin }) => {
|
||||
if (right.score < left.score) return -1;
|
||||
if (right.score > left.score) return 1;
|
||||
return left.plugin.name.localeCompare(right.plugin.name);
|
||||
});
|
||||
let t: UiPlugin[] = [];
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
t.push(results[i].plugin);
|
||||
}
|
||||
return t;
|
||||
} catch (e: any) {
|
||||
alert("getFilteredPlugins: " + e.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
changedSearchString?: string = undefined;
|
||||
hSearchTimeout?: number;
|
||||
|
||||
private scrollToRequested = false;
|
||||
requestScrollTo() {
|
||||
this.scrollToRequested = true;
|
||||
}
|
||||
|
||||
private cachedGridItems: UiPlugin[] = []; // retained for the scrollTo callback.
|
||||
|
||||
handleScrollToCallback(element: FixedSizeGrid): void {
|
||||
if (element) {
|
||||
if (this.scrollToRequested) {
|
||||
this.scrollToRequested = false;
|
||||
let position = -1;
|
||||
let gridItems = this.cachedGridItems;
|
||||
for (let i = 0; i < gridItems.length; ++i) {
|
||||
if (this.state.selected_uri === gridItems[i].uri) {
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position !== -1) {
|
||||
element.scrollToItem({ rowIndex: Math.floor(position / this.state.grid_cell_columns) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleSearchStringReady() {
|
||||
if (!this.mounted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (this.changedSearchString !== undefined) {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_string: this.changedSearchString
|
||||
});
|
||||
this.changedSearchString = undefined;
|
||||
}
|
||||
this.hSearchTimeout = undefined;
|
||||
}
|
||||
|
||||
handleApplyFilter()
|
||||
{
|
||||
this.handleSearchStringReady();
|
||||
}
|
||||
|
||||
cancelSearchTimeout()
|
||||
{
|
||||
if (this.hSearchTimeout) {
|
||||
clearTimeout(this.hSearchTimeout);
|
||||
this.hSearchTimeout = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleSearchStringChanged(text: string): void {
|
||||
this.cancelSearchTimeout();
|
||||
this.changedSearchString = text;
|
||||
this.hSearchTimeout = setTimeout(
|
||||
this.handleSearchStringReady,
|
||||
2000);
|
||||
|
||||
}
|
||||
renderItem(gridItems: UiPlugin[], row: number, column: number): React.ReactNode {
|
||||
let item: number = (row) * this.gridColumnCount + (column);
|
||||
if (item >= gridItems.length) {
|
||||
return (<div />);
|
||||
}
|
||||
let value = gridItems[item];
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let isFavorite: boolean = this.state.favoritesList[value.uri] ?? false;
|
||||
|
||||
return (
|
||||
<div key={value.uri}
|
||||
onDoubleClick={(e) => { this.onDoubleClick(e, value.uri) }}
|
||||
onClick={(e) => { this.onClick(e, value.uri) }}
|
||||
onMouseEnter={(e) => { this.handleMouseEnter(e, value.uri) }}
|
||||
onMouseLeave={(e) => { this.handleMouseLeave(e, value.uri) }}
|
||||
>
|
||||
<ButtonBase className={classes.buttonBase} >
|
||||
<SelectHoverBackground selected={value.uri === this.state.selected_uri} showHover={true} />
|
||||
<div className={classes.content}>
|
||||
<div className={classes.iconBorder} >
|
||||
<PluginIcon pluginType={value.plugin_type} size={24} opacity={0.6} />
|
||||
</div>
|
||||
<div className={classes.content2}>
|
||||
<div className={classes.label} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }} >
|
||||
|
||||
<Typography color="textPrimary" noWrap sx={{ display: "block", flex: "0 1 auto", }} >
|
||||
{value.name}
|
||||
</Typography>
|
||||
{
|
||||
isFavorite && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<StarIcon sx={{ color: "#C80", fontSize: 16, marginRight: "2px" }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
<Typography color="textSecondary" noWrap>
|
||||
{value.plugin_display_type} {this.stereo_indicator(value)}
|
||||
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.favoriteDecoration}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
setFavorite(pluginUri: string | undefined, isFavorite: boolean): void {
|
||||
if (!pluginUri) return;
|
||||
this.model.setFavorite(pluginUri, isFavorite);
|
||||
}
|
||||
gridColumnCount: number = 1;
|
||||
|
||||
|
||||
render() {
|
||||
const classes= withStyles.getClasses(this.props);
|
||||
|
||||
let selectedPlugin: UiPlugin | undefined = undefined;
|
||||
if (this.state.selected_uri) {
|
||||
let t = this.model.getUiPlugin(this.state.selected_uri);
|
||||
if (t) selectedPlugin = t;
|
||||
}
|
||||
|
||||
let showSearchIcon = true;
|
||||
if (this.state.client_width < 500) {
|
||||
showSearchIcon = this.state.search_collapsed
|
||||
}
|
||||
let gridColumnCount = this.state.grid_cell_columns;
|
||||
this.gridColumnCount = gridColumnCount;
|
||||
let isFavorite = this.state.favoritesList[this.state.selected_uri ?? ""];
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogEx tag="plugins"
|
||||
onEnterKey={()=>{ this.handleOk(); }}
|
||||
onKeyPress={(e) => { this.handleKeyPress(e); }}
|
||||
|
||||
fullScreen={true}
|
||||
TransitionComponent={undefined}
|
||||
maxWidth={false}
|
||||
open={this.props.open}
|
||||
scroll="body"
|
||||
onClose={this.handleCancel}
|
||||
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
|
||||
aria-labelledby="select-plugin-dialog-title">
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54 }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
|
||||
<IconButton onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography display="inline" noWrap variant="h6" style={{
|
||||
flex: "0 0 auto", height: "auto", verticalAlign: "center",
|
||||
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
|
||||
width: (this.state.search_collapsed ? undefined : "0px")
|
||||
}}>
|
||||
{this.state.client_width > 520 ? "Select Plugin" : ""}
|
||||
</Typography>
|
||||
<div style={{ flex: "1 1 auto" }} >
|
||||
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
|
||||
inputRef={this.searchInputRef}
|
||||
showSearchIcon={showSearchIcon}
|
||||
onTextChanged={(text) => {
|
||||
this.handleSearchStringChanged(text);
|
||||
}}
|
||||
onApplyFilter={() => {
|
||||
this.handleApplyFilter();
|
||||
}}
|
||||
onClearFilterClick={() => {
|
||||
if (this.state.search_string !== "") {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_collapsed: true,
|
||||
search_string: ""
|
||||
});
|
||||
|
||||
} else {
|
||||
this.setState({ search_collapsed: true });
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (this.state.search_collapsed) {
|
||||
this.setState({ search_collapsed: false });
|
||||
} else {
|
||||
|
||||
if (this.state.search_string !== "") {
|
||||
this.requestScrollTo();
|
||||
this.setState({
|
||||
search_collapsed: true,
|
||||
search_string: ""
|
||||
});
|
||||
|
||||
} else {
|
||||
this.setState({ search_collapsed: true });
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }} >
|
||||
<IconButton onClick={() => { this.onClearFilter(); }}>
|
||||
{this.state.filterType === PluginType.Plugin ? (
|
||||
<FilterListIcon fontSize='small' style={{ opacity: 0.75 }} />
|
||||
) : (
|
||||
<FilterListOffIcon fontSize='small' style={{ opacity: 0.75 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
|
||||
|
||||
<TextField select variant="standard"
|
||||
defaultValue={this.state.filterType}
|
||||
key={this.state.filterType}
|
||||
onChange={(e) => { this.onFilterChange(e); }}
|
||||
sx={{ minWidth: 160 }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</TextField>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers style={{
|
||||
height: "100px",
|
||||
padding: 8,
|
||||
flex: "1 1 100px",
|
||||
}} >
|
||||
<AutoSizer>
|
||||
{(arg: any) => {
|
||||
if ((!arg.width) || (!arg.height)) {
|
||||
return (<div></div>);
|
||||
}
|
||||
let width = arg.width ?? 1;
|
||||
let height = arg.height ?? 1;
|
||||
let gridItems = this.getFilteredPlugins(
|
||||
this.state.uiPlugins, this.state.search_string, this.state.filterType, this.state.favoritesList);
|
||||
this.cachedGridItems = gridItems;
|
||||
|
||||
let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); }
|
||||
|
||||
|
||||
return (
|
||||
<FixedSizeGrid
|
||||
ref={scrollRef}
|
||||
width={width}
|
||||
columnCount={this.gridColumnCount}
|
||||
columnWidth={(width - 40) / this.gridColumnCount}
|
||||
height={height}
|
||||
rowHeight={64}
|
||||
overscanRowCount={10}
|
||||
rowCount={Math.ceil(gridItems.length / this.gridColumnCount)}
|
||||
itemKey={(args: { columnIndex: number, data: any, rowIndex: number }) => {
|
||||
let index = args.columnIndex + this.gridColumnCount * args.rowIndex;
|
||||
if (index >= gridItems.length) {
|
||||
return "blank-" + args.columnIndex + "-" + args.rowIndex;
|
||||
}
|
||||
let plugin = gridItems[index];
|
||||
return plugin.uri + "-" + args.rowIndex + "-" + args.columnIndex;
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
{(arg: { columnIndex: number, rowIndex: number, style: CSSProperties }) => (
|
||||
<div style={arg.style} >
|
||||
{
|
||||
this.renderItem(gridItems, arg.rowIndex, arg.columnIndex)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</FixedSizeGrid>
|
||||
);
|
||||
}
|
||||
}
|
||||
</AutoSizer>
|
||||
</DialogContent>
|
||||
{(this.state.client_width >= NARROW_DISPLAY_THRESHOLD) ? (
|
||||
<DialogActions style={{ flex: "0 0 auto" }} >
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ display: "flex", textOverflow: "ellipsis", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
{this.info_string(selectedPlugin)}
|
||||
{this.vst3_indicator(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri !== "" ? "block" : "none"),
|
||||
position: "relative", top: -2
|
||||
}}>
|
||||
<IconButton color="inherit" aria-label="Set as favorite"
|
||||
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
|
||||
>
|
||||
<StarBorderIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ position: "relative", float: "right", flex: "0 1 200px", width: 200, display: "flex", alignItems: "center" }}>
|
||||
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ width: 180 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
) : (
|
||||
<DialogActions style={{ flex: "0 0 auto", display: "block" }} >
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<div style={{ width: 1, height: 48 }} />
|
||||
{this.info_string(selectedPlugin)}
|
||||
<div style={{
|
||||
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri ? "block" : "block"),
|
||||
position: "relative"
|
||||
}}>
|
||||
{
|
||||
this.state.selected_uri !== "uri://two-play/pipedal/pedalboard#Empty" && (
|
||||
<IconButton color="inherit" aria-label="Set as favorite"
|
||||
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
|
||||
>
|
||||
<StarBorderIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom} style={{ height: 64 }}>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
|
||||
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</DialogEx>
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
}
|
||||
,
|
||||
pluginGridStyles
|
||||
));
|
||||
export default LoadPluginDialog;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,647 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import {
|
||||
Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType
|
||||
} from './Pedalboard';
|
||||
import Button from '@mui/material/Button';
|
||||
import InputIcon from '@mui/icons-material/Input';
|
||||
import LoadPluginDialog from './LoadPluginDialog';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import PedalboardView from './PedalboardView';
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import PluginInfoDialog from './PluginInfoDialog';
|
||||
import { GetControlView } from './ControlViewFactory';
|
||||
import MidiBindingsDialog from './MidiBindingsDialog';
|
||||
import PluginPresetSelector from './PluginPresetSelector';
|
||||
import OldDeleteIcon from "./svg/old_delete_outline_24dp.svg?react";
|
||||
import MidiIcon from "./svg/ic_midi.svg?react";
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Snapshot0Icon from "./svg/snapshot_0.svg?react";
|
||||
import Snapshot1Icon from "./svg/snapshot_1.svg?react";
|
||||
import Snapshot2Icon from "./svg/snapshot_2.svg?react";
|
||||
import Snapshot3Icon from "./svg/snapshot_3.svg?react";
|
||||
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
|
||||
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
|
||||
import Snapshot6Icon from "./svg/snapshot_6.svg?react";
|
||||
|
||||
import SnapshotDialog from './SnapshotDialog';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
const SPLIT_CONTROLBAR_THRESHHOLD = 750;
|
||||
const DISPLAY_AUTHOR_THRESHHOLD = 750;
|
||||
const DISPLAY_AUTHOR_SPLIT_THRESHOLD = 500;
|
||||
const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// const HORIZONTAL_LAYOUT_MQ = "@media (max-height: " + HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK + "px)";
|
||||
|
||||
const styles = ({ palette }: Theme) => { return {
|
||||
frame: css({
|
||||
position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden"
|
||||
}),
|
||||
pedalboardScroll: css({
|
||||
position: "relative", width: "100%",
|
||||
flex: "0 0 auto", overflow: "auto", maxHeight: 220
|
||||
}),
|
||||
pedalboardScrollSmall: css({
|
||||
position: "relative", width: "100%",
|
||||
flex: "1 1 1px", overflow: "auto"
|
||||
}),
|
||||
separator: css({
|
||||
width: "100%", height: "1px", background: "#888", opacity: "0.5",
|
||||
flex: "0 0 1px"
|
||||
}),
|
||||
|
||||
controlToolBar: css({
|
||||
flex: "0 0 auto", width: "100%", height: 48
|
||||
}),
|
||||
splitControlBar: css({
|
||||
flex: "0 0 64px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||
}),
|
||||
controlContent: css({
|
||||
flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240
|
||||
}),
|
||||
controlContentSmall: css({
|
||||
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
||||
}),
|
||||
title: css({ fontSize: "1.1em", fontWeight: 700, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 }),
|
||||
author: css({ fontWeight: 500, marginRight: 8, textOverflow: "ellipsis", whiteSpace: "nowrap", opacity: 0.75 })
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
interface MainProps extends WithStyles<typeof styles> {
|
||||
hasTinyToolBar: boolean;
|
||||
theme: Theme;
|
||||
enableStructureEditing: boolean;
|
||||
}
|
||||
|
||||
interface MainState {
|
||||
selectedPedal: number;
|
||||
selectedSnapshot: number;
|
||||
loadDialogOpen: boolean;
|
||||
snapshotDialogOpen: boolean;
|
||||
pedalboard: Pedalboard;
|
||||
addMenuAnchorEl: HTMLElement | null;
|
||||
splitControlBar: boolean;
|
||||
displayAuthor: boolean;
|
||||
horizontalScrollLayout: boolean;
|
||||
showMidiBindingsDialog: boolean;
|
||||
screenHeight: number;
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const MainPage =
|
||||
withTheme(
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<MainProps, MainState> {
|
||||
model: PiPedalModel;
|
||||
|
||||
getSplitToolbar() {
|
||||
return this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD;
|
||||
}
|
||||
getDisplayAuthor()
|
||||
{
|
||||
if (this.getSplitToolbar())
|
||||
{
|
||||
return this.windowSize.width >= DISPLAY_AUTHOR_SPLIT_THRESHOLD;
|
||||
} else {
|
||||
return this.windowSize.width >= DISPLAY_AUTHOR_THRESHHOLD;
|
||||
}
|
||||
}
|
||||
constructor(props: MainProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let selectedPedal = pedalboard.getFirstSelectableItem();
|
||||
|
||||
this.state = {
|
||||
selectedPedal: selectedPedal,
|
||||
selectedSnapshot: -1,
|
||||
loadDialogOpen: false,
|
||||
snapshotDialogOpen: false,
|
||||
pedalboard: pedalboard,
|
||||
addMenuAnchorEl: null,
|
||||
splitControlBar: this.getSplitToolbar(),
|
||||
displayAuthor: this.getDisplayAuthor(),
|
||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||
showMidiBindingsDialog: false,
|
||||
screenHeight: this.windowSize.height
|
||||
|
||||
|
||||
};
|
||||
this.onSelectionChanged = this.onSelectionChanged.bind(this);
|
||||
this.onPedalDoubleClick = this.onPedalDoubleClick.bind(this);
|
||||
this.onLoadClick = this.onLoadClick.bind(this);
|
||||
this.onLoadOk = this.onLoadOk.bind(this);
|
||||
this.onLoadCancel = this.onLoadCancel.bind(this);
|
||||
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
||||
this.onSelectedSnapshotChanged = this.onSelectedSnapshotChanged.bind(this);
|
||||
this.handleEnableCurrentItemChanged = this.handleEnableCurrentItemChanged.bind(this);
|
||||
}
|
||||
|
||||
onInsertPedal(instanceId: number) {
|
||||
this.setAddMenuAnchorEl(null);
|
||||
let newId = this.model.addPedalboardItem(instanceId, false);
|
||||
this.setSelection(newId);
|
||||
}
|
||||
onAppendPedal(instanceId: number) {
|
||||
this.setAddMenuAnchorEl(null);
|
||||
let newId = this.model.addPedalboardItem(instanceId, true);
|
||||
|
||||
this.setSelection(newId);
|
||||
|
||||
}
|
||||
onInsertSplit(instanceId: number) {
|
||||
this.setAddMenuAnchorEl(null);
|
||||
let newId = this.model.addPedalboardSplitItem(instanceId, false);
|
||||
this.setSelection(newId);
|
||||
|
||||
}
|
||||
onAppendSplit(instanceId: number) {
|
||||
this.setAddMenuAnchorEl(null);
|
||||
let newId = this.model.addPedalboardSplitItem(instanceId, true);
|
||||
this.setSelection(newId);
|
||||
|
||||
}
|
||||
setAddMenuAnchorEl(value: HTMLElement | null) {
|
||||
this.setState({ addMenuAnchorEl: value });
|
||||
}
|
||||
onAddClick(e: SyntheticEvent) {
|
||||
this.setAddMenuAnchorEl(e.currentTarget as HTMLElement);
|
||||
}
|
||||
|
||||
handleMidiBindingsDialogClose() {
|
||||
this.setState({ showMidiBindingsDialog: false })
|
||||
}
|
||||
handleAddClose(): void {
|
||||
this.setAddMenuAnchorEl(null);
|
||||
}
|
||||
|
||||
handleMidiConfiguration(instanceId: number): void {
|
||||
this.setState({ showMidiBindingsDialog: true });
|
||||
}
|
||||
handleEnableCurrentItemChanged(event: any): void {
|
||||
let newValue = event.target.checked;
|
||||
let item = this.getSelectedPedalboardItem();
|
||||
if (item != null) {
|
||||
this.model.setPedalboardItemEnabled(item.getInstanceId(), newValue);
|
||||
|
||||
}
|
||||
}
|
||||
handleSelectPluginPreset(instanceId: number, presetInstanceId: number) {
|
||||
this.model.loadPluginPreset(instanceId, presetInstanceId);
|
||||
}
|
||||
onPedalboardChanged(value: Pedalboard) {
|
||||
let selectedItem = -1;
|
||||
if (this.state.selectedPedal === Pedalboard.START_CONTROL || this.state.selectedPedal === Pedalboard.END_CONTROL) {
|
||||
selectedItem = this.state.selectedPedal;
|
||||
} else if (value.hasItem(this.state.selectedPedal)) {
|
||||
selectedItem = this.state.selectedPedal;
|
||||
} else {
|
||||
selectedItem = value.getFirstSelectableItem();
|
||||
}
|
||||
this.setState({
|
||||
pedalboard: value,
|
||||
selectedPedal: selectedItem,
|
||||
selectedSnapshot: value.selectedSnapshot
|
||||
});
|
||||
}
|
||||
onSelectedSnapshotChanged(selectedSnapshot: number) {
|
||||
this.setState({
|
||||
selectedSnapshot: selectedSnapshot
|
||||
});
|
||||
|
||||
}
|
||||
onDeletePedal(instanceId: number): void {
|
||||
let result = this.model.deletePedalboardPedal(instanceId);
|
||||
if (result != null) {
|
||||
this.setState({ selectedPedal: result });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged);
|
||||
this.model.selectedSnapshot.addOnChangedHandler(this.onSelectedSnapshotChanged);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.selectedSnapshot.removeOnChangedHandler(this.onSelectedSnapshotChanged);
|
||||
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
updateResponsive() {
|
||||
this.setState({
|
||||
splitControlBar: this.getSplitToolbar(),
|
||||
displayAuthor: this.getDisplayAuthor(),
|
||||
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
|
||||
screenHeight: this.windowSize.height
|
||||
});
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width, height);
|
||||
this.updateResponsive();
|
||||
}
|
||||
|
||||
|
||||
setSelection(selectedId_: number): void {
|
||||
this.setState({ selectedPedal: selectedId_ });
|
||||
}
|
||||
|
||||
onSelectionChanged(selectedId: number): void {
|
||||
this.setSelection(selectedId);
|
||||
}
|
||||
onPedalDoubleClick(selectedId: number): void {
|
||||
this.setSelection(selectedId);
|
||||
let item = this.getPedalboardItem(selectedId);
|
||||
if (item != null) {
|
||||
if (item.isStart() || item.isEnd())
|
||||
{
|
||||
// do nothing.
|
||||
} else if (item.isSplit()) {
|
||||
let split = item as PedalboardSplitItem;
|
||||
if (split.getSplitType() === SplitType.Ab) {
|
||||
let cv = split.getToggleAbControlValue();
|
||||
if (split.instanceId === undefined) throw new PiPedalStateError("Split without valid id.");
|
||||
this.model.setPedalboardControl(split.instanceId, cv.key, cv.value);
|
||||
}
|
||||
} else {
|
||||
this.setState({ loadDialogOpen: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
onLoadCancel(): void {
|
||||
this.setState({ loadDialogOpen: false });
|
||||
}
|
||||
onLoadOk(selectedUri: string): void {
|
||||
this.setState({ loadDialogOpen: false });
|
||||
let itemId = this.state.selectedPedal;
|
||||
let newSelectedItem = this.model.loadPedalboardPlugin(itemId, selectedUri);
|
||||
this.setState({ selectedPedal: newSelectedItem });
|
||||
}
|
||||
onSnapshotDialogOk(): void {
|
||||
this.setState({ snapshotDialogOpen: false });
|
||||
}
|
||||
|
||||
onLoadClick(e: SyntheticEvent) {
|
||||
this.setState({ loadDialogOpen: true });
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
onSnapshotClick() {
|
||||
this.setState({ snapshotDialogOpen: true });
|
||||
}
|
||||
|
||||
getPedalboardItem(selectedId?: number): PedalboardItem | null {
|
||||
if (selectedId === undefined) return null;
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
if (!pedalboard) return null;
|
||||
|
||||
if (selectedId === Pedalboard.START_CONTROL) // synthetic input volume item.
|
||||
{
|
||||
return pedalboard.makeStartItem();
|
||||
} else if (selectedId === Pedalboard.END_CONTROL) // synthetic output volume.
|
||||
{
|
||||
return pedalboard.makeEndItem();
|
||||
}
|
||||
|
||||
let it = pedalboard.itemsGenerator();
|
||||
if (!selectedId) return null;
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
let item = v.value;
|
||||
if (item.instanceId === selectedId) {
|
||||
return item;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
onPedalboardPropertyChanged(instanceId: number, key: string, value: number) {
|
||||
this.model.setPedalboardControl(instanceId, key, value);
|
||||
}
|
||||
getSelectedPedalboardItem(): PedalboardItem | null {
|
||||
return this.getPedalboardItem(this.state.selectedPedal);
|
||||
}
|
||||
getSelectedUri(): string {
|
||||
let pedalboardItem = this.getSelectedPedalboardItem();
|
||||
if (pedalboardItem === null) return "";
|
||||
return pedalboardItem.uri;
|
||||
}
|
||||
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
|
||||
let title = "";
|
||||
let author = "";
|
||||
let infoPluginUri = "";
|
||||
let presetsUri = "";
|
||||
let missing = false;
|
||||
if (pedalboardItem) {
|
||||
if (pedalboardItem.isEmpty()) {
|
||||
title = "";
|
||||
} else if (pedalboardItem.isSplit()) {
|
||||
title = "Split";
|
||||
} else if (pedalboardItem.isSyntheticItem()) {
|
||||
title = pedalboardItem.pluginName ?? "#error";
|
||||
author = "";
|
||||
presetsUri = "";
|
||||
infoPluginUri = "";
|
||||
}
|
||||
else {
|
||||
let uiPlugin = this.model.getUiPlugin(pedalboardItem.uri);
|
||||
if (!uiPlugin) {
|
||||
missing = true;
|
||||
title = pedalboardItem?.pluginName ?? "Missing plugin";
|
||||
} else {
|
||||
title = uiPlugin.name;
|
||||
author = uiPlugin.author_name;
|
||||
presetsUri = uiPlugin.uri;
|
||||
// if (uiPlugin.description.length > 20) {
|
||||
// }
|
||||
infoPluginUri = uiPlugin.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
if (missing) {
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8, minWidth: 0,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto", minWidth: 0 }}>
|
||||
<span style={{ color: isDarkMode() ? "#F02020" : "#800000" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 1 auto", minWidth: 0, overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
{this.state.displayAuthor&&(
|
||||
<span className={classes.author}>{author}</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", verticalAlign: "center" }}>
|
||||
<PluginInfoDialog plugin_uri={infoPluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
snapshotIcon(theme: Theme, snapshotNumber: number) {
|
||||
switch (snapshotNumber + 1) {
|
||||
case 0:
|
||||
default:
|
||||
return (<Snapshot0Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 1:
|
||||
return (<Snapshot1Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 2:
|
||||
return (<Snapshot2Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 3:
|
||||
return (<Snapshot3Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 4:
|
||||
return (<Snapshot4Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 5:
|
||||
return (<Snapshot5Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
case 6:
|
||||
return (<Snapshot6Icon style={{ height: 24, width: 24, fill: theme.palette.text.primary, opacity: 0.6 }} />);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let pedalboardItem = this.getSelectedPedalboardItem();
|
||||
let uiPlugin = null;
|
||||
let bypassVisible = false;
|
||||
let bypassChecked = false;
|
||||
let canDelete = false;
|
||||
let canInsert = false;
|
||||
let canAppend = false;
|
||||
let canLoad = true;
|
||||
let instanceId = -1;
|
||||
let missing = false;
|
||||
let pluginUri = "#error";
|
||||
|
||||
if (pedalboardItem) {
|
||||
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
|
||||
instanceId = pedalboardItem.instanceId;
|
||||
if (pedalboardItem.isEmpty()) {
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
|
||||
} else if (pedalboardItem.isStart()) {
|
||||
canAppend = true;
|
||||
canDelete = false;
|
||||
canLoad = false;
|
||||
} else if (pedalboardItem.isEnd()) {
|
||||
canInsert = true;
|
||||
canDelete = false;
|
||||
canLoad = false;
|
||||
} else if (pedalboardItem.isSplit()) {
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
canLoad = false;
|
||||
} else {
|
||||
pluginUri = pedalboardItem.uri;
|
||||
uiPlugin = this.model.getUiPlugin(pluginUri);
|
||||
canInsert = true;
|
||||
canAppend = true;
|
||||
if (uiPlugin) {
|
||||
bypassVisible = true;
|
||||
bypassChecked = pedalboardItem.isEnabled;
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let horizontalScrollLayout = this.state.horizontalScrollLayout;
|
||||
|
||||
return (
|
||||
<div className={classes.frame}>
|
||||
<div id="pedalboardScroll" className={horizontalScrollLayout ? classes.pedalboardScrollSmall : classes.pedalboardScroll}
|
||||
style={{ maxHeight: horizontalScrollLayout ? undefined : this.state.screenHeight / 2 }}>
|
||||
<PedalboardView key={pluginUri} selectedId={this.state.selectedPedal}
|
||||
enableStructureEditing={this.props.enableStructureEditing}
|
||||
onSelectionChanged={this.onSelectionChanged}
|
||||
onDoubleClick={this.onPedalDoubleClick}
|
||||
hasTinyToolBar={this.props.hasTinyToolBar}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.separator} />
|
||||
<div className={classes.controlToolBar}>
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", alignItems: "center", justifyContent: "center", minWidth: 0,
|
||||
width: "100%", height: 48, paddingLeft: 16, paddingRight: 16
|
||||
}} >
|
||||
<div style={{ flex: "0 0 auto", width: this.state.splitControlBar? undefined: 80 }} >
|
||||
<div style={{ display: bypassVisible ? "block" : "none", width: this.state.splitControlBar? undefined: 80 }} >
|
||||
<Switch color="secondary" checked={bypassChecked} onChange={this.handleEnableCurrentItemChanged} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(!this.state.splitControlBar || !this.props.enableStructureEditing) && this.titleBar(pedalboardItem)
|
||||
}
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
</div>
|
||||
{this.props.enableStructureEditing && (
|
||||
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",alignItems: "center" }}>
|
||||
<div style={{ flex: "0 0 auto", display: (canInsert || canAppend) ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton onClick={(e) => { this.onAddClick(e) }} size="large">
|
||||
<AddIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="add-menu"
|
||||
anchorEl={this.state.addMenuAnchorEl}
|
||||
keepMounted
|
||||
open={Boolean(this.state.addMenuAnchorEl)}
|
||||
onClose={() => this.handleAddClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>)}
|
||||
<Divider />
|
||||
{canInsert && (<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>)}
|
||||
{canAppend && (<MenuItem onClick={() => this.onAppendSplit(instanceId)}>Append split</MenuItem>)}
|
||||
</Menu>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canDelete ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton
|
||||
onClick={() => { this.onDeletePedal(pedalboardItem?.instanceId ?? -1) }}
|
||||
size="large">
|
||||
<OldDeleteIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={this.onLoadClick}
|
||||
disabled={this.state.selectedPedal === -1 || (!canLoad) || (this.getSelectedPedalboardItem()?.isSplit() ?? true)}
|
||||
startIcon={<InputIcon />}
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined)
|
||||
}}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
onClick={(e) => { this.handleMidiConfiguration(instanceId); }}
|
||||
size="large">
|
||||
<MidiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
onClick={(e) => { this.setState({ snapshotDialogOpen: true }); }}
|
||||
size="large">
|
||||
{this.snapshotIcon(this.props.theme, this.state.selectedSnapshot)}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.splitControlBar && this.props.enableStructureEditing && (
|
||||
<div className={classes.splitControlBar}>
|
||||
{
|
||||
this.titleBar(pedalboardItem)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
{
|
||||
missing ? (
|
||||
<div style={{ marginLeft: 40, marginTop: 20 }}>
|
||||
<Typography variant="body1" paragraph={true}>Error: Plugin is not installed.</Typography>
|
||||
<Typography variant="body2" paragraph={true}>{pluginUri}</Typography>
|
||||
</div>
|
||||
|
||||
) :
|
||||
(
|
||||
GetControlView(pedalboardItem)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<MidiBindingsDialog open={this.state.showMidiBindingsDialog}
|
||||
onClose={() => this.setState({ showMidiBindingsDialog: false })}
|
||||
/>
|
||||
{
|
||||
(this.state.loadDialogOpen) && (
|
||||
<LoadPluginDialog open={this.state.loadDialogOpen} uri={this.getSelectedUri()}
|
||||
onOk={this.onLoadOk} onCancel={this.onLoadCancel}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
{(this.state.snapshotDialogOpen) && (
|
||||
<SnapshotDialog open={this.state.snapshotDialogOpen} onOk={() => this.onSnapshotDialogOk()} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
styles
|
||||
));
|
||||
|
||||
export default MainPage
|
||||
@@ -0,0 +1,356 @@
|
||||
import { isDarkMode } from "./DarkMode";
|
||||
|
||||
export const materialColors: { [Name: string]: { [Name: number]: string } } = {
|
||||
"grey": {
|
||||
50: '#FAFAFA',
|
||||
100: '#F5F5F5',
|
||||
200: '#EEEEEE',
|
||||
300: '#E0E0E0',
|
||||
400: '#BDBDBD',
|
||||
500: '#9E9E9E',
|
||||
600: '#757575',
|
||||
700: '#616161',
|
||||
800: '#424242',
|
||||
900: '#212121'
|
||||
},
|
||||
"blueGrey": {
|
||||
50: '#ECEFF1',
|
||||
100: '#CFD8DC',
|
||||
200: '#B0BEC5',
|
||||
300: '#90A4AE',
|
||||
400: '#78909C',
|
||||
500: '#607D8B',
|
||||
600: '#546E7A',
|
||||
700: '#455A64',
|
||||
800: '#37474F',
|
||||
900: '#263238'
|
||||
},
|
||||
|
||||
"red": {
|
||||
50: '#FFEBEE',
|
||||
100: '#FFCDD2',
|
||||
200: '#EF9A9A',
|
||||
300: '#E57373',
|
||||
400: '#EF5350',
|
||||
500: '#F44336',
|
||||
600: '#E53935',
|
||||
700: '#D32F2F',
|
||||
800: '#C62828',
|
||||
900: '#B71C1C',
|
||||
// A100: '#FF8A80',
|
||||
// A200: '#FF5252',
|
||||
// A400: '#FF1744',
|
||||
// A700: '#D50000'
|
||||
},
|
||||
"pink": {
|
||||
50: '#FCE4EC',
|
||||
100: '#F8BBD0',
|
||||
200: '#F48FB1',
|
||||
300: '#F06292',
|
||||
400: '#EC407A',
|
||||
500: '#E91E63',
|
||||
600: '#D81B60',
|
||||
700: '#C2185B',
|
||||
800: '#AD1457',
|
||||
900: '#880E4F',
|
||||
// A100: '#FF80AB',
|
||||
// A200: '#FF4081',
|
||||
// A400: '#F50057',
|
||||
// A700: '#C51162'
|
||||
},
|
||||
"purple": {
|
||||
50: '#F3E5F5',
|
||||
100: '#E1BEE7',
|
||||
200: '#CE93D8',
|
||||
300: '#BA68C8',
|
||||
400: '#AB47BC',
|
||||
500: '#9C27B0',
|
||||
600: '#8E24AA',
|
||||
700: '#7B1FA2',
|
||||
800: '#6A1B9A',
|
||||
900: '#4A148C',
|
||||
// A100: '#EA80FC',
|
||||
// A200: '#E040FB',
|
||||
// A400: '#D500F9',
|
||||
// A700: '#AA00FF'
|
||||
},
|
||||
"deepPurple": {
|
||||
50: '#EDE7F6',
|
||||
100: '#D1C4E9',
|
||||
200: '#B39DDB',
|
||||
300: '#9575CD',
|
||||
400: '#7E57C2',
|
||||
500: '#673AB7',
|
||||
600: '#5E35B1',
|
||||
700: '#512DA8',
|
||||
800: '#4527A0',
|
||||
900: '#311B92',
|
||||
// A100: '#B388FF',
|
||||
// A200: '#7C4DFF',
|
||||
// A400: '#651FFF',
|
||||
// A700: '#6200EA'
|
||||
},
|
||||
"indigo": {
|
||||
50: '#E8EAF6',
|
||||
100: '#C5CAE9',
|
||||
200: '#9FA8DA',
|
||||
300: '#7986CB',
|
||||
400: '#5C6BC0',
|
||||
500: '#3F51B5',
|
||||
600: '#3949AB',
|
||||
700: '#303F9F',
|
||||
800: '#283593',
|
||||
900: '#1A237E',
|
||||
// A100: '#8C9EFF',
|
||||
// A200: '#536DFE',
|
||||
// A400: '#3D5AFE',
|
||||
// A700: '#304FFE'
|
||||
},
|
||||
"blue": {
|
||||
50: '#E3F2FD',
|
||||
100: '#BBDEFB',
|
||||
200: '#90CAF9',
|
||||
300: '#64B5F6',
|
||||
400: '#42A5F5',
|
||||
500: '#2196F3',
|
||||
600: '#1E88E5',
|
||||
700: '#1976D2',
|
||||
800: '#1565C0',
|
||||
900: '#0D47A1',
|
||||
// A100: '#82B1FF',
|
||||
// A200: '#448AFF',
|
||||
// A400: '#2979FF',
|
||||
// A700: '#2962FF'
|
||||
},
|
||||
"lightBlue": {
|
||||
50: '#E1F5FE',
|
||||
100: '#B3E5FC',
|
||||
200: '#81D4FA',
|
||||
300: '#4FC3F7',
|
||||
400: '#29B6F6',
|
||||
500: '#03A9F4',
|
||||
600: '#039BE5',
|
||||
700: '#0288D1',
|
||||
800: '#0277BD',
|
||||
900: '#01579B',
|
||||
// A100: '#80D8FF',
|
||||
// A200: '#40C4FF',
|
||||
// A400: '#00B0FF',
|
||||
// A700: '#0091EA'
|
||||
},
|
||||
"cyan": {
|
||||
50: '#E0F7FA',
|
||||
100: '#B2EBF2',
|
||||
200: '#80DEEA',
|
||||
300: '#4DD0E1',
|
||||
400: '#26C6DA',
|
||||
500: '#00BCD4',
|
||||
600: '#00ACC1',
|
||||
700: '#0097A7',
|
||||
800: '#00838F',
|
||||
900: '#006064',
|
||||
// A100: '#84FFFF',
|
||||
// A200: '#18FFFF',
|
||||
// A400: '#00E5FF',
|
||||
// A700: '#00B8D4'
|
||||
},
|
||||
"teal": {
|
||||
50: '#E0F2F1',
|
||||
100: '#B2DFDB',
|
||||
200: '#80CBC4',
|
||||
300: '#4DB6AC',
|
||||
400: '#26A69A',
|
||||
500: '#009688',
|
||||
600: '#00897B',
|
||||
700: '#00796B',
|
||||
800: '#00695C',
|
||||
900: '#004D40',
|
||||
// A100: '#A7FFEB',
|
||||
// A200: '#64FFDA',
|
||||
// A400: '#1DE9B6',
|
||||
// A700: '#00BFA5'
|
||||
},
|
||||
"green": {
|
||||
50: '#E8F5E9',
|
||||
100: '#C8E6C9',
|
||||
200: '#A5D6A7',
|
||||
300: '#81C784',
|
||||
400: '#66BB6A',
|
||||
500: '#4CAF50',
|
||||
600: '#43A047',
|
||||
700: '#388E3C',
|
||||
800: '#2E7D32',
|
||||
900: '#1B5E20',
|
||||
// A100: '#B9F6CA',
|
||||
// A200: '#69F0AE',
|
||||
// A400: '#00E676',
|
||||
// A700: '#00C853'
|
||||
},
|
||||
"lightGreen": {
|
||||
50: '#F1F8E9',
|
||||
100: '#DCEDC8',
|
||||
200: '#C5E1A5',
|
||||
300: '#AED581',
|
||||
400: '#9CCC65',
|
||||
500: '#8BC34A',
|
||||
600: '#7CB342',
|
||||
700: '#689F38',
|
||||
800: '#558B2F',
|
||||
900: '#33691E',
|
||||
// A100: '#CCFF90',
|
||||
// A200: '#B2FF59',
|
||||
// A400: '#76FF03',
|
||||
// A700: '#64DD17'
|
||||
},
|
||||
"lime": {
|
||||
50: '#F9FBE7',
|
||||
100: '#F0F4C3',
|
||||
200: '#E6EE9C',
|
||||
300: '#DCE775',
|
||||
400: '#D4E157',
|
||||
500: '#CDDC39',
|
||||
600: '#C0CA33',
|
||||
700: '#AFB42B',
|
||||
800: '#9E9D24',
|
||||
900: '#827717',
|
||||
// A100: '#F4FF81',
|
||||
// A200: '#EEFF41',
|
||||
// A400: '#C6FF00',
|
||||
// A700: '#AEEA00'
|
||||
},
|
||||
"yellow": {
|
||||
50: '#FFFDE7',
|
||||
100: '#FFF9C4',
|
||||
200: '#FFF59D',
|
||||
300: '#FFF176',
|
||||
400: '#FFEE58',
|
||||
500: '#FFEB3B',
|
||||
600: '#FDD835',
|
||||
700: '#FBC02D',
|
||||
800: '#F9A825',
|
||||
900: '#F57F17',
|
||||
// A100: '#FFFF8D',
|
||||
// A200: '#FFFF00',
|
||||
// A400: '#FFEA00',
|
||||
// A700: '#FFD600'
|
||||
},
|
||||
"amber": {
|
||||
50: '#FFF8E1',
|
||||
100: '#FFECB3',
|
||||
200: '#FFE082',
|
||||
300: '#FFD54F',
|
||||
400: '#FFCA28',
|
||||
500: '#FFC107',
|
||||
600: '#FFB300',
|
||||
700: '#FFA000',
|
||||
800: '#FF8F00',
|
||||
900: '#FF6F00',
|
||||
// A100: '#FFE57F',
|
||||
// A200: '#FFD740',
|
||||
// A400: '#FFC400',
|
||||
// A700: '#FFAB00'
|
||||
},
|
||||
"orange": {
|
||||
50: '#FFF3E0',
|
||||
100: '#FFE0B2',
|
||||
200: '#FFCC80',
|
||||
300: '#FFB74D',
|
||||
400: '#FFA726',
|
||||
500: '#FF9800',
|
||||
600: '#FB8C00',
|
||||
700: '#F57C00',
|
||||
800: '#EF6C00',
|
||||
900: '#E65100',
|
||||
// A100: '#FFD180',
|
||||
// A200: '#FFAB40',
|
||||
// A400: '#FF9100',
|
||||
// A700: '#FF6D00'
|
||||
},
|
||||
"deepOrange": {
|
||||
50: '#FBE9E7',
|
||||
100: '#FFCCBC',
|
||||
200: '#FFAB91',
|
||||
300: '#FF8A65',
|
||||
400: '#FF7043',
|
||||
500: '#FF5722',
|
||||
600: '#F4511E',
|
||||
700: '#E64A19',
|
||||
800: '#D84315',
|
||||
900: '#BF360C',
|
||||
// A100: '#FF9E80',
|
||||
// A200: '#FF6E40',
|
||||
// A400: '#FF3D00',
|
||||
// A700: '#DD2C00'
|
||||
},
|
||||
"brown": {
|
||||
50: '#EFEBE9',
|
||||
100: '#D7CCC8',
|
||||
200: '#BCAAA4',
|
||||
300: '#A1887F',
|
||||
400: '#8D6E63',
|
||||
500: '#795548',
|
||||
600: '#6D4C41',
|
||||
700: '#5D4037',
|
||||
800: '#4E342E',
|
||||
900: '#3E2723'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function makeLabels() {
|
||||
let result: string[] = [];
|
||||
for (let [key] of Object.entries(materialColors)) {
|
||||
result.push(key.toString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const colorKeys: string[] = makeLabels();
|
||||
|
||||
const darkMode: boolean = isDarkMode();
|
||||
|
||||
export function getBackgroundColor(colorKey: string): string {
|
||||
let list = materialColors[colorKey];
|
||||
if (!list) {
|
||||
list = materialColors["grey"];
|
||||
}
|
||||
if (darkMode) {
|
||||
if (colorKey === "grey" || colorKey === "yellow")
|
||||
{
|
||||
return list[900];
|
||||
}
|
||||
else {
|
||||
return list[800];
|
||||
}
|
||||
} else {
|
||||
if (colorKey === "grey")
|
||||
{
|
||||
return list[200];
|
||||
}
|
||||
else {
|
||||
return list[100];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getBorderColor(colorKey: string): string {
|
||||
let list = materialColors[colorKey];
|
||||
if (!list)
|
||||
{
|
||||
list = materialColors["grey"];
|
||||
}
|
||||
if (darkMode) {
|
||||
if (colorKey === "grey" || colorKey === "yellow")
|
||||
{
|
||||
return list[300];
|
||||
}
|
||||
return list[200];
|
||||
} else {
|
||||
if (colorKey === "grey")
|
||||
{
|
||||
return list[500];
|
||||
}
|
||||
return list[400];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export default class MidiBinding {
|
||||
deserialize(input: any) : MidiBinding {
|
||||
this.symbol = input.symbol;
|
||||
this.bindingType = input.bindingType;
|
||||
this.note = input.note;
|
||||
this.control = input.control;
|
||||
this.minValue = input.minValue;
|
||||
this.maxValue = input.maxValue;
|
||||
this.linearControlType = input.linearControlType;
|
||||
this.switchControlType = input.switchControlType;
|
||||
return this;
|
||||
}
|
||||
static systemBinding(symbol: string): MidiBinding {
|
||||
let result = new MidiBinding();
|
||||
result.symbol = symbol;
|
||||
return result;
|
||||
}
|
||||
static deserialize_array(input: any): MidiBinding[] {
|
||||
let result: MidiBinding[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new MidiBinding().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
clone(): MidiBinding { return new MidiBinding().deserialize(this);}
|
||||
equals(other: MidiBinding) : boolean
|
||||
{
|
||||
return (this.symbol === other.symbol)
|
||||
&& (this.bindingType === other.bindingType)
|
||||
&& (this.note === other.note)
|
||||
&& (this.control === other.control)
|
||||
&& (this.minValue === other.minValue)
|
||||
&& (this.maxValue === other.maxValue)
|
||||
&& (this.linearControlType === other.linearControlType)
|
||||
&& (this.switchControlType === other.switchControlType)
|
||||
}
|
||||
|
||||
static BINDING_TYPE_NONE: number = 0;
|
||||
static BINDING_TYPE_NOTE: number = 1;
|
||||
static BINDING_TYPE_CONTROL: number = 2;
|
||||
|
||||
setBindingType(bindingType:number)
|
||||
{
|
||||
this.bindingType = bindingType;
|
||||
}
|
||||
|
||||
|
||||
symbol: string = "";
|
||||
|
||||
bindingType: number = MidiBinding.BINDING_TYPE_NONE;
|
||||
note: number = 12*4+24; // C4.
|
||||
control: number = 1;
|
||||
minValue: number = 0;
|
||||
maxValue: number = 1;
|
||||
rotaryScale: number = 1;
|
||||
|
||||
static LINEAR_CONTROL_TYPE:number = 0;
|
||||
static CIRCULAR_CONTROL_TYPE: number = 1;
|
||||
|
||||
linearControlType: number = MidiBinding.LINEAR_CONTROL_TYPE;
|
||||
|
||||
static TRIGGER_ON_RISING_EDGE: number = 0;
|
||||
static TOGGLE_ON_RISING_EDGE: number = 0;
|
||||
static MOMENTARY_CONTROL_TYPE: number = 1;
|
||||
static TOGGLE_ON_VALUE: number = 1;
|
||||
static TOGGLE_ON_ANY: number = 2;
|
||||
static TRIGGER_ON_ANY: number = 2;
|
||||
|
||||
|
||||
switchControlType: number = MidiBinding.TRIGGER_ON_RISING_EDGE;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,407 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { Component } from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { UiPlugin } from './Lv2Plugin';
|
||||
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import Utility from './Utility';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined';
|
||||
import MicOutlinedIcon from '@mui/icons-material/MicOutlined';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import NumericInput from './NumericInput';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
controlDiv: { flex: "0 0 auto", marginRight: 12, verticalAlign: "center", height: 48, paddingTop: 8, paddingBottom: 8 },
|
||||
controlDiv2: {
|
||||
flex: "0 0 auto", marginRight: 12, verticalAlign: "center",
|
||||
height: 48, paddingTop: 0, paddingBottom: 0, whiteSpace: "nowrap"
|
||||
}
|
||||
});
|
||||
|
||||
enum MidiControlType {
|
||||
None,
|
||||
Toggle,
|
||||
Trigger,
|
||||
Dial,
|
||||
Select,
|
||||
}
|
||||
|
||||
interface MidiBindingViewProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
listen: boolean;
|
||||
midiBinding: MidiBinding;
|
||||
uiPlugin: UiPlugin;
|
||||
onChange: (instanceId: number, newBinding: MidiBinding) => void;
|
||||
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
|
||||
}
|
||||
|
||||
|
||||
interface MidiBindingViewState {
|
||||
midiControlType: MidiControlType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const MidiBindingView =
|
||||
withStyles(
|
||||
class extends Component<MidiBindingViewProps, MidiBindingViewState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MidiBindingViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
midiControlType: this.getControlType()
|
||||
};
|
||||
}
|
||||
|
||||
handleBindingTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.setBindingType(newValue);
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleNoteChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.note = newValue;
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleControlChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.control = newValue;
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleLatchControlTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.switchControlType = newValue;
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleTriggerControlTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.switchControlType = newValue;
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleLinearControlTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.linearControlType = newValue;
|
||||
this.validateSwitchControlType(newBinding);
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleMinChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.minValue = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleMaxChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.maxValue = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleScaleChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.rotaryScale = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
|
||||
|
||||
generateMidiNoteSelects(): React.ReactNode[] {
|
||||
let result: React.ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < 127; ++i) {
|
||||
result.push(
|
||||
<MenuItem value={i}>{Utility.midiNoteName(i)}</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
validateSwitchControlType(midiBinding: MidiBinding) {
|
||||
// :-(
|
||||
|
||||
let controlType = this.state.midiControlType;
|
||||
if (controlType === MidiControlType.Toggle
|
||||
&& midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) {
|
||||
if (midiBinding.switchControlType !== MidiBinding.TRIGGER_ON_RISING_EDGE // Toggle on Note On.
|
||||
&& midiBinding.switchControlType !== MidiBinding.MOMENTARY_CONTROL_TYPE // Note on/Note off.
|
||||
) {
|
||||
midiBinding.switchControlType = MidiBinding.TRIGGER_ON_RISING_EDGE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
generateControlSelects(): React.ReactNode[] {
|
||||
|
||||
return Utility.validMidiControllers.map((control) => (
|
||||
<MenuItem key={control.value} value={control.value}>{control.displayName}</MenuItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getControlType(): MidiControlType {
|
||||
|
||||
if (this.props.midiBinding.symbol === "__bypass") {
|
||||
return MidiControlType.Toggle;
|
||||
}
|
||||
if (!this.props.uiPlugin) return MidiControlType.None;
|
||||
|
||||
let port = this.props.uiPlugin.getControl(this.props.midiBinding.symbol);
|
||||
|
||||
if (!port) return MidiControlType.None;
|
||||
if (port.trigger_property) {
|
||||
return MidiControlType.Trigger;
|
||||
}
|
||||
if (port.isAbToggle() || port.isOnOffSwitch()) {
|
||||
return MidiControlType.Toggle;
|
||||
}
|
||||
if (port.isSelect()) {
|
||||
return MidiControlType.Select;
|
||||
}
|
||||
return MidiControlType.Dial;
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let midiBinding = this.props.midiBinding;
|
||||
let uiPlugin = this.props.uiPlugin;
|
||||
if (!uiPlugin) {
|
||||
return (<div />);
|
||||
}
|
||||
let controlType = this.state.midiControlType;
|
||||
|
||||
let showLinearRange =
|
||||
controlType === MidiControlType.Dial &&
|
||||
midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL
|
||||
&& midiBinding.linearControlType === MidiBinding.LINEAR_CONTROL_TYPE;
|
||||
|
||||
|
||||
let canRotaryScale: boolean =
|
||||
controlType === MidiControlType.Dial &&
|
||||
midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL
|
||||
&& midiBinding.linearControlType === MidiBinding.CIRCULAR_CONTROL_TYPE;
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "left", alignItems: "center", minHeight: 48 }}>
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{ width: 80, }}
|
||||
onChange={(e, extra) => this.handleBindingTypeChange(e, extra)}
|
||||
value={midiBinding.bindingType}
|
||||
>
|
||||
<MenuItem value={0}>None</MenuItem>
|
||||
{(controlType === MidiControlType.Toggle || controlType === MidiControlType.Trigger) && (
|
||||
<MenuItem value={1}>Note</MenuItem>
|
||||
)}
|
||||
<MenuItem value={2}>Control</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
|
||||
(
|
||||
<div className={classes.controlDiv2} >
|
||||
<Select variant="standard"
|
||||
style={{ width: 80 }}
|
||||
onChange={(e, extra) => this.handleNoteChange(e, extra)}
|
||||
value={midiBinding.note}
|
||||
|
||||
>
|
||||
{
|
||||
this.generateMidiNoteSelects()
|
||||
}
|
||||
</Select>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (this.props.listen) {
|
||||
this.props.onListen(-2, "", false)
|
||||
} else {
|
||||
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
|
||||
}
|
||||
}}
|
||||
size="large">
|
||||
{this.props.listen ? (
|
||||
<MicOutlinedIcon />
|
||||
) : (
|
||||
<MicNoneOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
<div className={classes.controlDiv2} >
|
||||
|
||||
<Select variant="standard"
|
||||
style={{ width: 80 }}
|
||||
onChange={(e, extra) => this.handleControlChange(e, extra)}
|
||||
value={midiBinding.control}
|
||||
renderValue={(value) => { return "CC-" + value }}
|
||||
>
|
||||
{
|
||||
this.generateControlSelects()
|
||||
}
|
||||
</Select>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (this.props.listen) {
|
||||
this.props.onListen(-2, "", false)
|
||||
} else {
|
||||
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, true)
|
||||
}
|
||||
}}
|
||||
size="large">
|
||||
{this.props.listen ? (
|
||||
<MicOutlinedIcon />
|
||||
) : (
|
||||
<MicNoneOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
|
||||
(
|
||||
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{}}
|
||||
onChange={(e, extra) => this.handleLatchControlTypeChange(e, extra)}
|
||||
value={midiBinding.switchControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.TRIGGER_ON_RISING_EDGE}>Toggle on Note On</MenuItem>
|
||||
<MenuItem value={MidiBinding.MOMENTARY_CONTROL_TYPE}>Note On/Note off</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
((controlType === MidiControlType.Toggle && midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{}}
|
||||
onChange={(e, extra) => this.handleLatchControlTypeChange(e, extra)}
|
||||
value={midiBinding.switchControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.TOGGLE_ON_RISING_EDGE}>Toggle on rising edge</MenuItem>
|
||||
<MenuItem value={MidiBinding.TOGGLE_ON_ANY}>Toggle on any value</MenuItem>
|
||||
<MenuItem value={MidiBinding.TOGGLE_ON_VALUE}>Use control value</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
(controlType === MidiControlType.Trigger && midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{}}
|
||||
onChange={(e, extra) => this.handleTriggerControlTypeChange(e, extra)}
|
||||
value={midiBinding.switchControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.TRIGGER_ON_RISING_EDGE}>Trigger on rising edge</MenuItem>
|
||||
<MenuItem value={MidiBinding.TRIGGER_ON_ANY}>
|
||||
Trigger on any value
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
{
|
||||
(controlType === MidiControlType.Dial && midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{ width: 120 }}
|
||||
onChange={(e, extra) => this.handleLinearControlTypeChange(e, extra)}
|
||||
value={midiBinding.linearControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.LINEAR_CONTROL_TYPE}>Linear</MenuItem>
|
||||
<MenuItem value={MidiBinding.CIRCULAR_CONTROL_TYPE}>Rotary</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
showLinearRange && (
|
||||
<div className={classes.controlDiv}>
|
||||
<Typography display="inline">Min: </Typography>
|
||||
<NumericInput defaultValue={midiBinding.minValue} ariaLabel='min'
|
||||
min={0} max={1} onChange={(value) => { this.handleMinChange(value); }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showLinearRange && (
|
||||
<div className={classes.controlDiv}>
|
||||
<Typography display="inline">Max: </Typography>
|
||||
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='max'
|
||||
min={0} max={1} onChange={(value) => { this.handleMaxChange(value); }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
canRotaryScale && (
|
||||
<div className={classes.controlDiv}>
|
||||
<Typography display="inline">Scale: </Typography>
|
||||
<NumericInput defaultValue={midiBinding.maxValue} ariaLabel='scale'
|
||||
min={-100} max={100} onChange={(value) => { this.handleScaleChange(value); }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default MidiBindingView;
|
||||
@@ -0,0 +1,384 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import MidiBindingView from './MidiBindingView';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import { UiPlugin,makeSplitUiPlugin } from './Lv2Plugin';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}),
|
||||
dialogTitle: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: "1 1 auto",
|
||||
}),
|
||||
pluginTable: css({
|
||||
border: "collapse",
|
||||
width: "100%",
|
||||
|
||||
}),
|
||||
pluginHead: css({
|
||||
borderTop: "1pt #DDD solid", paddingLeft: 22, paddingRight: 22
|
||||
}),
|
||||
bindingTd: css({
|
||||
verticalAlign: "top",
|
||||
paddingLeft: 12, paddingBottom: 8
|
||||
}),
|
||||
nameTd: css({
|
||||
paddingLeft: 48,
|
||||
verticalAlign: "top",
|
||||
paddingTop: 12
|
||||
}),
|
||||
});
|
||||
|
||||
function not_null<T>(value: T | null) {
|
||||
if (!value) throw Error("Unexpected null value");
|
||||
return value;
|
||||
}
|
||||
|
||||
export interface MidiBindingDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export interface MidiBindingDialogState {
|
||||
listenInstanceId: number;
|
||||
listenSymbol: string;
|
||||
listenSnackbarOpen: boolean;
|
||||
|
||||
}
|
||||
|
||||
export const MidiBindingDialog =
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<MidiBindingDialogProps, MidiBindingDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MidiBindingDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
listenInstanceId: -2,
|
||||
listenSymbol: "",
|
||||
listenSnackbarOpen: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
handleClose() {
|
||||
this.cancelListenForControl();
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
listenTimeoutHandle?: number;
|
||||
|
||||
listenHandle?: ListenHandle;
|
||||
|
||||
cancelListenForControl() {
|
||||
if (this.listenTimeoutHandle) {
|
||||
clearTimeout(this.listenTimeoutHandle);
|
||||
this.listenTimeoutHandle = undefined;
|
||||
}
|
||||
if (this.listenHandle)
|
||||
{
|
||||
this.model.cancelListenForMidiEvent(this.listenHandle)
|
||||
this.listenHandle = undefined;
|
||||
}
|
||||
|
||||
this.setState({ listenInstanceId: -2, listenSymbol: "" });
|
||||
|
||||
}
|
||||
|
||||
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number)
|
||||
{
|
||||
this.cancelListenForControl();
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let item = pedalboard.getItem(instanceId);
|
||||
if (!item) return;
|
||||
|
||||
let binding = item.getMidiBinding(symbol);
|
||||
let newBinding = binding.clone();
|
||||
if (isNote) {
|
||||
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE;
|
||||
newBinding.note = noteOrControl;
|
||||
} else {
|
||||
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL;
|
||||
newBinding.control = noteOrControl;
|
||||
}
|
||||
|
||||
this.model.setMidiBinding(instanceId,newBinding);
|
||||
}
|
||||
|
||||
|
||||
handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void {
|
||||
this.cancelListenForControl();
|
||||
this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true });
|
||||
this.listenTimeoutHandle = setTimeout(() => {
|
||||
this.cancelListenForControl();
|
||||
}, 8000);
|
||||
|
||||
this.listenHandle = this.model.listenForMidiEvent(listenForControl,
|
||||
(isNote: boolean, noteOrControl: number) => {
|
||||
this.handleListenSucceeded(instanceId,symbol,isNote, noteOrControl);
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
this.mounted = false;
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
handleItemChanged(instanceId: number, newBinding: MidiBinding) {
|
||||
this.model.setMidiBinding(instanceId, newBinding);
|
||||
}
|
||||
generateTable(): React.ReactNode[] {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let result: React.ReactNode[] = [];
|
||||
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let iter = pedalboard.itemsGenerator();
|
||||
while (true) {
|
||||
let v = iter.next();
|
||||
if (v.done) break;
|
||||
let item = v.value;
|
||||
|
||||
|
||||
let isSplit = item.uri === "uri://two-play/pipedal/pedalboard#Split";
|
||||
let plugin : UiPlugin | null = this.model.getUiPlugin(item.uri);
|
||||
if (plugin === null && isSplit)
|
||||
{
|
||||
plugin = makeSplitUiPlugin();
|
||||
let splitType = item.getControlValue("splitType");
|
||||
not_null(plugin.getControl("splitType")).not_on_gui = true;
|
||||
switch (splitType)
|
||||
{
|
||||
case 0: // A/B
|
||||
not_null(plugin.getControl("select")).not_on_gui = false;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 1: //mixer
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = true;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = true;
|
||||
break;
|
||||
case 2: // L/R
|
||||
not_null(plugin.getControl("select")).not_on_gui = true;
|
||||
not_null(plugin.getControl("mix")).not_on_gui = true;
|
||||
not_null(plugin.getControl("volL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panL")).not_on_gui = false;
|
||||
not_null(plugin.getControl("volR")).not_on_gui = false;
|
||||
not_null(plugin.getControl("panR")).not_on_gui = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// xxx
|
||||
}
|
||||
if (plugin) {
|
||||
result.push(
|
||||
<tr>
|
||||
<td colSpan={2} className={classes.pluginHead}>
|
||||
<Typography variant="caption" color="textSecondary" noWrap>
|
||||
{plugin.name}
|
||||
</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
if (!isSplit)
|
||||
{
|
||||
result.push(
|
||||
<tr>
|
||||
<td className={classes.nameTd}>
|
||||
<Typography noWrap style={{ verticalAlign: "center", height: 48 }}>
|
||||
Bypass
|
||||
</Typography>
|
||||
</td>
|
||||
<td className={classes.bindingTd}>
|
||||
<MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")}
|
||||
uiPlugin={plugin}
|
||||
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
|
||||
if (instanceId === -2)
|
||||
{
|
||||
this.cancelListenForControl();
|
||||
} else {
|
||||
this.handleListenForControl(instanceId, symbol, listenForControl);
|
||||
}
|
||||
}}
|
||||
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === "__bypass"}
|
||||
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < plugin.controls.length; ++i) {
|
||||
let control = plugin.controls[i];
|
||||
if (control.isHidden())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!control.is_input)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let symbol = control.symbol;
|
||||
result.push(
|
||||
<tr>
|
||||
<td className={classes.nameTd}>
|
||||
<Typography noWrap style={{ verticalAlign: "middle", maxWidth: 120 }} >
|
||||
{control.name}
|
||||
</Typography>
|
||||
</td>
|
||||
<td className={classes.bindingTd}>
|
||||
<MidiBindingView instanceId={item.instanceId}
|
||||
uiPlugin={plugin}
|
||||
midiBinding={item.getMidiBinding(symbol)}
|
||||
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === symbol}
|
||||
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
|
||||
this.handleListenForControl(instanceId, symbol, listenForControl);
|
||||
}}
|
||||
|
||||
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
supressDefault(e: SyntheticEvent) {
|
||||
//e.preventDefault();
|
||||
//e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open} = props;
|
||||
const classes = withStyles.getClasses(props);
|
||||
if (!open) {
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogEx tag="midiBindings" open={open} fullWidth onClose={this.handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={true}
|
||||
style={{userSelect: "none"}}
|
||||
onEnterKey={()=>{}}
|
||||
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} >
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={this.handleClose}
|
||||
aria-label="back"
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
Preset MIDI Bindings
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ overflow: "auto", flex: "1 1 auto", width: "100%" }}>
|
||||
<table className={classes.pluginTable} >
|
||||
<colgroup>
|
||||
<col style={{ width: "auto" }} />
|
||||
<col style={{ width: "100%" }} />
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
{this.generateTable()}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.listenSnackbarOpen}
|
||||
autoHideDuration={1500}
|
||||
onClose={() => this.setState({ listenSnackbarOpen: false })}
|
||||
message="Listening for MIDI input"
|
||||
/>
|
||||
</DialogEx >
|
||||
);
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
|
||||
export default MidiBindingDialog;
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024 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.
|
||||
|
||||
|
||||
export enum MidiDeviceSelection {
|
||||
DeviceAny = 0,
|
||||
DeviceNone = 1,
|
||||
DeviceList = 2
|
||||
}
|
||||
|
||||
export default class MidiChannelBinding {
|
||||
deserialize(input: any) : MidiChannelBinding {
|
||||
this.deviceSelection = input.deviceSelection;
|
||||
this.midiDevices = input.midiDevices;
|
||||
|
||||
this.channel = input.channel;
|
||||
this.acceptProgramChanges = input.acceptProgramChanges;
|
||||
this.acceptCommonMessages = input.acceptCommonMessages;
|
||||
return this;
|
||||
}
|
||||
|
||||
clone() { return new MidiChannelBinding().deserialize(this);}
|
||||
|
||||
deviceSelection: number = MidiDeviceSelection.DeviceAny as number;
|
||||
midiDevices: string[] = [];
|
||||
channel: number = -1;
|
||||
acceptProgramChanges: boolean = true;
|
||||
acceptCommonMessages: boolean = true;
|
||||
|
||||
|
||||
static CreateMissingValue() : MidiChannelBinding {
|
||||
let result = new MidiChannelBinding();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2024 Robin E. R. Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Component } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { pluginControlStyles } from './PluginControl';
|
||||
import MidiChannelBinding from './MidiChannelBinding';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import MidiIcon from './svg/ic_midi.svg?react';
|
||||
import MidiChannelBindingDialog from './MidiChannelBindingDialog';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
|
||||
|
||||
|
||||
export interface MidiChannelBindingControlProps extends WithStyles<typeof pluginControlStyles> {
|
||||
midiChannelBinding: MidiChannelBinding
|
||||
theme: Theme;
|
||||
onChange: (result: MidiChannelBinding) => void;
|
||||
}
|
||||
type MidiChannelBindingControlState = {
|
||||
dialogOpen: boolean;
|
||||
};
|
||||
|
||||
const MidiChannelBindingControl =
|
||||
withTheme(
|
||||
withStyles(
|
||||
class extends Component<MidiChannelBindingControlProps, MidiChannelBindingControlState> {
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MidiChannelBindingControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dialogOpen: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let item_width = 80;
|
||||
return (
|
||||
<div
|
||||
className={classes.controlFrame}
|
||||
style={{ width: item_width }}
|
||||
>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={
|
||||
{
|
||||
alignSelf: "stretch"
|
||||
|
||||
}}>
|
||||
<Tooltip title={(
|
||||
<React.Fragment>
|
||||
<Typography variant="caption">title</Typography>
|
||||
<Divider />
|
||||
<Typography variant="caption">Controls which MIDI messages get forward to the instrument.</Typography>
|
||||
|
||||
</React.Fragment>
|
||||
)}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
>
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> MIDI</Typography>
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}>
|
||||
<ButtonBase style={{ width: 48, height: 48, borderRadius: 9999 }}
|
||||
onClick={()=> {
|
||||
this.setState({dialogOpen: true});
|
||||
}}
|
||||
sx={{
|
||||
':hover': {
|
||||
bgcolor: 'action.hover', // theme.palette.primary.main
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MidiIcon fill={this.props.theme.palette.text.secondary} style={{ width: 36, height: 36 }} />
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div className={classes.editSection}
|
||||
style={{ display: "flex", flexFlow: "column nowrap", justifyContent: "center" }}
|
||||
>
|
||||
<Typography variant="caption" display="block" noWrap
|
||||
style={{ width: "100%", textAlign: "center" }}
|
||||
>OMNI</Typography>
|
||||
</div>
|
||||
{this.state.dialogOpen&&(
|
||||
<MidiChannelBindingDialog
|
||||
open={this.state.dialogOpen}
|
||||
midiChannelBinding={this.props.midiChannelBinding}
|
||||
onClose={() =>{ this.setState({dialogOpen: false});}}
|
||||
onChanged={(midiChannelBinding: MidiChannelBinding)=> {}}
|
||||
midiDevices={[]}
|
||||
|
||||
/>
|
||||
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
},
|
||||
pluginControlStyles
|
||||
));
|
||||
|
||||
export default MidiChannelBindingControl;
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
// Copyright (c) 2024 Robin E. R. Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import ChannelBindingHelpDialog from './ChannelBindingsHelpDialog';
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import { AlsaMidiDeviceInfo } from './AlsaMidiDeviceInfo';
|
||||
import DialogEx from './DialogEx';
|
||||
import MidiChannelBinding, { MidiDeviceSelection } from './MidiChannelBinding';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
|
||||
|
||||
export interface MidiChannelBindingDialogProps {
|
||||
open: boolean;
|
||||
midiChannelBinding: MidiChannelBinding;
|
||||
onClose: () => void;
|
||||
onChanged: (midiChannelBinding: MidiChannelBinding) => void;
|
||||
midiDevices: AlsaMidiDeviceInfo[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
function MidiChannelBindingDialog(props: MidiChannelBindingDialogProps) {
|
||||
const { onClose, midiChannelBinding, open } = props;
|
||||
|
||||
const [currentChannel, setCurrentChannel] = useState(midiChannelBinding.channel);
|
||||
const [allowProgramChanges, setAllowProgramChanges] = useState(midiChannelBinding.acceptCommonMessages);
|
||||
const [helpDialog, setHelpDialog] = useState(false);
|
||||
|
||||
const model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
const handleClose = (): void => {
|
||||
onClose();
|
||||
};
|
||||
const handleOk = (): void => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const generateDeviceSelect = () => {
|
||||
let result: React.ReactElement[] = [
|
||||
(<MenuItem key={0} value={0}>Any</MenuItem>),
|
||||
(<MenuItem key={1} value={1}>None</MenuItem>)
|
||||
];
|
||||
let i = 2;
|
||||
for (let midiDevice of model.jackConfiguration.get().inputMidiDevices) {
|
||||
result.push((<MenuItem key={i} value={i}>{midiDevice.description}</MenuItem>));
|
||||
++i;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getDefaultDeviceSelect = (): number => {
|
||||
if (midiChannelBinding.deviceSelection === MidiDeviceSelection.DeviceAny as number) {
|
||||
return 0;
|
||||
}
|
||||
if (midiChannelBinding.deviceSelection === MidiDeviceSelection.DeviceNone as number) {
|
||||
return 1;
|
||||
}
|
||||
if (midiChannelBinding.midiDevices.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
let selectedDevice = midiChannelBinding.midiDevices[0];
|
||||
|
||||
let ix = 2;
|
||||
for (let midiDevice of model.jackConfiguration.get().inputMidiDevices) {
|
||||
if (midiDevice.name === selectedDevice) {
|
||||
return ix;
|
||||
}
|
||||
++ix;
|
||||
}
|
||||
return 0; // any.
|
||||
}
|
||||
|
||||
const [currentDeviceSelect, setCurrentDeviceSelect] = useState(getDefaultDeviceSelect())
|
||||
|
||||
|
||||
const handleDeviceSelecChange = (event: SelectChangeEvent) => {
|
||||
setCurrentDeviceSelect(parseInt(event.target.value));
|
||||
};
|
||||
const handleChannelChange = (event: SelectChangeEvent) => {
|
||||
setCurrentChannel(parseInt(event.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogEx tag="midiChannelBinding" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
||||
onEnterKey={()=>{}} fullWidth maxWidth="xs"
|
||||
>
|
||||
<DialogTitle id="simple-dialog-title">MIDI Channel Filters</DialogTitle>
|
||||
<DialogContent>
|
||||
<div style={{
|
||||
display: "grid", alignItems: "center", columnGap: 24, rowGap: 24,
|
||||
gridTemplateColumns: "1fr"
|
||||
}}>
|
||||
<div >
|
||||
<Typography display="block" variant="caption">MIDI Devices</Typography>
|
||||
<Select variant='standard' value={currentDeviceSelect.toString()} fullWidth
|
||||
onChange={handleDeviceSelecChange} >
|
||||
{
|
||||
generateDeviceSelect()
|
||||
}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography display="block" variant="caption">Channels</Typography>
|
||||
<Select variant='standard' label="Channels" value={currentChannel.toString()} fullWidth
|
||||
onChange={handleChannelChange}>
|
||||
<MenuItem value={-1}>Omni</MenuItem>
|
||||
<MenuItem value={0}>Channel 1</MenuItem>
|
||||
<MenuItem value={1}>Channel 2</MenuItem>
|
||||
<MenuItem value={2}>Channel 3</MenuItem>
|
||||
<MenuItem value={3}>Channel 4</MenuItem>
|
||||
<MenuItem value={4}>Channel 5</MenuItem>
|
||||
<MenuItem value={5}>Channel 6</MenuItem>
|
||||
<MenuItem value={6}>Channel 7</MenuItem>
|
||||
<MenuItem value={7}>Channel 8</MenuItem>
|
||||
<MenuItem value={8}>Channel 9</MenuItem>
|
||||
<MenuItem value={9}>Channel 10</MenuItem>
|
||||
<MenuItem value={10}>Channel 11</MenuItem>
|
||||
<MenuItem value={11}>Channel 12</MenuItem>
|
||||
<MenuItem value={12}>Channel 13</MenuItem>
|
||||
<MenuItem value={13}>Channel 14</MenuItem>
|
||||
<MenuItem value={14}>Channel 15</MenuItem>
|
||||
<MenuItem value={15}>Channel 16</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "center" }}>
|
||||
<FormControlLabel control={
|
||||
<Checkbox checked={allowProgramChanges}
|
||||
onChange={(event) => {
|
||||
setAllowProgramChanges(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
} label="Allow Program Changes"
|
||||
/>
|
||||
{false&&( // wait until the implementation stabilizes before exposing the help dialog.
|
||||
<IconButton
|
||||
onClick={() => { setHelpDialog(true); }}
|
||||
size="large">
|
||||
<InfoOutlinedIcon color='inherit'
|
||||
style={{
|
||||
fill: "var(--mui-palette.text.primary)",
|
||||
opacity: 0.6
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
<ChannelBindingHelpDialog open={helpDialog} onClose={()=>setHelpDialog(false)}
|
||||
/>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
export default MidiChannelBindingDialog;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export interface ModDirectory {
|
||||
modType: string;
|
||||
pipedalPath: string;
|
||||
displayName: string;
|
||||
defaultFileExtensions: string[];
|
||||
};
|
||||
|
||||
|
||||
export const modDirectories: ModDirectory[] = [
|
||||
{modType: "audioloop", pipedalPath: "shared/audio/Loops",displayName: "Loops", defaultFileExtensions: ["audio/*"]}, // Audio Loops, meant to be used for looper-style plugins
|
||||
//"audiorecording","shared/audio/Audio Recordings", defaultFileExtensions: ["audio/*"]}, : Audio Recordings, triggered by plugins and stored in the unit
|
||||
{modType: "audiosample", pipedalPath: "shared/audio/Samples", displayName: "Samples", defaultFileExtensions: ["audio/*"]}, // One-shot Audio Samples, meant to be used for sampler-style plugins
|
||||
{modType: "audiotrack", pipedalPath: "shared/audio/Tracks",displayName: "Tracks", defaultFileExtensions: ["audio/*"]}, // Audio Tracks, meant to be used as full-performance/song or backtrack
|
||||
{modType: "cabsim", pipedalPath: "CabIR", displayName: "Cab IRs", defaultFileExtensions: ["audio/*"]}, // Speaker Cabinets, meant as small IR audio files
|
||||
|
||||
/// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension
|
||||
{modType: "ir", pipedalPath: "ReverbImpulseFiles", displayName: "Impulse Responses", defaultFileExtensions: ["audio/*"]}, // Impulse Responses
|
||||
{modType: "midiclip", pipedalPath: "shared/midiClips",displayName: "MIDI Clips", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension
|
||||
{modType: "midisong", pipedalPath: "shared/midiSongs", displayName: "MIDI Songs", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Songs, meant to be used as full-performance/song or backtrack
|
||||
{modType: "sf2", pipedalPath: "shared/sf2",displayName: "Sound Fonts", defaultFileExtensions: [".sf2", ".sf3"]}, // SF2 Instruments, must have sf2 or sf3 file extension
|
||||
{modType: "sfz", pipedalPath: "shared/sfz",displayName: "Sfz Files", defaultFileExtensions: [".sfz"]}, // SFZ Instruments, must have sfz file extension
|
||||
// extensions observed in the field.
|
||||
{modType: "audio", pipedalPath: "shared/audio", displayName: "Audio", defaultFileExtensions: ["audio/*"]}, // all audio files (Ratatoille)
|
||||
{modType: "nammodel", pipedalPath: "NeuralAmpModels", displayName: "Neural Amp Models", defaultFileExtensions: [".nam"]}, // Ratatoille, Mike's NAM.
|
||||
{modType: "aidadspmodel", pipedalPath: "shared/aidaaix", displayName: "AIDA IAX Models", defaultFileExtensions: [".json", ".aidaiax"]}, // Ratatoille
|
||||
{modType: "mlmodel", pipedalPath: "ToobMlModels", displayName: "ML Models", defaultFileExtensions: [".json"]}, //
|
||||
|
||||
];
|
||||
|
||||
export function getModDirectory(modFileType: string): ModDirectory | undefined {
|
||||
for (let i = 0; i < modDirectories.length; ++i)
|
||||
{
|
||||
if (modDirectories[i].modType === modFileType)
|
||||
{
|
||||
return modDirectories[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
|
||||
|
||||
interface NoChangePasswordProps {
|
||||
hasPassword: boolean;
|
||||
label: string;
|
||||
defaultValue: string;
|
||||
onPasswordChange: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
helperText?: string;
|
||||
error?: boolean;
|
||||
inputRef?: React.RefObject<any>
|
||||
};
|
||||
|
||||
interface NoChangePasswordState {
|
||||
focused: boolean;
|
||||
passwordText: string;
|
||||
showPassword: boolean;
|
||||
};
|
||||
|
||||
class NoChangePassword extends React.Component<NoChangePasswordProps, NoChangePasswordState> {
|
||||
refText: React.Ref<HTMLInputElement | HTMLTextAreaElement>;
|
||||
|
||||
constructor(props: NoChangePasswordProps) {
|
||||
super(props);
|
||||
this.refText = React.createRef();
|
||||
this.state = {
|
||||
focused: false,
|
||||
showPassword: false,
|
||||
passwordText: this.props.defaultValue as string
|
||||
};
|
||||
}
|
||||
|
||||
handleShowPassword() {
|
||||
this.setState({
|
||||
showPassword: !this.state.showPassword
|
||||
});
|
||||
}
|
||||
handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||
this.textChanged = true;
|
||||
this.props.onPasswordChange(e.target.value);
|
||||
}
|
||||
|
||||
textChanged: boolean = false;
|
||||
handleFocus(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||
console.log("onFocus");
|
||||
if (!this.state.focused) {
|
||||
this.textChanged = false;
|
||||
e.target.value = this.state.passwordText;
|
||||
}
|
||||
e.currentTarget.removeAttribute("readonly");
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
handleBlur(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) {
|
||||
console.log("onBlur");
|
||||
let text = e.target.value as string;
|
||||
this.setState({ focused: false, passwordText: text });
|
||||
this.props.onPasswordChange(e.target.value);
|
||||
this.textChanged = false;
|
||||
if (text.length === 0 && this.props.hasPassword)
|
||||
{
|
||||
e.target.value = "(Unchanged)";
|
||||
}
|
||||
e.currentTarget.setAttribute("readonly","true");
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log("Render focus: " + this.state.focused)
|
||||
let showUnchanged = (this.state.passwordText.length === 0) && this.props.hasPassword && (!this.state.focused);
|
||||
let thisDefaultValue = showUnchanged ? "(Unchanged)" : this.props.defaultValue;
|
||||
|
||||
let isText = showUnchanged || this.state.showPassword;
|
||||
|
||||
|
||||
return (
|
||||
<FormControl disabled={this.props.disabled} fullWidth error={this.props.error} >
|
||||
<InputLabel htmlFor="standard-adornment-password">{this.props.label}</InputLabel>
|
||||
<Input
|
||||
fullWidth
|
||||
color="primary"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
inputRef={this.props.inputRef}
|
||||
readOnly
|
||||
id="standard-adornment-password"
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
value={thisDefaultValue}
|
||||
onFocus={(e) => this.handleFocus(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
type={isText ? 'text' : 'password'}
|
||||
style={ { color: showUnchanged? '#888': '#000'} }
|
||||
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={(e) => this.handleShowPassword()}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
size="large">
|
||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
<FormHelperText >{ this.props.helperText}</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NoChangePassword;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import Input from '@mui/material/Input';
|
||||
|
||||
|
||||
|
||||
const styles = ({ palette }: Theme) => createStyles({
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface NumericInputProps extends WithStyles<typeof styles> {
|
||||
ariaLabel: string;
|
||||
defaultValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
interface NumericInputState {
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export const NumericInput =
|
||||
withStyles(
|
||||
class extends Component<NumericInputProps, NumericInputState>
|
||||
{
|
||||
|
||||
|
||||
constructor(props: NumericInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: false
|
||||
};
|
||||
}
|
||||
|
||||
changed: boolean = false;
|
||||
|
||||
handleChange(event: any): void {
|
||||
this.changed = true;
|
||||
let strValue = event.target.value;
|
||||
try {
|
||||
let value = Number(strValue);
|
||||
if (isNaN(value))
|
||||
{
|
||||
this.setState({ error: true });
|
||||
} else if (value >= this.props.min && value <= this.props.max) {
|
||||
this.setState({ error: false });
|
||||
} else {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
toDisplayValue(value: number): string {
|
||||
if (value <= -1000 || value >= 1000)
|
||||
{
|
||||
return value.toFixed(0);
|
||||
}
|
||||
if (value <= -100 || value >= 100)
|
||||
{
|
||||
return value.toFixed(1);
|
||||
}
|
||||
if (value <= -10 || value >= 10)
|
||||
{
|
||||
return value.toFixed(2);
|
||||
} else {
|
||||
return value.toFixed(3);
|
||||
}
|
||||
}
|
||||
|
||||
apply(input: HTMLInputElement) {
|
||||
if (this.changed) {
|
||||
let strValue = input.value;
|
||||
let value = Number(strValue);
|
||||
if (!isNaN(value))
|
||||
{
|
||||
if (value < this.props.min) value = this.props.min;
|
||||
if (value > this.props.max) value = this.props.max;
|
||||
input.value = this.toDisplayValue(value);
|
||||
this.props.onChange(value);
|
||||
} else {
|
||||
input.value = this.toDisplayValue(this.props.defaultValue);
|
||||
}
|
||||
this.changed = false;
|
||||
this.setState({ error: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleLostFocus(e: any) {
|
||||
this.apply(e.currentTarget as HTMLInputElement);
|
||||
}
|
||||
handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === "Enter")
|
||||
{
|
||||
this.apply(e.currentTarget as HTMLInputElement);
|
||||
} else if (e.key === "Escape")
|
||||
{
|
||||
e.currentTarget.value = this.toDisplayValue(this.props.defaultValue);
|
||||
this.changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
return (<Input style={{ width: 50 }}
|
||||
inputProps={{
|
||||
'aria-label': this.props.ariaLabel,
|
||||
style: { textAlign: 'right' },
|
||||
"onBlur": (e: any) => this.handleLostFocus(e),
|
||||
"onKeyDown": (e: any) => this.handleKeyDown(e)
|
||||
}}
|
||||
onChange={(event) => this.handleChange(event)}
|
||||
defaultValue={this.toDisplayValue(this.props.defaultValue)}
|
||||
error={this.state.error}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default NumericInput;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export type ObservableEventHandler<VALUE_TYPE> = (newValue: VALUE_TYPE) => void;
|
||||
|
||||
|
||||
export default class ObservableEvent<VALUE_TYPE> {
|
||||
|
||||
private _on_event_handlers: ObservableEventHandler<VALUE_TYPE>[] = [];
|
||||
|
||||
|
||||
addEventHandler(handler: ObservableEventHandler<VALUE_TYPE> ) : void
|
||||
{
|
||||
this._on_event_handlers.push(handler);
|
||||
}
|
||||
removeEventHandler(handler: ObservableEventHandler<VALUE_TYPE> ) : void
|
||||
{
|
||||
let newArray: ObservableEventHandler<VALUE_TYPE>[] = [];
|
||||
|
||||
for (let myHandler of this._on_event_handlers)
|
||||
{
|
||||
if (myHandler !== handler)
|
||||
{
|
||||
newArray.push(myHandler);
|
||||
}
|
||||
}
|
||||
this._on_event_handlers = newArray;
|
||||
}
|
||||
fire(value: VALUE_TYPE)
|
||||
{
|
||||
for (let handler of this._on_event_handlers)
|
||||
{
|
||||
handler(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export type OnChangedHandler<VALUE_TYPE> = (newValue: VALUE_TYPE) => void;
|
||||
|
||||
|
||||
export class ObservableProperty<VALUE_TYPE> {
|
||||
|
||||
_value_type: VALUE_TYPE;
|
||||
_on_changed_handlers: OnChangedHandler<VALUE_TYPE>[] = [];
|
||||
|
||||
constructor(default_value: VALUE_TYPE)
|
||||
{
|
||||
this._value_type = default_value;
|
||||
}
|
||||
|
||||
addOnChangedHandler(handler: OnChangedHandler<VALUE_TYPE> ) : void
|
||||
{
|
||||
this._on_changed_handlers.push(handler);
|
||||
handler(this._value_type);
|
||||
}
|
||||
removeOnChangedHandler(handler: OnChangedHandler<VALUE_TYPE> ) : void
|
||||
{
|
||||
let oldArray = this._on_changed_handlers;
|
||||
|
||||
let newArray: OnChangedHandler<VALUE_TYPE>[] = [];
|
||||
|
||||
let outIx = 0;
|
||||
for (let i = 0; i < oldArray.length; ++i)
|
||||
{
|
||||
if (oldArray[i] !== handler)
|
||||
{
|
||||
newArray[outIx++] = oldArray[i];
|
||||
}
|
||||
}
|
||||
this._on_changed_handlers = newArray;
|
||||
}
|
||||
|
||||
get() : VALUE_TYPE {
|
||||
return this._value_type;
|
||||
}
|
||||
|
||||
set(value: VALUE_TYPE) : void
|
||||
{
|
||||
this._value_type = value;
|
||||
|
||||
let t = this._on_changed_handlers; // take an copy in case removes happen while iteratiing.
|
||||
t.forEach(c => c(value));
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
|
||||
|
||||
export interface OkCancelDialogProps {
|
||||
open: boolean,
|
||||
text: string,
|
||||
okButtonText: string,
|
||||
onOk: () => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface OkCancelDialogState {
|
||||
};
|
||||
|
||||
export default class OkCancelDialog extends React.Component<OkCancelDialogProps, OkCancelDialogState> {
|
||||
|
||||
|
||||
constructor(props: OkCancelDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open,okButtonText,text, onClose, onOk } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
onOk();
|
||||
}
|
||||
return (
|
||||
<DialogEx tag="okCancel" open={open} onClose={handleClose}
|
||||
onEnterKey={handleOk}
|
||||
style={{userSelect: "none"}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography
|
||||
>{text}</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={handleOk} >
|
||||
{okButtonText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { createSvgIcon } from '@mui/material/utils';
|
||||
|
||||
const OldDeleteIcon = createSvgIcon(
|
||||
<path d="M20,4h-4.5l-1-1h-5l-1,1H4v2h1v13c0,1.1,0.9,2,2,2h10c1.1,0,2-0.9,2-2V6h1V4z M17,19H7V6h10V19z M9 8h2v8h-2z M13,8h2v8h-2z"/>,
|
||||
"Delete",
|
||||
);
|
||||
|
||||
export default OldDeleteIcon;
|
||||
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { useState } from 'react';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
|
||||
|
||||
export interface OptionItem {
|
||||
key: string | number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface OptionsDialogProps {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
options: OptionItem[];
|
||||
value: string | number;
|
||||
onOk: (result: OptionItem) => void;
|
||||
minWidth?: number | string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
function OptionsDialog(props: OptionsDialogProps) {
|
||||
//const classes = useStyles();
|
||||
const { options, value, onOk, onClose, open,title,minWidth } = props;
|
||||
|
||||
let initialSelection : OptionItem | undefined = undefined;
|
||||
for (let option of options)
|
||||
{
|
||||
if (option.key === value)
|
||||
{
|
||||
initialSelection = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const [currentSelection, setCurrentSelection] = useState(initialSelection)
|
||||
|
||||
const handleListItemClick = (item: OptionItem) => {
|
||||
setCurrentSelection(item);
|
||||
onOk(item);
|
||||
};
|
||||
const handleCancel = () => {
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
<DialogEx tag="options" onClose={handleCancel} aria-labelledby="select-option" open={open} onEnterKey={()=>{}} >
|
||||
{title && (
|
||||
<DialogTitle style={{paddingLeft: 8}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{title}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
)}
|
||||
{title && (
|
||||
<Divider />
|
||||
)}
|
||||
|
||||
<DialogContent style={{padding: 0, margin: 0}}>
|
||||
<List style={{ marginLeft: 3, marginRight: 3 ,minWidth: minWidth }}>
|
||||
{options.map(
|
||||
(item: OptionItem,index) => {
|
||||
return (
|
||||
<ListItemButton onClick={() => handleListItemClick(item)} key={item.key} selected={item === currentSelection} >
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionsDialog;
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { Lv2Plugin } from './Lv2Plugin'
|
||||
|
||||
|
||||
|
||||
|
||||
interface PedalProps {
|
||||
plugin: Lv2Plugin;
|
||||
children?: React.ReactNode;
|
||||
|
||||
}
|
||||
|
||||
type PedalState = {
|
||||
|
||||
};
|
||||
|
||||
export const TemporaryDrawer =
|
||||
class extends Component<PedalProps, PedalState>
|
||||
{
|
||||
state: PedalState;
|
||||
|
||||
constructor(props: PedalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
render() : ReactNode {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,772 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { PiPedalArgumentError } from './PiPedalError';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import MidiChannelBinding from './MidiChannelBinding';
|
||||
|
||||
|
||||
const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Split";
|
||||
const EMPTY_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Empty";
|
||||
|
||||
interface Deserializable<T> {
|
||||
deserialize(input: any): T;
|
||||
}
|
||||
|
||||
export class ControlValue implements Deserializable<ControlValue> {
|
||||
constructor(key?: string, value?: number)
|
||||
{
|
||||
this.key = key??"";
|
||||
this.value = value?? 0;
|
||||
}
|
||||
deserialize(input: any): ControlValue {
|
||||
this.key = input.key;
|
||||
this.value = input.value;
|
||||
return this;
|
||||
}
|
||||
static EmptyArray: ControlValue[] = [];
|
||||
|
||||
static deserializeArray(input: any[]): ControlValue[] {
|
||||
let result: ControlValue[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
result[i] = new ControlValue().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
setValue(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
key: string;
|
||||
value: number;
|
||||
|
||||
}
|
||||
|
||||
export class PedalboardItem implements Deserializable<PedalboardItem> {
|
||||
deserializePedalboardItem(input: any): PedalboardItem {
|
||||
this.instanceId = input.instanceId ?? -1;
|
||||
this.uri = input.uri;
|
||||
this.pluginName = input.pluginName;
|
||||
this.isEnabled = input.isEnabled;
|
||||
this.midiBindings = MidiBinding.deserialize_array(input.midiBindings);
|
||||
if (input.midiChannelBinding)
|
||||
{
|
||||
this.midiChannelBinding = input.midiChannelBinding;
|
||||
} else {
|
||||
this.midiChannelBinding = null;
|
||||
}
|
||||
|
||||
this.controlValues = ControlValue.deserializeArray(input.controlValues);
|
||||
this.vstState = input.vstState ?? "";
|
||||
this.stateUpdateCount = input.stateUpdateCount;
|
||||
this.lv2State = input.lv2State;
|
||||
this.lilvPresetUri = input.lilvPresetUri;
|
||||
this.pathProperties = input.pathProperties;
|
||||
return this;
|
||||
}
|
||||
deserialize(input: any): PedalboardItem {
|
||||
return this.deserializePedalboardItem(input);
|
||||
}
|
||||
static deserializeArray(input: any): PedalboardItem[] {
|
||||
let result: PedalboardItem[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
let inputItem: any = input[i];
|
||||
let uri: string = inputItem.uri as string;
|
||||
let outputItem: PedalboardItem;
|
||||
|
||||
if (uri === SPLIT_PEDALBOARD_ITEM_URI) {
|
||||
outputItem = new PedalboardSplitItem().deserialize(inputItem);
|
||||
} else {
|
||||
outputItem = new PedalboardItem().deserialize(inputItem);
|
||||
}
|
||||
result[i] = outputItem;
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isSyntheticItem(): boolean {
|
||||
return this.instanceId === Pedalboard.START_CONTROL
|
||||
|| this.instanceId === Pedalboard.END_CONTROL;
|
||||
}
|
||||
getInstanceId() : number {
|
||||
if (this.instanceId === undefined)
|
||||
{
|
||||
throw new PiPedalArgumentError("Item does not have an id.");
|
||||
}
|
||||
return this.instanceId;
|
||||
}
|
||||
isEmpty(): boolean {
|
||||
return this.uri === EMPTY_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
isSplit(): boolean {
|
||||
return this.uri === SPLIT_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
isStart(): boolean {
|
||||
return this.uri === Pedalboard.START_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
isEnd(): boolean {
|
||||
return this.uri === Pedalboard.END_PEDALBOARD_ITEM_URI;
|
||||
}
|
||||
|
||||
getControl(key: string): ControlValue {
|
||||
for (let i = 0; i < this.controlValues.length; ++i) {
|
||||
let v = this.controlValues[i];
|
||||
if (v.key === key) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
throw new PiPedalArgumentError("Invalid key.");
|
||||
|
||||
}
|
||||
|
||||
getControlValue(key: string): number {
|
||||
for (let i = 0; i < this.controlValues.length; ++i) {
|
||||
let v = this.controlValues[i];
|
||||
if (v.key === key) {
|
||||
return v.value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
setControlValue(key: string, value: number): boolean {
|
||||
for (let i = 0; i < this.controlValues.length; ++i) {
|
||||
let v = this.controlValues[i];
|
||||
if (v.key === key) {
|
||||
if (v.value === value) return false;
|
||||
v.setValue(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
setMidiBinding(midiBinding: MidiBinding): boolean {
|
||||
if (this.midiBindings)
|
||||
{
|
||||
for (let i = 0; i < this.midiBindings.length; ++i)
|
||||
{
|
||||
let binding = this.midiBindings[i];
|
||||
if (midiBinding.symbol === binding.symbol)
|
||||
{
|
||||
if (binding.equals(midiBinding))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.midiBindings.splice(i,1,midiBinding);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this.midiBindings.push(midiBinding);
|
||||
return true;
|
||||
}
|
||||
this.midiBindings = [ midiBinding];
|
||||
return true;
|
||||
}
|
||||
getMidiBinding(symbol: string): MidiBinding {
|
||||
if (this.midiBindings)
|
||||
{
|
||||
for (let i = 0; i < this.midiBindings.length; ++i)
|
||||
{
|
||||
let midiBinding = this.midiBindings[i];
|
||||
if (midiBinding.symbol === symbol)
|
||||
{
|
||||
return midiBinding;
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = new MidiBinding();
|
||||
result.symbol = symbol;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static EmptyArray: PedalboardItem[] = [];
|
||||
|
||||
instanceId: number = -1;
|
||||
isEnabled: boolean = false;
|
||||
uri: string = "";
|
||||
pluginName?: string;
|
||||
controlValues: ControlValue[] = ControlValue.EmptyArray;
|
||||
midiBindings: MidiBinding[] = [];
|
||||
midiChannelBinding: MidiChannelBinding | null = null;
|
||||
vstState: string = "";
|
||||
stateUpdateCount: number = 0;
|
||||
lv2State: [boolean,any] = [false,{}];
|
||||
lilvPresetUri: string = "";
|
||||
pathProperties: {[Name: string]: string} = {};
|
||||
};
|
||||
|
||||
export class SnapshotValue {
|
||||
deserialize(input: any): SnapshotValue {
|
||||
this.isEnabled = input.isEnabled;
|
||||
this.instanceId = input.instanceId;
|
||||
this.controlValues = ControlValue.deserializeArray(input.controlValues);
|
||||
this.lv2State = input.lv2state;
|
||||
this.pathProperties = input.pathProperties;
|
||||
return this;
|
||||
}
|
||||
static deserializeArray(input: any): SnapshotValue[] {
|
||||
let result: SnapshotValue[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
let inputItem: any = input[i];
|
||||
let outputItem = new SnapshotValue().deserialize(inputItem);
|
||||
result[i] = outputItem;
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static createFromPedalboardItem(item: PedalboardItem)
|
||||
{
|
||||
let result = new SnapshotValue();
|
||||
result.instanceId = item.instanceId;
|
||||
result.isEnabled = item.isEnabled;
|
||||
result.controlValues = ControlValue.deserializeArray(item.controlValues);
|
||||
result.lv2State = item.lv2State; // we can do this, because lv2State is immutable.
|
||||
result.pathProperties = {...item.pathProperties}; // clone the dictionary.
|
||||
return result;
|
||||
}
|
||||
instanceId: number = -1;
|
||||
isEnabled: boolean = true;
|
||||
controlValues: ControlValue[] = ControlValue.EmptyArray;
|
||||
lv2State: [boolean,any] = [false,{}];
|
||||
pathProperties: {[Name: string]: string} = {};
|
||||
}
|
||||
export class Snapshot {
|
||||
deserialize(input: any): Snapshot {
|
||||
this.values = SnapshotValue.deserializeArray(input.values);
|
||||
this.isModified = input.isModified;
|
||||
this.name = input.name;
|
||||
this.color = input.color;
|
||||
return this;
|
||||
}
|
||||
static deserializeArray(input: any): (Snapshot| null)[] {
|
||||
let result: (Snapshot|null)[] = [];
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
let inputItem: any = input[i];
|
||||
let outputItem: (Snapshot|null) = null;
|
||||
if (inputItem !== null)
|
||||
{
|
||||
outputItem = new Snapshot().deserialize(inputItem);
|
||||
}
|
||||
result[i] = outputItem;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static readonly MAX_SNAPSHOTS: number = 6;
|
||||
|
||||
static cloneSnapshots(snapshots: (Snapshot|null)[]): (Snapshot|null)[]
|
||||
{
|
||||
let result: (Snapshot|null)[] = [];
|
||||
for (let i = 0; i < Snapshot.MAX_SNAPSHOTS; ++i)
|
||||
{
|
||||
if (i >= snapshots.length)
|
||||
{
|
||||
result.push(null);
|
||||
} else {
|
||||
result.push(snapshots[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
name: string = "";
|
||||
isModified: boolean = false;
|
||||
color: string = "";
|
||||
values: SnapshotValue[] = [];
|
||||
};
|
||||
|
||||
export enum SplitType {
|
||||
Ab = 0,
|
||||
Mix = 1,
|
||||
Lr = 2
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class PedalboardSplitItem extends PedalboardItem {
|
||||
static PANL_KEY: string = "panL";
|
||||
static PANR_KEY: string = "panR";
|
||||
static VOLL_KEY: string = "volL";
|
||||
static VOLR_KEY: string = "volR";
|
||||
|
||||
static MIX_KEY: string = "mix";
|
||||
static TYPE_KEY: string = "splitType";
|
||||
static SELECT_KEY: string = "select";
|
||||
|
||||
|
||||
deserialize(input: any): PedalboardSplitItem {
|
||||
this.deserializePedalboardItem(input);
|
||||
this.topChain = PedalboardItem.deserializeArray(input.topChain);
|
||||
this.bottomChain = PedalboardItem.deserializeArray(input.bottomChain);
|
||||
|
||||
return this;
|
||||
}
|
||||
getSplitType(): SplitType {
|
||||
let rawValue = this.getControlValue(PedalboardSplitItem.TYPE_KEY);
|
||||
|
||||
if (rawValue < 1) return SplitType.Ab;
|
||||
if (rawValue < 2) return SplitType.Mix;
|
||||
return SplitType.Lr;
|
||||
}
|
||||
getToggleAbControlValue(): ControlValue {
|
||||
let cv = new ControlValue();
|
||||
cv.key = "select";
|
||||
cv.value = this.isASelected() ? 1 : 0;
|
||||
return cv;
|
||||
|
||||
}
|
||||
getMixControl(): ControlValue {
|
||||
return this.getControl(PedalboardSplitItem.MIX_KEY);
|
||||
}
|
||||
getMix(): number {
|
||||
return this.getControlValue(PedalboardSplitItem.MIX_KEY);
|
||||
}
|
||||
isASelected(): boolean {
|
||||
return this.getSplitType() !== SplitType.Ab || this.getControlValue(PedalboardSplitItem.SELECT_KEY) === 0;
|
||||
}
|
||||
isBSelected(): boolean {
|
||||
return this.getSplitType() !== SplitType.Ab || this.getControlValue(PedalboardSplitItem.SELECT_KEY) !== 0;
|
||||
}
|
||||
|
||||
topChain: PedalboardItem[] = PedalboardItem.EmptyArray;
|
||||
bottomChain: PedalboardItem[] = PedalboardItem.EmptyArray;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class Pedalboard implements Deserializable<Pedalboard> {
|
||||
|
||||
static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume.
|
||||
static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume.
|
||||
static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start";
|
||||
static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End";
|
||||
|
||||
deserialize(input: any): Pedalboard {
|
||||
this.name = input.name;
|
||||
this.input_volume_db = input.input_volume_db;
|
||||
this.output_volume_db = input.output_volume_db;
|
||||
this.items = PedalboardItem.deserializeArray(input.items);
|
||||
this.nextInstanceId = input.nextInstanceId ?? -1;
|
||||
this.snapshots = input.snapshots ? Snapshot.deserializeArray(input.snapshots): [];
|
||||
this.selectedSnapshot = input.selectedSnapshot;
|
||||
this.pathProperties = input.pathProperties;
|
||||
return this;
|
||||
}
|
||||
|
||||
clone(): Pedalboard {
|
||||
return new Pedalboard().deserialize(this);
|
||||
}
|
||||
name: string = "";
|
||||
input_volume_db: number = 0;
|
||||
output_volume_db: number = 0;
|
||||
items: PedalboardItem[] = [];
|
||||
nextInstanceId: number = -1;
|
||||
|
||||
snapshots: (Snapshot | null)[] = [];
|
||||
selectedSnapshot: number = -1;
|
||||
pathProperties: {[Name: string]: string} = {};
|
||||
|
||||
*itemsGenerator(): Generator<PedalboardItem, void, undefined> {
|
||||
let it = itemGenerator_(this.items);
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
yield v.value;
|
||||
}
|
||||
}
|
||||
|
||||
makeSnapshot(): Snapshot {
|
||||
let result = new Snapshot();
|
||||
let it = this.itemsGenerator();
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
let pedalboardItem = v.value;
|
||||
let snapshotValue = SnapshotValue.createFromPedalboardItem(pedalboardItem);
|
||||
result.values.push(snapshotValue);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
hasItem(instanceId: number): boolean
|
||||
{
|
||||
let it = this.itemsGenerator();
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
if (v.value.instanceId === instanceId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
getFirstSelectableItem(): number
|
||||
{
|
||||
if (this.items.length !== 0)
|
||||
{
|
||||
return this.items[0].instanceId;
|
||||
}
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
maybeGetItem(instanceId: number): PedalboardItem | null{
|
||||
let it = this.itemsGenerator();
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
if (v.value.instanceId === instanceId)
|
||||
{
|
||||
return v.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
makeStartItem(): PedalboardItem {
|
||||
let result = new PedalboardItem();
|
||||
result.pluginName = "Input";
|
||||
result.instanceId = Pedalboard.START_CONTROL;
|
||||
result.uri = Pedalboard.START_PEDALBOARD_ITEM_URI;
|
||||
result.isEnabled = true;
|
||||
result.controlValues = [new ControlValue("volume_db",this.input_volume_db)];
|
||||
return result;
|
||||
|
||||
}
|
||||
makeEndItem(): PedalboardItem {
|
||||
let result = new PedalboardItem();
|
||||
result.pluginName = "Output";
|
||||
result.instanceId = Pedalboard.END_CONTROL;
|
||||
result.uri = Pedalboard.END_PEDALBOARD_ITEM_URI;
|
||||
result.isEnabled = true;
|
||||
result.controlValues = [new ControlValue("volume_db",this.output_volume_db)];
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
getItem(instanceId: number): PedalboardItem {
|
||||
let it = this.itemsGenerator();
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
if (v.value.instanceId === instanceId)
|
||||
{
|
||||
return v.value;
|
||||
}
|
||||
}
|
||||
throw new PiPedalArgumentError("Item not found.");
|
||||
}
|
||||
deleteItem_(instanceId: number,items: PedalboardItem[]): number | null
|
||||
{
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
let item = items[i];
|
||||
if (item.instanceId === instanceId)
|
||||
{
|
||||
if (items.length > 1) {
|
||||
items.splice(i,1);
|
||||
let nextSelectedItem = i;
|
||||
if (i >= items.length) --nextSelectedItem;
|
||||
return items[nextSelectedItem].instanceId;
|
||||
} else {
|
||||
// replace with an empty item.
|
||||
let newItem = this.createEmptyItem();
|
||||
items[i] = newItem;
|
||||
return newItem.instanceId;
|
||||
|
||||
}
|
||||
} else {
|
||||
if (item.isSplit())
|
||||
{
|
||||
let splitItem = item as PedalboardSplitItem;
|
||||
let t = this.deleteItem_(instanceId,splitItem.topChain);
|
||||
if (t != null) return t;
|
||||
|
||||
t = this.deleteItem_(instanceId,splitItem.bottomChain);
|
||||
if (t != null) return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
canDeleteItem_(instanceId: number,items: PedalboardItem[]): boolean
|
||||
{
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
let item = items[i];
|
||||
if (item.instanceId === instanceId)
|
||||
{
|
||||
if (items.length > 1) return true;
|
||||
return !item.isEmpty(); // can delete if there's one non-empty item.
|
||||
}
|
||||
if (item.isSplit())
|
||||
{
|
||||
let splitItem = item as PedalboardSplitItem;
|
||||
if (this.canDeleteItem_(instanceId,splitItem.topChain))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (this.canDeleteItem_(instanceId,splitItem.bottomChain))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canDeleteItem(instanceId: number): boolean
|
||||
{
|
||||
return this.canDeleteItem_(instanceId,this.items);
|
||||
}
|
||||
// Returns the next selected instanceId, or null if no deletion occurred.
|
||||
deleteItem(instanceId: number): number | null {
|
||||
return this.deleteItem_(instanceId,this.items);
|
||||
}
|
||||
|
||||
setMidiBinding(instanceId: number, midiBinding: MidiBinding): boolean
|
||||
{
|
||||
let item = this.getItem(instanceId);
|
||||
if (!item) return false;
|
||||
return item.setMidiBinding(midiBinding);
|
||||
}
|
||||
addToStart(item: PedalboardItem)
|
||||
{
|
||||
this.items.splice(0,0,item);
|
||||
}
|
||||
addToEnd(item: PedalboardItem)
|
||||
{
|
||||
this.items.splice(this.items.length,0,item);
|
||||
}
|
||||
static _addRelative(items: PedalboardItem[],newItem: PedalboardItem, instanceId: number, addBefore: boolean): boolean
|
||||
{
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
let item = items[i];
|
||||
if (item.instanceId === instanceId)
|
||||
{
|
||||
if (addBefore)
|
||||
{
|
||||
items.splice(i,0,newItem);
|
||||
} else {
|
||||
items.splice(i+1,0,newItem);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (item.isSplit())
|
||||
{
|
||||
let split = item as PedalboardSplitItem;
|
||||
if (this._addRelative(split.topChain,newItem,instanceId,addBefore))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (this._addRelative(split.bottomChain,newItem,instanceId,addBefore))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
addBefore(item: PedalboardItem, instanceId: number)
|
||||
{
|
||||
if (item.instanceId === instanceId) return;
|
||||
let result = Pedalboard._addRelative(this.items,item, instanceId, true);
|
||||
if (!result) {
|
||||
throw new PiPedalArgumentError("instanceId not found.");
|
||||
}
|
||||
|
||||
}
|
||||
addAfter(item: PedalboardItem, instanceId: number)
|
||||
{
|
||||
if (item.instanceId === instanceId) return;
|
||||
let result = Pedalboard._addRelative(this.items,item, instanceId, false);
|
||||
if (!result) {
|
||||
throw new PiPedalArgumentError("instanceId not found.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ensurePedalboardIds() {
|
||||
if (this.nextInstanceId === -1) {
|
||||
let minId = 1;
|
||||
let it = this.itemsGenerator();
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
if (v.value.instanceId) {
|
||||
let t = v.value.instanceId;
|
||||
if (t+1 > minId) minId = t+1;
|
||||
}
|
||||
|
||||
}
|
||||
this.nextInstanceId = minId;
|
||||
}
|
||||
let it = this.itemsGenerator();
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
if (v.value.instanceId === -1) {
|
||||
v.value.instanceId = ++this.nextInstanceId;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
createEmptySplit(): PedalboardSplitItem {
|
||||
let result: PedalboardSplitItem = new PedalboardSplitItem();
|
||||
result.uri = SPLIT_PEDALBOARD_ITEM_URI;
|
||||
result.instanceId = ++this.nextInstanceId;
|
||||
result.pluginName = "";
|
||||
result.isEnabled = true;
|
||||
result.topChain = [ this.createEmptyItem()];
|
||||
result.bottomChain = [ this.createEmptyItem()];
|
||||
result.controlValues = [
|
||||
new ControlValue(PedalboardSplitItem.TYPE_KEY, 0),
|
||||
new ControlValue(PedalboardSplitItem.SELECT_KEY,0),
|
||||
new ControlValue(PedalboardSplitItem.MIX_KEY,0),
|
||||
new ControlValue(PedalboardSplitItem.PANL_KEY,0),
|
||||
new ControlValue(PedalboardSplitItem.VOLL_KEY,-3),
|
||||
new ControlValue(PedalboardSplitItem.PANR_KEY,0),
|
||||
new ControlValue(PedalboardSplitItem.VOLR_KEY,-3)
|
||||
];
|
||||
|
||||
|
||||
return result;
|
||||
|
||||
|
||||
}
|
||||
createEmptyItem(): PedalboardItem {
|
||||
let result: PedalboardItem = new PedalboardItem();
|
||||
result.uri = EMPTY_PEDALBOARD_ITEM_URI;
|
||||
result.instanceId = ++this.nextInstanceId;
|
||||
result.pluginName = "";
|
||||
result.isEnabled = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
setItemEmpty(item: PedalboardItem)
|
||||
{
|
||||
item.uri = EMPTY_PEDALBOARD_ITEM_URI;
|
||||
item.instanceId = ++this.nextInstanceId;
|
||||
item.pluginName = "";
|
||||
item.isEnabled = true;
|
||||
item.controlValues = [];
|
||||
item.vstState = "";
|
||||
item.lv2State = [false,{}];
|
||||
|
||||
}
|
||||
|
||||
_replaceItem(items: PedalboardItem[], instanceId: number, newItem: PedalboardItem): boolean {
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
let item = items[i];
|
||||
if (item.instanceId === instanceId)
|
||||
{
|
||||
items[i] = newItem;
|
||||
return true;
|
||||
}
|
||||
if (items[i].isSplit())
|
||||
{
|
||||
let splitItem = item as PedalboardSplitItem;
|
||||
if (this._replaceItem(splitItem.topChain,instanceId,newItem))
|
||||
return true;
|
||||
if (this._replaceItem(splitItem.bottomChain,instanceId,newItem))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
replaceItem(instanceId: number, newItem: PedalboardItem)
|
||||
{
|
||||
let result = this._replaceItem(this.items,instanceId,newItem);
|
||||
if (!result)
|
||||
{
|
||||
throw new PiPedalArgumentError("instanceId not found.");
|
||||
}
|
||||
}
|
||||
private _addItem(items: PedalboardItem[], newItem: PedalboardItem, instanceId: number, append: boolean)
|
||||
{
|
||||
for (let i = 0; i < items.length; ++i)
|
||||
{
|
||||
let item = items[i];
|
||||
if (item.instanceId === instanceId)
|
||||
{
|
||||
if (append)
|
||||
{
|
||||
items.splice(i+1,0,newItem);
|
||||
return true;
|
||||
} else {
|
||||
items.splice(i,0,newItem);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (item.isSplit())
|
||||
{
|
||||
let splitItem = item as PedalboardSplitItem;
|
||||
if (this._addItem(splitItem.topChain,newItem,instanceId,append)) return true;
|
||||
if (this._addItem(splitItem.bottomChain,newItem,instanceId,append)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addItem(newItem: PedalboardItem, instanceId: number, append: boolean): void
|
||||
{
|
||||
this._addItem(this.items,newItem,instanceId,append);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function* itemGenerator_(items: PedalboardItem[]): Generator<PedalboardItem, void, undefined> {
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
let item = items[i];
|
||||
yield item;
|
||||
if (item.uri === SPLIT_PEDALBOARD_ITEM_URI) {
|
||||
|
||||
let splitItem = item as PedalboardSplitItem;
|
||||
|
||||
let it = itemGenerator_(splitItem.topChain);
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
yield v.value;
|
||||
}
|
||||
it = itemGenerator_(splitItem.bottomChain);
|
||||
while (true) {
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
yield v.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,417 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import SaveIconOutline from '@mui/icons-material/Save';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import SnapshotPanel from './SnapshotPanel';
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
//import ArrowLeftOutlined from '@mui/icons-material/ArrowLeft';
|
||||
//import ArrowRightOutlined from '@mui/icons-material/ArrowRight';
|
||||
import Select from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import { BankIndex } from './Banks';
|
||||
import SnapshotEditor from './SnapshotEditor';
|
||||
import { Snapshot } from './Pedalboard';
|
||||
import JackStatusView from './JackStatusView';
|
||||
import {IDialogStackable, popDialogStack, pushDialogStack} from './DialogStack';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden"
|
||||
}),
|
||||
select: css({ // fu fu fu.Overrides for white selector on dark background.
|
||||
'&:before': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:after': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:hover:not(.Mui-disabled):before': {
|
||||
borderColor: selectColor,
|
||||
}
|
||||
}),
|
||||
select_icon: css({
|
||||
fill: selectColor,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
interface PerformanceViewProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
interface PerformanceViewState {
|
||||
wrapSelects: boolean
|
||||
presets: PresetIndex;
|
||||
banks: BankIndex;
|
||||
showSnapshotEditor: boolean;
|
||||
snapshotEditorIndex: number;
|
||||
presetModified: boolean;
|
||||
}
|
||||
|
||||
|
||||
export const PerformanceView =
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<PerformanceViewProps, PerformanceViewState> implements IDialogStackable {
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PerformanceViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
// let pedalboard = this.model.pedalboard.get();
|
||||
// let selectedPedal = pedalboard.getFirstSelectableItem();
|
||||
|
||||
this.state = {
|
||||
presets: this.model.presets.get(),
|
||||
banks: this.model.banks.get(),
|
||||
wrapSelects: false,
|
||||
showSnapshotEditor: false,
|
||||
snapshotEditorIndex: 0,
|
||||
presetModified: this.model.presetChanged.get()
|
||||
};
|
||||
this.onPresetsChanged = this.onPresetsChanged.bind(this);
|
||||
this.onBanksChanged = this.onBanksChanged.bind(this);
|
||||
this.onPresetChangedChanged = this.onPresetChangedChanged.bind(this);
|
||||
|
||||
}
|
||||
|
||||
getTag() { return "performView"}
|
||||
isOpen() { return this.props.open }
|
||||
onDialogStackClose() {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
|
||||
private hasHooks: boolean = false;
|
||||
|
||||
updateHooks() : void {
|
||||
let wantHooks = this.mounted && this.props.open;
|
||||
|
||||
if (wantHooks !== this.hasHooks)
|
||||
{
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks)
|
||||
{
|
||||
|
||||
pushDialogStack(this);
|
||||
} else {
|
||||
popDialogStack(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ wrapSelects: width < 700 });
|
||||
}
|
||||
|
||||
onPresetsChanged(newValue: PresetIndex) {
|
||||
this.setState({ presets: this.model.presets.get() })
|
||||
}
|
||||
onBanksChanged(newValue: BankIndex) {
|
||||
this.setState({ banks: this.model.banks.get() })
|
||||
}
|
||||
onPresetChangedChanged(newValue: boolean) {
|
||||
this.setState({ presetModified:newValue});
|
||||
}
|
||||
|
||||
private mounted: boolean = false;
|
||||
componentDidMount(): void {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.model.presets.addOnChangedHandler(this.onPresetsChanged);
|
||||
this.model.banks.addOnChangedHandler(this.onBanksChanged);
|
||||
this.model.presetChanged.addOnChangedHandler(this.onPresetChangedChanged)
|
||||
this.setState({
|
||||
presets: this.model.presets.get(),
|
||||
banks: this.model.banks.get(),
|
||||
presetModified: this.model.presetChanged.get()
|
||||
|
||||
});
|
||||
|
||||
this.updateHooks();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.model.presetChanged.removeOnChangedHandler(this.onPresetChangedChanged)
|
||||
this.model.presets.removeOnChangedHandler(this.onPresetsChanged);
|
||||
this.model.banks.removeOnChangedHandler(this.onBanksChanged);
|
||||
|
||||
this.mounted = false;
|
||||
this.updateHooks();
|
||||
|
||||
super.componentWillUnmount();
|
||||
|
||||
}
|
||||
handlePresetSelectClose(event: any): void {
|
||||
let value = event.currentTarget.getAttribute("data-value");
|
||||
if (value && value.length > 0) {
|
||||
this.model.loadPreset(parseInt(value));
|
||||
}
|
||||
}
|
||||
|
||||
handleBankSelectClose(event: any): void {
|
||||
let value = event.currentTarget.getAttribute("data-value");
|
||||
if (value && value.length > 0) {
|
||||
this.model.openBank(parseInt(value));
|
||||
}
|
||||
}
|
||||
handleOnEdit(index: number): boolean {
|
||||
// load it so we can edit it.
|
||||
this.model.selectSnapshot(index);
|
||||
this.setState({
|
||||
showSnapshotEditor: true,
|
||||
snapshotEditorIndex: index
|
||||
});
|
||||
return true;
|
||||
}
|
||||
handleSnapshotEditOk(index: number, name: string, color: string,newSnapshots: (Snapshot|null)[])
|
||||
{
|
||||
// results have been sent to the server. we would perform a short-cut commit to our local state, to avoid laggy display updates.
|
||||
// but we don't actually have that in our state.
|
||||
|
||||
}
|
||||
|
||||
handlePreviousBank()
|
||||
{
|
||||
this.model.previousBank();
|
||||
}
|
||||
|
||||
handleNextBank()
|
||||
{
|
||||
this.model.nextBank();
|
||||
}
|
||||
handlePreviousPreset()
|
||||
{
|
||||
this.model.previousPreset();
|
||||
}
|
||||
handleNextPreset()
|
||||
{
|
||||
this.model.nextPreset();
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let wrapSelects = this.state.wrapSelects;
|
||||
let presets = this.state.presets;
|
||||
let banks = this.state.banks;
|
||||
return (
|
||||
<div className={classes.frame} style={{ overflow: "clip", height: "100%" }} >
|
||||
<div className={classes.frame} style={{ overflow: "clip", height: "100%" }} >
|
||||
<AppBar id="select-plugin-dialog-title"
|
||||
style={{
|
||||
position: "static", flex: "0 0 auto", height: wrapSelects ? 108 : 58,
|
||||
display: this.state.showSnapshotEditor ? "none" : undefined
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex", flexFlow: "row nowrap", alignContent: "center",
|
||||
paddingTop: 3, paddingBottom: 3, paddingRight: 8, paddingLeft: 8,
|
||||
alignItems: "start"
|
||||
}}>
|
||||
<IconButton aria-label="menu" color="inherit"
|
||||
onClick={() => { this.props.onClose(); }}
|
||||
style={{
|
||||
position: "relative",top: 3,
|
||||
flex: "0 0 auto" }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
<div style={{
|
||||
flex: "1 1 1px", display: "flex",
|
||||
flexFlow: wrapSelects ? "column nowrap" : "row nowrap",
|
||||
alignItems: wrapSelects ? "stretch" : undefined,
|
||||
justifyContent: wrapSelects ? undefined : "space-evenly",
|
||||
marginLeft: 8,
|
||||
gap: 8
|
||||
|
||||
}}>
|
||||
{/********* BANKS *******************/}
|
||||
<div style={{ flex: "1 1 1px", display: "flex", flexFlow: "row nowrap", maxWidth: 500 }}>
|
||||
<IconButton
|
||||
aria-label="previous-bank"
|
||||
onClick={() => { this.handlePreviousBank(); }}
|
||||
size="medium"
|
||||
color="inherit"
|
||||
style={{
|
||||
borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
|
||||
<Select variant="standard"
|
||||
className={classes.select}
|
||||
style={{ flex: "1 1 1px", width: "100%", position: "relative", top: 0, color: "#FFFFFF" }} disabled={false}
|
||||
displayEmpty
|
||||
onClose={(e) => this.handleBankSelectClose(e)}
|
||||
value={banks.selectedBank === 0 ? undefined : banks.selectedBank}
|
||||
inputProps={{
|
||||
classes: { icon: classes.select_icon },
|
||||
'aria-label': "Select preset"
|
||||
}}
|
||||
>
|
||||
{
|
||||
banks.entries.map((entry) => {
|
||||
return (
|
||||
<MenuItem key={entry.instanceId} value={entry.instanceId} >
|
||||
{entry.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
|
||||
<IconButton
|
||||
aria-label="next-bank"
|
||||
onClick={() => { this.handleNextBank(); }}
|
||||
color="inherit"
|
||||
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
|
||||
>
|
||||
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
|
||||
|
||||
</IconButton>
|
||||
|
||||
{/** spacer */}
|
||||
<IconButton
|
||||
aria-label="next-bank"
|
||||
onClick={() => { this.handleNextBank(); }}
|
||||
size="medium"
|
||||
color="inherit"
|
||||
style={{visibility: "hidden"}}
|
||||
>
|
||||
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
|
||||
|
||||
</IconButton>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{/********* PRESETS *******************/}
|
||||
<div style={{ flex: "1 1 1px", display: "flex", flexFlow: "row nowrap", maxWidth: 500 }}>
|
||||
<IconButton
|
||||
aria-label="previous-presest"
|
||||
onClick={() => { this.handlePreviousPreset(); }}
|
||||
color="inherit"
|
||||
style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }}
|
||||
>
|
||||
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
<Select variant="standard"
|
||||
className={classes.select}
|
||||
style={{
|
||||
flex: "1 1 1px", width: "100%", color: "#FFFFFF"
|
||||
}} disabled={false}
|
||||
displayEmpty
|
||||
onClose={(e) => this.handlePresetSelectClose(e)}
|
||||
value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
|
||||
|
||||
inputProps={{
|
||||
classes: { icon: classes.select_icon },
|
||||
'aria-label': "Select preset"
|
||||
}
|
||||
}
|
||||
>
|
||||
{
|
||||
presets.presets.map((preset) => {
|
||||
let name = preset.name;
|
||||
if (this.state.presets.selectedInstanceId === preset.instanceId && this.state.presetModified)
|
||||
{
|
||||
name += "*";
|
||||
}
|
||||
return (
|
||||
<MenuItem key={preset.instanceId} value={preset.instanceId} >
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
|
||||
<IconButton
|
||||
aria-label="next-preset"
|
||||
onClick={() => { this.handleNextPreset(); }}
|
||||
color="inherit"
|
||||
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
|
||||
>
|
||||
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
aria-label="save-preset"
|
||||
onClick={() => { this.model.saveCurrentPreset(); }}
|
||||
size="medium"
|
||||
color="inherit"
|
||||
style={{flexShrink: 0,visibility: this.state.presetModified? "visible": "hidden"}}
|
||||
>
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} color="inherit" />
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppBar >
|
||||
<div style={{ flex: "1 0 auto", display: "flex", marginTop: 16,marginBottom: 20 }}>
|
||||
<SnapshotPanel onEdit={(index) => { return this.handleOnEdit(index); }} />
|
||||
</div>
|
||||
|
||||
|
||||
{!this.state.showSnapshotEditor && (
|
||||
<JackStatusView />)
|
||||
}
|
||||
</div>
|
||||
{this.state.showSnapshotEditor && (
|
||||
<SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex}
|
||||
onClose={() => {
|
||||
this.setState({ showSnapshotEditor: false });
|
||||
}}
|
||||
onOk={(index,name,color,newSnapshots)=>{
|
||||
this.setState({ showSnapshotEditor: false});
|
||||
this.handleSnapshotEditOk(index,name,color,newSnapshots);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
export class PiPedalError extends Error {
|
||||
constructor(message: string)
|
||||
{
|
||||
super(message);
|
||||
this.name = "PiPedalError";
|
||||
}
|
||||
}
|
||||
|
||||
export class PiPedalArgumentError extends PiPedalError {
|
||||
constructor(message: string)
|
||||
{
|
||||
super(message);
|
||||
this.name = "PiPedalArgumentError";
|
||||
}
|
||||
}
|
||||
export class PiPedalStateError extends PiPedalError {
|
||||
constructor(message: string)
|
||||
{
|
||||
super(message);
|
||||
this.name = "PiPedalStateError";
|
||||
}
|
||||
}
|
||||
|
||||
export default PiPedalError;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import Utility from './Utility';
|
||||
|
||||
export type MessageHandler = (header: PiPedalMessageHeader, body: any | null) => void;
|
||||
export type ErrorHandler = (message: string, exception?: Error) => void;
|
||||
export type ReconnectHandler = () => void;
|
||||
export type ReconnectingHandler = (retry: number, maxRetries: number) => void;
|
||||
|
||||
const MAX_RETRIES = 6;
|
||||
const MAX_RETRY_TIME = 90 * 1000;
|
||||
|
||||
export type PiPedalMessageHeader = {
|
||||
replyTo?: number;
|
||||
reply?: number;
|
||||
message: string;
|
||||
}
|
||||
type ReplyHandler = (header: PiPedalMessageHeader, body: any | null) => void;
|
||||
|
||||
|
||||
export interface PiPedalSocketListener {
|
||||
onMessageReceived: (header: PiPedalMessageHeader, body: any | null) => void;
|
||||
onError: (message: string, exception?: Error) => void;
|
||||
onConnectionLost: () => void;
|
||||
onReconnect: () => void;
|
||||
onReconnecting: (retry: number, maxRetries: number) => boolean;
|
||||
};
|
||||
|
||||
class PiPedalSocket {
|
||||
|
||||
listener: PiPedalSocketListener;
|
||||
|
||||
|
||||
socket?: WebSocket;
|
||||
nextResponseCode: number = 0;
|
||||
url: string;
|
||||
retrying: boolean = false;
|
||||
retryCount: number = 0;
|
||||
retryDelay: number = 0;
|
||||
totalRetryDelay: number = 0;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
listener: PiPedalSocketListener
|
||||
) {
|
||||
this.url = url;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
handleOpen(event: Event): any {
|
||||
|
||||
}
|
||||
|
||||
sendInternal_(json: string) {
|
||||
if (!this.retrying) {
|
||||
this.socket?.send(json);
|
||||
}
|
||||
}
|
||||
send(message: string, jsonObject?: any) {
|
||||
let msg: any;
|
||||
if (jsonObject === undefined) {
|
||||
msg = [{ message: message }];
|
||||
} else {
|
||||
msg = [{ message: message }, jsonObject];
|
||||
}
|
||||
let json = JSON.stringify(msg);
|
||||
this.sendInternal_(json);
|
||||
}
|
||||
reply(replyTo: number, message: string, jsonObject?: any) {
|
||||
if (replyTo !== -1) {
|
||||
let msg: any;
|
||||
if (jsonObject === undefined) {
|
||||
msg = [{ reply: replyTo, message: message }];
|
||||
} else {
|
||||
msg = [{ reply: replyTo, message: message }, jsonObject];
|
||||
}
|
||||
let json = JSON.stringify(msg);
|
||||
this.sendInternal_(json);
|
||||
}
|
||||
}
|
||||
|
||||
_nextResponseCode(): number {
|
||||
return ++this.nextResponseCode;
|
||||
}
|
||||
|
||||
_replyMap: Map<number, ReplyHandler> = new Map<number, ReplyHandler>();
|
||||
|
||||
_discardReplyReservations() {
|
||||
// it's ok. All pending reservations disappear into the GC.
|
||||
this._replyMap = new Map<number, ReplyHandler>();
|
||||
}
|
||||
|
||||
|
||||
_addReservation(replyCode: number, handler: ReplyHandler): void {
|
||||
this._replyMap.set(replyCode, handler);
|
||||
}
|
||||
|
||||
request<Type = any>(message_: string, requestArgs?: any): Promise<Type> {
|
||||
let responseCode = this._nextResponseCode();
|
||||
|
||||
return new Promise<Type>((resolve, reject) => {
|
||||
try {
|
||||
this._addReservation(responseCode, (header: PiPedalMessageHeader, jsonObject?: any) => {
|
||||
if (header.message === "error") {
|
||||
reject(jsonObject + "");
|
||||
} else {
|
||||
resolve(jsonObject as Type);
|
||||
}
|
||||
});
|
||||
let msg: any;
|
||||
if (requestArgs !== undefined) {
|
||||
msg = [{ message: message_, replyTo: responseCode }, requestArgs];
|
||||
} else {
|
||||
msg = [{ message: message_, replyTo: responseCode }];
|
||||
}
|
||||
let jsonMessage = JSON.stringify(msg);
|
||||
this.sendInternal_(jsonMessage);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
handleMessage(event: MessageEvent<string>): any {
|
||||
try {
|
||||
let message: any = JSON.parse(event.data);
|
||||
if (!Array.isArray(message)) {
|
||||
throw new PiPedalStateError("Invalid message received from server.");
|
||||
}
|
||||
let header = message[0] as PiPedalMessageHeader;
|
||||
let body = undefined;
|
||||
if (message.length === 2) {
|
||||
body = message[1];
|
||||
}
|
||||
if (header.reply !== undefined) {
|
||||
let handler = this._replyMap.get(header.reply);
|
||||
if (handler) {
|
||||
handler(header, body);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (header.message === "error") {
|
||||
throw new PiPedalStateError("Server error: " + body);
|
||||
}
|
||||
this.listener.onMessageReceived(header, body);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.listener) {
|
||||
this.listener.onError("Invalid server response. " + error, error as Error);
|
||||
} else {
|
||||
throw new PiPedalStateError("Invalid server response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleError(_event: Event): any {
|
||||
if (this.listener) {
|
||||
this.listener.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection lost.");
|
||||
}
|
||||
}
|
||||
|
||||
canReconnect: boolean = false;
|
||||
|
||||
handleClose(_event: any): any {
|
||||
if (this.retrying) {
|
||||
this.close();
|
||||
// treat this as a fatal error.
|
||||
if (this.listener) {
|
||||
this.listener.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection closed.");
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
if (this.canReconnect) {
|
||||
this.listener.onConnectionLost();
|
||||
this._reconnect();
|
||||
}
|
||||
}
|
||||
_reconnect() {
|
||||
this._discardReplyReservations();
|
||||
this.retrying = true;
|
||||
this.retryCount = 0;
|
||||
this.socket = undefined;
|
||||
this.retryDelay = 10000;
|
||||
this.totalRetryDelay = 0;
|
||||
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
isBackground: boolean = false;
|
||||
|
||||
enterBackgroundState() {
|
||||
this.isBackground = true;
|
||||
this.close();
|
||||
|
||||
}
|
||||
exitBackgroundState() {
|
||||
this.isBackground = false;
|
||||
this._reconnect();
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
|
||||
if (this.socket) {
|
||||
this.close();
|
||||
}
|
||||
|
||||
if (!this.listener.onReconnecting(this.retryCount, MAX_RETRIES)) {
|
||||
return;
|
||||
}
|
||||
++this.retryCount;
|
||||
|
||||
this.connectInternal_()
|
||||
.then((socket) => {
|
||||
this.socket = socket;
|
||||
this.retrying = false;
|
||||
this.listener.onReconnect();
|
||||
})
|
||||
.catch(error => {
|
||||
if (this.totalRetryDelay >= MAX_RETRY_TIME) {
|
||||
this.listener.onError("Server connection lost.");
|
||||
return;
|
||||
} else {
|
||||
this.totalRetryDelay += this.retryDelay;
|
||||
Utility.delay(this.retryDelay).then(() => this.reconnect());
|
||||
this.retryDelay *= 2;
|
||||
if (this.retryDelay > 3000) this.retryDelay = 3000;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
try {
|
||||
if (this.socket) {
|
||||
this.socket.onclose = null;
|
||||
this.socket.onerror = null;
|
||||
this.socket.onmessage = null;
|
||||
this.socket.onopen = null;
|
||||
this.socket.close();
|
||||
this.socket = undefined;
|
||||
}
|
||||
} catch (ignored) {
|
||||
|
||||
}
|
||||
this.socket = undefined;
|
||||
}
|
||||
connectInternal_(): Promise<WebSocket> {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
try {
|
||||
let ws = new WebSocket(this.url);
|
||||
|
||||
let self = this;
|
||||
|
||||
ws.onmessage = this.handleMessage.bind(this);
|
||||
ws.onclose = (event: Event) => {
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
reject("Connection closed unexpectedly.");
|
||||
};
|
||||
ws.onerror = (evt: Event) => {
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
reject("Connection not accepted.");
|
||||
};
|
||||
ws.onopen = (event: Event) => {
|
||||
ws.onerror = self.handleError.bind(self);
|
||||
ws.onclose = self.handleClose.bind(self);
|
||||
ws.onopen = null;
|
||||
resolve(ws);
|
||||
};
|
||||
} catch (e: any){
|
||||
reject("Failed to connect: " + e.toString());
|
||||
};
|
||||
});
|
||||
}
|
||||
connect(): Promise < void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this.connectInternal_()
|
||||
.then((socket) => {
|
||||
this.socket = socket;
|
||||
resolve();
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PiPedalSocket;
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import {PluginType} from './Lv2Plugin';
|
||||
|
||||
class PluginClass {
|
||||
deserialize(input: any) : PluginClass {
|
||||
this.uri = input.uri;
|
||||
this.display_name = input.display_name;
|
||||
this.parent_uri = input.parent_uri;
|
||||
this.plugin_type = input.plugin_type as PluginType;
|
||||
this.children = PluginClass.deserialize_array(input.children);
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): PluginClass[] {
|
||||
let result: PluginClass[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new PluginClass().deserialize(input[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
findChild(classUri: string): PluginClass | null {
|
||||
if (this.uri === classUri) return this;
|
||||
for (let i = 0; i < this.children.length; ++i)
|
||||
{
|
||||
let t = this.children[i].findChild(classUri);
|
||||
if (t != null) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
findChildType(pluginType: PluginType): PluginClass | null {
|
||||
if (this.plugin_type === pluginType) return this;
|
||||
for (let i = 0; i < this.children.length; ++i)
|
||||
{
|
||||
let t = this.children[i].findChildType(pluginType);
|
||||
if (t != null) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
is_a(parentClassUri: string,childClassUri: string): boolean {
|
||||
let parent = this.findChild(parentClassUri);
|
||||
if (parent === null) return false;
|
||||
let child = parent.findChild(childClassUri);
|
||||
return child != null;
|
||||
}
|
||||
is_type_of(parentType: PluginType,childType: PluginType): boolean {
|
||||
let parent = this.findChildType(parentType);
|
||||
if (parent === null) return false;
|
||||
let child = parent.findChildType(childType);
|
||||
return child != null;
|
||||
}
|
||||
|
||||
uri: string = "";
|
||||
display_name: string = "";
|
||||
parent_uri: string = "";
|
||||
plugin_type: PluginType = PluginType.Plugin;
|
||||
children: PluginClass[] = [];
|
||||
}
|
||||
|
||||
export default PluginClass;
|
||||
@@ -0,0 +1,918 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { UiControl, ScalePoint } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Input from '@mui/material/Input';
|
||||
import Select from '@mui/material/Select';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Utility, { nullCast } from './Utility';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import DialIcon from './svg/fx_dial.svg?react';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import ControlTooltip from './ControlTooltip';
|
||||
|
||||
const MIN_ANGLE = -135;
|
||||
const MAX_ANGLE = 135;
|
||||
const FONT_SIZE = "0.8em";
|
||||
|
||||
|
||||
|
||||
const SELECTED_OPACITY = 0.8;
|
||||
const DEFAULT_OPACITY = 0.6;
|
||||
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.
|
||||
const ULTRA_FINE_RANGE_SCALE = RANGE_SCALE * 50; // 12000 pixels to move from 0 to 1.
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 140 }
|
||||
|
||||
|
||||
|
||||
export const pluginControlStyles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
}),
|
||||
switchTrack: css({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: 14 / 2,
|
||||
zIndex: -1,
|
||||
transition: theme.transitions.create(['opacity', 'background-color'], {
|
||||
duration: theme.transitions.duration.shortest
|
||||
}),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
|
||||
}),
|
||||
controlFrame: css({
|
||||
display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-between", height: 116
|
||||
}),
|
||||
|
||||
titleSection: css({
|
||||
flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 0, marginRight: 0
|
||||
}),
|
||||
displayValue: css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: theme.mainBackground,
|
||||
color: theme.palette.text.secondary,
|
||||
// zIndex: -1,
|
||||
}),
|
||||
midSection: css({
|
||||
flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center"
|
||||
}),
|
||||
editSection: css({
|
||||
flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center", position: "relative", width: 60, height: 28, minHeight: 28
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
export interface PluginControlProps extends WithStyles<typeof pluginControlStyles> {
|
||||
uiControl?: UiControl;
|
||||
instanceId: number;
|
||||
value: number;
|
||||
onPreviewChange?: (value: number) => void;
|
||||
onChange: (value: number) => void;
|
||||
theme: Theme;
|
||||
requestIMEEdit: (uiControl: UiControl, value: number) => void;
|
||||
}
|
||||
type PluginControlState = {
|
||||
error: boolean;
|
||||
};
|
||||
|
||||
const PluginControl =
|
||||
withTheme(withStyles(
|
||||
class extends Component<PluginControlProps, PluginControlState> {
|
||||
|
||||
frameRef: React.RefObject<HTMLDivElement|null>;
|
||||
imgRef: React.RefObject<SVGSVGElement|null>;
|
||||
inputRef: React.RefObject<HTMLInputElement|null>;
|
||||
selectRef: React.RefObject<HTMLSelectElement|null>;
|
||||
|
||||
displayValueRef: React.RefObject<HTMLDivElement|null>;
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
constructor(props: PluginControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.imgRef = React.createRef();
|
||||
this.inputRef = React.createRef();
|
||||
this.selectRef = React.createRef();
|
||||
this.displayValueRef = React.createRef();
|
||||
this.frameRef = React.createRef();
|
||||
|
||||
|
||||
this.onPointerDown = this.onPointerDown.bind(this);
|
||||
this.onPointerUp = this.onPointerUp.bind(this);
|
||||
this.onPointerMove = this.onPointerMove.bind(this);
|
||||
this.onPointerLostCapture = this.onPointerLostCapture.bind(this);
|
||||
this.onBodyPointerDownCapture = this.onBodyPointerDownCapture.bind(this);
|
||||
|
||||
this.onTouchStart = this.onTouchStart.bind(this);
|
||||
this.onTouchMove = this.onTouchMove.bind(this);
|
||||
//this.onTouchCancel = this.onTouchCancel.bind(this);
|
||||
|
||||
this.onSelectChanged = this.onSelectChanged.bind(this);
|
||||
this.onInputChange = this.onInputChange.bind(this);
|
||||
this.onInputLostFocus = this.onInputLostFocus.bind(this);
|
||||
this.onInputFocus = this.onInputFocus.bind(this);
|
||||
this.onInputKeyPress = this.onInputKeyPress.bind(this);
|
||||
}
|
||||
isTouchDevice(): boolean {
|
||||
return Utility.needsZoomedControls();
|
||||
}
|
||||
|
||||
showZoomedControl() {
|
||||
if (this.props.uiControl && this.frameRef.current) {
|
||||
this.model.zoomUiControl(this.frameRef.current, this.props.instanceId, this.props.uiControl);
|
||||
}
|
||||
}
|
||||
hideZoomedControl() {
|
||||
if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current) {
|
||||
this.model.clearZoomedControl();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.hideZoomedControl();
|
||||
}
|
||||
inputChanged: boolean = false;
|
||||
|
||||
onInputLostFocus(event: any): void {
|
||||
if (this.inputChanged) // validation requried?
|
||||
{
|
||||
this.inputChanged = false;
|
||||
this.validateInput(event, true);
|
||||
|
||||
}
|
||||
this.displayValueRef.current!.style.display = "block";
|
||||
}
|
||||
onInputFocus(event: SyntheticEvent): void {
|
||||
this.displayValueRef.current!.style.display = "none";
|
||||
if (Utility.hasIMEKeyboard()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.inputRef.current?.blur();
|
||||
this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
|
||||
}
|
||||
}
|
||||
onInputKeyPress(e: any): void {
|
||||
if (e.charCode === 13 && this.inputChanged) {
|
||||
this.inputChanged = false;
|
||||
this.validateInput(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
onInputChange(event: any): void {
|
||||
this.inputChanged = true;
|
||||
this.validateInput(event, false);
|
||||
}
|
||||
validateInput(event: any, commitValue: boolean) {
|
||||
if (!this.inputRef.current) return;
|
||||
|
||||
let text = this.inputRef.current.value;
|
||||
let valid = false;
|
||||
let result: number = this.currentValue;
|
||||
try {
|
||||
if (text.length === 0) {
|
||||
valid = false;
|
||||
} else {
|
||||
let v = Number(text);
|
||||
if (isNaN(v)) {
|
||||
valid = false;
|
||||
} else {
|
||||
valid = true;
|
||||
result = v;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (commitValue) {
|
||||
this.setState({ error: false });
|
||||
if (!valid) {
|
||||
result = this.currentValue; // reset the value!
|
||||
}
|
||||
// clamp and quantize.
|
||||
let range = this.valueToRange(result);
|
||||
result = this.rangeToValue(range);
|
||||
let displayVal = this.props.uiControl?.formatShortValue(result) ?? "";
|
||||
if (event.currentTarget) {
|
||||
event.currentTarget.value = displayVal;
|
||||
}
|
||||
this.previewInputValue(result, true);
|
||||
this.inputRef.current.value = displayVal; // no rerender because the value won't change.
|
||||
} else {
|
||||
this.setState({ error: !valid });
|
||||
if (valid) {
|
||||
this.previewInputValue(result, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startX: number = 0;
|
||||
startY: number = 0;
|
||||
mouseDown: boolean = false;
|
||||
|
||||
onDrag(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
isValidPointer(e: PointerEvent<SVGSVGElement>): boolean {
|
||||
if (e.pointerType === "mouse") {
|
||||
return e.button === 0;
|
||||
} else if (e.pointerType === "pen") {
|
||||
return true;
|
||||
} else if (e.pointerType === "touch") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
touchDown: boolean = false;
|
||||
touchIdentifier: number = -1;
|
||||
|
||||
private isTap: boolean = false;
|
||||
|
||||
onTouchStart(e: TouchEvent<SVGSVGElement>) {
|
||||
}
|
||||
onTouchMove(e: TouchEvent<SVGSVGElement>) {
|
||||
// e.preventDefault();
|
||||
e.stopPropagation(); // cancels scroll!!!
|
||||
|
||||
}
|
||||
|
||||
capturedPointers: number[] = [];
|
||||
|
||||
onBodyPointerDownCapture(e_: any): any {
|
||||
let e = e_ as PointerEvent;
|
||||
if (this.isExtraTouch(e)) {
|
||||
this.isTap = false;
|
||||
this.captureElement!.setPointerCapture(e.pointerId);
|
||||
this.capturedPointers.push(e.pointerId);
|
||||
++this.pointersDown;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pointerId: number = 0;
|
||||
pointerType: string = "";
|
||||
|
||||
isCapturedPointer(e: PointerEvent<SVGSVGElement>): boolean {
|
||||
return this.mouseDown
|
||||
&& e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
|
||||
}
|
||||
|
||||
lastX: number = 0;
|
||||
lastY: number = 0;
|
||||
dRange: number = 0;
|
||||
|
||||
pointersDown: number = 0;
|
||||
captureElement?: SVGSVGElement = undefined;
|
||||
|
||||
onPointerDown(e: PointerEvent<SVGSVGElement>): void {
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.isTouchDevice()) {
|
||||
if (this.props.uiControl?.isDial() ?? false) {
|
||||
this.isTap = false;
|
||||
this.showZoomedControl();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
++this.pointersDown;
|
||||
|
||||
this.mouseDown = true;
|
||||
if (this.pointersDown === 1) {
|
||||
this.isTap = true;
|
||||
this.tapStartMs = Date.now();
|
||||
} else {
|
||||
this.isTap = false;
|
||||
}
|
||||
|
||||
this.pointerId = e.pointerId;
|
||||
this.pointerType = e.pointerType;
|
||||
this.startX = e.clientX;
|
||||
this.startY = e.clientY;
|
||||
this.lastX = e.clientX;
|
||||
this.lastY = e.clientY;
|
||||
this.dRange = 0;
|
||||
|
||||
let img = this.imgRef.current;
|
||||
|
||||
if (img) {
|
||||
this.captureElement = img;
|
||||
document.body.addEventListener(
|
||||
"pointerdown",
|
||||
this.onBodyPointerDownCapture, true
|
||||
);
|
||||
|
||||
img.setPointerCapture(e.pointerId);
|
||||
if (img.style) {
|
||||
img.style.opacity = "" + SELECTED_OPACITY;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this.isExtraTouch(e)) {
|
||||
++this.pointersDown;
|
||||
this.isTap = false;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
isExtraTouch(e: PointerEvent): boolean {
|
||||
return (this.mouseDown && this.pointerType === "touch" && e.pointerType === "touch" && e.pointerId !== this.pointerId);
|
||||
|
||||
}
|
||||
|
||||
onPointerLostCapture(e: PointerEvent<SVGSVGElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
--this.pointersDown;
|
||||
this.isTap = false;
|
||||
|
||||
|
||||
this.releaseCapture(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
updateRange(e: PointerEvent<SVGSVGElement>): number {
|
||||
|
||||
let ultraHigh = false;
|
||||
let high = false;
|
||||
if (e.ctrlKey) {
|
||||
ultraHigh = true;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
high = true;
|
||||
}
|
||||
if (e.pointerType === "touch") {
|
||||
if (this.pointersDown >= 3) {
|
||||
ultraHigh = true;
|
||||
} else if (this.pointersDown === 2) {
|
||||
high = true;
|
||||
}
|
||||
}
|
||||
if (ultraHigh) {
|
||||
this.dRange += (this.lastY - e.clientY) / ULTRA_FINE_RANGE_SCALE;
|
||||
|
||||
} else if (high) {
|
||||
this.dRange += (this.lastY - e.clientY) / FINE_RANGE_SCALE;
|
||||
|
||||
} else {
|
||||
this.dRange += (this.lastY - e.clientY) / RANGE_SCALE;
|
||||
}
|
||||
this.lastY = e.clientY;
|
||||
this.lastX = e.clientX;
|
||||
return this.dRange;
|
||||
}
|
||||
private lastTapMs = 0;
|
||||
|
||||
resetToDefaultValue(uiControl: UiControl): void {
|
||||
let value = uiControl.default_value;
|
||||
this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, value);
|
||||
}
|
||||
onPointerDoubleTap() {
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
if (uiControl.isDial()) {
|
||||
this.resetToDefaultValue(uiControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
onPointerTap() {
|
||||
let tapTime = Date.now();
|
||||
let dT = tapTime - this.lastTapMs;
|
||||
this.lastTapMs = tapTime;
|
||||
|
||||
if (dT < 500) {
|
||||
this.onPointerDoubleTap();
|
||||
}
|
||||
}
|
||||
private tapStartMs: number = 0;
|
||||
|
||||
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
||||
|
||||
if (this.isCapturedPointer(e)) {
|
||||
--this.pointersDown;
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
let dRange = this.updateRange(e)
|
||||
this.previewRange(dRange, true);
|
||||
|
||||
this.releaseCapture(e);
|
||||
if (this.isTap) {
|
||||
let ms = Date.now() - this.tapStartMs;
|
||||
if (ms < 200) {
|
||||
this.onPointerTap();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
--this.pointersDown;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
releaseCapture(e: PointerEvent<SVGSVGElement>) {
|
||||
let img = this.imgRef.current;
|
||||
|
||||
if (img && img.style) {
|
||||
img.releasePointerCapture(e.pointerId);
|
||||
img.style.opacity = "" + DEFAULT_OPACITY;
|
||||
// they get automaticlly released.
|
||||
// for (let i = 0; i < this.capturedPointers.length; ++i)
|
||||
// {
|
||||
// img.releasePointerCapture (this.capturedPointers[i]);
|
||||
// }
|
||||
this.capturedPointers = [];
|
||||
}
|
||||
document.body.removeEventListener(
|
||||
"pointerdown",
|
||||
this.onBodyPointerDownCapture, true
|
||||
);
|
||||
|
||||
this.mouseDown = false;
|
||||
|
||||
}
|
||||
|
||||
clickSlop() {
|
||||
return 3.5; // maybe larger on touch devices.
|
||||
}
|
||||
onPointerMove(e: PointerEvent<SVGSVGElement>): void {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
e.preventDefault();
|
||||
let dRange = this.updateRange(e)
|
||||
this.previewRange(dRange, false);
|
||||
|
||||
let x = e.clientX;
|
||||
let y = e.clientY;
|
||||
let dx = x - this.startX;
|
||||
let dy = y - this.startY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance >= this.clickSlop()) {
|
||||
this.isTap = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
handleTriggerMouseDown() {
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
let value = uiControl.max_value;
|
||||
if (uiControl.max_value === uiControl.default_value) {
|
||||
value = uiControl.min_value;
|
||||
}
|
||||
this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value);
|
||||
}
|
||||
}
|
||||
handleTriggerMouseUp() {
|
||||
// triggers are reset on the audio thread.
|
||||
}
|
||||
previewInputValue(value: number, commitValue: boolean) {
|
||||
let range = this.valueToRange(value);
|
||||
value = this.rangeToValue(range);
|
||||
|
||||
let transform = this.rangeToRotationTransform(range);
|
||||
if (this.mouseDown && !commitValue) {
|
||||
transform += " scale(1.5, 1.5)";
|
||||
}
|
||||
let imgElement = this.imgRef.current
|
||||
if (imgElement) {
|
||||
if (imgElement.style) {
|
||||
imgElement.style.transform = transform;
|
||||
}
|
||||
}
|
||||
if (commitValue) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
if (this.props.onPreviewChange) {
|
||||
this.props.onPreviewChange(value);
|
||||
}
|
||||
|
||||
}
|
||||
previewRange(dRange: number, commitValue: boolean): void {
|
||||
let range = this.valueToRange(this.currentValue) + dRange;
|
||||
if (range > 1) range = 1;
|
||||
if (range < 0) range = 0;
|
||||
let value = this.rangeToValue(range);
|
||||
|
||||
// apply value quantization and clipping.
|
||||
range = this.valueToRange(value);
|
||||
|
||||
let imgElement = this.imgRef.current
|
||||
if (imgElement) {
|
||||
if (imgElement.style) {
|
||||
imgElement.style.transform = this.rangeToRotationTransform(range);
|
||||
}
|
||||
}
|
||||
let inputElement = this.inputRef.current;
|
||||
if (inputElement) {
|
||||
let v = this.props.uiControl?.formatShortValue(value) ?? "";
|
||||
inputElement.value = v;
|
||||
}
|
||||
let displayValue = this.displayValueRef.current;
|
||||
if (displayValue) {
|
||||
let v = this.formatDisplayValue(this.props.uiControl, value);
|
||||
displayValue.childNodes[0].textContent = v;
|
||||
}
|
||||
let selectElement = this.selectRef.current;
|
||||
if (selectElement) {
|
||||
//let v = this.formatValue(this.props.uiControl,value);
|
||||
selectElement.selectedIndex = value;
|
||||
|
||||
}
|
||||
if (commitValue) {
|
||||
this.currentValue = value;
|
||||
this.props.onChange(value);
|
||||
} else {
|
||||
if (this.props.onPreviewChange) {
|
||||
this.props.onPreviewChange(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCheckChanged(checked: boolean): void {
|
||||
this.props.onChange(checked ? 1 : 0);
|
||||
}
|
||||
onSelectChanged(e: any, value: any) {
|
||||
let target = e.target;
|
||||
setTimeout(() => {
|
||||
this.props.onChange(target.value);
|
||||
}, 0);
|
||||
|
||||
|
||||
}
|
||||
|
||||
makeSelect(control: UiControl, value: number): ReactNode {
|
||||
if (control.isOnOffSwitch()) {
|
||||
// normal gray unchecked state.
|
||||
return (
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (control.isAbToggle()) {
|
||||
let classes = withStyles.getClasses(this.props);
|
||||
// unchecked color is not gray.
|
||||
return (
|
||||
<Switch checked={value !== 0} color="primary"
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
classes={{
|
||||
track: classes.switchTrack
|
||||
}}
|
||||
style={{ color: this.props.theme.palette.primary.main }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Select variant="standard"
|
||||
ref={this.selectRef}
|
||||
value={control.clampSelectValue(value)}
|
||||
onChange={this.onSelectChanged}
|
||||
inputProps={{
|
||||
name: control.name,
|
||||
id: 'id' + control.symbol,
|
||||
style: { fontSize: FONT_SIZE }
|
||||
}}
|
||||
style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 14 }}
|
||||
>
|
||||
{control.scale_points.map((scale_point: ScalePoint) => (
|
||||
<MenuItem key={scale_point.value} value={scale_point.value}>{scale_point.label}</MenuItem>
|
||||
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
formatDisplayValue(uiControl: UiControl | undefined, value: number): string {
|
||||
if (!uiControl) return "";
|
||||
|
||||
return uiControl.formatDisplayValue(value);
|
||||
|
||||
|
||||
}
|
||||
|
||||
valueToRange(value: number): number {
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
if (uiControl.integer_property) {
|
||||
value = Math.round(value);
|
||||
}
|
||||
let range: number;
|
||||
if (uiControl.is_logarithmic) {
|
||||
let minValue = uiControl.min_value;
|
||||
if (minValue === 0) // LSP plugins do this.
|
||||
{
|
||||
minValue = 0.0001;
|
||||
}
|
||||
range = Math.log(value / minValue) / Math.log(uiControl.max_value / minValue);
|
||||
if (!isFinite(range)) {
|
||||
if (range < 0)
|
||||
{
|
||||
range = 0;
|
||||
} else {
|
||||
range = 1.0;
|
||||
}
|
||||
} else if (isNaN(range)) {
|
||||
range = 0;
|
||||
}
|
||||
if (uiControl.range_steps > 1) {
|
||||
range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1);
|
||||
}
|
||||
} else {
|
||||
range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value);
|
||||
if (!isFinite(range)) {
|
||||
if (range < 0)
|
||||
{
|
||||
range = 0;
|
||||
} else {
|
||||
range = 1.0;
|
||||
}
|
||||
} else if (isNaN(range)) {
|
||||
range = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (range > 1) range = 1;
|
||||
if (range < 0) range = 0;
|
||||
return range;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
rangeToValue(range: number): number {
|
||||
if (range < 0) range = 0;
|
||||
if (range > 1) range = 1;
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
if (uiControl.range_steps > 1) {
|
||||
range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1);
|
||||
}
|
||||
let value: number;
|
||||
if (uiControl.min_value === uiControl.max_value) {
|
||||
value = uiControl.min_value;
|
||||
} else {
|
||||
if (uiControl.is_logarithmic) {
|
||||
let minValue = uiControl.min_value;
|
||||
if (minValue === 0) // LSP controls.
|
||||
{
|
||||
minValue = 0.0001;
|
||||
}
|
||||
value = minValue * Math.pow(uiControl.max_value / minValue, range);
|
||||
if (!isFinite(value)) {
|
||||
value = uiControl.max_value;
|
||||
} else if (isNaN(value)) {
|
||||
value = uiControl.min_value;
|
||||
}
|
||||
} else {
|
||||
value = range * (uiControl.max_value - uiControl.min_value) + uiControl.min_value;
|
||||
}
|
||||
if (uiControl.integer_property) {
|
||||
value = Math.round(value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
rangeToRotationTransform(range: number): string {
|
||||
let angle = range * (MAX_ANGLE - MIN_ANGLE) + MIN_ANGLE;
|
||||
return "rotate(" + angle + "deg)";
|
||||
}
|
||||
|
||||
getRotationTransform(): string {
|
||||
let range = 0;
|
||||
let uiControl = this.props.uiControl;
|
||||
if (uiControl) {
|
||||
let value = this.props.value;
|
||||
range = this.valueToRange(value);
|
||||
}
|
||||
return this.rangeToRotationTransform(range);
|
||||
}
|
||||
|
||||
currentValue: number = 0;
|
||||
uiControl?: UiControl = undefined;
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let t = this.props.uiControl;
|
||||
if (!t) {
|
||||
return (<div>#Error</div>);
|
||||
|
||||
}
|
||||
let dialColor = this.props.theme.palette.text.primary;
|
||||
let control: UiControl = t;
|
||||
this.uiControl = control;
|
||||
let value = this.props.value;
|
||||
this.currentValue = value;
|
||||
let switchText = "";
|
||||
|
||||
let isSelect = control.isSelect();
|
||||
let isAbSwitch = control.isAbToggle();
|
||||
let isOnOffSwitch = control.isOnOffSwitch();
|
||||
let isTrigger = control.isTrigger();
|
||||
|
||||
if (isAbSwitch) {
|
||||
switchText = control.scale_points[0].value === value ? control.scale_points[0].label : control.scale_points[1].label;
|
||||
}
|
||||
|
||||
let item_width: number | undefined = isSelect ? 160 : 80;
|
||||
if (isTrigger) {
|
||||
item_width = undefined;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div ref={this.frameRef}
|
||||
className={classes.controlFrame}
|
||||
style={{ width: item_width }}
|
||||
>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={
|
||||
{
|
||||
alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
|
||||
|
||||
}}>
|
||||
<ControlTooltip uiControl={control} >
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: isSelect ? "left" : "center"
|
||||
}}> {isTrigger ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}>
|
||||
|
||||
{isTrigger ?
|
||||
(
|
||||
control.name.length !== 1 ? (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleTriggerMouseDown(); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleTriggerMouseUp(); }
|
||||
}
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8, minWidth: 60,
|
||||
marginTop: 0
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
{control.name}
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Button variant="contained" color="primary" size="small"
|
||||
onMouseDown={
|
||||
(evt) => { this.handleTriggerMouseDown(); }
|
||||
}
|
||||
onMouseUp={
|
||||
(evt) => { this.handleTriggerMouseUp(); }
|
||||
}
|
||||
style={{
|
||||
textTransform: "none",
|
||||
background: (isDarkMode() ? "#6750A4" : undefined),
|
||||
marginLeft: 8, marginRight: 8,
|
||||
paddingLeft: 0, paddingRight: 0,
|
||||
width: 36, height: 36,
|
||||
marginTop: 0,
|
||||
borderRadius: 8,
|
||||
minWidth: 0,
|
||||
fontSize: "1.2em"
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
{control.name}
|
||||
</Button>
|
||||
)
|
||||
)
|
||||
|
||||
: ((isSelect || isAbSwitch || isOnOffSwitch) ? (
|
||||
this.makeSelect(control, value)
|
||||
) : (
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<DialIcon ref={this.imgRef}
|
||||
style={{
|
||||
overscrollBehavior: "none", touchAction: "none", fill: dialColor,
|
||||
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform()
|
||||
}}
|
||||
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
|
||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*/}
|
||||
<div className={classes.editSection} >
|
||||
{(!(isSelect || isOnOffSwitch || isTrigger)) &&
|
||||
(
|
||||
(isAbSwitch) ? (
|
||||
<Typography variant="caption" display="block" textAlign="center" noWrap style={{
|
||||
width: "100%"
|
||||
}}> {switchText} </Typography>
|
||||
) : (
|
||||
<div>
|
||||
<Input key={value}
|
||||
type="number"
|
||||
defaultValue={control.formatShortValue(value)}
|
||||
error={this.state.error}
|
||||
inputProps={{
|
||||
'aria-label':
|
||||
control.symbol + " value",
|
||||
style: { textAlign: "center", fontSize: FONT_SIZE },
|
||||
}}
|
||||
inputRef={this.inputRef} onChange={this.onInputChange}
|
||||
onBlur={this.onInputLostFocus}
|
||||
onFocus={this.onInputFocus}
|
||||
|
||||
onKeyPress={this.onInputKeyPress} />
|
||||
<div className={classes.displayValue} ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }} >
|
||||
<Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop: 4, paddingBottom: 6 }}
|
||||
|
||||
>
|
||||
{this.formatDisplayValue(control, value)}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
/*
|
||||
isSamePointer(PointerEvent<SVGSVGElement> e): boolean
|
||||
{
|
||||
return e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
}
|
||||
*/
|
||||
touchPointerId: number = 0;
|
||||
|
||||
|
||||
},
|
||||
pluginControlStyles
|
||||
));
|
||||
|
||||
export default PluginControl;
|
||||
@@ -0,0 +1,818 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
|
||||
import {
|
||||
Pedalboard, PedalboardItem, ControlValue
|
||||
} from './Pedalboard';
|
||||
import PluginControl from './PluginControl';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import VuMeter from './VuMeter';
|
||||
import { nullCast } from './Utility'
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import FullScreenIME from './FullScreenIME';
|
||||
import FilePropertyControl from './FilePropertyControl';
|
||||
import FilePropertyDialog from './FilePropertyDialog';
|
||||
import JsonAtom from './JsonAtom';
|
||||
import PluginOutputControl from './PluginOutputControl';
|
||||
import Units from './Units';
|
||||
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import MidiChannelBindingControl from './MidiChannelBindingControl';
|
||||
import MidiChannelBinding from './MidiChannelBinding';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 110 };
|
||||
|
||||
|
||||
|
||||
const LANDSCAPE_HEIGHT_BREAK = 500;
|
||||
|
||||
|
||||
function makeIoPluginInfo(name: string, uri: string): UiPlugin {
|
||||
let result = new UiPlugin();
|
||||
result.name = name;
|
||||
result.uri = uri;
|
||||
let volumeControl = new UiControl();
|
||||
volumeControl.name = "Volume";
|
||||
volumeControl.symbol = "volume_db";
|
||||
volumeControl.index = 0;
|
||||
volumeControl.is_input = true;
|
||||
|
||||
volumeControl.min_value = -60;
|
||||
volumeControl.max_value = 30;
|
||||
volumeControl.default_value = 0;
|
||||
volumeControl.units = Units.db;
|
||||
volumeControl.scale_points = [
|
||||
new ScalePoint().deserialize({ label: "-INF", value: -60 })
|
||||
];
|
||||
result.controls = [
|
||||
volumeControl
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
let startPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Input", Pedalboard.START_PEDALBOARD_ITEM_URI);
|
||||
|
||||
let endPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Output", Pedalboard.END_PEDALBOARD_ITEM_URI);
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
display: "block",
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "0px",
|
||||
height: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden"
|
||||
}),
|
||||
frameScrollLandscape: css({
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: 0, top: 0, right: 0, bottom:0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden"
|
||||
}),
|
||||
frameScrollPortrait: css({
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
left: 0, top: 0, right: 0, bottom:0,
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto"
|
||||
}),
|
||||
vuMeterL: css({
|
||||
position: "absolute",
|
||||
left: 0, top: 0,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 4,
|
||||
paddingBottom: 12, // cover bottom line of a portgroup in landscape.
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
vuMeterR: css({
|
||||
position: "absolute",
|
||||
right: 0, top: 0,
|
||||
marginRight: 20, // has to potentially clear a scrollbar.
|
||||
paddingLeft: 4,
|
||||
paddingBottom: 12,
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
vuMeterRLandscape: css({
|
||||
position: "absolute",
|
||||
right: 0, top: 0,
|
||||
paddingRight: 6,
|
||||
paddingLeft: 12,
|
||||
paddingBottom: 12, // cover bottom line of a portgroup in landscape.
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
|
||||
normalGrid: css({
|
||||
position: "relative",
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
paddingTop: 8,
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start",
|
||||
rowGap: 14,
|
||||
height: "fit-content"
|
||||
|
||||
|
||||
|
||||
}),
|
||||
landscapeGrid: css({
|
||||
paddingLeft: 40, paddingRight: 40, paddingTop: 8,
|
||||
// marginRight: 40, : bug in chrome layout engine wrt/ right margin/padding.
|
||||
// See the spacer div added after all controls in render() with provides the same effect.
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", alignItems: "flex-start",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
flex: "0 0 auto",
|
||||
width: "fit-content"
|
||||
}),
|
||||
portgroupControlPadding: css({
|
||||
flex: "0 0 auto",
|
||||
marginTop: 12,
|
||||
marginBottom: 8
|
||||
|
||||
}),
|
||||
|
||||
controlPadding: css({
|
||||
flex: "0 0 auto",
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
height: 116
|
||||
|
||||
}),
|
||||
controlPair: css({
|
||||
display: "flex", flexFlow: "row nowrap",
|
||||
flex: "0 0 auto",
|
||||
height: 116
|
||||
}),
|
||||
controlSpacer: css({
|
||||
display: "none",
|
||||
minWidth: 0,
|
||||
width: 0,
|
||||
height: 116
|
||||
}),
|
||||
|
||||
portGroup: css({
|
||||
marginLeft: 8,
|
||||
marginTop: 0,
|
||||
marginRight: 8,
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
paddingLeft: 3,
|
||||
paddingRight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
borderRadius: 8,
|
||||
elevation: 12,
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
flex: "0 1 auto",
|
||||
}),
|
||||
portGroupLandscape: css({
|
||||
marginLeft: 8,
|
||||
marginTop: 0,
|
||||
marginRight: 8,
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
borderRadius: 8,
|
||||
elevation: 12,
|
||||
display: "inline-flex",
|
||||
textOverflow: "ellipsis",
|
||||
flexDirection: "row", flexWrap: "nowrap",
|
||||
flex: "0 0 auto",
|
||||
width: "fit-content",
|
||||
minWidth: "max-content"
|
||||
}),
|
||||
portGroupTitle: css({
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
background: theme.mainBackground,
|
||||
textOverflow: "ellipsis",
|
||||
minWidth: 0,
|
||||
marginLeft: 20,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
margin_right: 28
|
||||
}),
|
||||
portGroupControls: css({
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
paddingTop: 6,
|
||||
paddingBottom: 8
|
||||
}),
|
||||
portGroupControlsLandscape: css({
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
width: "fit-content",
|
||||
paddingTop: 6,
|
||||
paddingBottom: 8
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
export class ControlGroup {
|
||||
constructor(name: string, indexes: number[], controls: ReactNode[]) {
|
||||
this.name = name;
|
||||
this.indexes = indexes;
|
||||
this.controls = controls;
|
||||
}
|
||||
name: string;
|
||||
indexes: number[];
|
||||
controls: React.ReactNode[];
|
||||
}
|
||||
|
||||
export type ControlNodes = (ReactNode | ControlGroup)[];
|
||||
|
||||
|
||||
export interface ControlViewCustomization {
|
||||
ModifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[];
|
||||
|
||||
}
|
||||
|
||||
export interface PluginControlViewProps extends WithStyles<typeof styles> {
|
||||
theme: Theme;
|
||||
instanceId: number;
|
||||
item: PedalboardItem;
|
||||
customization?: ControlViewCustomization;
|
||||
customizationId?: number;
|
||||
}
|
||||
type PluginControlViewState = {
|
||||
landscapeGrid: boolean;
|
||||
imeUiControl?: UiControl;
|
||||
imeValue: number;
|
||||
imeCaption: string;
|
||||
imeInitialHeight: number;
|
||||
showFileDialog: boolean,
|
||||
dialogFileProperty: UiFileProperty,
|
||||
dialogFileValue: string
|
||||
};
|
||||
|
||||
const PluginControlView =
|
||||
withTheme(withStyles(
|
||||
class extends ResizeResponsiveComponent<PluginControlViewProps, PluginControlViewState> {
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PluginControlViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
landscapeGrid: false,
|
||||
imeUiControl: undefined,
|
||||
imeValue: 0,
|
||||
imeCaption: "",
|
||||
imeInitialHeight: 0,
|
||||
showFileDialog: false,
|
||||
dialogFileProperty: new UiFileProperty(),
|
||||
dialogFileValue: ""
|
||||
|
||||
}
|
||||
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
||||
this.onControlValueChanged = this.onControlValueChanged.bind(this);
|
||||
this.onPreviewChange = this.onPreviewChange.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onPreviewChange(key: string, value: number): void {
|
||||
this.model.previewPedalboardValue(this.props.instanceId, key, value);
|
||||
}
|
||||
|
||||
onControlValueChanged(key: string, value: number): void {
|
||||
this.model.setPedalboardControl(this.props.instanceId, key, value);
|
||||
}
|
||||
|
||||
onPedalboardChanged(value?: Pedalboard) {
|
||||
//let item = this.model.pedalboard.get().maybeGetItem(this.props.instanceId);
|
||||
//this.setState({ pedalboardItem: item });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ landscapeGrid: height < LANDSCAPE_HEIGHT_BREAK });
|
||||
}
|
||||
filterNotOnGui(controlValues: ControlValue[], uiPlugin: UiPlugin): ControlValue[] {
|
||||
let result: ControlValue[] = [];
|
||||
|
||||
for (let i = 0; i < controlValues.length; ++i) {
|
||||
let controlValue = controlValues[i];
|
||||
let control = uiPlugin.getControl(controlValue.key);
|
||||
if (control && !control.isHidden()) {
|
||||
result.push(controlValue);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
requestImeEdit(uiControl: UiControl, value: number) {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
||||
this.setState({
|
||||
imeUiControl: uiControl,
|
||||
imeValue: value,
|
||||
imeCaption: uiControl.name,
|
||||
imeInitialHeight: window.innerHeight
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
makeFilePropertyUI(fileProperty: UiFileProperty): ReactNode {
|
||||
return ((
|
||||
|
||||
<FilePropertyControl pedalboardItem={this.props.item}
|
||||
fileProperty={fileProperty}
|
||||
onFileClick={(fileProperty, selectedFile) => {
|
||||
this.setState({ showFileDialog: true, dialogFileProperty: fileProperty, dialogFileValue: selectedFile });
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
makeFrequencyPlotUI(frequencyPlot: UiFrequencyPlot): ReactNode {
|
||||
return ((
|
||||
<ToobFrequencyResponseView instanceId={this.props.instanceId}
|
||||
propertyName={frequencyPlot.patchProperty}
|
||||
width={frequencyPlot.width}
|
||||
/>
|
||||
));
|
||||
}
|
||||
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
||||
let symbol = uiControl.symbol;
|
||||
if (!uiControl.is_input) {
|
||||
return (
|
||||
<PluginOutputControl instanceId={this.props.instanceId} uiControl={uiControl} />
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
let controlValue: ControlValue | undefined = undefined;
|
||||
for (let i = 0; i < controlValues.length; ++i) {
|
||||
if (controlValues[i].key === symbol) {
|
||||
controlValue = controlValues[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!controlValue) {
|
||||
throw new PiPedalStateError("Missing control value.");
|
||||
}
|
||||
return ((
|
||||
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
|
||||
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
||||
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
|
||||
|
||||
/>
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
push_control(controls:ControlNodes, pluginControl: UiControl,controlValues: ControlValue[])
|
||||
{
|
||||
// combine lamps with their previous control
|
||||
if ((pluginControl.isLamp() || pluginControl.isDbVu()) && controls.length !== 0)
|
||||
{
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let newControl = this.makeStandardControl(pluginControl,controlValues);
|
||||
let previousControl = controls[controls.length-1];
|
||||
if (!(previousControl instanceof ControlGroup)) {
|
||||
let pair = (
|
||||
<div className={classes.controlPair}>
|
||||
{previousControl as ReactNode}
|
||||
{newControl}
|
||||
</div>
|
||||
);
|
||||
controls[controls.length-1] = pair;
|
||||
// push a spacer control in order to make placing of extended controls predictable.
|
||||
// (e.g.. inserting at position 4 still places the extended control after four previous controls
|
||||
controls.push((
|
||||
|
||||
<div className={classes.controlSpacer} />
|
||||
));
|
||||
} else {
|
||||
controls.push(newControl);
|
||||
}
|
||||
|
||||
} else {
|
||||
controls.push(
|
||||
this.makeStandardControl(pluginControl, controlValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getStandardControlNodes(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes {
|
||||
let result: ControlNodes = [];
|
||||
let portGroupMap: { [id: string]: ControlGroup } = {};
|
||||
|
||||
for (let i = 0; i < plugin.controls.length; ++i) {
|
||||
let pluginControl = plugin.controls[i];
|
||||
if (!pluginControl.isHidden()) {
|
||||
if (pluginControl.port_group !== "" && plugin.getPortGroupBySymbol(pluginControl.port_group)) {
|
||||
let portGroup = nullCast(plugin.getPortGroupBySymbol(pluginControl.port_group));
|
||||
|
||||
let groupControls: ReactNode[] = [];
|
||||
let indexes: number[] = [];
|
||||
groupControls.push(
|
||||
this.makeStandardControl(pluginControl, controlValues)
|
||||
);
|
||||
indexes.push(pluginControl.index);
|
||||
while (i + 1 < plugin.controls.length && plugin.controls[i + 1].port_group === pluginControl.port_group) {
|
||||
++i;
|
||||
pluginControl = plugin.controls[i];
|
||||
|
||||
if (!pluginControl.isHidden()) {
|
||||
this.push_control(groupControls,pluginControl,controlValues);
|
||||
indexes.push(pluginControl.index);
|
||||
}
|
||||
}
|
||||
let controlGroup = new ControlGroup(portGroup.name, indexes, groupControls);
|
||||
result.push(
|
||||
controlGroup
|
||||
)
|
||||
portGroupMap[pluginControl.port_group] = controlGroup;
|
||||
} else {
|
||||
this.push_control(result,pluginControl,controlValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < plugin.fileProperties.length; ++i) {
|
||||
let fileProperty = plugin.fileProperties[i];
|
||||
let filePropertyUi = this.makeFilePropertyUI(fileProperty);
|
||||
|
||||
if (fileProperty.portGroup !== "" && plugin.getPortGroupByUri(fileProperty.portGroup)) {
|
||||
let portGroup = nullCast(plugin.getPortGroupByUri(fileProperty.portGroup));
|
||||
let controlGroup = portGroupMap[portGroup.symbol];
|
||||
if (controlGroup) {
|
||||
let insertPosition = controlGroup.indexes.length;
|
||||
if (fileProperty.index !== -1) {
|
||||
for (let i = 0; i < controlGroup.controls.length; ++i) {
|
||||
if (controlGroup.indexes[i] >= fileProperty.index) {
|
||||
insertPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let index = fileProperty.index !== -1 ? fileProperty.index : 100;
|
||||
controlGroup.controls.splice(insertPosition, 0, filePropertyUi);
|
||||
controlGroup.indexes.splice(insertPosition, 0, index);
|
||||
|
||||
} else {
|
||||
let index = fileProperty.index !== -1 ? fileProperty.index : 100;
|
||||
let controlGroup = new ControlGroup(
|
||||
portGroup.name,
|
||||
[index],
|
||||
[filePropertyUi]);
|
||||
result.push(
|
||||
controlGroup
|
||||
);
|
||||
portGroupMap[portGroup.symbol] = controlGroup;
|
||||
|
||||
}
|
||||
} else if (fileProperty.index !== -1) {
|
||||
result.splice(fileProperty.index, 0, filePropertyUi);
|
||||
} else {
|
||||
result.push(
|
||||
filePropertyUi
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < plugin.frequencyPlots.length; ++i) {
|
||||
let frequencyPlot = plugin.frequencyPlots[i];
|
||||
let frequencyPlotUi = this.makeFrequencyPlotUI(frequencyPlot);
|
||||
|
||||
if (frequencyPlot.portGroup !== "" && plugin.getPortGroupByUri(frequencyPlot.portGroup)) {
|
||||
let portGroup = nullCast(plugin.getPortGroupByUri(frequencyPlot.portGroup));
|
||||
let controlGroup = portGroupMap[portGroup.symbol];
|
||||
if (controlGroup) {
|
||||
let insertPosition = controlGroup.indexes.length;
|
||||
if (frequencyPlot.index !== -1) {
|
||||
for (let i = 0; i < controlGroup.controls.length; ++i) {
|
||||
if (controlGroup.indexes[i] >= frequencyPlot.index) {
|
||||
insertPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let index = frequencyPlot.index !== -1 ? frequencyPlot.index : 100;
|
||||
controlGroup.controls.splice(insertPosition, 0, frequencyPlotUi);
|
||||
controlGroup.indexes.splice(insertPosition, 0, index);
|
||||
|
||||
} else {
|
||||
let index = frequencyPlot.index !== -1 ? frequencyPlot.index : 100;
|
||||
let controlGroup = new ControlGroup(
|
||||
portGroup.name,
|
||||
[index],
|
||||
[frequencyPlotUi]);
|
||||
result.push(
|
||||
controlGroup
|
||||
);
|
||||
portGroupMap[portGroup.symbol] = controlGroup;
|
||||
|
||||
}
|
||||
} else if (frequencyPlot.index !== -1) {
|
||||
result.splice(frequencyPlot.index, 0, frequencyPlotUi);
|
||||
} else {
|
||||
result.push(
|
||||
frequencyPlotUi
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getControl(controlValues: ControlValue[], key: string) {
|
||||
for (let i = 0; i < controlValues.length; ++i) {
|
||||
if (controlValues[i].key === key) {
|
||||
return controlValues[i];
|
||||
}
|
||||
}
|
||||
throw new Error("Not found.");
|
||||
}
|
||||
|
||||
onImeValueChange(key: string, value: number) {
|
||||
this.model.setPedalboardControl(this.props.instanceId, key, value);
|
||||
this.onImeClose();
|
||||
}
|
||||
onImeClose() {
|
||||
this.setState({
|
||||
imeUiControl: undefined
|
||||
});
|
||||
}
|
||||
|
||||
hasGroups(nodes: (ReactNode | ControlGroup)[]): boolean {
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let node = nodes[i];
|
||||
if (node instanceof ControlGroup) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
controlKeyIndex: number = 0;
|
||||
controlNodesToNodes(nodes: (ReactNode | ControlGroup)[]): ReactNode[] {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let isLandscapeGrid = this.state.landscapeGrid;
|
||||
let hasGroups = this.hasGroups(nodes);
|
||||
|
||||
let result: ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
let node = nodes[i];
|
||||
if (node instanceof ControlGroup) {
|
||||
let controlGroup = node as ControlGroup;
|
||||
let controls: ReactNode[] = [];
|
||||
for (let j = 0; j < controlGroup.controls.length; ++j) {
|
||||
let item = controlGroup.controls[j];
|
||||
controls.push(
|
||||
(
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={classes.controlPadding}>
|
||||
{item}
|
||||
</div>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
result.push((
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
|
||||
<div className={classes.portGroupTitle}>
|
||||
<Tooltip title={controlGroup.name}
|
||||
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
|
||||
>
|
||||
<Typography noWrap variant="caption" >{controlGroup.name}</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className={
|
||||
this.state.landscapeGrid ? classes.portGroupControlsLandscape : classes.portGroupControls} >
|
||||
{
|
||||
controls
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
} else {
|
||||
result.push((
|
||||
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
|
||||
{node as ReactNode}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static startPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Input", Pedalboard.START_PEDALBOARD_ITEM_URI);
|
||||
|
||||
static endPluginInfo: UiPlugin =
|
||||
makeIoPluginInfo("Output", Pedalboard.END_PEDALBOARD_ITEM_URI);
|
||||
|
||||
midiBindingControl(pedalboardItem: PedalboardItem): ReactNode {
|
||||
if (!pedalboardItem.midiChannelBinding) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<MidiChannelBindingControl key="channelBindingCtl" midiChannelBinding={pedalboardItem.midiChannelBinding}
|
||||
onChange={(result)=> {
|
||||
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
render(): ReactNode {
|
||||
this.controlKeyIndex = 0;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboardItem: PedalboardItem;
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
if (this.props.instanceId === Pedalboard.START_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeStartItem();
|
||||
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
|
||||
pedalboardItem = pedalboard.makeEndItem();
|
||||
} else {
|
||||
pedalboardItem = pedalboard.getItem(this.props.instanceId);
|
||||
}
|
||||
|
||||
if (!pedalboardItem)
|
||||
return (<div className={classes.frame} ></div>);
|
||||
|
||||
let controlValues = pedalboardItem.controlValues;
|
||||
|
||||
|
||||
let plugin: UiPlugin;
|
||||
if (pedalboardItem.isStart()) {
|
||||
plugin = startPluginInfo;
|
||||
controlValues = [new ControlValue("volume_db", pedalboard.input_volume_db)];
|
||||
} else if (pedalboardItem.isEnd()) {
|
||||
plugin = endPluginInfo;
|
||||
controlValues = [new ControlValue("volume_db", pedalboard.output_volume_db)];
|
||||
} else {
|
||||
plugin = nullCast(this.model.getUiPlugin(pedalboardItem.uri));
|
||||
}
|
||||
|
||||
|
||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||
|
||||
|
||||
|
||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let controlNodes: ControlNodes;
|
||||
|
||||
controlNodes = this.getStandardControlNodes(plugin, controlValues);
|
||||
|
||||
if (this.props.customization) {
|
||||
// allow wrapper class to insert/remove/rebuild controls.
|
||||
controlNodes = this.props.customization.ModifyControls(controlNodes);
|
||||
}
|
||||
|
||||
let nodes = this.controlNodesToNodes(controlNodes);
|
||||
|
||||
if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding)
|
||||
{
|
||||
pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue();
|
||||
|
||||
}
|
||||
if (pedalboardItem.midiChannelBinding) {
|
||||
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={classes.frame}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={scrollClass}>
|
||||
<div className={gridClass} >
|
||||
{
|
||||
nodes
|
||||
}
|
||||
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
||||
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
||||
{
|
||||
(!this.state.landscapeGrid) && (
|
||||
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{this.state.showFileDialog && (
|
||||
|
||||
<FilePropertyDialog open={this.state.showFileDialog}
|
||||
fileProperty={this.state.dialogFileProperty}
|
||||
instanceId={this.props.instanceId}
|
||||
selectedFile={this.state.dialogFileValue}
|
||||
onCancel={() => {
|
||||
this.setState({ showFileDialog: false });
|
||||
}}
|
||||
onOk={(fileProperty, selectedFile) => {
|
||||
|
||||
this.model.setPatchProperty(
|
||||
this.props.instanceId,
|
||||
fileProperty.patchProperty,
|
||||
JsonAtom.Path(selectedFile)
|
||||
)
|
||||
.then(() => {
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert("Unable to complete the operation. Audio is not running." + error);
|
||||
});
|
||||
this.setState({ showFileDialog: false });
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
|
||||
|
||||
onChange={(key, value) => this.onImeValueChange(key, value)}
|
||||
initialHeight={
|
||||
this.state.imeInitialHeight}
|
||||
caption={this.state.imeCaption}
|
||||
onClose={() => this.onImeClose()} />
|
||||
</div >
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
));
|
||||
|
||||
export default PluginControlView;
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
import { PluginType } from './Lv2Plugin';
|
||||
|
||||
|
||||
import FxSimulatorIcon from './svg/fx_simulator.svg?react';
|
||||
import FxPhaserIcon from './svg/fx_phaser.svg?react';
|
||||
import FxFilterIcon from './svg/fx_filter.svg?react';
|
||||
import FxDelayIcon from './svg/fx_delay.svg?react';
|
||||
|
||||
import FxAmplifierIcon from './svg/fx_amplifier.svg?react';
|
||||
import FxChorusIcon from './svg/fx_chorus.svg?react';
|
||||
import FxModulatorIcon from './svg/fx_modulator.svg?react';
|
||||
import FxReverbIcon from './svg/fx_reverb.svg?react';
|
||||
import FxDistortionIcon from './svg/fx_distortion.svg?react';
|
||||
import FxFlangerIcon from './svg/fx_flanger.svg?react';
|
||||
import FxFilterHpIcon from './svg/fx_filter_hp.svg?react';
|
||||
import FxParametricEqIcon from './svg/fx_parametric_eq.svg?react';
|
||||
import FxEqIcon from './svg/fx_eq.svg?react';
|
||||
import FxUtilityIcon from './svg/fx_utility.svg?react';
|
||||
import FxCompressorIcon from './svg/fx_compressor.svg?react';
|
||||
import FxLimiterIcon from './svg/fx_limiter.svg?react';
|
||||
import FxConstantIcon from './svg/fx_constant.svg?react';
|
||||
import FxFunctionIcon from './svg/fx_function.svg?react';
|
||||
import FxGateIcon from './svg/fx_gate.svg?react';
|
||||
import FxConverterIcon from './svg/fx_converter.svg?react';
|
||||
import FxGeneratorIcon from './svg/fx_generator.svg?react';
|
||||
import FxOscillatorIcon from './svg/fx_oscillator.svg?react';
|
||||
import FxInstrumentIcon from './svg/fx_instrument.svg?react';
|
||||
import FxMixerIcon from './svg/fx_mixer.svg?react';
|
||||
import FxPitchIcon from './svg/fx_pitch.svg?react';
|
||||
import FxSpatialIcon from './svg/fx_spatial.svg?react';
|
||||
import FxSpectralIcon from './svg/fx_spectral.svg?react';
|
||||
import FxAnalyzerIcon from './svg/fx_analyzer.svg?react';
|
||||
import FxPluginIcon from './svg/fx_plugin.svg?react';
|
||||
import SplitAIcon from './svg/fx_split_a.svg?react';
|
||||
import SplitBIcon from './svg/fx_split_b.svg?react';
|
||||
import SplitMixIcon from './svg/fx_dial.svg?react';
|
||||
import SplitLrIcon from './svg/fx_lr.svg?react';
|
||||
import FxEmptyIcon from './svg/fx_empty.svg?react';
|
||||
|
||||
import FxTerminalIcon from './svg/fx_terminal.svg?react';
|
||||
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
icon: {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6",
|
||||
color: theme.palette.text.primary,
|
||||
fill: theme.palette.text.primary
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export interface PluginIconProps extends WithStyles<typeof styles> {
|
||||
size?: number;
|
||||
opacity?: number;
|
||||
offsetY?: number;
|
||||
pluginType: PluginType;
|
||||
pluginMissing?: boolean;
|
||||
}
|
||||
|
||||
export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false) {
|
||||
let color = "";
|
||||
if (missingPlugin) {
|
||||
color = isDarkMode() ? "#C00000" : "#B00000";
|
||||
}
|
||||
let myStyle = { width: size, height: size, opacity: opacity, color: color, fill: color };
|
||||
switch (plugin_type) {
|
||||
case PluginType.PhaserPlugin:
|
||||
return <FxPhaserIcon className={className} style={myStyle} />;
|
||||
case PluginType.FilterPlugin:
|
||||
return <FxFilterIcon className={className} style={myStyle} />;
|
||||
case PluginType.DelayPlugin:
|
||||
return <FxDelayIcon className={className} style={myStyle} />;
|
||||
case PluginType.SimulatorPlugin:
|
||||
return <FxSimulatorIcon className={className} style={myStyle} />;
|
||||
case PluginType.AmplifierPlugin:
|
||||
return <FxAmplifierIcon className={className} style={myStyle} />;
|
||||
case PluginType.ChorusPlugin:
|
||||
return <FxChorusIcon className={className} style={myStyle} />;
|
||||
case PluginType.ModulatorPlugin:
|
||||
case PluginType.CombPlugin:
|
||||
return <FxModulatorIcon className={className} style={myStyle} />;
|
||||
case PluginType.ReverbPlugin:
|
||||
return <FxReverbIcon className={className} style={myStyle} />;
|
||||
case PluginType.DistortionPlugin:
|
||||
return <FxDistortionIcon className={className} style={myStyle} />;
|
||||
case PluginType.FlangerPlugin:
|
||||
return <FxFlangerIcon className={className} style={myStyle} />;
|
||||
case PluginType.LowpassPlugin:
|
||||
return <FxFilterIcon className={className} style={myStyle} />;
|
||||
case PluginType.HighpassPlugin:
|
||||
return <FxFilterHpIcon className={className} style={myStyle} />;
|
||||
case PluginType.ParaEQPlugin:
|
||||
return <FxParametricEqIcon className={className} style={myStyle} />;
|
||||
case PluginType.MultiEQPlugin:
|
||||
case PluginType.EQPlugin:
|
||||
return <FxEqIcon className={className} style={myStyle} />;
|
||||
case PluginType.UtilityPlugin:
|
||||
return <FxUtilityIcon className={className} style={myStyle} />;
|
||||
case PluginType.CompressorPlugin:
|
||||
case PluginType.DynamicsPlugin:
|
||||
case PluginType.ExpanderPlugin:
|
||||
return <FxCompressorIcon className={className} style={myStyle} />;
|
||||
|
||||
case PluginType.LimiterPlugin:
|
||||
return <FxLimiterIcon className={className} style={myStyle} />;
|
||||
case PluginType.ConstantPlugin:
|
||||
return <FxConstantIcon className={className} style={myStyle} />;
|
||||
case PluginType.FunctionPlugin:
|
||||
case PluginType.WaveshaperPlugin:
|
||||
return <FxFunctionIcon className={className} style={myStyle} />;
|
||||
case PluginType.GatePlugin:
|
||||
return <FxGateIcon className={className} style={myStyle} />;
|
||||
case PluginType.ConverterPlugin:
|
||||
return <FxConverterIcon className={className} style={myStyle} />;
|
||||
case PluginType.GeneratorPlugin:
|
||||
return <FxGeneratorIcon className={className} style={myStyle} />;
|
||||
case PluginType.OscillatorPlugin:
|
||||
return <FxOscillatorIcon className={className} style={myStyle} />;
|
||||
case PluginType.InstrumentPlugin:
|
||||
return <FxInstrumentIcon className={className} style={myStyle} />;
|
||||
case PluginType.MixerPlugin:
|
||||
return <FxMixerIcon className={className} style={myStyle} />;
|
||||
case PluginType.PitchPlugin:
|
||||
return <FxPitchIcon className={className} style={myStyle} />;
|
||||
case PluginType.SpatialPlugin:
|
||||
return <FxSpatialIcon className={className} style={myStyle} />;
|
||||
case PluginType.SpectralPlugin:
|
||||
return <FxSpectralIcon className={className} style={myStyle} />;
|
||||
case PluginType.AnalyserPlugin:
|
||||
return <FxAnalyzerIcon className={className} style={myStyle} />;
|
||||
case PluginType.SplitA:
|
||||
return <SplitAIcon className={className} style={myStyle} />;
|
||||
case PluginType.SplitB:
|
||||
return <SplitBIcon className={className} style={myStyle} />;
|
||||
case PluginType.SplitMix:
|
||||
return <SplitMixIcon className={className} style={myStyle} />;
|
||||
case PluginType.SplitLR:
|
||||
return <SplitLrIcon className={className} style={myStyle} />;
|
||||
|
||||
case PluginType.None:
|
||||
return <FxEmptyIcon className={className} style={myStyle} />;
|
||||
case PluginType.Terminal:
|
||||
return <FxTerminalIcon className={className} style={myStyle} />;
|
||||
|
||||
default:
|
||||
case PluginType.Plugin:
|
||||
return <FxPluginIcon className={className} style={myStyle} />;
|
||||
}
|
||||
}
|
||||
export function SelectBaseIcon(plugin_type: PluginType): string {
|
||||
switch (plugin_type) {
|
||||
case PluginType.PhaserPlugin:
|
||||
return "fx_phaser.svg";
|
||||
case PluginType.FilterPlugin:
|
||||
return "fx_filter.svg";
|
||||
case PluginType.DelayPlugin:
|
||||
return "fx_delay.svg";
|
||||
case PluginType.SimulatorPlugin:
|
||||
return "fx_simulator.svg";
|
||||
case PluginType.AmplifierPlugin:
|
||||
return "fx_amplifier.svg";
|
||||
case PluginType.ChorusPlugin:
|
||||
return "fx_chorus.svg";
|
||||
case PluginType.ModulatorPlugin:
|
||||
case PluginType.CombPlugin:
|
||||
return "fx_modulator.svg";
|
||||
case PluginType.ReverbPlugin:
|
||||
return "fx_reverb.svg";
|
||||
case PluginType.DistortionPlugin:
|
||||
return "fx_distortion.svg";
|
||||
case PluginType.FlangerPlugin:
|
||||
return "fx_flanger.svg";
|
||||
case PluginType.LowpassPlugin:
|
||||
return "fx_filter.svg";
|
||||
case PluginType.HighpassPlugin:
|
||||
return "fx_filter_hp.svg";
|
||||
case PluginType.ParaEQPlugin:
|
||||
return "fx_parametric_eq.svg";
|
||||
case PluginType.MultiEQPlugin:
|
||||
case PluginType.EQPlugin:
|
||||
return "fx_eq.svg";
|
||||
case PluginType.UtilityPlugin:
|
||||
return "fx_utility.svg";
|
||||
case PluginType.CompressorPlugin:
|
||||
case PluginType.DynamicsPlugin:
|
||||
case PluginType.ExpanderPlugin:
|
||||
return "fx_compressor.svg";
|
||||
case PluginType.LimiterPlugin:
|
||||
return "fx_limiter.svg";
|
||||
case PluginType.ConstantPlugin:
|
||||
return "fx_constant.svg";
|
||||
case PluginType.FunctionPlugin:
|
||||
case PluginType.WaveshaperPlugin:
|
||||
return "fx_function.svg";
|
||||
case PluginType.GatePlugin:
|
||||
return "fx_gate.svg";
|
||||
case PluginType.ConverterPlugin:
|
||||
return "fx_converter.svg"
|
||||
case PluginType.GeneratorPlugin:
|
||||
return "fx_generator.svg";
|
||||
case PluginType.OscillatorPlugin:
|
||||
return "fx_oscillator.svg";
|
||||
case PluginType.InstrumentPlugin:
|
||||
return "fx_instrument.svg";
|
||||
case PluginType.MixerPlugin:
|
||||
return "fx_mixer.svg";
|
||||
case PluginType.PitchPlugin:
|
||||
return "fx_pitch.svg";
|
||||
case PluginType.SpatialPlugin:
|
||||
return "fx_spatial.svg";
|
||||
case PluginType.SpectralPlugin:
|
||||
return "fx_spectral.svg";
|
||||
case PluginType.AnalyserPlugin:
|
||||
return "fx_analyzer.svg";
|
||||
case PluginType.Plugin:
|
||||
default:
|
||||
return "fx_plugin.svg";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function SelectIconUri(plugin_type: PluginType) {
|
||||
let icon = SelectBaseIcon(plugin_type);
|
||||
return PiPedalModelFactory.getInstance().svgImgUrl(icon);
|
||||
}
|
||||
|
||||
const PluginIcon = withStyles((props: PluginIconProps) => {
|
||||
const { pluginType, opacity, pluginMissing } = props;
|
||||
const classes = withStyles.getClasses(props);
|
||||
|
||||
let pluginMissing_: boolean = pluginMissing ?? false;
|
||||
|
||||
let size: number = 24;
|
||||
if (props.size) size = props.size;
|
||||
let topVal: number = (props.offsetY ?? 0);
|
||||
|
||||
let svgIcon = SelectSvgIcon(pluginType, classes.icon, size, opacity, pluginMissing_);
|
||||
if (svgIcon) {
|
||||
return svgIcon;
|
||||
}
|
||||
|
||||
// colored icons that don't have dark/light variants.
|
||||
return (
|
||||
<img src={SelectIconUri(pluginType)} className={classes.icon} style={{ width: size, height: size, position: "relative", top: topVal, opacity: " " }} alt=''
|
||||
/>);
|
||||
|
||||
// let maskText = "url(" + SelectIcon(pluginType, pluginUri) + ") no-repeat center";
|
||||
// let color=colorFromHash(pluginUri);
|
||||
// return (
|
||||
// <div style={{ display: "block", opacity: 0.8, width: size, height: size, backgroundColor: color,
|
||||
// mask: maskText, WebkitMask: maskText, WebkitMaskSize: size + "px", maskSize: size + "px" }}
|
||||
// > </div>
|
||||
// );
|
||||
},
|
||||
styles);
|
||||
|
||||
export default PluginIcon;
|
||||
@@ -0,0 +1,343 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
import MuiDialogTitle from '@mui/material/DialogTitle';
|
||||
import MuiDialogContent from '@mui/material/DialogContent';
|
||||
import MuiDialogActions from '@mui/material/DialogActions';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import { UiPlugin, UiControl } from './Lv2Plugin';
|
||||
import PluginIcon from './PluginIcon';
|
||||
import { Remark } from 'react-remark';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return createStyles({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
closeButton: css({
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
}),
|
||||
icon: {
|
||||
fill: theme.palette.text.primary,
|
||||
opacity: 0.6
|
||||
}
|
||||
});
|
||||
};
|
||||
export interface PluginInfoDialogTitleProps extends WithStyles<typeof styles> {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// const PluginInfoDialogTitle = withStyles(styles)((props: PluginInfoDialogTitleProps) => {
|
||||
// const { children, classes, onClose, ...other } = props;
|
||||
// return (
|
||||
// <MuiDialogTitle style={{ display: "flex", flexDirection: "row", alignItems: "start", flexWrap: "nowrap" }}>
|
||||
// <div style={{ flex: "0 0 auto", marginRight: 8 }}>
|
||||
// <PluginIcon pluginType={plugin.plugin_type} pluginUri={plugin.uri} offsetY={5} />
|
||||
// </div>
|
||||
// <div style={{ flex: "1 1 auto" }}>
|
||||
// {children}
|
||||
// </div>
|
||||
// <IconButton aria-label="close" className={classes.closeButton} onClick={() => handleClose()}
|
||||
// style={{ flex: "0 0 auto" }}>
|
||||
// <CloseIcon />
|
||||
// </IconButton>
|
||||
// </MuiDialogTitle>
|
||||
// );
|
||||
// });
|
||||
|
||||
const PluginInfoDialogContent = withStyles(
|
||||
MuiDialogContent,
|
||||
(theme: Theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const PluginInfoDialogActions = withStyles(MuiDialogActions,
|
||||
(theme: Theme) => ({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface PluginInfoProps extends WithStyles<typeof styles> {
|
||||
plugin_uri: string
|
||||
}
|
||||
|
||||
function displayChannelCount(count: number): string {
|
||||
if (count === 0) return "None";
|
||||
if (count === 1) return "Mono";
|
||||
if (count === 2) return "Stereo";
|
||||
return count + " channels";
|
||||
}
|
||||
function ioDescription(plugin: UiPlugin): string {
|
||||
let result = "Input: " + displayChannelCount(plugin.audio_inputs) + ". Output: " + displayChannelCount(plugin.audio_outputs) + ".";
|
||||
if (plugin.has_midi_input) {
|
||||
if (plugin.has_midi_output) {
|
||||
result += " Midi in/out.";
|
||||
} else {
|
||||
result += " Midi in.";
|
||||
}
|
||||
} else {
|
||||
if (plugin.has_midi_output) {
|
||||
result += "Midi out.";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
|
||||
}
|
||||
|
||||
// function makeParagraphs(description: string) {
|
||||
// description = description.replaceAll('\r', '');
|
||||
// description = description.replaceAll('\n\n', '\r');
|
||||
// description = description.replaceAll('\n', ' ');
|
||||
|
||||
// let paragraphs: string[] = description.split('\r');
|
||||
// return (
|
||||
// <div style={{ paddingLeft: "24px" }}>
|
||||
// {paragraphs.map((para) => (
|
||||
// <Typography variant="body2" paragraph >
|
||||
// {para}
|
||||
// </Typography>
|
||||
// ))}
|
||||
// </div>
|
||||
|
||||
// );
|
||||
// }
|
||||
function makeControls(controls: UiControl[]) {
|
||||
let hasComments = false;
|
||||
|
||||
for (let i = 0; i < controls.length; ++i) {
|
||||
if (controls[i].comment !== "") {
|
||||
hasComments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hasComments = true;
|
||||
if (hasComments) {
|
||||
let trs: React.ReactElement[] = [];
|
||||
for (let i = 0; i < controls.length; ++i) {
|
||||
let control = controls[i];
|
||||
if (!(control.not_on_gui) && control.is_input)
|
||||
trs.push((
|
||||
<tr>
|
||||
<td style={{ verticalAlign: "top" }}>
|
||||
<Typography variant="body2" style={{ whiteSpace: "nowrap" }}>
|
||||
{control.name}
|
||||
</Typography>
|
||||
</td>
|
||||
<td style={{ paddingLeft: "16px", verticalAlign: "top" }}>
|
||||
<Typography variant="body2">
|
||||
{control.comment}
|
||||
</Typography>
|
||||
</td>
|
||||
</tr >
|
||||
));
|
||||
|
||||
}
|
||||
return (
|
||||
<table style={{ paddingLeft: "24px", verticalAlign: "top" }}>
|
||||
{
|
||||
trs
|
||||
}
|
||||
</table>
|
||||
);
|
||||
|
||||
} else {
|
||||
return (
|
||||
<Grid container direction="row" justifyContent="flex-start" alignItems="flex-start" spacing={1} style={{ paddingLeft: "24px" }}>
|
||||
{
|
||||
controls.map((control) => (
|
||||
<Grid xs={6} sm={4} key={control.symbol + "x"} >
|
||||
<Typography variant="body2">
|
||||
{control.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const PluginInfoDialog = withStyles((props: PluginInfoProps) => {
|
||||
|
||||
let model = PiPedalModelFactory.getInstance();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
let { plugin_uri } = props;
|
||||
let classes = withStyles.getClasses(props);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
let uri = props.plugin_uri;
|
||||
let visible = true;
|
||||
if (uri === null || uri === "") {
|
||||
visible = false;
|
||||
}
|
||||
if (!visible) {
|
||||
return (<div></div>);
|
||||
};
|
||||
let plugin = model.getUiPlugin(plugin_uri);
|
||||
|
||||
if (plugin === null) {
|
||||
return (<div></div>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
style={{ display: (props.plugin_uri !== "") ? "inline-flex" : "none" }}
|
||||
onClick={handleClickOpen}
|
||||
size="large">
|
||||
<InfoOutlinedIcon className={classes.icon} color='inherit' />
|
||||
</IconButton>
|
||||
{open && (
|
||||
<DialogEx tag="info" onClose={handleClose} open={open} fullWidth maxWidth="md"
|
||||
onEnterKey={handleClose}
|
||||
>
|
||||
<MuiDialogTitle >
|
||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "start", flexWrap: "nowrap" }}>
|
||||
<div style={{ flex: "0 0 auto", marginRight: 16 }}>
|
||||
<PluginIcon pluginType={plugin.plugin_type} offsetY={3} />
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto" }}>
|
||||
<Typography variant="h6">{plugin.name}</Typography>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={() => handleClose()}
|
||||
style={{ flex: "0 0 auto" }}
|
||||
size="large">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</MuiDialogTitle>
|
||||
<PluginInfoDialogContent dividers style={{ width: "100%", maxHeight: "80%", overflowX: "hidden" }}>
|
||||
<div style={{ width: "100%", display: "flex", flexFlow: "row", justifyItems: "stretch", flexWrap: "nowrap" }} >
|
||||
<div style={{ flex: "1 1 auto", whiteSpace: "nowrap", minWidth: "auto" }}>
|
||||
<Typography gutterBottom variant="body2" >
|
||||
Author:
|
||||
{(plugin.author_homepage !== "")
|
||||
? (<a href={plugin.author_homepage} target="_blank" rel="noopener noreferrer">
|
||||
{plugin.author_name}
|
||||
</a>
|
||||
)
|
||||
: (
|
||||
<span>{plugin.author_name}</span>
|
||||
)
|
||||
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Typography gutterBottom variant="body2" >
|
||||
{ioDescription(plugin)}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<Typography variant="body2" gutterBottom style={{ paddingTop: "1em" }}>
|
||||
Controls:
|
||||
</Typography>
|
||||
{
|
||||
makeControls(plugin.controls)
|
||||
}
|
||||
{plugin.description.length > 0 && (
|
||||
<div>
|
||||
<Typography gutterBottom variant="body2" style={{ paddingTop: "1em" }}>
|
||||
Description:
|
||||
</Typography>
|
||||
<div style={{ marginLeft: 24, marginTop: 16 }}>
|
||||
<Remark
|
||||
rehypeReactOptions={{
|
||||
components: {
|
||||
p: (props: any) => {
|
||||
// return (
|
||||
// <p className="MuiTypography-root MuiTypography-body2" {...props} />
|
||||
// );
|
||||
return (
|
||||
<Typography variant="body2" paragraph={true} {...props} />
|
||||
);
|
||||
|
||||
},
|
||||
code: (props: any) => {
|
||||
return (<code style={{ fontSize: 14 }} {...props} />);
|
||||
},
|
||||
a: (props: any) => {
|
||||
return (
|
||||
<a target="_blank" {...props} />
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{plugin.description}
|
||||
</Remark>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PluginInfoDialogContent>
|
||||
<PluginInfoDialogActions>
|
||||
<Button variant="dialogPrimary" autoFocus onClick={handleClose} style={{ width: "130px" }}>
|
||||
OK
|
||||
</Button>
|
||||
</PluginInfoDialogActions>
|
||||
</DialogEx>
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
},
|
||||
styles);
|
||||
|
||||
export default PluginInfoDialog;
|
||||
@@ -0,0 +1,558 @@
|
||||
// Copyright (c) 2023 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { UiControl } from './Lv2Plugin';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State } from './PiPedalModel';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import GxTunerControl from './GxTunerControl';
|
||||
import Units from './Units';
|
||||
import ControlTooltip from './ControlTooltip';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
|
||||
function makeLedGradient(color: string) {
|
||||
if (color === "green") {
|
||||
if (isDarkMode()) {
|
||||
return "radial-gradient(circle at center, #0F0 0, #0F0 20%, #333 100%)";
|
||||
} else {
|
||||
return "radial-gradient(circle at center, #4F4 0, #4F4 40%, #464 100%)";
|
||||
}
|
||||
}
|
||||
if (color === "blue") {
|
||||
if (isDarkMode()) {
|
||||
return "radial-gradient(circle at center, #00F 0, #00F 20%, #333 100%)";
|
||||
} else {
|
||||
return "radial-gradient(circle at center, #44F 0, #44F 40%, #446 100%)";
|
||||
}
|
||||
}
|
||||
if (color === "yellow") {
|
||||
if (isDarkMode()) {
|
||||
return "radial-gradient(circle at center, #FF0 0, #FF0 20%, #333 100%)";
|
||||
} else {
|
||||
return "radial-gradient(circle at center, #FF4 0, #FF4 40%, #664 100%)";
|
||||
}
|
||||
}
|
||||
if (isDarkMode()) {
|
||||
return "radial-gradient(circle at center, #F000 0, #F00 20%, #333 100%)";
|
||||
} else {
|
||||
return "radial-gradient(circle at center, #F44 0, #F44 40%, #644 100%)";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
}),
|
||||
switchTrack: css({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: 14 / 2,
|
||||
zIndex: -1,
|
||||
transition: theme.transitions.create(['opacity', 'background-color'], {
|
||||
duration: theme.transitions.duration.shortest
|
||||
}),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
|
||||
}),
|
||||
displayValue: css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: "white",
|
||||
color: "#666",
|
||||
// zIndex: -1,
|
||||
}),
|
||||
controlFrame: css({
|
||||
display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-between", height: 116
|
||||
}),
|
||||
|
||||
titleSection: css({
|
||||
flex: "0 0 auto", alignSelf:"stretch", marginBottom: 8, marginLeft: 0, marginRight: 0
|
||||
}),
|
||||
|
||||
midSection: css({
|
||||
flex: "1 1 1", display: "flex",flexFlow: "column nowrap",alignContent: "center",justifyContent: "center"
|
||||
}),
|
||||
|
||||
editSection: css({
|
||||
flex: "0 0 28", position: "relative", width: 60, height: 28, minHeight: 28
|
||||
}),
|
||||
editSectionNoContent: css({
|
||||
flex: "0 0 28", position: "relative", width: 1, height: 28, minHeight: 28
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
export interface PluginOutputControlProps extends WithStyles<typeof styles> {
|
||||
uiControl: UiControl;
|
||||
instanceId: number;
|
||||
}
|
||||
type PluginOutputControlState = {
|
||||
hasValue: boolean;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const PluginOutputControl =
|
||||
withStyles(
|
||||
class extends Component<PluginOutputControlProps, PluginOutputControlState> {
|
||||
|
||||
private model: PiPedalModel;
|
||||
private vuRef: React.RefObject<HTMLDivElement|null>;
|
||||
private dbVuRef: React.RefObject<HTMLDivElement|null>;
|
||||
private dbVuTelltaleRef: React.RefObject<HTMLDivElement|null>;
|
||||
private lampRef: React.RefObject<HTMLDivElement|null>;
|
||||
|
||||
|
||||
constructor(props: PluginOutputControlProps) {
|
||||
super(props);
|
||||
this.vuRef = React.createRef<HTMLDivElement>();
|
||||
this.dbVuRef = React.createRef<HTMLDivElement>();
|
||||
this.dbVuTelltaleRef = React.createRef<HTMLDivElement>();
|
||||
this.lampRef = React.createRef<HTMLDivElement>();
|
||||
this.state = {
|
||||
hasValue: false,
|
||||
value: 0
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.onConnectionStateChanged = this.onConnectionStateChanged.bind(this);
|
||||
}
|
||||
|
||||
private onConnectionStateChanged(state: State) {
|
||||
if (state === State.Ready) {
|
||||
this.unsubscribe();
|
||||
this.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private mounted: boolean = false;
|
||||
componentWillUnmount() {
|
||||
if (super.componentWillUnmount) {
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
this.mounted = false;
|
||||
this.model.state.removeOnChangedHandler(this.onConnectionStateChanged);
|
||||
this.unsubscribe();
|
||||
}
|
||||
componentDidMount(): void {
|
||||
this.mounted = true;
|
||||
this.subscribe();
|
||||
this.model.state.addOnChangedHandler(this.onConnectionStateChanged);
|
||||
|
||||
if (super.componentDidMount) {
|
||||
super.componentDidMount();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<PluginOutputControlProps>, prevState: Readonly<PluginOutputControlState>, snapshot?: any): void {
|
||||
if (prevProps.instanceId !== this.props.instanceId) {
|
||||
this.unsubscribe();
|
||||
this.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private VU_HEIGHT = 60 - 4;
|
||||
private DB_VU_HEIGHT = 60 - 4;
|
||||
private animationHandle: number | undefined = undefined;
|
||||
|
||||
private dbVuTelltale = -96.0;
|
||||
private dbVuValue = -96.0;
|
||||
|
||||
private dbVuHoldTime = 0.0;
|
||||
|
||||
private requestDbVuAnimation() {
|
||||
if (!this.animationHandle) {
|
||||
this.animationHandle = requestAnimationFrame(
|
||||
() => {
|
||||
let value = this.dbVuValue;
|
||||
let range = this.vuMap(value);
|
||||
let top = range - this.VU_HEIGHT;
|
||||
if (top > 0) top = 0;
|
||||
|
||||
if (value > this.dbVuTelltale) {
|
||||
this.dbVuTelltale = value;
|
||||
this.dbVuHoldTime = Date.now() + 2000;
|
||||
}
|
||||
if (this.dbVuRef.current) {
|
||||
this.dbVuRef.current.style.marginTop = top + "px";
|
||||
}
|
||||
this.animationHandle = undefined;
|
||||
this.updateDbVuTelltale();
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
updateDbVuTelltale() {
|
||||
let telltaleDone = true;
|
||||
if (this.dbVuHoldTime !== 0) {
|
||||
telltaleDone = false;
|
||||
let t = Date.now();
|
||||
if (t >= this.dbVuHoldTime) {
|
||||
let dt = t - this.dbVuHoldTime;
|
||||
let telltaleValue = this.dbVuTelltale - 30 * dt / 1000;
|
||||
if (telltaleValue < -200) {
|
||||
telltaleValue = -200;
|
||||
telltaleDone = true;
|
||||
}
|
||||
this.dbVuTelltale = telltaleValue;
|
||||
this.dbVuHoldTime = t;
|
||||
|
||||
}
|
||||
let y = this.dbVuMap(this.dbVuTelltale);
|
||||
if (y < 0) y = 0;
|
||||
|
||||
if (this.dbVuTelltaleRef.current) {
|
||||
let telltaleStyle = this.dbVuTelltaleRef.current.style;
|
||||
telltaleStyle.marginTop = y + "px";
|
||||
let telltaleColor = "#0C0";
|
||||
if (this.dbVuTelltale >= 0) {
|
||||
telltaleColor = "#F00";
|
||||
} else if (this.dbVuTelltale >= -10) {
|
||||
telltaleColor = "#FF0";
|
||||
}
|
||||
telltaleStyle.background = telltaleColor;
|
||||
}
|
||||
}
|
||||
if (!telltaleDone) {
|
||||
this.requestDbVuAnimation();
|
||||
}
|
||||
}
|
||||
private updateValue(value: number) {
|
||||
if (this.lampRef.current) {
|
||||
let control = this.props.uiControl;
|
||||
let range = (value - control.min_value) / (control.max_value - control.min_value);
|
||||
this.lampRef.current.style.opacity = range + "";
|
||||
} else if (this.dbVuRef.current) {
|
||||
this.dbVuValue = value;
|
||||
this.requestDbVuAnimation();
|
||||
}
|
||||
else if (this.vuRef.current) {
|
||||
let control = this.props.uiControl;
|
||||
let range = (value - control.min_value) / (control.max_value - control.min_value);
|
||||
let top = this.VU_HEIGHT - range * this.VU_HEIGHT;
|
||||
|
||||
if (!this.animationHandle) {
|
||||
this.animationHandle = requestAnimationFrame(
|
||||
() => {
|
||||
if (this.vuRef.current) {
|
||||
this.vuRef.current.style.marginTop = top + "px";
|
||||
}
|
||||
this.animationHandle = undefined;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
if (this.mounted) {
|
||||
this.setState({ hasValue: true, value: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private monitorPortHandle?: MonitorPortHandle;
|
||||
|
||||
private subscribe(): void {
|
||||
this.unsubscribe();
|
||||
this.monitorPortHandle = this.model.monitorPort(
|
||||
this.props.instanceId,
|
||||
this.props.uiControl.symbol,
|
||||
1 / 15.0, // update rate.
|
||||
(value) => {
|
||||
this.updateValue(value);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
private unsubscribe(): void {
|
||||
if (this.monitorPortHandle) {
|
||||
this.model.unmonitorPort(this.monitorPortHandle)
|
||||
this.monitorPortHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
isShortSelectOrText(control: UiControl) {
|
||||
if (control.scale_points.length === 0) {
|
||||
return true;
|
||||
}
|
||||
for (let scale_point of control.scale_points) {
|
||||
if (scale_point.label.length > 12) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
formatDisplayValue(uiControl: UiControl | undefined, value: number): string {
|
||||
if (!uiControl) return "";
|
||||
|
||||
return uiControl.formatDisplayValue(value);
|
||||
}
|
||||
|
||||
|
||||
dbVuMap(value: number): number {
|
||||
let control = this.props.uiControl;
|
||||
let y = (control.max_value - value) * this.DB_VU_HEIGHT / (control.max_value - control.min_value);
|
||||
return y;
|
||||
}
|
||||
|
||||
vuMap(value: number): number {
|
||||
let control = this.props.uiControl;
|
||||
let y = (control.max_value - value) * this.VU_HEIGHT / (control.max_value - control.min_value);
|
||||
return y;
|
||||
}
|
||||
render() {
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let control: UiControl = this.props.uiControl;
|
||||
|
||||
|
||||
let text = "";
|
||||
if (this.state.hasValue) {
|
||||
let value = this.state.value;
|
||||
if (control.isOnOffSwitch()) {
|
||||
text = value === 0 ? "OFF" : "ON";
|
||||
} else {
|
||||
text = this.formatDisplayValue(control, value);
|
||||
}
|
||||
}
|
||||
|
||||
let isText = control.isOutputText();
|
||||
|
||||
let item_width: number | undefined = isText ? 160 : 80;
|
||||
if (isText) {
|
||||
if (this.isShortSelectOrText(control)) {
|
||||
item_width = 80;
|
||||
}
|
||||
}
|
||||
if (control.isTuner()) {
|
||||
item_width = undefined;
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", width: item_width, margin: 8, height: 98 }}>
|
||||
<GxTunerControl instanceId={this.props.instanceId} symbolName={control.symbol}
|
||||
valueIsMidi={control.units === Units.midiNote}
|
||||
/>
|
||||
</div >
|
||||
|
||||
);
|
||||
|
||||
} else if (control.isDbVu()) {
|
||||
item_width = undefined;
|
||||
let redLevel = this.dbVuMap(0);
|
||||
let yellowLevel = this.dbVuMap(-10);
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{ width: item_width }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ width: "100%" }}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> {control.name === "" ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}
|
||||
style={{ flex: "1 1 1", display: "flex", justifyContent: "center", alignItems: "start", flexFlow: "row nowrap" }}>
|
||||
<div style={{ width: 8, height: this.DB_VU_HEIGHT + 4, background: "#000", }}>
|
||||
<div style={{ height: this.DB_VU_HEIGHT, width: 4, overflow: "hidden", position: "absolute", margin: 2 }}>
|
||||
<div style={{ width: 4, height: redLevel, position: "absolute", marginTop: 0, background: "#F00" }} />
|
||||
<div style={{ width: 4, height: (yellowLevel - redLevel), position: "absolute", marginTop: redLevel, background: "#CC0" }} />
|
||||
<div style={{ width: 4, height: (this.DB_VU_HEIGHT - yellowLevel), position: "absolute", marginTop: yellowLevel, background: "#0A0" }} />
|
||||
|
||||
<div ref={this.dbVuRef} style={{ width: 4, position: "absolute", marginTop: 0, height: this.VU_HEIGHT, background: "#000" }} />
|
||||
<div ref={this.dbVuTelltaleRef} style={{ width: 4, position: "absolute", marginTop: 100, height: 3, background: "#C00" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={classes.editSectionNoContent}>
|
||||
|
||||
</div>
|
||||
|
||||
</div >
|
||||
|
||||
);
|
||||
}
|
||||
else if (control.isVu()) {
|
||||
// yyx: convert this to a horizontal progress bar.
|
||||
item_width = undefined;
|
||||
return (
|
||||
<div className={classes.controlFrame} style={{ width: item_width}}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ width: "100%" }}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography noWrap display="block" variant="caption" style={{
|
||||
width: item_width,
|
||||
textAlign: "center"
|
||||
}}> {control.name === "" ? "\u00A0" : control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}
|
||||
style={{ display: "flex", justifyContent: "center", alignItems: "start", flexFlow: "row nowrap" }}>
|
||||
<div style={{ width: 8, height: this.VU_HEIGHT + 4, background: "#000", }}>
|
||||
<div style={{ height: this.VU_HEIGHT, overflow: "hidden", position: "absolute", margin: 2 }}>
|
||||
<div ref={this.vuRef} style={{ width: 4, height: this.VU_HEIGHT, background: "#0C0", }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div className={classes.editSectionNoContent}>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
);
|
||||
} else if (control.isLamp()) {
|
||||
item_width = undefined;
|
||||
let attachedLamp = control.name === "" || control.name === "\u00A0";
|
||||
|
||||
let ledGradient: string;
|
||||
if (this.props.uiControl.pipedal_ledColor.length === 0) {
|
||||
ledGradient = (isDarkMode() ? "radial-gradient(circle at center, #F00 0, #F00 20%, #333 100%)"
|
||||
: "radial-gradient(circle at center, #F44 0, #F44 40%, #644 100%)");
|
||||
} else {
|
||||
ledGradient = makeLedGradient(this.props.uiControl.pipedal_ledColor);
|
||||
}
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{width: attachedLamp? 15: item_width}}
|
||||
>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ flex: "0 0 auto", width: "100%"}}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography noWrap variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "center"
|
||||
}}> {control.name === "" ? "\u00A0" : (control.name)}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}
|
||||
style={{
|
||||
display: "flex", justifyContent: "center", alignItems: "center", flexFlow: "row nowrap",
|
||||
}}>
|
||||
<div style={{
|
||||
width: 12, height: 12, background: isDarkMode() ? "#111" :
|
||||
"#444", borderRadius: 5, position: "relative"
|
||||
}}>
|
||||
<div ref={this.lampRef} style={{
|
||||
width: 8, height: 8,
|
||||
background: ledGradient,
|
||||
opacity: 0, borderRadius: 3, margin: 2, position: "absolute"
|
||||
}} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div className={classes.editSectionNoContent}>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
} if (control.isOutputText()) {
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{ display: "flex", flexDirection: "column", width: item_width }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ flex: "0 0 auto", width: "100%" }}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography noWrap variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "left"
|
||||
}}> {control.name === "" ? "\u00A0": control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection} style={{
|
||||
display: "flex", justifyContent: "center",
|
||||
flexFlow: "row nowrap", paddingTop: 6
|
||||
|
||||
}}>
|
||||
<Typography variant="caption" display="block" noWrap style={{ width: "100%" }}>
|
||||
{text}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div className={classes.editSectionNoContent}>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
|
||||
} else {
|
||||
return (
|
||||
<div className={classes.controlFrame}
|
||||
style={{ display: "flex", flexDirection: "column", width: item_width }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div className={classes.titleSection}
|
||||
style={{ flex: "0 0 auto", width: "100%" }}>
|
||||
<ControlTooltip uiControl={control}>
|
||||
<Typography noWrap variant="caption" display="block" style={{
|
||||
width: "100%",
|
||||
textAlign: "left"
|
||||
}}> {control.name}</Typography>
|
||||
</ControlTooltip>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div className={classes.midSection}
|
||||
style={{ display: "flex", justifyContent: "start", alignItems: "center", flexFlow: "row nowrap" }}>
|
||||
<Typography noWrap variant="caption" display="block" style={{ width: "100%" }}>
|
||||
{text}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*, strictly a placeholder for visual alignment purposes.*/}
|
||||
<div className={classes.editSectionNoContent}>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default PluginOutputControl;
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
export class PluginUiPreset {
|
||||
deserialize(input: any): PluginUiPreset {
|
||||
this.instanceId = input.instanceId;
|
||||
this.label = input.label;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any) : PluginUiPreset[] {
|
||||
let result: PluginUiPreset[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new PluginUiPreset().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
instanceId: number = -1;
|
||||
label: string = "";
|
||||
|
||||
static equals(left: PluginUiPreset, right: PluginUiPreset): boolean
|
||||
{
|
||||
return left.instanceId === right.instanceId && left.label === right.label;
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginUiPresets {
|
||||
deserialize(input: any): PluginUiPresets {
|
||||
this.pluginUri = input.pluginUri;
|
||||
this.presets = PluginUiPreset.deserialize_array(input.presets);
|
||||
return this;
|
||||
}
|
||||
|
||||
clone(): PluginUiPresets {
|
||||
return new PluginUiPresets().deserialize(this);
|
||||
}
|
||||
movePreset(from: number, to: number): void
|
||||
{
|
||||
let t = this.presets[from];
|
||||
this.presets.splice(from,1);
|
||||
this.presets.splice(to,0,t);
|
||||
}
|
||||
getItem(instanceId: number): PluginUiPreset | undefined {
|
||||
for (let i = 0; i < this.presets.length; ++i)
|
||||
{
|
||||
if (this.presets[i].instanceId === instanceId)
|
||||
{
|
||||
return this.presets[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
static equals(left: PluginUiPresets,right: PluginUiPresets): boolean
|
||||
{
|
||||
if (left.pluginUri !== right.pluginUri) return false;
|
||||
if (left.presets.length !== right.presets.length) return false;
|
||||
for (let i = 0; i < left.presets.length; ++i)
|
||||
{
|
||||
if (!PluginUiPreset.equals(left.presets[i],right.presets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
pluginUri: string = "";
|
||||
presets: PluginUiPreset[] = [];
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { PiPedalModel, PiPedalModelFactory, PluginPresetsChangedHandle } from './PiPedalModel';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import PluginPresetsDialog from './PluginPresetsDialog';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import RenameDialog from './RenameDialog'
|
||||
import {PluginUiPresets} from './PluginPreset';
|
||||
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
import PluginPresetsIcon from "./svg/ic_pluginpreset.svg?react";
|
||||
import PluginPresetIcon from "./svg/ic_pluginpreset2.svg?react";
|
||||
|
||||
interface PluginPresetSelectorProps extends WithStyles<typeof styles> {
|
||||
pluginUri?: string;
|
||||
instanceId: number;
|
||||
}
|
||||
|
||||
|
||||
interface PluginPresetSelectorState {
|
||||
presets : PluginUiPresets;
|
||||
enabled: boolean;
|
||||
presetChanged: boolean;
|
||||
showPresetsDialog: boolean;
|
||||
showEditPresetsDialog: boolean;
|
||||
presetsMenuAnchorRef: HTMLElement | null;
|
||||
|
||||
renameDialogOpen: boolean;
|
||||
renameDialogDefaultName: string;
|
||||
renameDialogActionName: string;
|
||||
renameDialogOnOk?: (name: string) => void;
|
||||
|
||||
saveAsName: string;
|
||||
}
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
itemIcon: {
|
||||
width: 24, height: 24, marginRight: "4px", opacity: 0.6
|
||||
},
|
||||
pluginIcon: {
|
||||
width: 24, height: 24, opacity: 0.6,fill: theme.palette.text.primary
|
||||
},
|
||||
pluginMenuIcon: {
|
||||
width: 24, height: 24, opacity: 0.6,fill: theme.palette.text.primary,marginRight: 4
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
const PluginPresetSelector =
|
||||
withStyles(
|
||||
class extends Component<PluginPresetSelectorProps, PluginPresetSelectorState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PluginPresetSelectorProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
presets: new PluginUiPresets(),
|
||||
enabled: false,
|
||||
presetChanged: false,
|
||||
showPresetsDialog: false,
|
||||
showEditPresetsDialog: false,
|
||||
presetsMenuAnchorRef: null,
|
||||
renameDialogOpen: false,
|
||||
renameDialogDefaultName: "",
|
||||
renameDialogActionName: "",
|
||||
renameDialogOnOk: undefined,
|
||||
saveAsName: ""
|
||||
|
||||
};
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
|
||||
}
|
||||
|
||||
handleLoadPluginPreset(instanceId: number)
|
||||
{
|
||||
this.handlePresetsMenuClose();
|
||||
this.model.loadPluginPreset(this.props.instanceId,instanceId);
|
||||
let presetName = this.getPresetName(instanceId);
|
||||
this.setState({saveAsName: presetName});
|
||||
}
|
||||
getPresetName(instanceId: number)
|
||||
{
|
||||
for (let preset of this.state.presets.presets)
|
||||
{
|
||||
if (preset.instanceId === instanceId)
|
||||
{
|
||||
return preset.label;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
handlePresetMenuClick(e: SyntheticEvent): void {
|
||||
this.setState({ presetsMenuAnchorRef: (e.currentTarget as HTMLElement) });
|
||||
}
|
||||
handlePresetsMenuClose(): void {
|
||||
this.setState({ presetsMenuAnchorRef: null });
|
||||
}
|
||||
|
||||
handlePluginPresetsMenuSaveAs(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
let name = this.state.saveAsName;
|
||||
|
||||
this.renameDialogOpen(name, "Save As")
|
||||
.then((newName) => {
|
||||
this.setState({saveAsName: newName});
|
||||
return this.model.saveCurrentPluginPresetAs(this.props.instanceId,newName);
|
||||
})
|
||||
.then((newInstanceId) => {
|
||||
// s'fine. dealt with by updates, but we do need error handling.
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
showError(error: string) {
|
||||
this.model.showAlert(error);
|
||||
}
|
||||
renameDialogOpen(defaultText: string, acceptButtonText: string): Promise<string> {
|
||||
let result = new Promise<string>(
|
||||
(resolve, reject) => {
|
||||
this.setState(
|
||||
{
|
||||
renameDialogOpen: true,
|
||||
renameDialogDefaultName: defaultText,
|
||||
renameDialogActionName: acceptButtonText,
|
||||
renameDialogOnOk: (name) => {
|
||||
resolve(name);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
loadPresets():void {
|
||||
if (!this.currentUri) return;
|
||||
let captureUri: string = this.currentUri;
|
||||
this.model.getPluginPresets(captureUri)
|
||||
.then((presets: PluginUiPresets) => {
|
||||
if (captureUri === this.currentUri)
|
||||
{
|
||||
this.setState({presets: presets});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (captureUri === this.currentUri)
|
||||
{
|
||||
this.setState({presets: new PluginUiPresets()});
|
||||
}
|
||||
});
|
||||
}
|
||||
onPluginPresetsChanged(pluginUri: string): void {
|
||||
if (pluginUri === this.props.pluginUri)
|
||||
{
|
||||
this.loadPresets();
|
||||
|
||||
}
|
||||
}
|
||||
_pluginPresetsChangedHandle?: PluginPresetsChangedHandle;
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this._pluginPresetsChangedHandle = this.model.addPluginPresetsChangedListener(
|
||||
(pluginUri: string) => {
|
||||
this.onPluginPresetsChanged(pluginUri);
|
||||
}
|
||||
);
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
if (this._pluginPresetsChangedHandle)
|
||||
{
|
||||
this.model.removePluginPresetsChangedListener(this._pluginPresetsChangedHandle);
|
||||
this._pluginPresetsChangedHandle = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
currentUri?: string = "";
|
||||
componentDidUpdate()
|
||||
{
|
||||
if (this.currentUri !== this.props.pluginUri)
|
||||
{
|
||||
this.currentUri = this.props.pluginUri;
|
||||
if (!this.props.pluginUri)
|
||||
{
|
||||
this.setState({presets: new PluginUiPresets()});
|
||||
} else {
|
||||
this.loadPresets();
|
||||
}
|
||||
}
|
||||
}
|
||||
handleRenameDialogClose(): void {
|
||||
this.setState({
|
||||
renameDialogOpen: false,
|
||||
renameDialogOnOk: undefined
|
||||
});
|
||||
}
|
||||
|
||||
handleRenameDialogOk(name: string): void {
|
||||
let renameDialogOnOk = this.state.renameDialogOnOk;
|
||||
this.handleRenameDialogClose();
|
||||
|
||||
if (renameDialogOnOk) {
|
||||
renameDialogOnOk(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
handleMenuEditPluginPresets(): void {
|
||||
this.handlePresetsMenuClose();
|
||||
this.showEditPluginPresetsDialog(true);
|
||||
}
|
||||
|
||||
handleSave() {
|
||||
this.model.saveCurrentPreset();
|
||||
}
|
||||
|
||||
showPresetDialog(show: boolean) {
|
||||
this.setState({
|
||||
showPresetsDialog: show,
|
||||
showEditPresetsDialog: false
|
||||
});
|
||||
}
|
||||
showEditPluginPresetsDialog(show: boolean) {
|
||||
this.setState({
|
||||
showPresetsDialog: show,
|
||||
showEditPresetsDialog: true
|
||||
});
|
||||
}
|
||||
|
||||
handleDialogClose(): void {
|
||||
this.showPresetDialog(false);
|
||||
}
|
||||
handleChange(event: any, extra: any): void {
|
||||
// misses click on default.
|
||||
// this.model.loadPreset(event.target.value as number);
|
||||
}
|
||||
handleSelectClose(event: any): void {
|
||||
let value = event.currentTarget.getAttribute("data-value");
|
||||
if (value && value.length > 0) {
|
||||
this.model.loadPreset(parseInt(value));
|
||||
}
|
||||
//this.model.loadPreset(event.target.value as number);
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
if ((!this.props.pluginUri))
|
||||
{
|
||||
return (<div/>);
|
||||
}
|
||||
return (
|
||||
<div >
|
||||
<IconButton onClick={(e)=> this.handlePresetMenuClick(e)} size="large">
|
||||
<PluginPresetsIcon className={classes.pluginIcon}/>
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="edit-plugin-presets-menu"
|
||||
anchorEl={this.state.presetsMenuAnchorRef}
|
||||
open={Boolean(this.state.presetsMenuAnchorRef)}
|
||||
onClose={() => this.handlePresetsMenuClose()}
|
||||
TransitionComponent={Fade}
|
||||
MenuListProps={
|
||||
{
|
||||
style: {minWidth: 180}
|
||||
}
|
||||
}
|
||||
|
||||
>
|
||||
<MenuItem onClick={(e) => this.handlePluginPresetsMenuSaveAs(e)}>Save plugin preset...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handleMenuEditPluginPresets()}>Manage plugin presets...</MenuItem>
|
||||
|
||||
{ this.state.presets.presets.length !== 0 &&
|
||||
(<Divider/>)
|
||||
}
|
||||
{
|
||||
this.state.presets.presets.map((preset) => {
|
||||
return (<MenuItem key={preset.instanceId}
|
||||
onClick={(e) => this.handleLoadPluginPreset(preset.instanceId)}
|
||||
>
|
||||
<PluginPresetIcon className={classes.pluginMenuIcon}/>
|
||||
|
||||
{preset.label}</MenuItem>);
|
||||
})
|
||||
}
|
||||
</Menu>
|
||||
<PluginPresetsDialog
|
||||
instanceId={this.props.instanceId}
|
||||
presets={this.state.presets}
|
||||
show={this.state.showPresetsDialog}
|
||||
isEditDialog={this.state.showEditPresetsDialog}
|
||||
onDialogClose={() => this.handleDialogClose()} />
|
||||
<RenameDialog open={this.state.renameDialogOpen}
|
||||
defaultName={this.state.renameDialogDefaultName}
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default PluginPresetSelector;
|
||||
@@ -0,0 +1,505 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { SyntheticEvent,Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonBase from "@mui/material/ButtonBase";
|
||||
import DialogEx from './DialogEx';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
import {PluginUiPresets,PluginUiPreset} from './PluginPreset';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import RenameDialog from './RenameDialog';
|
||||
|
||||
import Slide, {SlideProps} from '@mui/material/Slide';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import PluginPresetIcon from "./svg/ic_pluginpreset2.svg?react";
|
||||
|
||||
import DownloadIcon from './svg/file_download_black_24dp.svg?react';
|
||||
import UploadIcon from './svg/file_upload_black_24dp.svg?react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
interface PluginPresetsDialogProps extends WithStyles<typeof styles> {
|
||||
show: boolean;
|
||||
isEditDialog: boolean;
|
||||
instanceId: number;
|
||||
presets :PluginUiPresets;
|
||||
|
||||
onDialogClose: () => void;
|
||||
|
||||
};
|
||||
|
||||
interface PluginPresetsDialogState {
|
||||
|
||||
showActionBar: boolean;
|
||||
|
||||
selectedItem: number;
|
||||
|
||||
renameOpen: boolean;
|
||||
|
||||
moreMenuAnchorEl: HTMLElement | null;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
listIcon: css({
|
||||
width: 24, height: 24, opacity: 0.6,fill: theme.palette.text.primary
|
||||
}),
|
||||
dialogAppBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}),
|
||||
itemBackground: css({
|
||||
background: theme.palette.background.paper
|
||||
}),
|
||||
dialogActionBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0,
|
||||
background: "black"
|
||||
}),
|
||||
dialogTitle: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
textOverflow: "ellipsis",
|
||||
flex: "1 1",
|
||||
}),
|
||||
itemFrame: css({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
height: "56px",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 8
|
||||
}),
|
||||
iconFrame: css({
|
||||
flex: "0 0 auto",
|
||||
|
||||
}),
|
||||
itemIcon: css({
|
||||
width: 24, height: 24, margin: 12, opacity: 0.6,
|
||||
fill: theme.palette.text.primary
|
||||
}),
|
||||
itemLabel: css({
|
||||
flex: "1 1 1px",
|
||||
marginLeft: 8
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: SlideProps, ref: React.Ref<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
|
||||
const PluginPresetsDialog = withStyles(
|
||||
class extends Component<PluginPresetsDialogProps, PluginPresetsDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
|
||||
constructor(props: PluginPresetsDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.state = {
|
||||
showActionBar: false,
|
||||
selectedItem: -1,
|
||||
renameOpen: false,
|
||||
moreMenuAnchorEl: null,
|
||||
openUploadPresetDialog: false
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
selectItemAtIndex(index: number) {
|
||||
let instanceId = this.props.presets.presets[index].instanceId;
|
||||
this.setState({ selectedItem: instanceId });
|
||||
}
|
||||
isEditMode() {
|
||||
return this.state.showActionBar || this.props.isEditDialog;
|
||||
}
|
||||
|
||||
onMoreClick(e: SyntheticEvent): void {
|
||||
this.setState({ moreMenuAnchorEl: e.currentTarget as HTMLElement })
|
||||
}
|
||||
|
||||
handleDownloadPresets() {
|
||||
this.handleMoreClose();
|
||||
if (this.props.presets.pluginUri !== "")
|
||||
{
|
||||
this.model.download("downloadPluginPresets", this.props.presets.pluginUri);
|
||||
}
|
||||
}
|
||||
handleUploadPresets() {
|
||||
this.handleMoreClose();
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
handleMoreClose(): void {
|
||||
this.setState({ moreMenuAnchorEl: null });
|
||||
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
if (!this.props.presets.getItem(this.state.selectedItem))
|
||||
{
|
||||
if (this.props.presets.presets.length !== 0)
|
||||
{
|
||||
this.setState({selectedItem: this.props.presets.presets[0].instanceId});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
// scroll selected item into view.
|
||||
}
|
||||
componentWillUnmount() {
|
||||
}
|
||||
|
||||
getSelectedIndex() {
|
||||
let instanceId = this.state.selectedItem;
|
||||
let presets = this.props.presets;
|
||||
for (let i = 0; i < presets.presets.length; ++i) {
|
||||
if (presets.presets[i].instanceId === instanceId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
handleDeleteClick() {
|
||||
let selectedItem = this.state.selectedItem;
|
||||
if (selectedItem !== -1) {
|
||||
let newPresets = this.props.presets.clone();
|
||||
for (let i = 0; i < newPresets.presets.length; ++i)
|
||||
{
|
||||
if (newPresets.presets[i].instanceId === selectedItem)
|
||||
{
|
||||
newPresets.presets.splice(i,1);
|
||||
this.model.updatePluginPresets(newPresets.pluginUri,newPresets)
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
|
||||
});
|
||||
let newPos = i;
|
||||
if (newPos >= newPresets.presets.length) {
|
||||
--i;
|
||||
}
|
||||
let newSelection = i < 0? -1: newPresets.presets[i].instanceId;
|
||||
this.setState({selectedItem: newSelection});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handleDialogClose() {
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
|
||||
handleItemClick(instanceId: number): void {
|
||||
if (this.isEditMode()) {
|
||||
this.setState({ selectedItem: instanceId });
|
||||
} else {
|
||||
this.model.loadPreset(instanceId);
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
}
|
||||
showActionBar(show: boolean): void {
|
||||
this.setState({ showActionBar: show });
|
||||
|
||||
}
|
||||
|
||||
|
||||
mapElement(el: any): React.ReactNode {
|
||||
let presetEntry = el as PluginUiPreset;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let selectedItem = this.state.selectedItem;
|
||||
return (
|
||||
<div key={presetEntry.instanceId} className="backgroundItem" >
|
||||
|
||||
<ButtonBase style={{ width: "100%", height: 48 }}
|
||||
onClick={() => this.handleItemClick(presetEntry.instanceId)}
|
||||
>
|
||||
<SelectHoverBackground selected={presetEntry.instanceId === selectedItem} showHover={true} />
|
||||
<div className={classes.itemFrame}>
|
||||
<div className={classes.iconFrame}>
|
||||
<PluginPresetIcon className={classes.itemIcon} />
|
||||
</div>
|
||||
<div className={classes.itemLabel}>
|
||||
<Typography noWrap>
|
||||
{presetEntry.label}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
updateServerPluginPresets(newPresets: PluginUiPresets) {
|
||||
newPresets = newPresets.clone();
|
||||
this.model.updatePluginPresets(this.getPluginUri(),newPresets)
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
}
|
||||
moveElement(from: number, to: number): void {
|
||||
let newPresets: PluginUiPresets = this.props.presets.clone();
|
||||
newPresets.movePreset(from, to);
|
||||
this.setState({
|
||||
selectedItem: newPresets.presets[to].instanceId
|
||||
});
|
||||
this.updateServerPluginPresets(newPresets);
|
||||
}
|
||||
|
||||
getSelectedName(): string {
|
||||
let item = this.props.presets.getItem(this.state.selectedItem);
|
||||
if (item) return item.label;
|
||||
return "";
|
||||
}
|
||||
|
||||
handleRenameClick() {
|
||||
let item = this.props.presets.getItem(this.state.selectedItem);
|
||||
if (item) {
|
||||
this.setState({ renameOpen: true });
|
||||
}
|
||||
}
|
||||
handleRenameOk(text: string) {
|
||||
let item = this.props.presets.getItem(this.state.selectedItem);
|
||||
if (!item) return;
|
||||
if (item.label !== text) {
|
||||
let newPresets = this.props.presets.clone();
|
||||
let newItem = newPresets.getItem(this.state.selectedItem);
|
||||
if (!newItem) return;
|
||||
newItem.label = text;
|
||||
this.updateServerPluginPresets(newPresets);
|
||||
}
|
||||
|
||||
this.setState({ renameOpen: false });
|
||||
}
|
||||
getPluginUri() : string {
|
||||
let pedalboardItem = this.model.pedalboard.get().getItem(this.props.instanceId);
|
||||
return pedalboardItem.uri;
|
||||
}
|
||||
handleCopy() {
|
||||
let item = this.props.presets.getItem(this.state.selectedItem);
|
||||
if (!item) return;
|
||||
|
||||
this.model.duplicatePluginPreset(this.getPluginUri(),this.state.selectedItem)
|
||||
.then((newId) => {
|
||||
this.setState({ selectedItem: newId });
|
||||
}).catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
onError(error: string): void {
|
||||
this.model?.showAlert(error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
|
||||
let defaultSelectedIndex = this.getSelectedIndex();
|
||||
let title: string = "";
|
||||
let pluginUri = this.getPluginUri();
|
||||
let plugin = this.model.getUiPlugin(pluginUri);
|
||||
if (plugin) {
|
||||
title = "Plugin Presets - " + plugin.name;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="pluginPresets" fullScreen open={this.props.show}
|
||||
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
|
||||
style={{userSelect: "none"}}
|
||||
onEnterKey={()=>{}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
disabled={this.isEditMode()}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
|
||||
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
|
||||
>
|
||||
<Toolbar>
|
||||
{(!this.props.isEditDialog) ? (
|
||||
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
)}
|
||||
<Typography noWrap variant="h6" className={classes.dialogTitle}>
|
||||
{ title }
|
||||
</Typography>
|
||||
{(this.props.presets.getItem(this.state.selectedItem) != null)
|
||||
&& (
|
||||
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Button color="inherit" onClick={(e) => this.handleCopy()}>
|
||||
Copy
|
||||
</Button>
|
||||
<Button color="inherit" onClick={() => this.handleRenameClick()}>
|
||||
Rename
|
||||
</Button>
|
||||
<RenameDialog
|
||||
open={this.state.renameOpen}
|
||||
defaultName={this.getSelectedName()}
|
||||
acceptActionName={"Rename"}
|
||||
onClose={() => { this.setState({ renameOpen: false }) }}
|
||||
onOk={(text: string) => {
|
||||
this.handleRenameOk(text);
|
||||
}
|
||||
}
|
||||
/>
|
||||
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
|
||||
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="more-menu"
|
||||
anchorEl={this.state.moreMenuAnchorEl}
|
||||
keepMounted
|
||||
open={Boolean(this.state.moreMenuAnchorEl)}
|
||||
onClose={() => this.handleMoreClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => { this.handleDownloadPresets(); }}
|
||||
disabled={this.props.presets.presets.length === 0}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DownloadIcon className={classes.listIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Download plugin presets
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { this.handleUploadPresets() }}>
|
||||
<ListItemIcon>
|
||||
<UploadIcon className={classes.listIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Upload plugin presets
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Toolbar>
|
||||
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
|
||||
<DraggableGrid
|
||||
onLongPress={(item) => this.showActionBar(true)}
|
||||
canDrag={this.isEditMode()}
|
||||
onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }}
|
||||
moveElement={(from, to) => { this.moveElement(from, to); }}
|
||||
scroll={ScrollDirection.Y}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
>
|
||||
{
|
||||
this.props.presets.presets.map((element) => {
|
||||
return this.mapElement(element);
|
||||
})
|
||||
}
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadPresetDialog
|
||||
title="Upload Plugin Presets"
|
||||
extension='.piPluginPresets'
|
||||
uploadPage='uploadPluginPresets'
|
||||
onUploaded={(instanceId) => {} }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadPresetDialog}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default PluginPresetsDialog;
|
||||
@@ -0,0 +1,483 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { SyntheticEvent,Component } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {isDarkMode} from './DarkMode';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonBase from "@mui/material/ButtonBase";
|
||||
import DialogEx from './DialogEx';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import RenameDialog from './RenameDialog';
|
||||
|
||||
import Slide, {SlideProps} from '@mui/material/Slide';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
|
||||
import DownloadIcon from './svg/file_download_black_24dp.svg?react';
|
||||
import UploadIcon from './svg/file_upload_black_24dp.svg?react';
|
||||
|
||||
|
||||
interface PresetDialogProps extends WithStyles<typeof styles> {
|
||||
show: boolean;
|
||||
isEditDialog: boolean;
|
||||
onDialogClose: () => void;
|
||||
|
||||
};
|
||||
|
||||
interface PresetDialogState {
|
||||
presets: PresetIndex;
|
||||
|
||||
showActionBar: boolean;
|
||||
|
||||
selectedItem: number;
|
||||
|
||||
renameOpen: boolean;
|
||||
|
||||
moreMenuAnchorEl: HTMLElement | null;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
listIcon: css({
|
||||
width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary
|
||||
}),
|
||||
dialogAppBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}),
|
||||
dialogActionBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0,
|
||||
background: "black"
|
||||
}),
|
||||
dialogTitle: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
}),
|
||||
itemBackground: css({
|
||||
background: theme.palette.background.paper
|
||||
}),
|
||||
itemFrame: css({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
height: "56px",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 8
|
||||
}),
|
||||
iconFrame: css({
|
||||
flex: "0 0 auto",
|
||||
|
||||
}),
|
||||
itemIcon: css({
|
||||
width: 24, height: 24, margin: 12, opacity: 0.6
|
||||
}),
|
||||
itemLabel: css({
|
||||
flex: "1 1 1px",
|
||||
marginLeft: 8
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: SlideProps, ref: React.Ref<unknown>
|
||||
) {
|
||||
return (<Slide direction="up" ref={ref} {...props} />);
|
||||
});
|
||||
|
||||
|
||||
const PresetDialog = withStyles(
|
||||
class extends Component<PresetDialogProps, PresetDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
|
||||
constructor(props: PresetDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
let presets = this.model.presets.get();
|
||||
this.state = {
|
||||
presets: presets,
|
||||
showActionBar: false,
|
||||
selectedItem: presets.selectedInstanceId,
|
||||
renameOpen: false,
|
||||
moreMenuAnchorEl: null,
|
||||
openUploadPresetDialog: false
|
||||
|
||||
};
|
||||
this.handlePresetsChanged = this.handlePresetsChanged.bind(this);
|
||||
|
||||
}
|
||||
|
||||
selectItemAtIndex(index: number) {
|
||||
let instanceId = this.state.presets.presets[index].instanceId;
|
||||
this.setState({ selectedItem: instanceId });
|
||||
}
|
||||
isEditMode() {
|
||||
return this.state.showActionBar || this.props.isEditDialog;
|
||||
}
|
||||
|
||||
onMoreClick(e: SyntheticEvent): void {
|
||||
this.setState({ moreMenuAnchorEl: e.currentTarget as HTMLElement })
|
||||
}
|
||||
|
||||
handleDownloadPreset() {
|
||||
this.handleMoreClose();
|
||||
this.model.download("downloadPreset", this.state.selectedItem);
|
||||
}
|
||||
handleUploadPreset() {
|
||||
this.handleMoreClose();
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
handleMoreClose(): void {
|
||||
this.setState({ moreMenuAnchorEl: null });
|
||||
|
||||
}
|
||||
|
||||
|
||||
handlePresetsChanged() {
|
||||
let presets = this.model.presets.get();
|
||||
|
||||
if (!presets.areEqual(this.state.presets, false)) // avoid a bunch of peculiar effects if we update while a drag is in progress
|
||||
{
|
||||
// if we don't have a valid selection, then use the current preset.
|
||||
if (this.state.presets.getItem(this.state.selectedItem) == null) {
|
||||
this.setState({ presets: presets, selectedItem: presets.selectedInstanceId });
|
||||
} else {
|
||||
this.setState({ presets: presets });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.model.presets.addOnChangedHandler(this.handlePresetsChanged);
|
||||
this.handlePresetsChanged();
|
||||
// scroll selected item into view.
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged);
|
||||
}
|
||||
|
||||
getSelectedIndex() {
|
||||
let instanceId = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId;
|
||||
let presets = this.state.presets;
|
||||
for (let i = 0; i < presets.presets.length; ++i) {
|
||||
if (presets.presets[i].instanceId === instanceId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
handleDeleteClick() {
|
||||
if (!this.state.selectedItem) return;
|
||||
let selectedItem = this.state.selectedItem;
|
||||
if (selectedItem !== -1) {
|
||||
this.model.deletePresetItem(selectedItem)
|
||||
.then((selectedItem: number) => {
|
||||
this.setState({ selectedItem: selectedItem });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
handleDialogClose() {
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
|
||||
handleItemClick(instanceId: number): void {
|
||||
if (this.isEditMode()) {
|
||||
this.setState({ selectedItem: instanceId });
|
||||
} else {
|
||||
this.model.loadPreset(instanceId);
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
}
|
||||
showActionBar(show: boolean): void {
|
||||
this.setState({ showActionBar: show });
|
||||
|
||||
}
|
||||
|
||||
|
||||
mapElement(el: any): React.ReactNode {
|
||||
let presetEntry = el as PresetIndexEntry;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId;
|
||||
return (
|
||||
<div key={presetEntry.instanceId} className="itemBackground">
|
||||
|
||||
<ButtonBase style={{ width: "100%", height: 48 }}
|
||||
onClick={() => this.handleItemClick(presetEntry.instanceId)}
|
||||
>
|
||||
<SelectHoverBackground selected={presetEntry.instanceId === selectedItem} showHover={true} />
|
||||
<div className={classes.itemFrame}>
|
||||
<div className={classes.iconFrame}>
|
||||
<img
|
||||
src={isDarkMode()? "img/ic_presets_white.svg": "img/ic_presets.svg"}
|
||||
className={classes.itemIcon} alt="" />
|
||||
</div>
|
||||
<div className={classes.itemLabel}>
|
||||
<Typography>
|
||||
{presetEntry.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
updateServerPresets(newPresets: PresetIndex) {
|
||||
newPresets = newPresets.clone();
|
||||
this.model.updatePresets(newPresets)
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
}
|
||||
moveElement(from: number, to: number): void {
|
||||
let newPresets = this.state.presets.clone();
|
||||
newPresets.movePreset(from, to);
|
||||
this.setState({
|
||||
presets: newPresets,
|
||||
selectedItem: newPresets.presets[to].instanceId
|
||||
});
|
||||
this.updateServerPresets(newPresets);
|
||||
}
|
||||
|
||||
getSelectedName(): string {
|
||||
let item = this.state.presets.getItem(this.state.selectedItem);
|
||||
if (item) return item.name;
|
||||
return "";
|
||||
}
|
||||
|
||||
handleRenameClick() {
|
||||
let item = this.state.presets.getItem(this.state.selectedItem);
|
||||
if (item) {
|
||||
this.setState({ renameOpen: true });
|
||||
}
|
||||
}
|
||||
handleRenameOk(text: string) {
|
||||
let item = this.state.presets.getItem(this.state.selectedItem);
|
||||
if (!item) return;
|
||||
if (item.name !== text) {
|
||||
this.model.renamePresetItem(this.state.selectedItem, text)
|
||||
.catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ renameOpen: false });
|
||||
}
|
||||
handleCopy() {
|
||||
let item = this.state.presets.getItem(this.state.selectedItem);
|
||||
if (!item) return;
|
||||
this.model.duplicatePreset(this.state.selectedItem)
|
||||
.then((newId) => {
|
||||
this.setState({ selectedItem: newId });
|
||||
}).catch((error) => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
|
||||
onError(error: string): void {
|
||||
this.model?.showAlert(error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
|
||||
let defaultSelectedIndex = this.getSelectedIndex();
|
||||
|
||||
return (
|
||||
<DialogEx tag="preset" fullScreen open={this.props.show}
|
||||
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
|
||||
style={{userSelect: "none"}}
|
||||
onEnterKey={()=>{}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
disabled={this.isEditMode()}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.dialogTitle}>
|
||||
Presets
|
||||
</Typography>
|
||||
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
|
||||
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
|
||||
>
|
||||
<Toolbar>
|
||||
{(!this.props.isEditDialog) ? (
|
||||
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
)}
|
||||
<Typography variant="h6" className={classes.dialogTitle}>
|
||||
Presets
|
||||
</Typography>
|
||||
{(this.state.presets.getItem(this.state.selectedItem) != null)
|
||||
&& (
|
||||
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<Button color="inherit" onClick={(e) => this.handleCopy()}>
|
||||
Copy
|
||||
</Button>
|
||||
<Button color="inherit" onClick={() => this.handleRenameClick()}>
|
||||
Rename
|
||||
</Button>
|
||||
<RenameDialog
|
||||
open={this.state.renameOpen}
|
||||
defaultName={this.getSelectedName()}
|
||||
acceptActionName={"Rename"}
|
||||
onClose={() => { this.setState({ renameOpen: false }) }}
|
||||
onOk={(text: string) => {
|
||||
this.handleRenameOk(text);
|
||||
}
|
||||
}
|
||||
/>
|
||||
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
|
||||
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="more-menu"
|
||||
anchorEl={this.state.moreMenuAnchorEl}
|
||||
keepMounted
|
||||
open={Boolean(this.state.moreMenuAnchorEl)}
|
||||
onClose={() => this.handleMoreClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
|
||||
<ListItemIcon>
|
||||
<DownloadIcon className={classes.listIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Download preset
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { this.handleUploadPreset() }}>
|
||||
<ListItemIcon>
|
||||
<UploadIcon className={classes.listIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Upload preset
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Toolbar>
|
||||
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
|
||||
<DraggableGrid
|
||||
onLongPress={(item) => this.showActionBar(true)}
|
||||
canDrag={this.isEditMode()}
|
||||
onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }}
|
||||
moveElement={(from, to) => { this.moveElement(from, to); }}
|
||||
scroll={ScrollDirection.Y}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
>
|
||||
{
|
||||
this.state.presets.presets.map((element) => {
|
||||
return this.mapElement(element);
|
||||
})
|
||||
}
|
||||
</DraggableGrid>
|
||||
</div>
|
||||
</div>
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadPresetDialog}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</DialogEx>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default PresetDialog;
|
||||
@@ -0,0 +1,387 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
|
||||
import SaveIconOutline from '@mui/icons-material/Save';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import PresetDialog from './PresetDialog';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import RenameDialog from './RenameDialog'
|
||||
import Select from '@mui/material/Select';
|
||||
import UploadPresetDialog from './UploadPresetDialog';
|
||||
import {isDarkMode} from './DarkMode';
|
||||
|
||||
interface PresetSelectorProps extends WithStyles<typeof styles> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface PresetSelectorState {
|
||||
presets: PresetIndex;
|
||||
enabled: boolean;
|
||||
showPresetsDialog: boolean;
|
||||
showEditPresetsDialog: boolean;
|
||||
presetsMenuAnchorRef: HTMLElement | null;
|
||||
|
||||
renameDialogOpen: boolean;
|
||||
renameDialogDefaultName: string;
|
||||
renameDialogActionName: string;
|
||||
renameDialogOnOk?: (name: string) => void;
|
||||
openUploadPresetDialog: boolean;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const selectColor = isDarkMode()? "#888": "#FFFFFF";
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
select: { // fu fu fu.Overrides for white selector on dark background.
|
||||
'&:before': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:after': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:hover:not(.Mui-disabled):before': {
|
||||
borderColor: selectColor,
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
fill: selectColor,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const PresetSelector =
|
||||
withStyles(
|
||||
class extends Component<PresetSelectorProps, PresetSelectorState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PresetSelectorProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
presets:
|
||||
this.model.presets.get(),
|
||||
enabled: false,
|
||||
showPresetsDialog: false,
|
||||
showEditPresetsDialog: false,
|
||||
presetsMenuAnchorRef: null,
|
||||
renameDialogOpen: false,
|
||||
renameDialogDefaultName: "",
|
||||
renameDialogActionName: "",
|
||||
renameDialogOnOk: undefined,
|
||||
openUploadPresetDialog: false
|
||||
|
||||
|
||||
};
|
||||
this.handlePresetsChanged = this.handlePresetsChanged.bind(this);
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
|
||||
}
|
||||
|
||||
|
||||
handlePresetMenuClick(event: SyntheticEvent): void {
|
||||
this.setState({ presetsMenuAnchorRef: (event.currentTarget as HTMLElement) });
|
||||
}
|
||||
handlePresetsMenuClose(): void {
|
||||
this.setState({ presetsMenuAnchorRef: null });
|
||||
}
|
||||
|
||||
handleDownloadPreset(e: SyntheticEvent) {
|
||||
this.handlePresetsMenuClose();
|
||||
e.preventDefault();
|
||||
|
||||
this.model.download("downloadPreset", this.model.presets.get().selectedInstanceId);
|
||||
}
|
||||
handleUploadPreset(e: SyntheticEvent) {
|
||||
this.handlePresetsMenuClose();
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ openUploadPresetDialog: true });
|
||||
|
||||
}
|
||||
|
||||
handlePresetsMenuSave(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.preventDefault();
|
||||
|
||||
this.model.saveCurrentPreset();
|
||||
}
|
||||
handlePresetsMenuSaveAs(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
let currentPresets = this.model.presets.get();
|
||||
let item = currentPresets.getItem(currentPresets.selectedInstanceId);
|
||||
if (item == null) return;
|
||||
let name = item.name;
|
||||
|
||||
this.renameDialogOpen(name, "Save As")
|
||||
.then((newName) => {
|
||||
return this.model.saveCurrentPresetAs(newName);
|
||||
})
|
||||
.then((newInstanceId) => {
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
})
|
||||
;
|
||||
|
||||
|
||||
}
|
||||
handlePresetsMenuRename(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
let currentPresets = this.model.presets.get();
|
||||
let item = currentPresets.getItem(currentPresets.selectedInstanceId);
|
||||
if (item == null) return;
|
||||
let name = item.name;
|
||||
|
||||
this.renameDialogOpen(name, "Rename")
|
||||
.then((newName) => {
|
||||
if (newName === name) return;
|
||||
return this.model.renamePresetItem(this.model.presets.get().selectedInstanceId, newName);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
})
|
||||
;
|
||||
}
|
||||
handlePresetsMenuNew(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
let currentPresets = this.model.presets.get();
|
||||
let item = currentPresets.getItem(currentPresets.selectedInstanceId);
|
||||
if (item == null) return;
|
||||
this.model.newPresetItem(currentPresets.selectedInstanceId)
|
||||
.then((instanceId) => {
|
||||
this.model.loadPreset(instanceId);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
showError(error: string) {
|
||||
this.model.showAlert(error);
|
||||
}
|
||||
renameDialogOpen(defaultText: string, acceptButtonText: string): Promise<string> {
|
||||
let result = new Promise<string>(
|
||||
(resolve, reject) => {
|
||||
this.setState(
|
||||
{
|
||||
renameDialogOpen: true,
|
||||
renameDialogDefaultName: defaultText,
|
||||
renameDialogActionName: acceptButtonText,
|
||||
renameDialogOnOk: (name) => {
|
||||
resolve(name);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
handleRenameDialogClose(): void {
|
||||
this.setState({
|
||||
renameDialogOpen: false,
|
||||
renameDialogOnOk: undefined
|
||||
});
|
||||
}
|
||||
|
||||
handleRenameDialogOk(name: string): void {
|
||||
let renameDialogOnOk = this.state.renameDialogOnOk;
|
||||
this.handleRenameDialogClose();
|
||||
|
||||
if (renameDialogOnOk) {
|
||||
renameDialogOnOk(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
handleMenuEditPresets(): void {
|
||||
this.handlePresetsMenuClose();
|
||||
this.showEditPresetsDialog(true);
|
||||
}
|
||||
|
||||
updatePresetState() {
|
||||
let presets = this.model.presets.get();
|
||||
let enabled = presets.presets.length > 0 && presets.selectedInstanceId !== -1;
|
||||
this.setState(
|
||||
{
|
||||
presets: presets,
|
||||
enabled: enabled,
|
||||
}
|
||||
);
|
||||
}
|
||||
handleSave() {
|
||||
this.model.saveCurrentPreset();
|
||||
}
|
||||
handlePresetsChanged() {
|
||||
this.updatePresetState();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.model.presets.addOnChangedHandler(this.handlePresetsChanged);
|
||||
this.updatePresetState();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged)
|
||||
}
|
||||
showPresetDialog(show: boolean) {
|
||||
this.setState({
|
||||
showPresetsDialog: show,
|
||||
showEditPresetsDialog: false
|
||||
});
|
||||
}
|
||||
showEditPresetsDialog(show: boolean) {
|
||||
this.setState({
|
||||
showPresetsDialog: show,
|
||||
showEditPresetsDialog: true
|
||||
});
|
||||
}
|
||||
|
||||
handleDialogClose(): void {
|
||||
this.showPresetDialog(false);
|
||||
}
|
||||
handleChange(event: any, extra: any): void {
|
||||
// misses click on default.
|
||||
// this.model.loadPreset(event.target.value as number);
|
||||
}
|
||||
handleSelectClose(event: any): void {
|
||||
let value = event.currentTarget.getAttribute("data-value");
|
||||
if (value && value.length > 0) {
|
||||
this.model.loadPreset(parseInt(value));
|
||||
}
|
||||
//this.model.loadPreset(event.target.value as number);
|
||||
}
|
||||
|
||||
render() {
|
||||
//const classes = withStyles.getClasses(this.props);
|
||||
let presets = this.state.presets;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
return (
|
||||
<div style={{
|
||||
marginLeft: 12, display: "flex", flexDirection: "row",
|
||||
justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative"
|
||||
}}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0, color: "#FFFFFF" }}
|
||||
onClick={(e) => { this.handleSave(); }}
|
||||
size="large">
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} color="inherit" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: "1 1 auto", minWidth: 60, maxWidth: 300, position: "relative", paddingRight: 12 }} >
|
||||
<Select variant="standard"
|
||||
className={classes.select}
|
||||
style={{ width: "100%", position: "relative", top: 0, color: "#FFFFFF" }} disabled={!this.state.enabled}
|
||||
onChange={(e, extra) => this.handleChange(e, extra)}
|
||||
onClose={(e) => this.handleSelectClose(e)}
|
||||
displayEmpty
|
||||
value={presets.selectedInstanceId === 0? '' : presets.selectedInstanceId}
|
||||
inputProps={{
|
||||
classes: { icon: classes.icon },
|
||||
'aria-label': "Select preset"
|
||||
}
|
||||
}
|
||||
>
|
||||
{
|
||||
presets.presets.map((preset) => {
|
||||
return (
|
||||
<MenuItem key={preset.instanceId} value={preset.instanceId} >
|
||||
{(presets.presetChanged && preset.instanceId === presets.selectedInstanceId)
|
||||
? (preset.name + "*")
|
||||
: (preset.name)
|
||||
}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto"}}>
|
||||
<IconButton
|
||||
style={{ flex: "0 0 auto", color: "#FFFFFF" }}
|
||||
onClick={(e) => this.handlePresetMenuClick(e)}
|
||||
size="large"
|
||||
>
|
||||
<MoreVertIcon style={{ opacity: 0.75 }} color="inherit" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="edit-presets-menu"
|
||||
anchorEl={this.state.presetsMenuAnchorRef}
|
||||
open={Boolean(this.state.presetsMenuAnchorRef)}
|
||||
onClose={() => this.handlePresetsMenuClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
|
||||
<MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
<PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()} />
|
||||
<RenameDialog open={this.state.renameDialogOpen}
|
||||
defaultName={this.state.renameDialogDefaultName}
|
||||
acceptActionName={this.state.renameDialogActionName}
|
||||
onClose={() => this.handleRenameDialogClose()}
|
||||
onOk={(name: string) => this.handleRenameDialogOk(name)} />
|
||||
<UploadPresetDialog
|
||||
title='Upload preset'
|
||||
extension='.piPreset'
|
||||
uploadPage='uploadPreset'
|
||||
onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
|
||||
open={this.state.openUploadPresetDialog}
|
||||
uploadAfter={-1}
|
||||
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
styles);
|
||||
|
||||
export default PresetSelector;
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
import Radio from '@mui/material/Radio';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
|
||||
|
||||
export interface RadioSelectDialogProps {
|
||||
open: boolean,
|
||||
title: string,
|
||||
width: number,
|
||||
selectedItem: string,
|
||||
items: string[],
|
||||
onOk: (selectedItem: string) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface RadioSelectDialogState {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export default class RadioSelectDialog extends ResizeResponsiveComponent<RadioSelectDialogProps, RadioSelectDialogState> {
|
||||
|
||||
refText: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: RadioSelectDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
};
|
||||
this.refText = React.createRef<HTMLInputElement>();
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<DialogEx tag="list" onClose={()=>this.props.onClose()} open={this.props.open}
|
||||
onEnterKey={()=>{ }}
|
||||
>
|
||||
<List sx={{pt: 0}}>
|
||||
{
|
||||
this.props.items.map(
|
||||
(value: string, index: number) => {
|
||||
return (
|
||||
<ListItem key={index} >
|
||||
<ListItemButton role={undefined} onClick={()=> this.props.onOk(value)} dense>
|
||||
<ListItemIcon>
|
||||
<Radio
|
||||
edge="start"
|
||||
checked={value === this.props.selectedItem}
|
||||
disableRipple
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={value} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</List>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
const LARGE_NUMBER = 0x1FFFFFFF;
|
||||
const EMPTY_WIDTH = -LARGE_NUMBER * 2;
|
||||
|
||||
class Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(x: number = LARGE_NUMBER, y: number = LARGE_NUMBER, width: number = EMPTY_WIDTH, height: number = EMPTY_WIDTH)
|
||||
{
|
||||
this.x = x; this.y = y; this.width = width; this.height = height;
|
||||
}
|
||||
|
||||
toString() { return '{' + this.x + "," + this.y + " " + this.width + "," + this.height +"}"; }
|
||||
copy(): Rect {
|
||||
let result = new Rect();
|
||||
result.x = this.x;
|
||||
result.y = this.y;
|
||||
result.width = this.width;
|
||||
result.height = this.height;
|
||||
return result;
|
||||
|
||||
}
|
||||
contains(x: number, y: number)
|
||||
{
|
||||
return (x >= this.x)
|
||||
&& x < this.x+this.width
|
||||
&& y >= this.y
|
||||
&& y < this.y + this.height;
|
||||
}
|
||||
get right(): number {
|
||||
return this.x + this.width;
|
||||
}
|
||||
get bottom(): number {
|
||||
return this.y + this.height;
|
||||
}
|
||||
isEmpty(): boolean {
|
||||
return this.width === 0 || this.height === 0;
|
||||
}
|
||||
clear(): void {
|
||||
this.x = LARGE_NUMBER;
|
||||
this.y = LARGE_NUMBER;
|
||||
this.width = EMPTY_WIDTH;
|
||||
this.height = EMPTY_WIDTH;
|
||||
|
||||
}
|
||||
accumulate(rect: Rect): void {
|
||||
let right = Math.max(rect.x + rect.width, this.x + this.width);
|
||||
let bottom = Math.max(rect.y + rect.height, this.y + this.height);
|
||||
this.x = Math.min(rect.x, this.x);
|
||||
this.y = Math.min(rect.y, this.y);
|
||||
this.width = right - this.x;
|
||||
this.height = bottom - this.y;
|
||||
}
|
||||
union(rect: Rect): Rect {
|
||||
let result = new Rect();
|
||||
result.x = Math.min(this.x, rect.x);
|
||||
result.y = Math.min(this.y, rect.y);
|
||||
result.width = Math.max(this.x + this.width, rect.x + rect.width) - result.x;
|
||||
result.height = Math.max(this.y + this.height, rect.x + rect.height) - result.y;
|
||||
return result;
|
||||
}
|
||||
offset(xOffset: number, yOffset: number) {
|
||||
this.x += xOffset;
|
||||
this.y += yOffset;
|
||||
}
|
||||
}
|
||||
|
||||
export default Rect;
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
|
||||
export default class Rectangle {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(top?: number, left?: number, width?: number, height?: number) {
|
||||
this.top = top ? top: 0;
|
||||
this.left = left ? left: 0;
|
||||
this.width = width ? width: 0;
|
||||
this.height = height ? height: 0;
|
||||
}
|
||||
|
||||
get right(): number {
|
||||
return this.left + this.width;
|
||||
}
|
||||
|
||||
get bottom(): number {
|
||||
return this.top + this.height;
|
||||
}
|
||||
|
||||
set right(value: number) {
|
||||
this.width = value - this.left;
|
||||
}
|
||||
|
||||
set bottom(value: number) {
|
||||
this.height = value - this.top;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { nullCast } from './Utility';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||
//import TextFieldEx from './TextFieldEx';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
||||
|
||||
export interface RenameDialogProps {
|
||||
open: boolean,
|
||||
defaultName: string,
|
||||
acceptActionName: string,
|
||||
onOk: (text: string) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface RenameDialogState {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export default class RenameDialog extends ResizeResponsiveComponent<RenameDialogProps, RenameDialogState> {
|
||||
|
||||
refText: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: RenameDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
};
|
||||
this.refText = React.createRef<HTMLInputElement>();
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void
|
||||
{
|
||||
this.setState({fullScreen: height < 200})
|
||||
}
|
||||
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
super.componentWillUnmount();
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
}
|
||||
checkForIllegalCharacters(filename: string) {
|
||||
if (filename.indexOf('/') !== -1)
|
||||
{
|
||||
throw new Error("Illegal character: '/'");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open, defaultName, acceptActionName, onClose, onOk } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
let text = nullCast(this.refText.current).value;
|
||||
text = text.trim();
|
||||
try {
|
||||
this.checkForIllegalCharacters(text);
|
||||
} catch (e:any)
|
||||
{
|
||||
let model:PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
model.showAlert(e.toString());
|
||||
return;
|
||||
}
|
||||
if (text.length === 0) return;
|
||||
onOk(text);
|
||||
}
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
// 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleOk();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DialogEx tag="nameDialog" open={open} fullWidth maxWidth="sm" onClose={handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={this.state.fullScreen}
|
||||
style={{userSelect: "none"}}
|
||||
onEnterKey={()=>{}}
|
||||
>
|
||||
<DialogContent style={{minHeight: 96}}>
|
||||
<TextField
|
||||
|
||||
onKeyDown={handleKeyDown}
|
||||
margin="dense"
|
||||
variant="outlined"
|
||||
slotProps={{
|
||||
input: {
|
||||
style: { scrollMargin: 24 }
|
||||
}
|
||||
}}
|
||||
id="name"
|
||||
label="Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
defaultValue={defaultName}
|
||||
inputRef={this.refText}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions style={{flexShrink: 1}}>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
{acceptActionName}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
class ResizeResponsiveComponent<PROPS={},STATE={},S=any> extends React.Component<PROPS,STATE,S> {
|
||||
constructor(props: PROPS) {
|
||||
super(props);
|
||||
this.handleWindowResize = this.handleWindowResize.bind(this);
|
||||
this.windowSize.width = window.innerWidth;
|
||||
this.windowSize.height = window.innerHeight;
|
||||
}
|
||||
|
||||
windowSize: {width: number,height:number} = {width: 1280, height: 1024};
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
|
||||
}
|
||||
handleWindowResize() {
|
||||
let width_ = window.innerWidth;
|
||||
let height_ = window.innerHeight;
|
||||
if (width_ !== this.windowSize.width || height_ !== this.windowSize.height)
|
||||
{
|
||||
this.windowSize = {width: width_, height: height_};
|
||||
this.onWindowSizeChanged(this.windowSize.width,this.windowSize.height);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
|
||||
let width_ = window.innerWidth;
|
||||
let height_ = window.innerHeight;
|
||||
this.windowSize = {width: width_, height: height_};
|
||||
this.onWindowSizeChanged(this.windowSize.width,this.windowSize.height);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ResizeResponsiveComponent;
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { KeyboardEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles, {withTheme} from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import Input from '@mui/material/Input';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
|
||||
interface SearchControlProps extends WithStyles<typeof styles> {
|
||||
theme: Theme,
|
||||
collapsed?: boolean;
|
||||
searchText: string;
|
||||
onTextChanged: (searchString: string) => void;
|
||||
onClick: () => void;
|
||||
onClearFilterClick: () => void;
|
||||
onApplyFilter: () => void;
|
||||
inputRef?: React.RefObject<HTMLInputElement|null>;
|
||||
showSearchIcon?: boolean
|
||||
|
||||
}
|
||||
|
||||
interface SearchControlState {
|
||||
}
|
||||
|
||||
const SearchControl = withTheme(withStyles(
|
||||
class extends React.Component<SearchControlProps, SearchControlState> {
|
||||
|
||||
defaultInputRef: React.RefObject<HTMLInputElement|null>;
|
||||
|
||||
constructor(props: SearchControlProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
||||
};
|
||||
this.defaultInputRef = React.createRef<HTMLInputElement>();
|
||||
}
|
||||
getInputRef(): React.RefObject<HTMLInputElement|null> {
|
||||
if (this.props.inputRef) {
|
||||
return this.props.inputRef;
|
||||
}
|
||||
return this.defaultInputRef;
|
||||
}
|
||||
onKeyDown(ev: KeyboardEvent)
|
||||
{
|
||||
ev.stopPropagation(); // don't let the ENTER key propagate propagate the enclosing dialog Enter key handler.
|
||||
if (ev.key === "Enter")
|
||||
{
|
||||
this.props.onApplyFilter();
|
||||
} else if (ev.key === "Escape")
|
||||
{
|
||||
this.props.onClearFilterClick();
|
||||
}
|
||||
}
|
||||
componentDidUpdate(oldProps: SearchControlProps) {
|
||||
if (oldProps.collapsed ?? false !== this.props.collapsed ?? false) {
|
||||
let inputRef = this.getInputRef();
|
||||
if (inputRef.current) {
|
||||
if (!(this.props.collapsed ?? false)) {
|
||||
setTimeout(() => {
|
||||
let inputRef = this.getInputRef();
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
} else {
|
||||
inputRef.current.value = "";
|
||||
this.props.onTextChanged("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "flex-end", alignItems: "center" }}>
|
||||
{(this.props.showSearchIcon ?? true) && (
|
||||
<IconButton onClick={() => { this.props.onClick(); }} size="large">
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
}
|
||||
<Input spellCheck={false}
|
||||
onKeyDown={(ev)=>{this.onKeyDown(ev);}}
|
||||
style={{
|
||||
flex: "1 1", visibility: (this.props.collapsed ?? false) ? "collapse" : "visible", marginRight: 12, height: 32,
|
||||
}}
|
||||
inputRef={this.getInputRef()}
|
||||
defaultValue={this.props.searchText}
|
||||
placeholder="Search"
|
||||
endAdornment={(
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="clear search filter"
|
||||
onClick={() => {
|
||||
this.props.onClearFilterClick();
|
||||
let inputRef = this.getInputRef();
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
}}
|
||||
edge="end"
|
||||
size="large">
|
||||
<ClearIcon fontSize='small' style={{ opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
|
||||
)}
|
||||
onChange={
|
||||
(e) => {
|
||||
this.props.onTextChanged(e.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
styles
|
||||
));
|
||||
|
||||
export default SearchControl;
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
|
||||
let escapedCharacters = /[^a-zA-Z0-9]/g;
|
||||
|
||||
function stringToRegex(text: string): RegExp
|
||||
{
|
||||
text = text.replace(escapedCharacters,"\\$&");
|
||||
|
||||
return new RegExp(text,"i");
|
||||
}
|
||||
|
||||
|
||||
class SearchFilter {
|
||||
regexes: RegExp[];
|
||||
|
||||
constructor(text: string)
|
||||
{
|
||||
let regexes: RegExp[] = [];
|
||||
|
||||
let bits = text.split(' ');
|
||||
for (let bit of bits)
|
||||
{
|
||||
if (bit.length !== 0)
|
||||
{
|
||||
regexes.push(stringToRegex(bit));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.regexes = regexes;
|
||||
}
|
||||
|
||||
score(...strings: string[]): number {
|
||||
let score = 0;
|
||||
if (this.regexes.length === 0) return 1;
|
||||
for (let r = 0; r < this.regexes.length; ++r)
|
||||
{
|
||||
let regex = this.regexes[r];
|
||||
regex.lastIndex = 0;
|
||||
let matched = false;
|
||||
for (let i = 0; i < strings.length; ++i)
|
||||
{
|
||||
let s = strings[i];
|
||||
let m = regex.exec(s);
|
||||
if (m) {
|
||||
matched = true;
|
||||
if (m.index === 0)
|
||||
{
|
||||
if (r === 0 && i === 0) {
|
||||
score += 16;
|
||||
} else {
|
||||
score += 8;
|
||||
}
|
||||
}
|
||||
let end = m.index + m[0].length;
|
||||
if (end === s.length || (end < s.length && s[end] === ' ')) // word boundary
|
||||
{
|
||||
score += 2;
|
||||
} else {
|
||||
score += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) return 0;
|
||||
}
|
||||
|
||||
return score;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default SearchFilter;
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogEx from './DialogEx';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import { ListItemButton } from '@mui/material';
|
||||
|
||||
|
||||
|
||||
export interface SelectChannelsDialogProps {
|
||||
open: boolean;
|
||||
selectedChannels: string[];
|
||||
availableChannels: string[];
|
||||
onClose: (selectedChannels: string[] | null) => void;
|
||||
}
|
||||
|
||||
function isChecked(selectedChannels: string[], channel: string): boolean {
|
||||
for (let i = 0; i < selectedChannels.length; ++i) {
|
||||
if (selectedChannels[i] === channel) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function addPort(availableChannels: string[], selectedChannels: string[], newChannel: string) {
|
||||
let result: string[] = [];
|
||||
for (let i = 0; i < availableChannels.length; ++i) {
|
||||
let channel = availableChannels[i];
|
||||
if (isChecked(selectedChannels, channel) || channel === newChannel) {
|
||||
result.push(channel);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function removePort(selectedChannels: string[], channel: string) {
|
||||
let result: string[] = [];
|
||||
for (let i = 0; i < selectedChannels.length; ++i) {
|
||||
if (selectedChannels[i] !== channel) {
|
||||
result.push(selectedChannels[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeChannelName(id: string)
|
||||
{
|
||||
let pos = id.lastIndexOf("_");
|
||||
if (pos === -1)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
let i = Number(id.substring(pos+1));
|
||||
return "Channel " + (i+1);
|
||||
}
|
||||
function SelectChannelsDialog(props: SelectChannelsDialogProps) {
|
||||
//const classes = useStyles();
|
||||
const { onClose, selectedChannels, availableChannels, open } = props;
|
||||
const [currentSelection, setCurrentSelection] = useState(selectedChannels);
|
||||
|
||||
let showCheckboxes = availableChannels.length !== 2;
|
||||
if (showCheckboxes) {
|
||||
|
||||
let toggleSelect = (value: string) => {
|
||||
if (!isChecked(currentSelection, value)) {
|
||||
setCurrentSelection(addPort(availableChannels, currentSelection, value));
|
||||
} else {
|
||||
setCurrentSelection(removePort(currentSelection, value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
onClose(null);
|
||||
};
|
||||
const handleOk = (): void => {
|
||||
if (currentSelection.length >= 1 && currentSelection.length <= 2)
|
||||
{
|
||||
onClose(currentSelection);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogEx tag="audioChannels" onClose={handleClose} aria-labelledby="select-channels-title" open={open} fullWidth maxWidth="xs"
|
||||
onEnterKey={handleOk}
|
||||
>
|
||||
<DialogTitle id="simple-dialog-title">Select Channels</DialogTitle>
|
||||
<List>
|
||||
{availableChannels.map((channel) => (
|
||||
<ListItemButton key={channel}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} style={{marginLeft: 24}} />
|
||||
}
|
||||
label={makeChannelName(channel)}
|
||||
/>
|
||||
</ListItemButton>
|
||||
)
|
||||
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} variant="dialogPrimary" disabled={currentSelection.length === 0 || currentSelection.length > 2} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose(null);
|
||||
}
|
||||
const handleOk = (channels: string[]) => {
|
||||
onClose(channels);
|
||||
};
|
||||
|
||||
const handleListItemClick = (value: string) => {
|
||||
switch (value) {
|
||||
case "Stereo":
|
||||
handleOk(availableChannels);
|
||||
break;
|
||||
case "Left":
|
||||
handleOk([availableChannels[0]]);
|
||||
break;
|
||||
case "Right":
|
||||
handleOk([availableChannels[1]]);
|
||||
break;
|
||||
case "None":
|
||||
default:
|
||||
handleOk([]);
|
||||
}
|
||||
};
|
||||
let selectionKey = "None";
|
||||
if (currentSelection.length === 2) {
|
||||
selectionKey = "Stereo";
|
||||
} else if (currentSelection.length === 1) {
|
||||
if (currentSelection[0] === availableChannels[0]) {
|
||||
selectionKey = "Left";
|
||||
} else {
|
||||
selectionKey = "Right";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogEx tag="channels" onClose={handleCancel} aria-labelledby="select-channels-title" open={open}
|
||||
onEnterKey={()=>{}}
|
||||
>
|
||||
|
||||
<List style={{ marginLeft: 0, marginRight: 0}}>
|
||||
<ListItemButton onClick={() => handleListItemClick("Stereo")} key={"Stereo"} selected={selectionKey === "Stereo"} >
|
||||
<ListItemText primary={"Stereo"} />
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => handleListItemClick("Left")} key={"LeftChannel"} selected={selectionKey === "Left"} >
|
||||
<ListItemText primary={"Mono (left channel only)"} />
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => handleListItemClick("Right")} key={"RightChannel"} selected={selectionKey === "Right"}>
|
||||
<ListItemText primary={"Mono (right channel only"} />
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectChannelsDialog;
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React,{ Component, SyntheticEvent } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
|
||||
import {createStyles} from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
const styles = ({ palette }: Theme) => createStyles({
|
||||
frame: css({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}),
|
||||
hover: css({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: palette.action.hover,
|
||||
//opacity: palette.action.hoverOpacity
|
||||
}),
|
||||
selected: css({
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: palette.action.selected,
|
||||
//opacity: palette.action.selectedOpacity
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface HoverProps extends WithStyles<typeof styles> {
|
||||
selected: boolean;
|
||||
showHover?: boolean;
|
||||
borderRadius?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface HoverState {
|
||||
}
|
||||
|
||||
|
||||
export const SelectHoverBackground =
|
||||
withStyles(
|
||||
class extends Component<HoverProps, HoverState>
|
||||
{
|
||||
constructor(props: HoverProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
this.selfRef = React.createRef<HTMLDivElement>();
|
||||
this.hoverElementRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
}
|
||||
|
||||
handleMouseOver(e: SyntheticEvent): void {
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseLeave(e: SyntheticEvent): void {
|
||||
if (this.props.showHover ?? true) {
|
||||
if (this.hoverElementRef.current)
|
||||
{
|
||||
this.hoverElementRef.current.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private selfRef: React.RefObject<HTMLDivElement| null>;
|
||||
private hoverElementRef: React.RefObject<HTMLDivElement| null>;
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let isSelected = this.props.selected;
|
||||
return (<div className={classes.frame} ref={this.selfRef}
|
||||
onMouseOver={(e) => { this.handleMouseOver(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
>
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none"), borderRadius: this.props.borderRadius }} />
|
||||
<div ref={this.hoverElementRef} className={classes.hover} style={{ display: "none", borderRadius: this.props.borderRadius }} />
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>);
|
||||
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default SelectHoverBackground;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
|
||||
import DialogEx from './DialogEx';
|
||||
|
||||
|
||||
|
||||
export interface SelectMidiChannelsDialogProps {
|
||||
open: boolean;
|
||||
selectedChannels: AlsaMidiDeviceInfo[];
|
||||
availableChannels: AlsaMidiDeviceInfo[];
|
||||
onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void;
|
||||
}
|
||||
|
||||
function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean {
|
||||
for (let i = 0; i < selectedChannels.length; ++i) {
|
||||
if (selectedChannels[i].equals(channel)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo)
|
||||
:AlsaMidiDeviceInfo[]
|
||||
{
|
||||
let result: AlsaMidiDeviceInfo[] = [];
|
||||
for (let i = 0; i < availableChannels.length; ++i) {
|
||||
let channel = availableChannels[i];
|
||||
if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) {
|
||||
result.push(channel);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo)
|
||||
:AlsaMidiDeviceInfo[]
|
||||
{
|
||||
let result: AlsaMidiDeviceInfo[] = [];
|
||||
for (let i = 0; i < selectedChannels.length; ++i) {
|
||||
if (!selectedChannels[i].equals(channel)) {
|
||||
result.push(selectedChannels[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
||||
//const classes = useStyles();
|
||||
const { onClose, selectedChannels, availableChannels, open } = props;
|
||||
const [currentSelection, setCurrentSelection] = useState(selectedChannels);
|
||||
|
||||
|
||||
|
||||
let toggleSelect = (value: AlsaMidiDeviceInfo) => {
|
||||
if (!isChecked(currentSelection, value)) {
|
||||
setCurrentSelection(addPort(availableChannels, currentSelection, value));
|
||||
} else {
|
||||
setCurrentSelection(removePort(currentSelection, value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
onClose(null);
|
||||
};
|
||||
const handleOk = (): void => {
|
||||
onClose(currentSelection);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="midiChannels" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
||||
onEnterKey={handleOk}
|
||||
>
|
||||
<DialogTitle id="simple-dialog-title">Select MIDI Device</DialogTitle>
|
||||
<List>
|
||||
{availableChannels.map((channel) => (
|
||||
<ListItemButton>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel.name} />
|
||||
}
|
||||
label={channel.description}
|
||||
/>
|
||||
</ListItemButton>
|
||||
)
|
||||
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMidiChannelsDialog;
|
||||
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// Copyright (c) 2023 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogEx from './DialogEx';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import {ColorTheme} from './DarkMode';
|
||||
|
||||
|
||||
|
||||
export interface SelectThemesDialogProps {
|
||||
open: boolean;
|
||||
defaultTheme: ColorTheme;
|
||||
onClose: () => void;
|
||||
onOk: (value: ColorTheme) => void;
|
||||
}
|
||||
|
||||
function SelectThemesDialog(props: SelectThemesDialogProps) {
|
||||
//const classes = useStyles();
|
||||
const { onClose, defaultTheme, open,onOk } = props;
|
||||
const [ selectedTheme, setSelectedTheme ] = useState(defaultTheme);
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = ColorTheme.Light;
|
||||
if ((event.target as HTMLInputElement).value === '1')
|
||||
{
|
||||
value = ColorTheme.Dark;
|
||||
} else if ((event.target as HTMLInputElement).value === '2')
|
||||
{
|
||||
value = ColorTheme.System;
|
||||
}
|
||||
setSelectedTheme(value);
|
||||
};
|
||||
const handleClose = (): void => {
|
||||
onClose();
|
||||
};
|
||||
const handleOk = (): void => {
|
||||
onOk(selectedTheme)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<DialogEx tag="theme" onClose={handleClose} open={open}
|
||||
onEnterKey={handleOk}
|
||||
>
|
||||
<DialogTitle id="simple-dialog-title">Theme</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
defaultValue={defaultTheme}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<FormControlLabel value={ColorTheme.Light} control={<Radio size='small' />} label="Light" />
|
||||
<FormControlLabel value={ColorTheme.Dark} control={<Radio size='small' />} label="Dark" />
|
||||
<FormControlLabel value={ColorTheme.System} control={<Radio size='small' />} label="Use system setting" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} variant="dialogSecondary" >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} variant="dialogPrimary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectThemesDialog;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import SaveIconOutline from '@mui/icons-material/Save';
|
||||
import Button from "@mui/material/Button";
|
||||
import EditIconOutline from '@mui/icons-material/Edit';
|
||||
import { Snapshot } from './Pedalboard';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
|
||||
import { colorKeys, getBackgroundColor, getBorderColor } from './MaterialColors';
|
||||
import { PiPedalModel, PiPedalModelFactory, SnapshotModifiedEvent } from './PiPedalModel';
|
||||
|
||||
export interface SnapshotButtonProps {
|
||||
snapshot: Snapshot | null;
|
||||
snapshotIndex: number;
|
||||
selected: boolean;
|
||||
largeText: boolean;
|
||||
collapseButtons: boolean;
|
||||
|
||||
onEditSnapshot: (index: number) => void;
|
||||
onSaveSnapshot: (index: number) => void;
|
||||
onSelectSnapshot: (index: number) => void;
|
||||
};
|
||||
|
||||
export interface SnapshotButtonState {
|
||||
modified: boolean;
|
||||
uiScale: number;
|
||||
};
|
||||
|
||||
export default class SnapshotButton extends ResizeResponsiveComponent<SnapshotButtonProps, SnapshotButtonState> {
|
||||
private model: PiPedalModel;
|
||||
|
||||
constructor(props: SnapshotButtonProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
modified: this.props.snapshot?.isModified ?? false,
|
||||
uiScale: 1.0
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.onSnapshotModified = this.onSnapshotModified.bind(this);
|
||||
|
||||
}
|
||||
|
||||
is2x3Layout(width: number, height: number) {
|
||||
return width * 2 < height * 3;
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
let uiScale: number;
|
||||
if (this.is2x3Layout(width,height)) {
|
||||
uiScale = width / 400;
|
||||
} else {
|
||||
uiScale = (width/3)/(400/2);
|
||||
}
|
||||
if (height < width) {
|
||||
// 1 at 400, 2 at 600
|
||||
let hUiScale = (height-340)/(400-340);
|
||||
hUiScale += (height-400)/(460-400);
|
||||
uiScale = Math.min(hUiScale,uiScale);
|
||||
}
|
||||
if (uiScale < 1) uiScale = 1;
|
||||
if (uiScale > 4) uiScale = 4;
|
||||
this.setState({uiScale: uiScale});
|
||||
}
|
||||
|
||||
onSnapshotModified(event: SnapshotModifiedEvent) {
|
||||
if (event.snapshotIndex === this.props.snapshotIndex) {
|
||||
this.setState({ modified: event.modified });
|
||||
}
|
||||
}
|
||||
componentDidMount(): void {
|
||||
super.componentDidMount?.();
|
||||
this.model.onSnapshotModified.addEventHandler(this.onSnapshotModified);
|
||||
}
|
||||
componentWillUnmount(): void {
|
||||
this.model.onSnapshotModified.removeEventHandler(this.onSnapshotModified);
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<SnapshotButtonProps>,
|
||||
prevState: Readonly<SnapshotButtonState>): void {
|
||||
super.componentDidUpdate?.(prevProps, prevState);
|
||||
|
||||
if (this.props.snapshot !== prevProps.snapshot) {
|
||||
this.setState({
|
||||
modified: this.props.snapshot?.isModified ?? false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { snapshot, snapshotIndex, selected } = this.props;
|
||||
let { modified, uiScale } = this.state;
|
||||
|
||||
// (snapshot: Snapshot | null, index: number) {
|
||||
//let state = this.state;
|
||||
let color: string;
|
||||
let bordercolor: string;
|
||||
if (snapshot) {
|
||||
if (colorKeys.indexOf(snapshot.color) === -1) {
|
||||
bordercolor = color = snapshot.color;
|
||||
} else {
|
||||
color = getBackgroundColor(snapshot.color);
|
||||
bordercolor = getBorderColor(snapshot.color);
|
||||
}
|
||||
} else {
|
||||
bordercolor = color = getBackgroundColor("grey");
|
||||
}
|
||||
let title = snapshot ? snapshot.name : "<unassigned>";
|
||||
if (modified) {
|
||||
title = title + "*";
|
||||
}
|
||||
let disabled = snapshot === null;
|
||||
|
||||
let fontSize = (16*uiScale) + "px";
|
||||
return (
|
||||
<div style={{ display: "flex", position: "relative", flexGrow: 1, flexBasis: 1, flexFlow: "column nowrap", alignItems: "stretch", borderRadius: 16 }}>
|
||||
|
||||
<ButtonBase style={{
|
||||
display: "flex", flexGrow: 1, flexBasis: 1, flexFlow: "column nowrap", alignItems: "stretch", borderRadius: 16,
|
||||
background: color,
|
||||
boxShadow: "1px 3px 6px #00000090"
|
||||
}}
|
||||
onMouseDown={(ev) => {
|
||||
if (!selected) ev.stopPropagation();
|
||||
}}
|
||||
|
||||
onClick={() => { if (snapshot) this.props.onSelectSnapshot(snapshotIndex); }}
|
||||
>
|
||||
{/* select background mask*/}
|
||||
<div style={{position: "absolute", left:0,top:0,right:0,bottom:0, visibility: this.props.selected ? "visible": "hidden",
|
||||
background: isDarkMode() ? "#FFFFFF": "#000000", borderRadius: 16,
|
||||
opacity: 0.2
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ flexGrow: 1, flexShrink: 1, display: "flex", flexFlow: "column nowrap", alignItems: "stretch" }}
|
||||
onMouseDown={(ev) => {
|
||||
if (disabled) ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
flexGrow: 1, flexShrink: 1, display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "center", margin: 6,
|
||||
borderColor: selected ? bordercolor : "transparent", borderStyle: "solid", borderWidth: 3, borderRadius: 10
|
||||
}}
|
||||
>
|
||||
<div style={{ flexGrow: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
|
||||
<Typography display="block" color="textPrimary" align="center" variant="body1"
|
||||
style={{ fontSize: fontSize }}
|
||||
>{title}</Typography>
|
||||
</div>
|
||||
<Typography variant="h3"
|
||||
style={{
|
||||
position: "absolute",bottom: 0, left: 0, paddingBottom: 12, paddingLeft: 16, paddingRight: 12,
|
||||
pointerEvents: "none", opacity: 0.35, fontSize: "44px", fontWeight: 900, fontFamily: "Arial Black", fontStyle: "italic"
|
||||
}}
|
||||
>
|
||||
{(snapshotIndex + 1).toString()}
|
||||
</Typography>
|
||||
<div style={{ height: 54, flexGrow: 0, flexShrink: 0 }}>
|
||||
{/* placeholder where we will float the buttons, which can't be witin another button (tons of DOM validation warning messages in chrome) */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase >
|
||||
|
||||
<div style={{ flexGrow: 0, position: "absolute", bottom: 0, right: 0, paddingBottom: 12, marginRight: 12 }} >
|
||||
{!this.props.collapseButtons ? (
|
||||
<div style={{ marginRight: 8, display: "flex", flexFlow: "row nowrap" }}>
|
||||
<Button variant="dialogSecondary" startIcon={<SaveIconOutline />} color="inherit" style={{ textTransform: "none" }}
|
||||
onMouseDown={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
onMouseUp={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
disabled={false}
|
||||
onClick={(ev) => { ev.stopPropagation(); this.props.onSaveSnapshot(snapshotIndex); }}
|
||||
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="dialogSecondary" color="inherit" startIcon={<EditIconOutline />} style={{ textTransform: "none" }}
|
||||
onMouseDown={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
onMouseUp={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
disabled={disabled}
|
||||
onClick={(ev) => { ev.stopPropagation(); this.props.onEditSnapshot(snapshotIndex); }}
|
||||
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<div style={{ marginLeft: "auto", marginRight: 8, display: "flex", flexFlow: "row nowrap" }}>
|
||||
<div style={{ flexGrow: 1, flexBasis: 1 }} ></div>
|
||||
<IconButton color="inherit" style={{ opacity: 0.66 }} disabled={false}
|
||||
onMouseDown={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
onClick={() => { this.props.onSaveSnapshot(snapshotIndex); }}
|
||||
>
|
||||
<SaveIconOutline />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" disabled={disabled} style={{ opacity: 0.66 }}
|
||||
onMouseDown={(ev) => { ev.stopPropagation(); /*don't prop to card*/ }}
|
||||
onClick={() => { this.props.onEditSnapshot(snapshotIndex); }}
|
||||
|
||||
>
|
||||
<EditIconOutline />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 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.
|
||||
*/
|
||||
|
||||
// const darkcolors: string[] =
|
||||
// [
|
||||
// "#b71C1C","#880E4F","#4A148c","#311B92","#1A237E","#0D47A1","#01579B","#006064","#004D40"
|
||||
// ];
|
||||
|
||||
// const lighcolors: string[] = [
|
||||
// "#FFcDD2","#F8BBd0","#E1BEE7","#D1c4E9","#C5CAE9","#BBDEFB","#B3E5FC","#B2EbF2","B2DFDB"
|
||||
// ];
|
||||
export function getLightSnapshotcolors() {
|
||||
let result:string[] = [];
|
||||
for (let h = 0; h < 360; h += 30)
|
||||
{
|
||||
result.push("hsl(" + h + ",100%,79%)");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getDarkSnapshotColors() {
|
||||
let result:string[] = [];
|
||||
for (let h = 0; h < 360; h += 30)
|
||||
{
|
||||
result.push("hsl(" + h + ",90%,20%)");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getSnapshotColors() {
|
||||
return getDarkSnapshotColors();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { TransitionProps } from '@mui/material/transitions';
|
||||
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import SnapshotPanel from './SnapshotPanel';
|
||||
|
||||
|
||||
|
||||
const FadeTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement<any, any>;
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Fade in={true} ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
const SlideTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement<any, any>;
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export interface SnapshotDialogProps {
|
||||
open: boolean,
|
||||
onOk: () => void,
|
||||
};
|
||||
|
||||
export interface SnapshotDialogState {
|
||||
fullScreen: boolean,
|
||||
};
|
||||
|
||||
export default class SnapshotDialog extends ResizeResponsiveComponent<SnapshotDialogProps, SnapshotDialogState> {
|
||||
|
||||
constructor(props: SnapshotDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: this.getFullScreen(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
super.componentDidMount();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
super.componentWillUnmount();
|
||||
|
||||
}
|
||||
|
||||
getFullScreen() {
|
||||
return window.innerHeight < 450 || window.innerWidth < 450;
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState(
|
||||
{
|
||||
fullScreen: this.getFullScreen(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DialogEx fullWidth={true} tag="shapshot" open={this.props.open} onClose={() => this.props.onOk()} fullScreen={this.state.fullScreen}
|
||||
TransitionComponent={this.state.fullScreen? SlideTransition: FadeTransition} keepMounted
|
||||
style={{ userSelect: "none", }}
|
||||
PaperProps={{
|
||||
style: {
|
||||
|
||||
},
|
||||
}}
|
||||
onEnterKey={()=>{} }
|
||||
>
|
||||
<DialogTitle>
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onOk(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" variant="body1" color="textSecondary" >Snapshots</Typography>
|
||||
</div>
|
||||
|
||||
</DialogTitle>
|
||||
<DialogContent style={{
|
||||
overflow: "hidden", margin: 0, height: "80vh",
|
||||
paddingLeft: 16, paddingRight: 16, paddingTop: 0, paddingBottom: 24,
|
||||
}}>
|
||||
<SnapshotPanel panelHeight="80vh" />
|
||||
</DialogContent>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// Copyright (c) 2024 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
import { css } from '@emotion/react';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel';
|
||||
import ZoomedUiControl from './ZoomedUiControl'
|
||||
import MainPage from './MainPage';
|
||||
import JackStatusView from './JackStatusView';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { isDarkMode } from './DarkMode';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { Snapshot } from './Pedalboard';
|
||||
import ColorDropdownButton, { DropdownAlignment } from './ColorDropdownButton';
|
||||
import { getBackgroundColor } from './MaterialColors';
|
||||
|
||||
|
||||
const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
|
||||
|
||||
|
||||
const appStyles = (theme: Theme) => createStyles({
|
||||
"&": css({ // :root
|
||||
colorScheme: (isDarkMode() ? "dark" : "light")
|
||||
}),
|
||||
|
||||
|
||||
mainFrame: css({
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
marginTop: 8,
|
||||
flex: "1 1 100%"
|
||||
}),
|
||||
heroContent: css({
|
||||
backgroundColor: theme.mainBackground,
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}),
|
||||
shadowCatcher: css({
|
||||
backgroundColor: theme.mainBackground
|
||||
}),
|
||||
|
||||
select: css({ // fu fu fu.Overrides for white selector on dark background.
|
||||
'&:before': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:after': {
|
||||
borderColor: selectColor,
|
||||
},
|
||||
'&:hover:not(.Mui-disabled):before': {
|
||||
borderColor: selectColor,
|
||||
}
|
||||
}),
|
||||
select_icon: css({
|
||||
fill: selectColor,
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
function supportsFullScreen(): boolean {
|
||||
let doc: any = window.document;
|
||||
let docEl: any = doc.documentElement;
|
||||
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
|
||||
return (!!requestFullScreen);
|
||||
|
||||
}
|
||||
|
||||
function setFullScreen(value: boolean) {
|
||||
let doc: any = window.document;
|
||||
let docEl: any = doc.documentElement;
|
||||
|
||||
var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
|
||||
var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
|
||||
|
||||
if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
|
||||
requestFullScreen.call(docEl);
|
||||
}
|
||||
else {
|
||||
cancelFullScreen.call(doc);
|
||||
}
|
||||
}
|
||||
|
||||
type SnapshotEditorState = {
|
||||
zoomedControlInfo: ZoomedControlInfo | undefined;
|
||||
zoomedControlOpen: boolean;
|
||||
|
||||
canFullScreen: boolean;
|
||||
isFullScreen: boolean;
|
||||
isDebug: boolean;
|
||||
collapseLabel: boolean;
|
||||
|
||||
showStatusMonitor: boolean;
|
||||
name: string,
|
||||
color: string
|
||||
|
||||
};
|
||||
interface SnapshotEditorProps extends WithStyles<typeof appStyles> {
|
||||
onClose: () => void;
|
||||
onOk: (snapshotIndex: number, name: string, color: string, newSnapshots: (Snapshot | null)[]) => void;
|
||||
snapshotIndex: number;
|
||||
}
|
||||
|
||||
const SnapshotEditor = withStyles(
|
||||
class extends ResizeResponsiveComponent<SnapshotEditorProps, SnapshotEditorState> {
|
||||
// Before the component mounts, we initialise our state
|
||||
|
||||
model_: PiPedalModel;
|
||||
|
||||
getCollapseLabel() {
|
||||
return this.windowSize.width < 500;
|
||||
}
|
||||
|
||||
constructor(props: SnapshotEditorProps) {
|
||||
super(props);
|
||||
this.model_ = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.model_.zoomedUiControl.addOnChangedHandler(
|
||||
() => {
|
||||
this.setState({
|
||||
zoomedControlOpen: this.model_.zoomedUiControl.get() !== undefined,
|
||||
zoomedControlInfo: this.model_.zoomedUiControl.get()
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
let snapshot = this.model_.pedalboard.get().snapshots[this.props.snapshotIndex] ?? new Snapshot();
|
||||
this.state = {
|
||||
zoomedControlOpen: false,
|
||||
zoomedControlInfo: this.model_.zoomedUiControl.get(),
|
||||
canFullScreen: supportsFullScreen() && !this.model_.isAndroidHosted(),
|
||||
isFullScreen: !!document.fullscreenElement,
|
||||
showStatusMonitor: this.model_.showStatusMonitor.get(),
|
||||
isDebug: true,
|
||||
name: snapshot.name,
|
||||
color: snapshot.color,
|
||||
collapseLabel: this.getCollapseLabel()
|
||||
|
||||
};
|
||||
|
||||
this.showStatusMonitorHandler = this.showStatusMonitorHandler.bind(this);
|
||||
}
|
||||
|
||||
showStatusMonitorHandler() {
|
||||
this.setState({
|
||||
showStatusMonitor: this.model_.showStatusMonitor.get()
|
||||
});
|
||||
}
|
||||
toggleFullScreen(): void {
|
||||
setFullScreen(this.state.isFullScreen);
|
||||
this.setState({ isFullScreen: !this.state.isFullScreen });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
super.componentDidMount();
|
||||
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
||||
|
||||
}
|
||||
|
||||
|
||||
updateOverscroll(): void {
|
||||
if (this.model_.serverVersion) {
|
||||
// no pull-down refresh on android devices once we're ready (unless we're debug)
|
||||
let preventOverscroll =
|
||||
this.model_.state.get() === State.Ready
|
||||
&& !this.model_.debug;
|
||||
|
||||
let overscrollBehavior = preventOverscroll ? "none" : "auto";
|
||||
document.body.style.overscrollBehavior = overscrollBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width, height);
|
||||
this.setState({
|
||||
collapseLabel: this.getCollapseLabel()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleOk() {
|
||||
let currentPedalboard = this.model_.pedalboard.get();
|
||||
let selectedSnapshot = this.props.snapshotIndex;
|
||||
let currentSnapshot: Snapshot | null = currentPedalboard.snapshots[selectedSnapshot];
|
||||
if (!currentSnapshot) {
|
||||
this.props.onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = this.state.name !== currentSnapshot.name
|
||||
|| this.state.color !== currentSnapshot.color
|
||||
|| currentSnapshot.isModified;
|
||||
if (!changed) {
|
||||
this.props.onClose();
|
||||
return;
|
||||
}
|
||||
let newSnapshots = Snapshot.cloneSnapshots(currentPedalboard.snapshots);
|
||||
let newSnapshot = this.model_.pedalboard.get().makeSnapshot();
|
||||
newSnapshot.name = this.state.name;
|
||||
newSnapshot.color = this.state.color;
|
||||
newSnapshots[selectedSnapshot] = newSnapshot;
|
||||
this.model_.setSnapshots(newSnapshots, selectedSnapshot);
|
||||
|
||||
this.props.onOk(selectedSnapshot, this.state.name, this.state.color, newSnapshots);
|
||||
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
className={classes.shadowCatcher}
|
||||
style={{
|
||||
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
|
||||
colorScheme: isDarkMode() ? "dark" : "light", // affects scrollbar color
|
||||
minHeight: 345, minWidth: 390,
|
||||
userSelect: "none",
|
||||
display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
overscrollBehavior: this.state.isDebug ? "auto" : "none"
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (!this.model_.debug) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CssBaseline />
|
||||
<AppBar position="static" sx={{ bgcolor: isDarkMode() ? getBackgroundColor("purple") : "#200040" }} >
|
||||
<Toolbar variant="dense" >
|
||||
<IconButton
|
||||
edge="start"
|
||||
aria-label="menu"
|
||||
color="inherit"
|
||||
onClick={() => { this.handleOk(); }}
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
{!this.state.collapseLabel && (
|
||||
<Typography style={{ flex: "0 1 auto", opacity: 0.66 }}
|
||||
variant="body1" noWrap
|
||||
>
|
||||
{'Snapshot ' + (this.props.snapshotIndex + 1)}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<div style={{ flex: "1 1 auto", display: "flex", flexFlow: "column nowrap", marginRight: 8 }}>
|
||||
<TextField
|
||||
sx={{
|
||||
input: { color: 'white' },
|
||||
'& .MuiInput-underline:before': { borderBottomColor: '#FFFFFFC0' },
|
||||
'& .MuiInput-underline:hover:before': { borderBottomColor: '#FFFFFFE0' },
|
||||
'& .MuiInput-underline:after': { borderBottomColor: '#FFFFFF' }
|
||||
}}
|
||||
variant="standard"
|
||||
style={{ flex: "1 1 auto", marginLeft: 16, maxWidth: 500, color: "white" }}
|
||||
value={this.state.name}
|
||||
onChange={(ev) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ColorDropdownButton
|
||||
currentColor={this.state.color}
|
||||
dropdownAlignment={DropdownAlignment.SW}
|
||||
onColorChange={(newColor) => {
|
||||
this.setState({ color: newColor });
|
||||
}} />
|
||||
{this.state.canFullScreen &&
|
||||
<IconButton
|
||||
aria-label="full-screen"
|
||||
onClick={() => { this.toggleFullScreen(); }}
|
||||
color="inherit"
|
||||
size="medium">
|
||||
{this.state.isFullScreen ? (
|
||||
<FullscreenExitIcon style={{ opacity: 0.75 }} />
|
||||
) : (
|
||||
<FullscreenIcon style={{ opacity: 0.75 }} />
|
||||
|
||||
)}
|
||||
|
||||
</IconButton>
|
||||
}
|
||||
<IconButton
|
||||
aria-label="cancel"
|
||||
onClick={() => { this.props.onClose(); }}
|
||||
color="inherit"
|
||||
size="medium">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<main className={classes.mainFrame} >
|
||||
<div style={{
|
||||
overflow: "hidden",
|
||||
flex: "1 1 auto"
|
||||
}} >
|
||||
<div className={classes.heroContent}>
|
||||
<MainPage hasTinyToolBar={false} enableStructureEditing={false} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ZoomedUiControl
|
||||
dialogOpen={this.state.zoomedControlOpen}
|
||||
controlInfo={this.state.zoomedControlInfo}
|
||||
onDialogClose={() => { this.setState({ zoomedControlOpen: false }); }}
|
||||
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
||||
}
|
||||
/>
|
||||
{this.state.showStatusMonitor && (<JackStatusView />)}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
},
|
||||
appStyles
|
||||
);
|
||||
|
||||
export default SnapshotEditor;
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import SnapshotButton from './SnapshotButton';
|
||||
import { Pedalboard, Snapshot } from './Pedalboard';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
|
||||
import SnapshotPropertiesDialog from './SnapshotPropertiesDialog';
|
||||
|
||||
|
||||
|
||||
export interface SnapshotPanelProps {
|
||||
panelHeight?: string,
|
||||
onEdit?: (snapshotIndex: number) => boolean
|
||||
};
|
||||
|
||||
export interface SnapshotPanelState {
|
||||
snapshots: (Snapshot | null)[],
|
||||
portraitOrientation: boolean,
|
||||
collapseButtons: boolean,
|
||||
largeText: boolean,
|
||||
|
||||
snapshotPropertiesDialogOpen: boolean,
|
||||
editTitle: string,
|
||||
editColor: string,
|
||||
editing: boolean,
|
||||
editId: number,
|
||||
selectedSnapshot: number
|
||||
|
||||
};
|
||||
|
||||
export default class SnapshotPanel extends ResizeResponsiveComponent<SnapshotPanelProps, SnapshotPanelState> {
|
||||
private model: PiPedalModel;
|
||||
|
||||
constructor(props: SnapshotPanelProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
snapshots: this.getCurrentSnapshots(),
|
||||
selectedSnapshot: this.getSelectedSnapshot(),
|
||||
portraitOrientation: this.getPortraitOrientation(),
|
||||
collapseButtons: this.getCollapseButtons(),
|
||||
largeText: this.getLargeText(),
|
||||
|
||||
snapshotPropertiesDialogOpen: false,
|
||||
editTitle: "",
|
||||
editColor: "",
|
||||
editing: false,
|
||||
editId: 0,
|
||||
|
||||
};
|
||||
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
||||
this.onSelectedSnapshotChanged = this.onSelectedSnapshotChanged.bind(this);
|
||||
}
|
||||
|
||||
getCurrentSnapshots() {
|
||||
let result: (Snapshot | null)[] = [];
|
||||
let currentSnapshots = this.model.pedalboard.get().snapshots;
|
||||
for (let i = 0; i < 6; ++i) {
|
||||
let mySnapshot = null;
|
||||
if (i < currentSnapshots.length) {
|
||||
if (currentSnapshots[i] !== null) {
|
||||
mySnapshot = new Snapshot().deserialize(currentSnapshots[i]); // a deep clone.
|
||||
}
|
||||
}
|
||||
result.push(mySnapshot);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getSelectedSnapshot() {
|
||||
return this.model.selectedSnapshot.get();
|
||||
}
|
||||
|
||||
onSelectedSnapshotChanged(value: number) {
|
||||
this.setState({ selectedSnapshot: value })
|
||||
}
|
||||
onPedalboardChanged(value: Pedalboard) {
|
||||
// boofs our current edit, oh well. we can't track property across a structural change.
|
||||
this.setState({
|
||||
snapshots: this.getCurrentSnapshots(),
|
||||
selectedSnapshot: this.getSelectedSnapshot()
|
||||
});
|
||||
}
|
||||
componentDidMount(): void {
|
||||
super.componentDidMount();
|
||||
this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged);
|
||||
this.model.selectedSnapshot.addOnChangedHandler(this.onSelectedSnapshotChanged);
|
||||
this.setState({
|
||||
snapshots: this.getCurrentSnapshots(),
|
||||
selectedSnapshot: this.getSelectedSnapshot()
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
super.componentWillUnmount();
|
||||
this.model.selectedSnapshot.removeOnChangedHandler(this.onSelectedSnapshotChanged);
|
||||
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged)
|
||||
|
||||
}
|
||||
|
||||
getCollapseButtons() {
|
||||
if (this.getPortraitOrientation())
|
||||
{
|
||||
return window.innerWidth < 550;
|
||||
} else {
|
||||
return window.innerWidth < 800;
|
||||
}
|
||||
}
|
||||
getPortraitOrientation() {
|
||||
return window.innerWidth * 2 < window.innerHeight * 3;
|
||||
}
|
||||
|
||||
getLargeText() {
|
||||
return window.innerHeight > 700 && window.innerWidth > 1000;
|
||||
}
|
||||
onSaveSnapshot(index: number) {
|
||||
let snapshot = this.state.snapshots[index];
|
||||
if (snapshot) {
|
||||
// no dialog required. just save it.
|
||||
this.handleSnapshotPropertyOk(
|
||||
index,
|
||||
snapshot.name,
|
||||
snapshot.color,
|
||||
false);
|
||||
} else {
|
||||
this.setState({
|
||||
editTitle: "",
|
||||
editColor: "",
|
||||
editId: index,
|
||||
snapshotPropertiesDialogOpen: true,
|
||||
editing: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
onEditSnapshot(index: number) {
|
||||
if (this.props.onEdit) {
|
||||
if (this.props.onEdit(index)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let snapshot = this.state.snapshots[index];
|
||||
if (snapshot) {
|
||||
this.setState({
|
||||
editTitle: snapshot.name,
|
||||
editColor: snapshot.color,
|
||||
editId: index,
|
||||
snapshotPropertiesDialogOpen: true,
|
||||
editing: true
|
||||
});
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState(
|
||||
{
|
||||
portraitOrientation: this.getPortraitOrientation(),
|
||||
collapseButtons: this.getCollapseButtons(),
|
||||
largeText: this.getLargeText()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
selectSnapshot(index: number) {
|
||||
this.model.selectSnapshot(index);
|
||||
}
|
||||
renderSnapshot(snapshot: Snapshot | null, index: number) {
|
||||
return (
|
||||
<SnapshotButton key={"s"+index}
|
||||
snapshot={snapshot}
|
||||
snapshotIndex={index}
|
||||
collapseButtons={this.state.collapseButtons}
|
||||
selected={this.state.selectedSnapshot === index}
|
||||
largeText={this.state.largeText}
|
||||
onEditSnapshot={(index)=>{this.onEditSnapshot(index)}}
|
||||
onSaveSnapshot={(index)=>{ this.onSaveSnapshot(index); }}
|
||||
onSelectSnapshot={(index)=>{this.model.selectSnapshot(index);}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
handleSnapshotPropertyOk(index: number, name: string, color: string, editing: boolean) {
|
||||
if (editing) {
|
||||
let snapshots = Snapshot.cloneSnapshots(this.state.snapshots);
|
||||
let snapshot = snapshots[index];
|
||||
if (snapshot) {
|
||||
snapshot.name = name;
|
||||
snapshot.color = color;
|
||||
}
|
||||
this.setState({
|
||||
snapshots: snapshots,
|
||||
snapshotPropertiesDialogOpen: false
|
||||
});
|
||||
this.model.setSnapshots(snapshots, -1);
|
||||
|
||||
} else {
|
||||
let pedalboard = this.model.pedalboard.get();
|
||||
let newSnapshot = pedalboard.makeSnapshot();
|
||||
newSnapshot.name = name;
|
||||
newSnapshot.color = color;
|
||||
let snapshots = Snapshot.cloneSnapshots(this.state.snapshots);
|
||||
snapshots[index] = newSnapshot;
|
||||
this.setState({
|
||||
snapshots: snapshots,
|
||||
snapshotPropertiesDialogOpen: false
|
||||
});
|
||||
this.model.setSnapshots(snapshots, index);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let snapshots = this.state.snapshots;
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 1 auto",
|
||||
height: this.props.panelHeight,
|
||||
overflow: "hidden", margin: 0,
|
||||
paddingLeft: 16, paddingRight: 16, paddingTop: 0, paddingBottom: 24,
|
||||
display: "flex", flexFlow: "column nowrap", alignContent: "stretch", justifyContent: "stretch"
|
||||
}}>
|
||||
|
||||
<div style={{
|
||||
flexGrow: 1, flexShrink: 1, flexBasis: 1,
|
||||
|
||||
display: "flex",
|
||||
flexFlow: this.state.portraitOrientation ?
|
||||
"row nowrap" : "column nowrap",
|
||||
alignItems: "stretch", gap: 8
|
||||
}}>
|
||||
<div style={{
|
||||
flexGrow: 1, flexShrink: 1, flexBasis: 1,
|
||||
display: "flex",
|
||||
flexFlow: this.state.portraitOrientation ?
|
||||
"column nowrap"
|
||||
: "row nowrap",
|
||||
alignItems: "stretch", gap: 8
|
||||
}}>
|
||||
{this.renderSnapshot(snapshots[0], 0)}
|
||||
{this.renderSnapshot(snapshots[1], 1)}
|
||||
{this.renderSnapshot(snapshots[2], 2)}
|
||||
</div>
|
||||
<div style={{
|
||||
flexGrow: 1, flexShrink: 1, flexBasis: 1,
|
||||
display: "flex",
|
||||
flexFlow: this.state.portraitOrientation ? "column nowrap" : "row nowrap", alignItems: "stretch", gap: 8
|
||||
}}>
|
||||
{this.renderSnapshot(snapshots[3], 3)}
|
||||
{this.renderSnapshot(snapshots[4], 4)}
|
||||
{this.renderSnapshot(snapshots[5], 5)}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.snapshotPropertiesDialogOpen && (
|
||||
<SnapshotPropertiesDialog
|
||||
open={this.state.snapshotPropertiesDialogOpen}
|
||||
name={this.state.editTitle}
|
||||
color={this.state.editColor}
|
||||
editing={this.state.editing}
|
||||
snapshotIndex={this.state.editId}
|
||||
onClose={() => { this.setState({ snapshotPropertiesDialogOpen: false }); }}
|
||||
onOk={(snapshotId, name, color, editing) => {
|
||||
this.handleSnapshotPropertyOk(snapshotId, name, color, editing);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import DialogEx from './DialogEx';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { colorKeys } from "./MaterialColors";
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import ColorDropdownButton, { DropdownAlignment } from './ColorDropdownButton';
|
||||
|
||||
const snapshotColors = colorKeys.slice(0,100);
|
||||
const NBSP = "\u00A0";
|
||||
|
||||
export interface SnapshotPropertiesDialogProps {
|
||||
open: boolean,
|
||||
name: string,
|
||||
editing: boolean,
|
||||
snapshotIndex: number,
|
||||
color: string,
|
||||
onOk: (snapshotId: number, name: string, color: string, editing: boolean) => void,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface SnapshotPropertiesDialogState {
|
||||
name: string,
|
||||
color: string,
|
||||
nameErrorMessage: string,
|
||||
compactVertical: boolean,
|
||||
};
|
||||
|
||||
export default class SnapshotPropertiesDialog extends ResizeResponsiveComponent<SnapshotPropertiesDialogProps, SnapshotPropertiesDialogState> {
|
||||
|
||||
|
||||
constructor(props: SnapshotPropertiesDialogProps) {
|
||||
super(props);
|
||||
this.state = this.stateFromProps();
|
||||
}
|
||||
getCompactVertical() {
|
||||
return window.innerHeight < 450;
|
||||
}
|
||||
stateFromProps() {
|
||||
let color = this.props.color;
|
||||
if (color.length === 0) {
|
||||
color = snapshotColors[0];
|
||||
}
|
||||
let name = this.props.name;
|
||||
if (name.length === 0 && !this.props.editing)
|
||||
{
|
||||
name = "Snapshot " + (this.props.snapshotIndex+1);
|
||||
}
|
||||
return {
|
||||
name: name,
|
||||
color: color,
|
||||
nameErrorMessage: "",
|
||||
compactVertical: this.getCompactVertical()
|
||||
};
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width,height);
|
||||
this.setState({compactVertical: this.getCompactVertical() })
|
||||
}
|
||||
|
||||
handleOk() {
|
||||
if (this.state.name === "") {
|
||||
this.setState({ nameErrorMessage: "* Required" });
|
||||
} else {
|
||||
this.props.onOk(
|
||||
this.props.snapshotIndex,
|
||||
this.state.name,
|
||||
this.state.color,
|
||||
this.props.editing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
const handleClose = () => {
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
let okButtonText = this.props.editing ? "OK" : "Save";
|
||||
return (
|
||||
<DialogEx maxWidth="sm" fullWidth={true} tag="snapshotProps" open={this.props.open} onClose={handleClose}
|
||||
style={{ userSelect: "none" }} fullScreen={this.state.compactVertical}
|
||||
onEnterKey={()=> { this.handleOk(); }}
|
||||
>
|
||||
<DialogTitle>
|
||||
<div>
|
||||
<IconButton edge="start" color="inherit" onClick={() => { this.props.onClose(); }} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon fontSize="small" style={{ opacity: "0.6" }} />
|
||||
</IconButton>
|
||||
<Typography display="inline" variant="body1" color="textSecondary" >
|
||||
{this.props.editing ? "Edit snapshot" : "Save snapshot"}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
</DialogTitle>
|
||||
<DialogContent style={{paddingBottom: 0}}>
|
||||
|
||||
<TextField variant="standard" style={{ flexGrow: 1, flexBasis: 1 }}
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
error={this.state.nameErrorMessage.length !== 0}
|
||||
id="name"
|
||||
label="Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
helperText={this.state.nameErrorMessage.length === 0 ? NBSP : this.state.nameErrorMessage}
|
||||
value={this.state.name}
|
||||
onChange={(e) => this.setState({ name: e.target.value, nameErrorMessage: "" })}
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" color="textSecondary">Color</Typography>
|
||||
<ColorDropdownButton aria-label="color" currentColor={this.state.color} dropdownAlignment={DropdownAlignment.SE}
|
||||
onColorChange={(newcolor) => {
|
||||
this.setState({color: newcolor});
|
||||
}} />
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={()=> {this.handleOk();}} >
|
||||
{okButtonText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogEx>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin } from './Lv2Plugin';
|
||||
import {
|
||||
Pedalboard, PedalboardSplitItem, ControlValue,
|
||||
} from './Pedalboard';
|
||||
import PluginControl from './PluginControl';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import VuMeter from './VuMeter';
|
||||
import { SplitAbControl, SplitTypeControl, SplitMixControl, SplitPanLeftControl,SplitVolLeftControl,SplitPanRightControl,SplitVolRightControl } from './SplitUiControls';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
|
||||
const LANDSCAPE_HEIGHT_BREAK = 500;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: css({
|
||||
display: "block",
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "0px",
|
||||
height: "100%",
|
||||
overflowX: "auto",
|
||||
overflowY: "auto"
|
||||
}),
|
||||
vuMeterL: css({
|
||||
position: "fixed",
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
left: 0,
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
vuMeterR: css({
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
marginRight: 22,
|
||||
paddingLeft: 12,
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
vuMeterRLandscape: css({
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
paddingRight: 22,
|
||||
paddingLeft: 12,
|
||||
background: theme.mainBackground,
|
||||
zIndex: 3
|
||||
|
||||
}),
|
||||
|
||||
normalGrid: css({
|
||||
position: "relative",
|
||||
paddingLeft: 40,
|
||||
paddingRight: 40,
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start"
|
||||
|
||||
}),
|
||||
landscapeGrid: css({
|
||||
paddingLeft: 40,
|
||||
marginRight: 40,
|
||||
display: "inline-flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start",
|
||||
height: "100%"
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
interface SplitControlViewProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalboardSplitItem;
|
||||
}
|
||||
type SplitControlViewState = {
|
||||
landscapeGrid: boolean;
|
||||
};
|
||||
|
||||
const SplitControlView =
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<SplitControlViewProps, SplitControlViewState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: SplitControlViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
landscapeGrid: false
|
||||
|
||||
}
|
||||
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
||||
this.onValueChanged = this.onValueChanged.bind(this);
|
||||
this.onPreviewChange = this.onPreviewChange.bind(this);
|
||||
}
|
||||
|
||||
|
||||
onPreviewChange(key: string, value: number): void {
|
||||
this.model.previewPedalboardValue(this.props.instanceId, key, value);
|
||||
}
|
||||
|
||||
onValueChanged(key: string, value: number): void {
|
||||
this.model.setPedalboardControl(this.props.instanceId, key, value);
|
||||
}
|
||||
|
||||
onPedalboardChanged(value?: Pedalboard) {
|
||||
//let item = this.model.pedalboard.get().maybeGetItem(this.props.instanceId);
|
||||
//this.setState({ pedalboardItem: item });
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
|
||||
renderControl(plugin: UiPlugin, controlValue: ControlValue): ReactNode {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row", alignContent: "center", justifyContent: "flex-start" }}>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ landscapeGrid: height < LANDSCAPE_HEIGHT_BREAK });
|
||||
}
|
||||
|
||||
|
||||
render(): ReactNode {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let pedalboardItem = this.model.pedalboard.get().getItem(this.props.instanceId);
|
||||
|
||||
if (!pedalboardItem)
|
||||
return (<div className={classes.frame}></div>);
|
||||
|
||||
|
||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let typeValue = pedalboardItem.getControl(PedalboardSplitItem.TYPE_KEY);
|
||||
let mixValue = pedalboardItem.getControl(PedalboardSplitItem.MIX_KEY);
|
||||
let selectValue = pedalboardItem.getControl(PedalboardSplitItem.SELECT_KEY);
|
||||
let panLValue = pedalboardItem.getControl(PedalboardSplitItem.PANL_KEY);
|
||||
let volumeLValue = pedalboardItem.getControl(PedalboardSplitItem.VOLL_KEY);
|
||||
let panRValue = pedalboardItem.getControl(PedalboardSplitItem.PANR_KEY);
|
||||
let volumeRValue = pedalboardItem.getControl(PedalboardSplitItem.VOLR_KEY);
|
||||
|
||||
return (
|
||||
<div className={classes.frame}>
|
||||
<div className={classes.vuMeterL}>
|
||||
<VuMeter display="input" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={vuMeterRClass}>
|
||||
<VuMeter display="output" instanceId={pedalboardItem.instanceId} />
|
||||
</div>
|
||||
<div className={gridClass} >
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitTypeControl} value={typeValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(typeValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(typeValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
typeValue.value === 0 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitAbControl} value={selectValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(selectValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(selectValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
typeValue.value === 1 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitMixControl} value={mixValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(mixValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(mixValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
typeValue.value === 2 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitPanLeftControl} value={panLValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(panLValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(panLValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
typeValue.value === 2 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitVolLeftControl} value={volumeLValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(volumeLValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(volumeLValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
typeValue.value === 2 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitPanRightControl} value={panRValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(panRValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(panRValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
typeValue.value === 2 && (
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginControl instanceId={this.props.instanceId} uiControl={SplitVolRightControl} value={volumeRValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(volumeRValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(volumeRValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => { }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default SplitControlView;
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import {UiControl,ScalePoint} from './Lv2Plugin';
|
||||
import {PedalboardSplitItem} from './Pedalboard';
|
||||
import Units from './Units'
|
||||
|
||||
|
||||
|
||||
export const SplitTypeControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.TYPE_KEY,
|
||||
name: "Type",
|
||||
index: 0,
|
||||
min_value: 0,
|
||||
max_value: 2,
|
||||
default_value: 0,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: true,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
|
||||
scale_points: ScalePoint.deserialize_array(
|
||||
[
|
||||
{ value: 0, label: "A/B" },
|
||||
{ value: 1, label: "Mix" },
|
||||
{ value: 2, label: "L/R" }
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
export const SplitAbControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.SELECT_KEY,
|
||||
name: "Select",
|
||||
index: 1,
|
||||
min_value: 0,
|
||||
max_value: 1,
|
||||
default_value: 0,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: true,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
scale_points: ScalePoint.deserialize_array(
|
||||
[
|
||||
{ value: 0, label: "A" },
|
||||
{ value: 1, label: "B" },
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
export const SplitMixControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.MIX_KEY,
|
||||
name: "Mix",
|
||||
index: 2,
|
||||
min_value: -1,
|
||||
max_value: 1,
|
||||
default_value: 0,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: false,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
|
||||
pipedal_ledColor: "",
|
||||
|
||||
scale_points: []
|
||||
});
|
||||
|
||||
export const SplitPanLeftControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.PANL_KEY,
|
||||
name: "Pan Top",
|
||||
index: 3,
|
||||
min_value: -1,
|
||||
max_value: 1,
|
||||
default_value: 0,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: false,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
|
||||
scale_points: []
|
||||
});
|
||||
|
||||
export const SplitVolLeftControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.VOLL_KEY,
|
||||
name: "Vol Top",
|
||||
index: 4,
|
||||
min_value: -60,
|
||||
max_value: 12,
|
||||
default_value: -3,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: false,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
|
||||
units: Units.db,
|
||||
scale_points: [
|
||||
new ScalePoint().deserialize({
|
||||
value: -60,
|
||||
label: "-INF"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
export const SplitPanRightControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.PANR_KEY,
|
||||
name: "Pan Bottom",
|
||||
index: 5,
|
||||
min_value: -1,
|
||||
max_value: 1,
|
||||
default_value: 0,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: false,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
|
||||
scale_points: []
|
||||
});
|
||||
|
||||
export const SplitVolRightControl: UiControl = new UiControl().deserialize({
|
||||
symbol: PedalboardSplitItem.VOLR_KEY,
|
||||
name: "Vol Bottom",
|
||||
index: 6,
|
||||
min_value: -60,
|
||||
max_value: 12,
|
||||
default_value: -3,
|
||||
range_steps: 0,
|
||||
is_logarithmic: false,
|
||||
display_priority: -1,
|
||||
integer_property: false,
|
||||
enumeration_property: false,
|
||||
not_on_gui: false,
|
||||
toggled_property: false,
|
||||
trigger_property: false,
|
||||
pipedal_ledColor: "",
|
||||
|
||||
units: Units.db,
|
||||
scale_points: [
|
||||
new ScalePoint().deserialize({
|
||||
value: -60,
|
||||
label: "-INF"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
export const SplitControls: UiControl[] =
|
||||
[
|
||||
SplitTypeControl,
|
||||
SplitAbControl,
|
||||
SplitMixControl,
|
||||
SplitPanLeftControl,
|
||||
SplitVolLeftControl,
|
||||
SplitPanRightControl,
|
||||
SplitVolRightControl,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2022 Robin 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.
|
||||
|
||||
|
||||
|
||||
class StringBuilder
|
||||
{
|
||||
_array: string[] = [];
|
||||
|
||||
append(value: any) {
|
||||
if (typeof(value) === "string")
|
||||
{
|
||||
this._array.push(value);
|
||||
} else {
|
||||
this._array.push(value+"");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
toString(): string {
|
||||
return this._array.join('');
|
||||
}
|
||||
}
|
||||
|
||||
export default StringBuilder;
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import StringBuilder from './StringBuilder';
|
||||
|
||||
class SvgPathBuilder {
|
||||
private _sb: StringBuilder = new StringBuilder();
|
||||
|
||||
moveTo(x: number, y: number): SvgPathBuilder
|
||||
{
|
||||
this._sb.append('M').append(x).append(',').append(y).append(' ');
|
||||
return this;
|
||||
}
|
||||
lineTo(x: number, y: number): SvgPathBuilder
|
||||
{
|
||||
this._sb.append('L').append(x).append(',').append(y).append(' ');
|
||||
return this;
|
||||
}
|
||||
bezierTo(x1: number, y1: number, x2: number, y2: number, x: number, y: number)
|
||||
{
|
||||
this._sb.append('C').append(x1).append(',').append(y1)
|
||||
.append(' ').append(x2).append(',').append(y2)
|
||||
.append(' ').append(x).append(',').append(y).append(' ');
|
||||
return this;
|
||||
}
|
||||
closePath(): SvgPathBuilder {
|
||||
this._sb.append("Z ");
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
toString(): string { return this._sb.toString(); }
|
||||
}
|
||||
|
||||
export default SvgPathBuilder;
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022-2023 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
import { Component } from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import Utility from './Utility';
|
||||
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined';
|
||||
import MicOutlinedIcon from '@mui/icons-material/MicOutlined';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
controlDiv: { flex: "0 0 auto", marginRight: 12, verticalAlign: "center", height: 48, paddingTop: 8, paddingBottom: 8 },
|
||||
controlDiv2: {
|
||||
flex: "0 0 auto", marginRight: 12, verticalAlign: "center",
|
||||
height: 48, paddingTop: 0, paddingBottom: 0, whiteSpace: "nowrap"
|
||||
}
|
||||
});
|
||||
|
||||
interface SystemMidiBindingViewProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
listen: boolean;
|
||||
midiBinding: MidiBinding;
|
||||
onChange: (instanceId: number, newBinding: MidiBinding) => void;
|
||||
onListen: (instanceId: number, key: string, listenForControl: boolean) => void;
|
||||
}
|
||||
|
||||
|
||||
interface SystemMidiBindingViewState {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const SystemMidiBindingView =
|
||||
withStyles(
|
||||
class extends Component<SystemMidiBindingViewProps, SystemMidiBindingViewState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: SystemMidiBindingViewProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
handleTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.bindingType = newValue;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleNoteChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.note = newValue;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleControlChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.control = newValue;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleLatchControlTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.switchControlType = newValue;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleLinearControlTypeChange(e: any, extra: any) {
|
||||
let newValue = parseInt(e.target.value);
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.linearControlType = newValue;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleMinChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.minValue = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleMaxChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.maxValue = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
handleScaleChange(value: number): void {
|
||||
let newBinding = this.props.midiBinding.clone();
|
||||
newBinding.rotaryScale = value;
|
||||
this.props.onChange(this.props.instanceId, newBinding);
|
||||
}
|
||||
|
||||
|
||||
generateMidiSelects(): React.ReactNode[] {
|
||||
let result: React.ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < 127; ++i) {
|
||||
result.push(
|
||||
<MenuItem value={i}>{Utility.midiNoteName(i)}</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
generateControlSelects(): React.ReactNode[] {
|
||||
|
||||
return Utility.validMidiControllers.map((control) => (
|
||||
<MenuItem key={control.value} value={control.value}>{control.displayName}</MenuItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let midiBinding = this.props.midiBinding;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "left", alignItems: "center", minHeight: 48 }}>
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{ width: 80, }}
|
||||
onChange={(e, extra) => this.handleTypeChange(e, extra)}
|
||||
value={midiBinding.bindingType}
|
||||
>
|
||||
<MenuItem value={0}>None</MenuItem>
|
||||
<MenuItem value={1}>Note</MenuItem>
|
||||
<MenuItem value={2}>Control</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
<Select variant="standard"
|
||||
style={{ width: 80, verticalAlign: 'center' }}
|
||||
onChange={(e, extra) => this.handleNoteChange(e, extra)}
|
||||
value={midiBinding.note}
|
||||
|
||||
>
|
||||
{
|
||||
this.generateMidiSelects()
|
||||
}
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
|
||||
<Select variant="standard"
|
||||
style={{ width: 80 }}
|
||||
onChange={(e, extra) => this.handleControlChange(e, extra)}
|
||||
value={midiBinding.control}
|
||||
renderValue={(value) => { return "CC-" + value }}
|
||||
>
|
||||
{
|
||||
this.generateControlSelects()
|
||||
}
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (this.props.listen) {
|
||||
this.props.onListen(-2, "", false)
|
||||
} else {
|
||||
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
|
||||
}
|
||||
}}
|
||||
size="large">
|
||||
{this.props.listen ? (
|
||||
<MicOutlinedIcon />
|
||||
) : (
|
||||
<MicNoneOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
export default SystemMidiBindingView;
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022-2023 Robin E. R. Davies
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
||||
* so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import DialogEx from './DialogEx';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import WithStyles from './WithStyles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import SystemMidiBindingView from './SystemMidiBindingView';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: css({
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
}),
|
||||
dialogTitle: css({
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: "1 1 auto",
|
||||
}),
|
||||
pluginTable: css({
|
||||
borderCollapse: "collapse",
|
||||
width: "100%",
|
||||
|
||||
}),
|
||||
pluginHead: css({
|
||||
borderTop: "1pt #DDD solid", paddingLeft: 22, paddingRight: 22
|
||||
}),
|
||||
bindingTd: css({
|
||||
verticalAlign: "top",
|
||||
paddingLeft: 12, paddingBottom: 8
|
||||
}),
|
||||
nameTd: css({
|
||||
paddingLeft: 16,
|
||||
verticalAlign: "top",
|
||||
paddingTop: 12
|
||||
}),
|
||||
plainRow: css({
|
||||
borderWidth: "1px 0px 0px 0px", borderStyle: "solid", borderColor: "transparent"
|
||||
}),
|
||||
dividerRow: css({
|
||||
borderWidth: "1px 0px 0px 0px", borderStyle: "solid", borderColor: theme.palette.divider
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
|
||||
export interface SystemMidiBindingDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
class BindingEntry {
|
||||
constructor(
|
||||
displayName: string,
|
||||
instanceId: number,
|
||||
midiBinding: MidiBinding,
|
||||
) {
|
||||
this.displayName = displayName;
|
||||
this.instanceId = instanceId;
|
||||
this.midiBinding = midiBinding;
|
||||
}
|
||||
displayName: string;
|
||||
instanceId: number;
|
||||
midiBinding: MidiBinding;
|
||||
}
|
||||
|
||||
export interface SystemMidiBindingDialogState {
|
||||
listenInstanceId: number;
|
||||
listenSymbol: string;
|
||||
listenSnackbarOpen: boolean;
|
||||
systemMidiBindings: BindingEntry[];
|
||||
|
||||
}
|
||||
|
||||
export const SystemMidiBindingDialog =
|
||||
withStyles(
|
||||
class extends ResizeResponsiveComponent<SystemMidiBindingDialogProps, SystemMidiBindingDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: SystemMidiBindingDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
listenInstanceId: -2,
|
||||
listenSymbol: "",
|
||||
listenSnackbarOpen: false,
|
||||
systemMidiBindings: this.createBindings()
|
||||
};
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.onMidiBindingsChanged = this.onMidiBindingsChanged.bind(this);
|
||||
}
|
||||
|
||||
createBindings(): BindingEntry[] {
|
||||
let result: BindingEntry[] = [];
|
||||
let listenInstanceId = 0;
|
||||
for (var item of this.model.systemMidiBindings.get()) {
|
||||
let displayName = "";
|
||||
let found = true;
|
||||
if (item.symbol === "nextBank") {
|
||||
displayName = "Next Bank";
|
||||
}
|
||||
else if (item.symbol === "prevBank") {
|
||||
displayName = "Previous Bank";
|
||||
}
|
||||
else if (item.symbol === "nextProgram") {
|
||||
displayName = "Next Preset";
|
||||
}else if (item.symbol === "prevProgram") {
|
||||
displayName = "Previous Preset";
|
||||
}
|
||||
else if (item.symbol === "snapshot1") {
|
||||
displayName = "Snapshot 1";
|
||||
}
|
||||
else if (item.symbol === "snapshot2") {
|
||||
displayName = "Snapshot 2";
|
||||
}
|
||||
else if (item.symbol === "snapshot3") {
|
||||
displayName = "Snapshot 3";
|
||||
}
|
||||
else if (item.symbol === "snapshot4") {
|
||||
displayName = "Snapshot 4";
|
||||
}
|
||||
else if (item.symbol === "snapshot5") {
|
||||
displayName = "Snapshot 5";
|
||||
}
|
||||
else if (item.symbol === "snapshot6") {
|
||||
displayName = "Snapshot 6";
|
||||
} else if (item.symbol === "startHotspot") {
|
||||
displayName = "Enable Hotspot";
|
||||
} else if (item.symbol === "stopHotspot") {
|
||||
displayName = "Disable Hotspot";
|
||||
} else if (item.symbol === "shutdown") {
|
||||
displayName = "Shutdown";
|
||||
} else if (item.symbol === "reboot") {
|
||||
displayName = "Reboot";
|
||||
} else {
|
||||
found = false;
|
||||
}
|
||||
if (found)
|
||||
{
|
||||
result.push(new BindingEntry(displayName, listenInstanceId, item));
|
||||
++listenInstanceId;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
handleClose() {
|
||||
this.cancelListenForControl();
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
listenTimeoutHandle?: number;
|
||||
|
||||
listenHandle?: ListenHandle;
|
||||
|
||||
cancelListenForControl() {
|
||||
if (this.listenTimeoutHandle) {
|
||||
clearTimeout(this.listenTimeoutHandle);
|
||||
this.listenTimeoutHandle = undefined;
|
||||
}
|
||||
if (this.listenHandle) {
|
||||
this.model.cancelListenForMidiEvent(this.listenHandle)
|
||||
this.listenHandle = undefined;
|
||||
}
|
||||
|
||||
this.setState({ listenInstanceId: -2, listenSymbol: "" });
|
||||
|
||||
}
|
||||
|
||||
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) {
|
||||
this.cancelListenForControl();
|
||||
|
||||
for (var binding of this.state.systemMidiBindings) {
|
||||
if (binding.instanceId === instanceId) {
|
||||
let newBinding = binding.midiBinding.clone();
|
||||
|
||||
if (isNote) {
|
||||
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE;
|
||||
newBinding.note = noteOrControl;
|
||||
} else {
|
||||
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL;
|
||||
newBinding.control = noteOrControl;
|
||||
}
|
||||
|
||||
this.model.setSystemMidiBinding(instanceId, newBinding);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void {
|
||||
this.cancelListenForControl();
|
||||
this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true });
|
||||
this.listenTimeoutHandle = setTimeout(() => {
|
||||
this.cancelListenForControl();
|
||||
}, 8000);
|
||||
|
||||
this.listenHandle = this.model.listenForMidiEvent(listenForControl,
|
||||
(isNote: boolean, noteOrControl: number) => {
|
||||
this.handleListenSucceeded(instanceId, symbol, isNote, noteOrControl);
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
}
|
||||
|
||||
onMidiBindingsChanged() {
|
||||
this.setState({ systemMidiBindings: this.createBindings() });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
|
||||
this.model.systemMidiBindings.addOnChangedHandler(this.onMidiBindingsChanged);
|
||||
this.onMidiBindingsChanged();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
this.mounted = false;
|
||||
this.model.systemMidiBindings.removeOnChangedHandler(this.onMidiBindingsChanged);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
handleItemChanged(instanceId: number, newBinding: MidiBinding) {
|
||||
this.model.setSystemMidiBinding(instanceId, newBinding);
|
||||
}
|
||||
generateTable(): React.ReactNode[] {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let result: React.ReactNode[] = [];
|
||||
|
||||
let items = this.state.systemMidiBindings;
|
||||
|
||||
for (var item of items) {
|
||||
let symbol = item.midiBinding.symbol;
|
||||
let hasDivider = symbol === "snapshot1" || symbol === "stopHotspot" || symbol === "shotdown";
|
||||
if (hasDivider)
|
||||
{
|
||||
result.push(
|
||||
<tr>
|
||||
<td colSpan={2} className={classes.dividerRow}><div style={{height: 1}} /></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
<tr key={item.instanceId} >
|
||||
<td className={classes.nameTd}>
|
||||
<Typography noWrap style={{ verticalAlign: "center", height: 48 }}>
|
||||
{item.displayName}
|
||||
</Typography>
|
||||
</td>
|
||||
<td className={classes.bindingTd}>
|
||||
<SystemMidiBindingView instanceId={item.instanceId} midiBinding={item.midiBinding}
|
||||
onListen={(instanceId: number, symbol: string, listenForControl: boolean) => {
|
||||
if (instanceId === -2) {
|
||||
this.cancelListenForControl();
|
||||
} else {
|
||||
this.handleListenForControl(instanceId, symbol, listenForControl);
|
||||
}
|
||||
}}
|
||||
listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === item.midiBinding.symbol}
|
||||
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
supressDefault(e: SyntheticEvent) {
|
||||
//e.preventDefault();
|
||||
//e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open} = props;
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
if (!open) {
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogEx tag="systemMidiBindings" open={open} fullWidth onClose={this.handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={true}
|
||||
style={{ userSelect: "none" }}
|
||||
onEnterKey={()=>{}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<AppBar className={classes.dialogAppBar} >
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={this.handleClose}
|
||||
aria-label="back"
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.dialogTitle}>
|
||||
System MIDI Bindings
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{ overflow: "auto", flex: "1 1 auto", width: "100%" }}>
|
||||
<table className={classes.pluginTable} >
|
||||
<colgroup>
|
||||
<col style={{ width: "auto" }} />
|
||||
<col style={{ width: "100%" }} />
|
||||
</colgroup>
|
||||
|
||||
<tbody>
|
||||
{this.generateTable()}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.listenSnackbarOpen}
|
||||
autoHideDuration={1500}
|
||||
onClose={() => this.setState({ listenSnackbarOpen: false })}
|
||||
message="Listening for MIDI input"
|
||||
/>
|
||||
</DialogEx >
|
||||
);
|
||||
}
|
||||
},
|
||||
styles
|
||||
);
|
||||
|
||||
|
||||
export default SystemMidiBindingDialog;
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2022 Robin Davies
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import WithStyles from './WithStyles';
|
||||
import { withStyles } from "tss-react/mui";
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
|
||||
import IconButton from '@mui/material/Toolbar';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import {isDarkMode} from './DarkMode';
|
||||
|
||||
const drawerStyles = (theme: Theme) => {
|
||||
return createStyles({
|
||||
list: {
|
||||
width: 250,
|
||||
},
|
||||
fullList: {
|
||||
width: 'auto',
|
||||
},
|
||||
|
||||
drawer_header: {
|
||||
color: theme.palette.primary.main,
|
||||
background: (isDarkMode()? theme.palette.background.default: 'white'),
|
||||
|
||||
}
|
||||
})};
|
||||
|
||||
type Anchor = 'top' | 'left' | 'bottom' | 'right';
|
||||
|
||||
type CloseEventHandler = () => void;
|
||||
|
||||
|
||||
|
||||
interface DrawerProps extends WithStyles<typeof drawerStyles> {
|
||||
title?: string;
|
||||
position: Anchor;
|
||||
is_open: boolean;
|
||||
onClose?: CloseEventHandler;
|
||||
children?: React.ReactNode;
|
||||
|
||||
}
|
||||
type DrawerState = {
|
||||
is_open: boolean;
|
||||
}
|
||||
|
||||
export const TemporaryDrawer = withStyles(
|
||||
class extends Component<DrawerProps, DrawerState>
|
||||
{
|
||||
constructor(props: DrawerProps) {
|
||||
super(props);
|
||||
this.state = { is_open: props.is_open };
|
||||
|
||||
}
|
||||
|
||||
toggleDrawer(anchor: Anchor, open: boolean): void {
|
||||
this.setState({ is_open: open });
|
||||
};
|
||||
|
||||
|
||||
fireClose() {
|
||||
let handler = this.props.onClose;
|
||||
if (handler != null) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<Drawer anchor={this.props.position} open={this.props.is_open} onClose={() => { this.fireClose(); }} >
|
||||
<div
|
||||
className={this.props.position === 'top' || this.props.position === 'bottom' ? classes.fullList : classes.list}
|
||||
role="presentation"
|
||||
onClick={() => { this.fireClose(); }}
|
||||
onKeyDown={() => { this.fireClose(); }}
|
||||
>
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap", justifyContent: "flex-start", alignItems: "center", width: "100%"}}>
|
||||
|
||||
<IconButton style={{ flex: "0 0 auto" }} >
|
||||
<ArrowBackIcon style={{ fill: '#666' }} />
|
||||
</IconButton>
|
||||
<img src="img/Pi-Logo-3.png" alt="" style={{height: 36}} />
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
},
|
||||
drawerStyles
|
||||
);
|
||||
@@ -0,0 +1,185 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import TextField, { TextFieldProps } from '@mui/material/TextField';
|
||||
import { withStyles, WithStyles } from '@mui/styles';
|
||||
import {createStyles} from './WithStyles';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import Modal from '@mui/material/Modal';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
|
||||
export interface TextFieldExPropsBase extends WithStyles<typeof styles> {
|
||||
label?: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
inputRef?: React.Ref<any>;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export type TextFieldExProps = TextFieldExPropsBase & TextFieldProps;
|
||||
|
||||
|
||||
export interface TextFieldExState {
|
||||
androidHosted: boolean;
|
||||
composingText: string;
|
||||
displayText: string;
|
||||
showIme: boolean;
|
||||
imeValue: string;
|
||||
};
|
||||
|
||||
|
||||
const TextFieldEx = withStyles(styles, { withTheme: true })(
|
||||
class extends React.Component<TextFieldExProps, TextFieldExState> {
|
||||
constructor(props: TextFieldExProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
composingText: "",
|
||||
displayText: "",
|
||||
androidHosted: true,
|
||||
showIme: false,
|
||||
imeValue: ""
|
||||
};
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleImeCompositionStart = this.handleImeCompositionStart.bind(this);
|
||||
this.handleCompositionEnd = this.handleCompositionEnd.bind(this);
|
||||
this.handleImeCompositionUpdate = this.handleImeCompositionUpdate.bind(this);
|
||||
this.handleImeCompositionEnd = this.handleImeCompositionEnd.bind(this);
|
||||
}
|
||||
|
||||
private getUseImeMask() {
|
||||
return this.state.androidHosted && window.innerHeight < 4500;
|
||||
}
|
||||
|
||||
private originalInputHref: HTMLInputElement | undefined = undefined;
|
||||
private imeInputHref: HTMLInputElement | undefined = undefined;
|
||||
|
||||
handleInputRef(input: HTMLInputElement) {
|
||||
if (input) {
|
||||
this.originalInputHref = input;
|
||||
if (this.getUseImeMask()) {
|
||||
input.addEventListener('click', this.handleClick);
|
||||
input.addEventListener('input', this.handleInput);
|
||||
}
|
||||
} else {
|
||||
if (this.originalInputHref) {
|
||||
this.originalInputHref.removeEventListener('click', this.handleClick);
|
||||
this.originalInputHref.removeEventListener('input', this.handleInput);
|
||||
this.originalInputHref = undefined;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event: FocusEvent) {
|
||||
(event.target as any).blur();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.setState({
|
||||
imeValue: (event.target as any).value as string,
|
||||
showIme: true
|
||||
});
|
||||
return false;
|
||||
}
|
||||
handleImeCompositionStart(event: any) {
|
||||
// this.setState({
|
||||
// composingText: '' ,
|
||||
// imeValue: '', //event.target.value as string,
|
||||
// showIme: true
|
||||
// });
|
||||
}
|
||||
|
||||
handleImeCompositionUpdate(event: CompositionEvent) {
|
||||
this.setState({ composingText: event.data });
|
||||
};
|
||||
|
||||
handleCompositionEnd = (event: CompositionEvent) => {
|
||||
this.setState(prevState => ({
|
||||
displayText: prevState.displayText + event.data,
|
||||
composingText: '',
|
||||
}));
|
||||
};
|
||||
|
||||
dismissIme() {
|
||||
if (this.originalInputHref && this.imeInputHref)
|
||||
{
|
||||
let value = this.imeInputHref.value;
|
||||
this.originalInputHref.value = this.imeInputHref.value;
|
||||
this.originalInputHref.blur();
|
||||
this.imeInputHref.blur();
|
||||
this.setState({
|
||||
showIme: false, imeValue: value
|
||||
});
|
||||
}
|
||||
}
|
||||
handleImeCompositionEnd()
|
||||
{
|
||||
this.dismissIme();
|
||||
}
|
||||
handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (!this.state.composingText) {
|
||||
this.setState({ displayText: target.value });
|
||||
}
|
||||
};
|
||||
render() {
|
||||
let { autoFocus, containerStyle, label, ...extra } = this.props;
|
||||
return (
|
||||
<div style={this.props.containerStyle}>
|
||||
<TextField {...extra} autoFocus={autoFocus} tabIndex={-1} inputRef={(input) => this.handleInputRef(input)} />
|
||||
<p>Display Text: {this.state.imeValue}</p>
|
||||
<p>Composing Text: {this.state.composingText}</p>
|
||||
{this.state.showIme && (
|
||||
<Modal open={this.state.showIme}>
|
||||
<div style={{
|
||||
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
|
||||
background: this.props.theme.palette.background.default,
|
||||
display: "flex", flexFlow: "row nowrap",justifyContent: "center"
|
||||
|
||||
}}>
|
||||
<TextField variant="standard"
|
||||
inputRef={(element: HTMLInputElement)=>{
|
||||
if (this.imeInputHref) {
|
||||
this.imeInputHref.removeEventListener("compositionend",this.handleImeCompositionEnd);
|
||||
this.imeInputHref.removeEventListener("compositionupdate",this.handleImeCompositionUpdate);
|
||||
this.imeInputHref.removeEventListener("compositionstart",this.handleImeCompositionStart);
|
||||
}
|
||||
this.imeInputHref = element;
|
||||
if (this.imeInputHref)
|
||||
{
|
||||
this.imeInputHref.addEventListener("compositionend",this.handleImeCompositionEnd);
|
||||
this.imeInputHref.addEventListener("compositionupdate",this.handleImeCompositionUpdate);
|
||||
this.imeInputHref.addEventListener("compositionstart",this.handleImeCompositionStart);
|
||||
|
||||
}
|
||||
}}
|
||||
style={{width: 200,marginTop: 16}}
|
||||
autoFocus
|
||||
value={this.state.imeValue}
|
||||
onChange={(event: any) => {
|
||||
this.setState({ imeValue: event.target.value as string });
|
||||
}}
|
||||
onBlur={() => {
|
||||
this.dismissIme();
|
||||
}}
|
||||
onKeyDown={(event)=> {
|
||||
if (event.key === "Enter")
|
||||
{
|
||||
this.setState({showIme: false});
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default TextFieldEx;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user