First commit.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import React, { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import JackHostStatus from './JackHostStatus';
|
||||
|
||||
|
||||
interface AboutDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
|
||||
};
|
||||
|
||||
interface AboutDialogState {
|
||||
jackStatus?: JackHostStatus;
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
},
|
||||
dialogTitle: {
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
},
|
||||
sectionHead: {
|
||||
marginLeft: 24,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 12
|
||||
|
||||
},
|
||||
textBlock: {
|
||||
marginLeft: 24,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 0,
|
||||
opacity: 0.95
|
||||
|
||||
},
|
||||
textBlockIndented: {
|
||||
marginLeft: 40,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 0,
|
||||
opacity: 0.95
|
||||
|
||||
},
|
||||
|
||||
setting: {
|
||||
minHeight: 64,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
paddingLeft: 22,
|
||||
paddingRight: 22
|
||||
},
|
||||
primaryItem: {
|
||||
|
||||
},
|
||||
secondaryItem: {
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
const AboutDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
class extends Component<AboutDialogProps, AboutDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
|
||||
constructor(props: AboutDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.state = {
|
||||
jackStatus: undefined
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
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?: NodeJS.Timeout;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.updateNotifications();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.updateNotifications();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.updateNotifications();
|
||||
}
|
||||
|
||||
handleDialogClose(e: SyntheticEvent) {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
return (
|
||||
<Dialog fullScreen open={this.props.open}
|
||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}>
|
||||
|
||||
<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.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.dialogTitle}>
|
||||
About
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: "1 1 auto", position: "relative", overflow: "hidden",
|
||||
overflowX: "hidden", overflowY: "auto", margin: 22
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Typography display="block" variant="h6" color="textPrimary">
|
||||
PiPedal <span style={{fontSize: "0.7em"}}>
|
||||
{(this.model.serverVersion? this.model.serverVersion.serverVersion : "")
|
||||
+ (this.model.serverVersion?.debug ? " (Debug)": "")}
|
||||
</span>
|
||||
</Typography>
|
||||
<Typography display="block" variant="body2" style={{marginBottom: 12}} >
|
||||
Copyright © 2021 Robin Davies. All rights reserved.
|
||||
</Typography>
|
||||
<Divider/>
|
||||
<Typography display="block" variant="caption" >
|
||||
JACK AUDIO STATUS
|
||||
</Typography>
|
||||
<div style={{paddingTop: 8, paddingBottom: 24}}>
|
||||
{
|
||||
JackHostStatus.getDisplayView("",this.state.jackStatus)
|
||||
}
|
||||
</div>
|
||||
<Divider/>
|
||||
<Typography display="block" variant="caption" >
|
||||
LEGAL NOTICES
|
||||
</Typography>
|
||||
<Typography display="block" variant="caption" style={{paddingTop: 12, paddingBottom: 12}} >
|
||||
Pi Pedal uses the following open-source components.
|
||||
</Typography>
|
||||
<Typography display="block" variant="caption" style={{paddingTop: 12, paddingBottom: 12}} >
|
||||
TBD
|
||||
</Typography>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
export default AboutDialog;
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,830 @@
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { ThemeProvider } from '@material-ui/styles';
|
||||
import { createMuiTheme } from '@material-ui/core/styles';
|
||||
|
||||
import './App.css';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { createStyles, withStyles, WithStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuButton from '@material-ui/icons/Menu';
|
||||
import { TemporaryDrawer } from './TemporaryDrawer';
|
||||
import { Theme } from '@material-ui/core/styles/createMuiTheme';
|
||||
import FullscreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import List from '@material-ui/core/List';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { OnChangedHandler } from './ObservableProperty';
|
||||
import ErrorOutlineIcon from '@material-ui/icons/Error';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import PresetSelector from './PresetSelector';
|
||||
import SettingsDialog from './SettingsDialog';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import BankDialog from './BankDialog';
|
||||
|
||||
import { PiPedalModelFactory, PiPedalModel, State } from './PiPedalModel';
|
||||
import MainPage from './MainPage';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import ListSubheader from '@material-ui/core/ListSubheader';
|
||||
import { BankIndex, BankIndexEntry } from './Banks';
|
||||
import RenameDialog from './RenameDialog';
|
||||
import JackStatusView from './JackStatusView';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#324c6c"
|
||||
}
|
||||
// secondary: {
|
||||
// main: "#00E676"
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
const appStyles = ({ palette, spacing, mixins }: Theme) => createStyles({
|
||||
loadingContent: {
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
minHeight: "10em",
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "#DDD",
|
||||
opacity: "0.95",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
zIndex: 2010
|
||||
},
|
||||
errorContent: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
position: "fixed",
|
||||
minHeight: "10em",
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
color: "#444",
|
||||
zIndex: 2000
|
||||
},
|
||||
errorContentMask: {
|
||||
position: "absolute", left: 0, top: 0,
|
||||
width: "100%", height: "100%",
|
||||
background: "#BBB",
|
||||
opacity: 0.95,
|
||||
zIndex: 1999
|
||||
},
|
||||
errorText: {
|
||||
marginTop: 0,
|
||||
padding: 0,
|
||||
marginBottom: 12,
|
||||
fontWeight: 500,
|
||||
fontSize: "13pt",
|
||||
maxWidth: 250,
|
||||
opacity: 1,
|
||||
zIndex: 2010,
|
||||
},
|
||||
progressText: {
|
||||
marginTop: 0,
|
||||
fontWeight: 500,
|
||||
fontSize: "13pt",
|
||||
maxWidth: 250,
|
||||
opacity: 1,
|
||||
zIndex: 2010,
|
||||
paddingTop: 12,
|
||||
},
|
||||
|
||||
errorMessageBox: {
|
||||
flex: "0 0 auto",
|
||||
width: 300,
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
zIndex: 2010
|
||||
|
||||
|
||||
},
|
||||
errorMessage: {
|
||||
color: '#000',
|
||||
textAlign: "left",
|
||||
zIndex: 2010
|
||||
|
||||
},
|
||||
loadingBox: {
|
||||
position: "relative",
|
||||
top: "20%",
|
||||
width: "120px",
|
||||
color: "#888",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
// border: "3px solid #888",
|
||||
borderRadius: "12px",
|
||||
padding: "12px",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
opacity: 0.8,
|
||||
zIndex: 2010
|
||||
|
||||
},
|
||||
loadingBoxItem: {
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
zIndex: 2010
|
||||
|
||||
},
|
||||
|
||||
|
||||
toolBarContent:
|
||||
{
|
||||
position: "absolute", top: 0, width: "100%"
|
||||
},
|
||||
|
||||
toolBarSpacer:
|
||||
{
|
||||
position: "relative", flex: "0 0 auto"
|
||||
},
|
||||
|
||||
|
||||
mainFrame: {
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "1 1 100%"
|
||||
},
|
||||
|
||||
toolbarSizingPosition: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
width: "100%",
|
||||
flexBasis: "auto"
|
||||
},
|
||||
mainSizingPosition: {
|
||||
overflow: "hidden",
|
||||
flex: "1 1 auto"
|
||||
},
|
||||
|
||||
heroContent: {
|
||||
backgroundColor: palette.background.paper,
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
},
|
||||
|
||||
drawerItem: {
|
||||
width: 350,
|
||||
background: "#FF8080"
|
||||
},
|
||||
drawerItemFullWidth: {
|
||||
width: 'auto',
|
||||
},
|
||||
|
||||
icon: {
|
||||
marginRight: spacing(2),
|
||||
},
|
||||
|
||||
heroButtons: {
|
||||
marginTop: spacing(4),
|
||||
},
|
||||
cardGrid: {
|
||||
paddingTop: spacing(8),
|
||||
paddingBottom: spacing(8),
|
||||
},
|
||||
card: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
cardMedia: {
|
||||
paddingTop: '56.25%', // 16:9
|
||||
},
|
||||
cardContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
footer: {
|
||||
backgroundColor: palette.background.paper,
|
||||
padding: spacing(6),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function preventDefault(e: SyntheticEvent): void {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
isDrawerOpen: boolean;
|
||||
errorMessage: string;
|
||||
displayState: State;
|
||||
|
||||
canFullScreen: boolean;
|
||||
isFullScreen: boolean;
|
||||
tinyToolBar: boolean;
|
||||
alertDialogOpen: boolean;
|
||||
alertDialogMessage: string;
|
||||
isSettingsDialogOpen: boolean;
|
||||
isDebug: boolean;
|
||||
|
||||
renameBankDialogOpen: boolean;
|
||||
saveBankAsDialogOpen: boolean;
|
||||
|
||||
aboutDialogOpen: boolean;
|
||||
bankDialogOpen: boolean;
|
||||
editBankDialogOpen: boolean;
|
||||
|
||||
|
||||
presetName: string;
|
||||
presetChanged: boolean;
|
||||
banks: BankIndex;
|
||||
bankDisplayItems: number;
|
||||
};
|
||||
interface AppProps extends WithStyles<typeof appStyles> {
|
||||
}
|
||||
|
||||
const App = withStyles(appStyles)(class extends ResizeResponsiveComponent<AppProps, AppState> {
|
||||
// Before the component mounts, we initialise our state
|
||||
|
||||
model_: PiPedalModel;
|
||||
errorChangeHandler_: OnChangedHandler<string>;
|
||||
stateChangeHandler_: OnChangedHandler<State>;
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
this.model_ = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
isDrawerOpen: false,
|
||||
errorMessage: this.model_.errorMessage.get(),
|
||||
displayState: this.model_.state.get(),
|
||||
canFullScreen: supportsFullScreen(),
|
||||
isFullScreen: !!document.fullscreenElement,
|
||||
tinyToolBar: false,
|
||||
alertDialogOpen: false,
|
||||
alertDialogMessage: "",
|
||||
presetName: this.model_.presets.get().getSelectedText(),
|
||||
isSettingsDialogOpen: false,
|
||||
isDebug: true,
|
||||
presetChanged: this.model_.presets.get().presetChanged,
|
||||
banks: this.model_.banks.get(),
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: false,
|
||||
aboutDialogOpen: false,
|
||||
bankDialogOpen: false,
|
||||
editBankDialogOpen: false,
|
||||
bankDisplayItems: 5
|
||||
|
||||
};
|
||||
|
||||
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
||||
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
||||
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
||||
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
||||
this.handleCloseAlert = this.handleCloseAlert.bind(this);
|
||||
this.banksChangedHandler = this.banksChangedHandler.bind(this);
|
||||
|
||||
}
|
||||
|
||||
onOpenBank(bankId: number) {
|
||||
this.model_.openBank(bankId)
|
||||
.catch((error) => this.model_.showAlert(error));
|
||||
|
||||
}
|
||||
|
||||
handleSaveBankAsOk(newName: string) {
|
||||
let currentName = this.model_.banks.get().getSelectedEntryName();
|
||||
if (currentName === newName) {
|
||||
this.setState({
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: false
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (this.model_.banks.get().nameExists(newName)) {
|
||||
this.model_.showAlert("A bank by that name already exists.");
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: false
|
||||
});
|
||||
this.model_.saveBankAs(this.model_.banks.get().selectedBank, newName)
|
||||
.catch((error) => {
|
||||
this.model_.showAlert(error);
|
||||
});
|
||||
|
||||
}
|
||||
handleBankRenameOk(newName: string) {
|
||||
let currentName = this.model_.banks.get().getSelectedEntryName();
|
||||
if (currentName === newName) {
|
||||
this.setState({
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: false
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (this.model_.banks.get().nameExists(newName)) {
|
||||
this.model_.showAlert("A bank by that name already exists.");
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
renameBankDialogOpen: false
|
||||
});
|
||||
this.model_.renameBank(this.model_.banks.get().selectedBank, newName)
|
||||
.catch((error) => {
|
||||
this.model_.showAlert(error);
|
||||
});
|
||||
|
||||
}
|
||||
handleSettingsDialogClose() {
|
||||
this.setState({
|
||||
isSettingsDialogOpen: false
|
||||
});
|
||||
}
|
||||
handleDrawerSettingsClick() {
|
||||
this.setState({
|
||||
isDrawerOpen: false,
|
||||
isSettingsDialogOpen: true
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleDrawerManageBanks() {
|
||||
this.setState({
|
||||
isDrawerOpen: false,
|
||||
bankDialogOpen: true,
|
||||
editBankDialogOpen: true
|
||||
});
|
||||
|
||||
}
|
||||
handleDrawerSelectBank() {
|
||||
this.setState({
|
||||
isDrawerOpen: false,
|
||||
bankDialogOpen: true,
|
||||
editBankDialogOpen: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleDrawerAboutClick() {
|
||||
this.setState({
|
||||
aboutDialogOpen: true
|
||||
});
|
||||
|
||||
}
|
||||
handleDrawerRenameBank() {
|
||||
this.setState({
|
||||
isDrawerOpen: false,
|
||||
renameBankDialogOpen: true,
|
||||
saveBankAsDialogOpen: false
|
||||
});
|
||||
|
||||
}
|
||||
handleDrawerSaveBankAs() {
|
||||
this.setState({
|
||||
isDrawerOpen: false,
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleCloseAlert(e?: any, reason?: any) {
|
||||
this.model_.alertMessage.set("");
|
||||
}
|
||||
banksChangedHandler() {
|
||||
this.setState({
|
||||
banks: this.model_.banks.get()
|
||||
});
|
||||
}
|
||||
presetChangedHandler() {
|
||||
let presets = this.model_.presets.get();
|
||||
|
||||
this.setState({
|
||||
presetName: presets.getSelectedText(),
|
||||
presetChanged: presets.presetChanged
|
||||
});
|
||||
}
|
||||
toggleFullScreen(): void {
|
||||
setFullScreen(this.state.isFullScreen);
|
||||
this.setState({ isFullScreen: !this.state.isFullScreen });
|
||||
}
|
||||
componentDidMount() {
|
||||
|
||||
super.componentDidMount();
|
||||
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.preventDefault();
|
||||
if (this.model_.state.get() === State.Ready)
|
||||
{
|
||||
e.returnValue = "Are you sure you want to leave this page?";
|
||||
return "Are you sure you want to leave this page?";
|
||||
}
|
||||
});
|
||||
|
||||
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
||||
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
||||
this.model_.pedalBoard.addOnChangedHandler(this.presetChangedHandler);
|
||||
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
||||
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
||||
this.alertMessageChangedHandler();
|
||||
}
|
||||
|
||||
|
||||
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_.serverVersion.debug;
|
||||
|
||||
let overscrollBehavior = preventOverscroll ? "none" : "auto";
|
||||
document.body.style.overscrollBehavior = overscrollBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
||||
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
||||
this.model_.pedalBoard.removeOnChangedHandler(this.presetChangedHandler);
|
||||
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
||||
|
||||
}
|
||||
|
||||
alertMessageChangedHandler() {
|
||||
let message = this.model_.alertMessage.get();
|
||||
if (message === "") {
|
||||
this.setState({ alertDialogOpen: false });
|
||||
// leave the message intact so the dialog can fade.
|
||||
} else {
|
||||
this.setState({
|
||||
alertDialogOpen: true,
|
||||
alertDialogMessage: message
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
updateResponsive() {
|
||||
// functional, but disabled.
|
||||
// let tinyToolBar_ = this.windowSize.height < 600;
|
||||
// this.setState({ tinyToolBar: tinyToolBar_ });
|
||||
|
||||
let height = this.windowSize.height;
|
||||
|
||||
const ENTRY_HEIGHT = 48;
|
||||
// ENTRY_HEIGHT*6 +K = 727 from observation.
|
||||
const K = 450;
|
||||
|
||||
let bankEntries = Math.floor((height-K)/ENTRY_HEIGHT);
|
||||
if (bankEntries < 1) bankEntries = 1;
|
||||
if (bankEntries > 7) bankEntries = 7;
|
||||
this.setState({ bankDisplayItems: bankEntries });
|
||||
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width, height);
|
||||
this.updateResponsive();
|
||||
}
|
||||
|
||||
|
||||
setErrorMessage(message: string): void {
|
||||
this.setState({ errorMessage: message });
|
||||
}
|
||||
setDisplayState(newState: State): void {
|
||||
this.updateOverscroll();
|
||||
|
||||
this.setState({ displayState: newState });
|
||||
}
|
||||
|
||||
showDrawer() {
|
||||
this.setState({ isDrawerOpen: true })
|
||||
}
|
||||
hideDrawer() {
|
||||
this.setState({ isDrawerOpen: false })
|
||||
}
|
||||
shortBankList(banks: BankIndex): BankIndexEntry[] {
|
||||
let n = this.state.bankDisplayItems;
|
||||
let entries = banks.entries;
|
||||
if (entries.length < n + 1) { // +1 for the .... entry.
|
||||
return entries;
|
||||
}
|
||||
let result: BankIndexEntry[] = [];
|
||||
let selectedIndex = -1;
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
if (entries[i].instanceId === banks.selectedBank) {
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n > entries.length) n = entries.length;
|
||||
if (selectedIndex > n) {
|
||||
for (let i = 0; i < n - 1; ++i) {
|
||||
result.push(entries[i]);
|
||||
}
|
||||
result.push(entries[selectedIndex]);
|
||||
} else {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
result.push(entries[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
render() {
|
||||
|
||||
const { classes } = this.props;
|
||||
|
||||
let shortBankList = this.shortBankList(this.state.banks);
|
||||
let showBankSelectDialog = shortBankList.length !== this.state.banks.entries.length;
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div style={{
|
||||
minHeight: 345, minWidth: 390,
|
||||
position: "absolute", width: "100%", height: "100%", background: "#F88", userSelect: "none",
|
||||
display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
overscrollBehavior: this.state.isDebug ? "auto" : "none"
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (!this.model_.serverVersion?.debug??false)
|
||||
{
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CssBaseline />
|
||||
{(!this.state.tinyToolBar) ?
|
||||
(
|
||||
<AppBar position="absolute">
|
||||
<Toolbar variant="dense" >
|
||||
<IconButton edge="start"
|
||||
color="inherit" aria-label="menu" onClick={() => { this.showDrawer() }}
|
||||
>
|
||||
<MenuButton />
|
||||
</IconButton>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ flex: "0 1 400px", minWidth: 100 }}>
|
||||
<PresetSelector />
|
||||
</div>
|
||||
<div style={{ flex: "2 2 30px" }} />
|
||||
{this.state.canFullScreen &&
|
||||
<IconButton
|
||||
color="inherit" aria-label="menu" onClick={() => { this.toggleFullScreen(); }}>
|
||||
{this.state.isFullScreen ? (
|
||||
<FullscreenExitIcon />
|
||||
) : (
|
||||
<FullscreenIcon />
|
||||
|
||||
)}
|
||||
|
||||
</IconButton>
|
||||
}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
) : (
|
||||
<div className={classes.toolBarContent} >
|
||||
<IconButton style={{ position: "absolute", left: 12, top: 8, zIndex: 2 }}
|
||||
color="inherit" aria-label="menu" onClick={() => { this.showDrawer() }}
|
||||
>
|
||||
<MenuButton />
|
||||
</IconButton>
|
||||
{this.state.canFullScreen && (
|
||||
<IconButton style={{ position: "absolute", right: 8, top: 8, zIndex: 2 }}
|
||||
color="inherit" aria-label="menu" onClick={() => { this.toggleFullScreen(); }}>
|
||||
{this.state.isFullScreen ? (
|
||||
<FullscreenExitIcon />
|
||||
) : (
|
||||
<FullscreenIcon />
|
||||
|
||||
)}
|
||||
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<TemporaryDrawer position='left' title="piddle"
|
||||
is_open={this.state.isDrawerOpen} onClose={() => { this.hideDrawer(); }} >
|
||||
<List subheader={
|
||||
<ListSubheader component="div" id="nested-list-subheader">Banks</ListSubheader>
|
||||
}>
|
||||
{
|
||||
shortBankList.map((bank) => {
|
||||
return (
|
||||
<ListItem button key={'bank' + bank.instanceId} selected={bank.instanceId === this.state.banks.selectedBank}
|
||||
onClick={() => this.onOpenBank(bank.instanceId)}
|
||||
>
|
||||
|
||||
<ListItemText primary={bank.name} />
|
||||
</ListItem>
|
||||
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
showBankSelectDialog && (
|
||||
<ListItem button key={'bankDOTDOTDOT'} selected={false}
|
||||
onClick={() => this.handleDrawerSelectBank()}
|
||||
>
|
||||
|
||||
<ListItemText primary={"..."} />
|
||||
</ListItem>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
<ListItem button key='RenameBank' onClick={() => { this.handleDrawerRenameBank() }}>
|
||||
<ListItemIcon><img src="img/drive_file_rename_outline_black_24dp.svg" alt="" style={{ opacity: 0.6 }} /></ListItemIcon>
|
||||
<ListItemText primary='Rename Bank' />
|
||||
</ListItem>
|
||||
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
|
||||
<ListItemIcon>
|
||||
<img src="img/save_bank_as.svg" alt="" style={{ opacity: 0.6 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Save As New Bank' />
|
||||
</ListItem>
|
||||
<ListItem button key='CreateBank' onClick={() => { this.handleDrawerManageBanks(); }}>
|
||||
<ListItemIcon>
|
||||
<img src="img/edit_banks.svg" alt="" style={{ opacity: 0.6 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Manage Banks...' />
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
<ListItem button key='Settings' onClick={() => { this.handleDrawerSettingsClick() }}>
|
||||
<ListItemIcon>
|
||||
<img src="img/settings_black_24dp.svg" alt="" style={{ opacity: 0.6 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Settings' />
|
||||
</ListItem>
|
||||
<ListItem button key='Settings' onClick={() => { this.handleDrawerAboutClick() }}>
|
||||
<ListItemIcon>
|
||||
<img src="img/help_outline_black_24dp.svg" alt="" style={{ opacity: 0.6 }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='About' />
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</TemporaryDrawer>
|
||||
{!this.state.tinyToolBar && (
|
||||
<Toolbar className={classes.toolBarSpacer} variant="dense" />
|
||||
)}
|
||||
<main className={classes.mainFrame} >
|
||||
<div className={classes.mainSizingPosition}>
|
||||
<div className={classes.heroContent}>
|
||||
{(this.state.displayState !== State.Loading) && (
|
||||
<MainPage hasTinyToolBar={this.state.tinyToolBar} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
||||
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
||||
<SettingsDialog open={this.state.isSettingsDialogOpen} onClose={() => this.handleSettingsDialogClose()} />
|
||||
<RenameDialog
|
||||
open={this.state.renameBankDialogOpen || this.state.saveBankAsDialogOpen}
|
||||
defaultName={this.model_.banks.get().getSelectedEntryName()}
|
||||
acceptActionName={"Rename"}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
renameBankDialogOpen: false,
|
||||
saveBankAsDialogOpen: false
|
||||
})
|
||||
}}
|
||||
onOk={(text: string) => {
|
||||
if (this.state.renameBankDialogOpen) {
|
||||
this.handleBankRenameOk(text);
|
||||
} else if (this.state.saveBankAsDialogOpen) {
|
||||
this.handleSaveBankAsOk(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={this.state.alertDialogOpen}
|
||||
onClose={this.handleCloseAlert}
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{
|
||||
this.model_.alertMessage.get()
|
||||
}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleCloseAlert} color="primary" autoFocus>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<JackStatusView />
|
||||
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Reconnecting ? "block" : "none" }}
|
||||
>
|
||||
<div className={classes.errorContentMask} />
|
||||
|
||||
<div className={classes.loadingBox}>
|
||||
<div className={classes.loadingBoxItem}>
|
||||
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
|
||||
</div>
|
||||
<Typography variant="body2" className={classes.progressText}>
|
||||
Reconnecting...
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Error ? "flex" : "none" }}
|
||||
onMouseDown={preventDefault} onKeyDown={preventDefault}
|
||||
>
|
||||
<div className={classes.errorContentMask} />
|
||||
<div style={{ flex: "2 2 3px", height: 20 }} > </div>
|
||||
<div className={classes.errorMessageBox} style={{ position: "relative" }} >
|
||||
<div style={{ fontSize: "30px", position: "absolute", left: 0, top: 0, color: "#A00" }}>
|
||||
<ErrorOutlineIcon color="inherit" fontSize="inherit" style={{ float: "left", marginRight: "12px" }} />
|
||||
</div>
|
||||
<div style={{ marginLeft: 40, marginTop: 3 }}>
|
||||
<p className={classes.errorText}>
|
||||
Error: {this.state.errorMessage}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ paddingTop: 50, paddingLeft: 36, textAlign: "left" }}>
|
||||
<Button variant='contained' color="primary" component='button'
|
||||
onClick={() => window.location.reload()} >
|
||||
Reload
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{ flex: "5 5 auto", height: 20 }} > </div>
|
||||
|
||||
</div>
|
||||
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Loading ? "block" : "none" }}>
|
||||
<div className={classes.errorContentMask} />
|
||||
<div className={classes.loadingBox}>
|
||||
<div className={classes.loadingBoxItem}>
|
||||
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
|
||||
</div>
|
||||
<Typography variant="body2" className={classes.loadingBoxItem}>
|
||||
Loading...
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div >
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,470 @@
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { BankIndexEntry, BankIndex } from './Banks';
|
||||
import Button from "@material-ui/core/Button";
|
||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import RenameDialog from './RenameDialog';
|
||||
|
||||
interface BankDialogProps extends WithStyles<typeof styles> {
|
||||
show: boolean;
|
||||
isEditDialog: boolean;
|
||||
onDialogClose: () => void;
|
||||
|
||||
};
|
||||
|
||||
interface BankDialogState {
|
||||
banks: BankIndex;
|
||||
|
||||
showActionBar: boolean;
|
||||
|
||||
selectedItem: number;
|
||||
|
||||
filenameDialogOpen: boolean;
|
||||
filenameSaveAs: boolean;
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
},
|
||||
dialogActionBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0,
|
||||
background: "black"
|
||||
},
|
||||
dialogTitle: {
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
},
|
||||
itemFrame: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
height: "56px",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 8
|
||||
},
|
||||
iconFrame: {
|
||||
flex: "0 0 auto",
|
||||
|
||||
},
|
||||
itemIcon: {
|
||||
width: 24, height: 24, margin: 12, opacity: 0.6
|
||||
},
|
||||
itemLabel: {
|
||||
flex: "1 1 1px",
|
||||
marginLeft: 8
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ActionBarTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="right" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
const BankDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
class extends Component<BankDialogProps, BankDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
|
||||
constructor(props: BankDialogProps) {
|
||||
super(props);
|
||||
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
|
||||
};
|
||||
this.handleBanksChanged = this.handleBanksChanged.bind(this);
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
|
||||
}
|
||||
|
||||
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 preset.
|
||||
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;
|
||||
|
||||
stateWasPopped: boolean = false;
|
||||
handlePopState(e: any): any {
|
||||
let state: any = e.state;
|
||||
if (!state || !state.bankDialog)
|
||||
{
|
||||
this.stateWasPopped = true;
|
||||
this.props.onDialogClose();
|
||||
}
|
||||
}
|
||||
|
||||
updateBackButtonHooks() : void {
|
||||
let wantHooks = this.mounted && this.props.show;
|
||||
if (wantHooks !== this.hasHooks)
|
||||
{
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks)
|
||||
{
|
||||
this.stateWasPopped = false;
|
||||
window.addEventListener("popstate",this.handlePopState);
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
let newState: any = history.state;
|
||||
if (!newState)
|
||||
{
|
||||
newState = {};
|
||||
}
|
||||
newState.bankDialog = true;
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.pushState(
|
||||
newState,
|
||||
"PiPedal - Banks",
|
||||
"#Banks"
|
||||
);
|
||||
} else {
|
||||
window.removeEventListener("popstate",this.handlePopState);
|
||||
if (!this.stateWasPopped)
|
||||
{
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.back();
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.replaceState({},"PiPedal","#");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
this.updateBackButtonHooks();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.model.banks.addOnChangedHandler(this.handleBanksChanged);
|
||||
this.handleBanksChanged();
|
||||
// scroll selected item into view.
|
||||
this.mounted = true;
|
||||
this.updateBackButtonHooks();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.banks.removeOnChangedHandler(this.handleBanksChanged);
|
||||
this.mounted = false;
|
||||
this.updateBackButtonHooks();
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
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 presetEntry = el as BankIndexEntry;
|
||||
let classes = this.props.classes;
|
||||
let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.banks.selectedBank;
|
||||
return (
|
||||
<div key={presetEntry.instanceId} style={{ background: "white" }} >
|
||||
|
||||
<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="img/ic_bank.svg" className={classes.itemIcon} alt="" />
|
||||
</div>
|
||||
<div className={classes.itemLabel}>
|
||||
<Typography>
|
||||
{presetEntry.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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
|
||||
let defaultSelectedIndex = this.getSelectedIndex();
|
||||
|
||||
return (
|
||||
<Dialog fullScreen open={this.props.show}
|
||||
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}>
|
||||
<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}>
|
||||
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 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>
|
||||
</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>
|
||||
</Dialog >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
export default BankDialog;
|
||||
@@ -0,0 +1,95 @@
|
||||
|
||||
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,58 @@
|
||||
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 '@material-ui/core/Typography';
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import ToobInputStageViewFactory from './ToobInputStageView';
|
||||
import ToobToneStackViewFactory from './ToobToneStackView';
|
||||
import ToobCabSimViewFactory from './ToobCabSimView';
|
||||
|
||||
|
||||
let pluginFactories: IControlViewFactory[] = [
|
||||
new ToobInputStageViewFactory(),
|
||||
new ToobToneStackViewFactory(),
|
||||
new ToobCabSimViewFactory()
|
||||
];
|
||||
|
||||
|
||||
export function GetControlView(pedalBoardItem?: PedalBoardItem| null): React.ReactNode
|
||||
{
|
||||
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||
|
||||
if (!pedalBoardItem) {
|
||||
return (<div/>);
|
||||
}
|
||||
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,479 @@
|
||||
import { MouseEvent, PointerEvent, ReactNode, Component } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
|
||||
|
||||
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: {
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
}
|
||||
});
|
||||
|
||||
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(styles, { withTheme: true })(
|
||||
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?: NodeJS.Timeout = 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() {
|
||||
return (
|
||||
<div 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>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default Draggable;
|
||||
@@ -0,0 +1,922 @@
|
||||
import React, { MouseEvent, PointerEvent, Component } from 'react';
|
||||
import { createStyles, Theme } from '@material-ui/core/styles';
|
||||
import { Property } from "csstype";
|
||||
import { nullCast} from './Utility'
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
margin: "12px",
|
||||
background: theme.palette.background.paper
|
||||
}
|
||||
});
|
||||
|
||||
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>;
|
||||
refGrid: React.RefObject<HTMLDivElement>;
|
||||
|
||||
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?: NodeJS.Timeout = 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 = "#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?: NodeJS.Timeout;
|
||||
|
||||
|
||||
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,212 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { UiControl } from './Lv2Plugin';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import { nullCast } from './Utility';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
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 =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends ResizeResponsiveComponent<FullScreenIMEProps, FullScreenIMEState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onInputKeyPress(e: any): void {
|
||||
if (e.charCode === 13) {
|
||||
if (this.inputChanged) {
|
||||
this.inputChanged = false;
|
||||
this.validateInput(true);
|
||||
}
|
||||
else {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiControl?: UiControl;
|
||||
|
||||
currentWidth?: number;
|
||||
|
||||
render(): ReactNode {
|
||||
//let classes = this.props.classes;
|
||||
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 (
|
||||
<Dialog fullScreen open={!!(this.props.uiControl)} onClose={(e, r) => this.handleClose(e, r)} >
|
||||
<div style={{
|
||||
width: "100%", height: "100%", position: "relative",
|
||||
display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
justifyContent: "center", alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">{this.props.caption}</Typography>
|
||||
<Input key={value}
|
||||
type="number"
|
||||
defaultValue={control.formatValue(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>
|
||||
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default FullScreenIME;
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
import React, {Component} from 'react';
|
||||
import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
|
||||
import { MonitorPortHandle, PiPedalModel, PiPedalModelFactory } from "./PiPedalModel";
|
||||
import SvgPathBuilder from './SvgPathBuilder'
|
||||
|
||||
//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 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> {
|
||||
theme: Theme;
|
||||
instanceId: number;
|
||||
};
|
||||
type GxTunerControlState = {
|
||||
pitchInfo: PitchInfo
|
||||
tet: number;
|
||||
refFrequency: number;
|
||||
};
|
||||
|
||||
const GxTunerControl =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<GxTunerControlProps, GxTunerControlState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
refRoot: React.RefObject<HTMLDivElement>;
|
||||
|
||||
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,
|
||||
refFrequency: 440
|
||||
|
||||
};
|
||||
this.onFrequencyUpdated = this.onFrequencyUpdated.bind(this);
|
||||
}
|
||||
|
||||
|
||||
noteToPitchInfo(hz: number) : PitchInfo
|
||||
{
|
||||
if (hz < 65) // ground hum. Ignore it.
|
||||
{
|
||||
hz = 0;
|
||||
}
|
||||
let tet = this.state.tet;
|
||||
let refFrequency = this.state.refFrequency;
|
||||
let aOffset: number;
|
||||
let names: string[];
|
||||
let semitoneCents = 100;
|
||||
let valid = false;
|
||||
|
||||
|
||||
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 (hz !== 0)
|
||||
{
|
||||
|
||||
let note = Math.log2(hz/refFrequency)*tet + aOffset;
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
subscribedInstanceId: number = -1;
|
||||
|
||||
monitorHandle?: MonitorPortHandle;
|
||||
|
||||
addSubscription() {
|
||||
this.subscribedInstanceId = this.props.instanceId;
|
||||
this.monitorHandle = this.model.monitorPort(this.props.instanceId,FREQUENCY_PORT_NAME,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();
|
||||
|
||||
}
|
||||
}
|
||||
componentDidMount()
|
||||
{
|
||||
this.addSubscription();
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.updateSubscription();
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
this.removeSubscription();
|
||||
}
|
||||
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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 = -25/100;
|
||||
|
||||
// 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;
|
||||
} 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>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (<div ref={this.refRoot} style={{width: DIAL_WIDTH, height: DIAL_HEIGHT, fontSize: "2em", fontWeight: 700, position: "relative",
|
||||
boxShadow: "1px 5px 6px #888 inset",
|
||||
fontFamily: "arial,roboto,helvetica,sans"}}>
|
||||
<div style={{position: "absolute", left: 0, bottom: 5, width: "50%",textAlign: "right"}}>
|
||||
<span style={{ marginRight: 20, color: "#444", 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: "#444", textAlign: "left" }}>{this.state.pitchInfo.fractionText}</span>
|
||||
</div>
|
||||
|
||||
{ this.renderDial(this.state.pitchInfo) }
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
export default GxTunerControl;
|
||||
@@ -0,0 +1,13 @@
|
||||
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,100 @@
|
||||
import {PiPedalArgumentError} from './PiPedalError';
|
||||
|
||||
export class JackChannelSelection {
|
||||
deserialize(input: any): JackChannelSelection
|
||||
{
|
||||
this.inputAudioPorts = input.inputAudioPorts.slice();
|
||||
this.outputAudioPorts = input.outputAudioPorts.slice();
|
||||
this.inputMidiPorts = input.inputMidiPorts.slice();
|
||||
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[] = [];
|
||||
inputMidiPorts: string[] = [];
|
||||
|
||||
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);
|
||||
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 "Invalid selection";
|
||||
|
||||
}
|
||||
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 {
|
||||
throw new PiPedalArgumentError("Invalid channel selection."); // should be subset of jackConfiguration.
|
||||
}
|
||||
} else {
|
||||
if (selectedChannels.length === 2) return "Stereo (custom selection)";
|
||||
if (selectedChannels.length === 1) return "Mono (custom selection)";
|
||||
throw new PiPedalArgumentError("Invalid channel selection."); // should be subset of jackConfiguration.
|
||||
}
|
||||
}
|
||||
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.inputMidiPorts = input.inputMidiPorts;
|
||||
this.outputMidiPorts = input.outputMidiPorts;
|
||||
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[] = [];
|
||||
inputMidiPorts: string[] = [];
|
||||
outputMidiPorts: string[] = [];
|
||||
|
||||
};
|
||||
|
||||
export default JackConfiguration;
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
|
||||
const RED_COLOR = "#C00";
|
||||
const GREEN_COLOR = "#666";
|
||||
|
||||
|
||||
|
||||
function tempDisplay(mC: number): string
|
||||
{
|
||||
return (mC/1000).toFixed(1) + "\u00B0C"; // degrees C.
|
||||
}
|
||||
function cpuDisplay(cpu: number): string
|
||||
{
|
||||
return cpu.toFixed(1)+"%";
|
||||
}
|
||||
|
||||
export default class JackHostStatus {
|
||||
deserialize(input: any): JackHostStatus
|
||||
{
|
||||
this.active = input.active;
|
||||
this.restarting = input.restarting;
|
||||
this.underruns = input.underruns;
|
||||
this.cpuUsage = input.cpuUsage;
|
||||
this.msSinceLastUnderrun = input.msSinceLastUnderrun;
|
||||
this.temperaturemC = input.temperaturemC;
|
||||
return this;
|
||||
}
|
||||
hasTemperature() : boolean {
|
||||
return this.temperaturemC >= -100000;
|
||||
}
|
||||
active: boolean = false;
|
||||
restarting: boolean = false;
|
||||
underruns: number = 0;
|
||||
cpuUsage: number = 0;
|
||||
msSinceLastUnderrun: number = -5000*1000;
|
||||
temperaturemC: number = -1000000;
|
||||
|
||||
static getDisplayView(label: string,status?: JackHostStatus): React.ReactNode {
|
||||
if (!status) {
|
||||
return (<div style={{whiteSpace: "nowrap"}}>
|
||||
<Typography variant="caption" color="textSecondary">{label}</Typography>
|
||||
<Typography variant="caption"> </Typography>
|
||||
</div>);
|
||||
}
|
||||
if (status.restarting)
|
||||
{
|
||||
return (
|
||||
<div style={{whiteSpace: "nowrap"}}>
|
||||
<Typography variant="caption" color="textSecondary">{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="textSecondary">{label}</Typography>
|
||||
|
||||
<span style={{color: RED_COLOR}}>
|
||||
<Typography variant="caption" color="inherit">Stopped </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="textSecondary">{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,35 @@
|
||||
|
||||
|
||||
export default class JackServerSettings {
|
||||
deserialize(input: any) : JackServerSettings{
|
||||
this.valid = input.valid;
|
||||
this.rebootRequired = input.rebootRequired;
|
||||
this.sampleRate = input.sampleRate;
|
||||
this.bufferSize = input.bufferSize;
|
||||
this.numberOfBuffers = input.numberOfBuffers;
|
||||
return this;
|
||||
}
|
||||
constructor(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;
|
||||
}
|
||||
}
|
||||
valid: boolean = false;
|
||||
rebootRequired = false;
|
||||
sampleRate = 48000;
|
||||
bufferSize = 64;
|
||||
numberOfBuffers = 3;
|
||||
|
||||
getSummaryText() {
|
||||
if (this.valid) {
|
||||
return "Sample Rate: " + this.sampleRate + " BufferSize: " + this.bufferSize + " Number of Buffers: " + this.numberOfBuffers;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { createStyles, WithStyles, withStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import JackServerSettings from './JackServerSettings';
|
||||
|
||||
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import { nullCast } from './Utility';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
interface JackServerSettingsDialogState {
|
||||
latencyText: string;
|
||||
};
|
||||
|
||||
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: (sampleRate: number, bufferSize: number, numberOfBuffers: number) => void;
|
||||
}
|
||||
|
||||
function getLatencyText(sampleRate: number, bufferSize: number, numberOfBuffers: number): string {
|
||||
let ms = bufferSize * numberOfBuffers / sampleRate * 1000;
|
||||
return ms.toFixed(1) + "ms";
|
||||
}
|
||||
|
||||
const JackServerSettingsDialog = withStyles(styles)(
|
||||
class extends Component<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
|
||||
constructor(props: JackServerSettingsDialogProps) {
|
||||
super(props);
|
||||
let jackServerSettings = props.jackServerSettings;
|
||||
|
||||
this.state = {
|
||||
latencyText:
|
||||
getLatencyText(
|
||||
jackServerSettings.sampleRate,
|
||||
jackServerSettings.bufferSize,
|
||||
jackServerSettings.numberOfBuffers)
|
||||
};
|
||||
}
|
||||
|
||||
updateLatency(e: any): void {
|
||||
setTimeout(()=> {
|
||||
let sampleRate: number = parseInt(nullCast<any>(document.getElementById('jsd_sampleRate')).value);
|
||||
let bufferSize: number = parseInt(nullCast<any>(document.getElementById('jsd_bufferSize')).value);
|
||||
let bufferCount: number = parseInt(nullCast<any>(document.getElementById('jsd_bufferCount')).value);
|
||||
this.setState({
|
||||
latencyText: getLatencyText(sampleRate, bufferSize, bufferCount)
|
||||
});
|
||||
},0);
|
||||
}
|
||||
handleApply() {
|
||||
let sampleRate = parseInt(nullCast<any>(document.getElementById('jsd_sampleRate')).value);
|
||||
let bufferSize = parseInt(nullCast<any>(document.getElementById('jsd_bufferSize')).value);
|
||||
let bufferCount = parseInt(nullCast<any>(document.getElementById('jsd_bufferCount')).value);
|
||||
|
||||
this.props.onApply(sampleRate,bufferSize,bufferCount);
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const classes = this.props.classes;
|
||||
|
||||
const { onClose, jackServerSettings, open } = this.props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
|
||||
<DialogContent>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="sampleRate">Sample rate</InputLabel>
|
||||
<Select
|
||||
onChange={(e) => this.updateLatency(e)}
|
||||
defaultValue={jackServerSettings.sampleRate}
|
||||
inputProps={{
|
||||
name: 'Sample Rate',
|
||||
id: 'jsd_sampleRate',
|
||||
style: {
|
||||
textAlign: "right"
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value={44100}>44100</MenuItem>
|
||||
<MenuItem value={48000}>48000</MenuItem>
|
||||
<MenuItem value={88200}>88200</MenuItem>
|
||||
<MenuItem value={96000}>96000</MenuItem>
|
||||
<MenuItem value={176400}>176400</MenuItem>
|
||||
<MenuItem value={192000}>192000</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div style={{ display: "inline", whiteSpace: "nowrap" }}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel>
|
||||
<Select
|
||||
onChange={(e) => this.updateLatency(e)}
|
||||
defaultValue={jackServerSettings.bufferSize}
|
||||
inputProps={{
|
||||
name: 'Buffer size',
|
||||
id: 'jsd_bufferSize',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={16}>16</MenuItem>
|
||||
<MenuItem value={32}>32</MenuItem>
|
||||
<MenuItem value={64}>64</MenuItem>
|
||||
<MenuItem value={128}>128</MenuItem>
|
||||
<MenuItem value={256}>256</MenuItem>
|
||||
<MenuItem value={512}>512</MenuItem>
|
||||
<MenuItem value={1024}>1024</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel>
|
||||
<Select
|
||||
onChange={(e) => this.updateLatency(e)}
|
||||
defaultValue={jackServerSettings.numberOfBuffers}
|
||||
inputProps={{
|
||||
name: 'Number of buffers',
|
||||
id: 'jsd_bufferCount',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
<MenuItem value={3}>3</MenuItem>
|
||||
<MenuItem value={4}>4</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<Typography display="block" variant="caption" style={{ textAlign: "left", marginTop: 8, marginLeft: 24 }}
|
||||
color="textSecondary">
|
||||
Latency: {this.state.latencyText}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={()=> this.handleApply()} color="secondary">
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default JackServerSettingsDialog;
|
||||
@@ -0,0 +1,63 @@
|
||||
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?: NodeJS.Timeout;
|
||||
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,453 @@
|
||||
import React, { ReactNode, SyntheticEvent } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, PluginType } from './Lv2Plugin';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import PluginInfoDialog from './PluginInfoDialog'
|
||||
import PluginIcon from './PluginIcon'
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import PluginClass from './PluginClass'
|
||||
import ClearIcon from '@material-ui/icons/Clear';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
|
||||
|
||||
export type CloseEventHandler = () => void;
|
||||
export type OkEventHandler = (pluginUri: string) => void;
|
||||
|
||||
const NARROW_DISPLAY_THRESHOLD = 600;
|
||||
const FILTER_STORAGE_KEY = "com.twoplay.piddle.load_dlg.filter";
|
||||
|
||||
|
||||
const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
|
||||
top: {
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
left: "0px",
|
||||
bottom: "64px",
|
||||
position: "relative",
|
||||
flexGrow: 1,
|
||||
overflowX: "hidden",
|
||||
overflowY: "visible"
|
||||
},
|
||||
bottom: {
|
||||
position: "relative",
|
||||
bottom: "0px",
|
||||
height: "64px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: "24px",
|
||||
paddingRight: "48px",
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
paper: {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
height: "56px",
|
||||
width: "100%",
|
||||
background: theme.palette.background.paper
|
||||
},
|
||||
buttonBase: {
|
||||
|
||||
width: "100%",
|
||||
height: "100%'"
|
||||
},
|
||||
content: {
|
||||
marginTop: "8px",
|
||||
marginBottom: "8px",
|
||||
marginLeft: "12px",
|
||||
marginRight: "12px",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
height: 48,
|
||||
alignItems: "start",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
content2: {
|
||||
display: "flex", flexDirection: "column", flex: "1 1 auto", width: "100%",
|
||||
paddingLeft: 16, whiteSpace: "nowrap"
|
||||
|
||||
},
|
||||
table: {
|
||||
borderCollapse: "collapse",
|
||||
},
|
||||
icon: {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6"
|
||||
},
|
||||
iconBorder: {
|
||||
flex: "0 0 auto"
|
||||
},
|
||||
label: {
|
||||
width: "100%"
|
||||
},
|
||||
label2: {
|
||||
marginTop: 8,
|
||||
color: theme.palette.text.secondary
|
||||
},
|
||||
control: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
tdText: {
|
||||
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,
|
||||
hover_uri?: string,
|
||||
filterType: PluginType,
|
||||
client_width: number,
|
||||
client_height: number,
|
||||
minimumItemWidth: number,
|
||||
|
||||
}
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
export const LoadPluginDialog =
|
||||
withStyles(pluginGridStyles, { withTheme: true })(
|
||||
class extends ResizeResponsiveComponent<PluginGridProps, PluginGridState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PluginGridProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
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,
|
||||
hover_uri: "",
|
||||
filterType: filterType_,
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight,
|
||||
minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220
|
||||
};
|
||||
|
||||
this.updateWindowSize = this.updateWindowSize.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.handleOk = this.handleOk.bind(this);
|
||||
}
|
||||
|
||||
updateWindowSize() {
|
||||
this.setState({
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.updateWindowSize();
|
||||
window.addEventListener('resize', this.updateWindowSize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
window.removeEventListener('resize', this.updateWindowSize);
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
super.onWindowSizeChanged(width, height);
|
||||
|
||||
}
|
||||
|
||||
onFilterChange(e: any) {
|
||||
let value = e.target.value as PluginType;
|
||||
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
|
||||
this.setState({ filterType: value });
|
||||
}
|
||||
onClearFilter(): void {
|
||||
let value = PluginType.Plugin;
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
|
||||
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 {
|
||||
e.preventDefault();
|
||||
this.cancel();
|
||||
}
|
||||
handleOk(e: SyntheticEvent): void {
|
||||
e.preventDefault();
|
||||
if (this.state.selected_uri) {
|
||||
this.props.onOk(this.state.selected_uri);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick(e: SyntheticEvent, uri: string): void {
|
||||
this.props.onOk(uri);
|
||||
}
|
||||
|
||||
|
||||
cancel(): void {
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
fireSelected(item?: UiPlugin) {
|
||||
if (item) {
|
||||
this.setState({ selected_uri: item.uri });
|
||||
}
|
||||
}
|
||||
setHoverUri(uri: string) {
|
||||
this.setState({ hover_uri: uri });
|
||||
}
|
||||
|
||||
onClick(e: SyntheticEvent, uri: string): void {
|
||||
this.setState({ selected_uri: 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;
|
||||
}
|
||||
|
||||
stereo_indicator(uiPlugin?: UiPlugin): string {
|
||||
if (!uiPlugin) return "";
|
||||
if (uiPlugin.audio_inputs === 2 || uiPlugin.audio_outputs === 2) {
|
||||
return " (Stereo)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
info_string(uiPlugin?: UiPlugin): string {
|
||||
if (uiPlugin === undefined) return "";
|
||||
let result = uiPlugin.name;
|
||||
if (uiPlugin.author_name !== "") {
|
||||
result += ", " + uiPlugin.author_name;
|
||||
}
|
||||
result += this.stereo_indicator(uiPlugin);
|
||||
return result;
|
||||
}
|
||||
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 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 value={PluginType.Plugin}> All</MenuItem>));
|
||||
this.createFilterChildren(result, classes, 1);
|
||||
return result;
|
||||
|
||||
}
|
||||
filterPlugins(plugins: UiPlugin[]): ReactNode[] {
|
||||
let result: ReactNode[] = [];
|
||||
let filterType = this.state.filterType;
|
||||
let classes = this.props.classes;
|
||||
let rootClass = this.model.plugin_classes.get();
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let value = plugins[i];
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, value.plugin_type)) {
|
||||
result.push(
|
||||
(
|
||||
<Grid key={value.uri} xs={12} sm={6} md={4} lg={3} xl={3} item
|
||||
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} pluginUri={value.uri} size={24} />
|
||||
</div>
|
||||
<div className={classes.content2}>
|
||||
<Typography color="textPrimary" noWrap className={classes.label} >
|
||||
{value.name}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" noWrap>
|
||||
{value.plugin_display_type} {this.stereo_indicator(value)}
|
||||
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</Grid>
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
|
||||
let model = this.model;
|
||||
let plugins = model.ui_plugins.get();
|
||||
|
||||
let selectedPlugin: UiPlugin | undefined = undefined;
|
||||
if (this.state.selected_uri) {
|
||||
let t = this.model.getUiPlugin(this.state.selected_uri);
|
||||
if (t) selectedPlugin = t;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
fullScreen={true}
|
||||
TransitionComponent={Transition}
|
||||
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">
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto" }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
|
||||
<IconButton onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography display="block" variant="h6" style={{ flex: "0 1 auto" }}>
|
||||
{this.state.client_width > 520 ? "Select Plugin" : ""}
|
||||
</Typography>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<Select defaultValue={this.state.filterType} key={this.state.filterType} onChange={(e) => { this.onFilterChange(e); }}
|
||||
style={{ flex: "0 0 160px" }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</Select>
|
||||
<div style={{ flex: "0 0 auto", marginRight: 24, visibility: this.state.filterType === PluginType.Plugin ? "hidden" : "visible" }} >
|
||||
<IconButton onClick={() => { this.onClearFilter(); }}>
|
||||
<ClearIcon fontSize='small' style={{ opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers style={{
|
||||
height: this.state.client_height - (this.state.client_width < NARROW_DISPLAY_THRESHOLD ? 150 + 64 : 160),
|
||||
padding: 8,
|
||||
display: "1 1 flex"
|
||||
}} >
|
||||
<Grid container justify="flex-start" >
|
||||
{
|
||||
this.filterPlugins(plugins)
|
||||
}
|
||||
|
||||
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
{(this.state.client_width >= NARROW_DISPLAY_THRESHOLD) ? (
|
||||
<DialogActions style={{ flex: "0 0 auto" }} >
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ position: "relative", float: "right", flex: "0 1 200px", width: 200, display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" 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 }} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120, height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 120, height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
)}
|
||||
</Dialog>
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
export default LoadPluginDialog;
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
import React, { ReactNode, SyntheticEvent } from 'react';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, PluginType } from './Lv2Plugin';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import PluginInfoDialog from './PluginInfoDialog'
|
||||
import PluginIcon from './PluginIcon'
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import PluginClass from './PluginClass'
|
||||
import ClearIcon from '@material-ui/icons/Clear';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
|
||||
|
||||
export type CloseEventHandler = () => void;
|
||||
export type OkEventHandler = (pluginUri: string) => void;
|
||||
|
||||
const NARROW_DISPLAY_THRESHOLD = 600;
|
||||
const FILTER_STORAGE_KEY = "com.twoplay.piddle.load_dlg.filter";
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
|
||||
const pluginGridStyles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
|
||||
top: {
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
left: "0px",
|
||||
bottom: "64px",
|
||||
position: "relative",
|
||||
flexGrow: 1,
|
||||
background: "#eee",
|
||||
overflowX: "hidden",
|
||||
overflowY: "visible"
|
||||
},
|
||||
bottom: {
|
||||
position: "relative",
|
||||
bottom: "0px",
|
||||
height: "64px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: "24px",
|
||||
paddingRight: "48px",
|
||||
background: theme.palette.background.paper,
|
||||
},
|
||||
paper: {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
height: "56px",
|
||||
width: "100%",
|
||||
background: theme.palette.background.paper
|
||||
},
|
||||
buttonBase: {
|
||||
|
||||
width: "100%",
|
||||
height: "100%'"
|
||||
},
|
||||
content: {
|
||||
marginTop: "8px",
|
||||
marginBottom: "8px",
|
||||
marginLeft: "12px",
|
||||
marginRight: "12px",
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
},
|
||||
table: {
|
||||
borderCollapse: "collapse",
|
||||
},
|
||||
icon: {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6"
|
||||
},
|
||||
label: {
|
||||
marginLeft: "8px",
|
||||
|
||||
},
|
||||
control: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
tdText: {
|
||||
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,
|
||||
hover_uri?: string,
|
||||
filterType: PluginType,
|
||||
client_width: number,
|
||||
client_height: number,
|
||||
minimumItemWidth: number,
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const LoadPluginDialog =
|
||||
withStyles(pluginGridStyles, { withTheme: true })(
|
||||
class extends ResizeResponsiveComponent<PluginGridProps, PluginGridState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PluginGridProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
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,
|
||||
hover_uri: "",
|
||||
filterType: filterType_,
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight,
|
||||
minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220
|
||||
};
|
||||
|
||||
this.updateWindowSize = this.updateWindowSize.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
this.handleOk = this.handleOk.bind(this);
|
||||
}
|
||||
|
||||
updateWindowSize() {
|
||||
this.setState({
|
||||
client_width: window.innerWidth,
|
||||
client_height: window.innerHeight
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.updateWindowSize();
|
||||
window.addEventListener('resize', this.updateWindowSize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
window.removeEventListener('resize', this.updateWindowSize);
|
||||
}
|
||||
|
||||
onWindowSizeChanged(width: number,height: number): void {
|
||||
super.onWindowSizeChanged(width,height);
|
||||
|
||||
}
|
||||
|
||||
onFilterChange(e: any) {
|
||||
let value = e.target.value as PluginType;
|
||||
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
|
||||
this.setState({ filterType: value });
|
||||
}
|
||||
onClearFilter(): void {
|
||||
let value = PluginType.Plugin;
|
||||
window.localStorage.setItem(FILTER_STORAGE_KEY, value as string);
|
||||
|
||||
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 {
|
||||
e.preventDefault();
|
||||
this.cancel();
|
||||
}
|
||||
handleOk(e: SyntheticEvent): void {
|
||||
e.preventDefault();
|
||||
if (this.state.selected_uri) {
|
||||
this.props.onOk(this.state.selected_uri);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick(e: SyntheticEvent, uri: string): void {
|
||||
this.props.onOk(uri);
|
||||
}
|
||||
|
||||
|
||||
cancel(): void {
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
fireSelected(item?: UiPlugin) {
|
||||
if (item) {
|
||||
this.setState({ selected_uri: item.uri });
|
||||
}
|
||||
}
|
||||
setHoverUri(uri: string) {
|
||||
this.setState({ hover_uri: uri });
|
||||
}
|
||||
|
||||
onClick(e: SyntheticEvent, uri: string): void {
|
||||
this.setState({ selected_uri: 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;
|
||||
}
|
||||
|
||||
stereo_indicator(uiPlugin?: UiPlugin): string {
|
||||
if (!uiPlugin) return "";
|
||||
if (uiPlugin.audio_inputs === 2 || uiPlugin.audio_outputs === 2) {
|
||||
return " (Stereo)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
info_string(uiPlugin?: UiPlugin): string {
|
||||
if (uiPlugin === undefined) return "";
|
||||
let result = uiPlugin.name;
|
||||
if (uiPlugin.author_name !== "") {
|
||||
result += ", " + uiPlugin.author_name;
|
||||
}
|
||||
result += this.stereo_indicator(uiPlugin);
|
||||
return result;
|
||||
}
|
||||
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 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 value={PluginType.Plugin}> All</MenuItem>));
|
||||
this.createFilterChildren(result, classes, 1);
|
||||
return result;
|
||||
|
||||
}
|
||||
filterPlugins(plugins: UiPlugin[]): ReactNode[] {
|
||||
let result: ReactNode[] = [];
|
||||
let filterType = this.state.filterType;
|
||||
let classes = this.props.classes;
|
||||
let rootClass = this.model.plugin_classes.get();
|
||||
|
||||
for (let i = 0; i < plugins.length; ++i) {
|
||||
let value = plugins[i];
|
||||
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, value.plugin_type)) {
|
||||
result.push(
|
||||
(
|
||||
<Grid key={value.uri} xs={6} sm={6} md={4} lg={3} xl={3} item
|
||||
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) }}
|
||||
>
|
||||
<Paper className={classes.paper} >
|
||||
<ButtonBase className={classes.buttonBase} focusRipple>
|
||||
<SelectHoverBackground selected={value.uri === this.state.selected_uri} showHover={true} />
|
||||
<div className={classes.content}>
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "flex-start", alignItems: "start", flexWrap: "nowrap" }} >
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<PluginIcon pluginType={value.plugin_type} pluginUri={value.uri} size={24} />
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: "1 1 auto", width: "100%" }}>
|
||||
<Typography className={classes.label}
|
||||
display='block' variant='body2' align="left" color="textPrimary" noWrap
|
||||
>{value.name}
|
||||
</Typography>
|
||||
<Typography className={classes.label}
|
||||
display='block' variant='caption' align="left" noWrap
|
||||
>
|
||||
{value.plugin_display_type} {this.stereo_indicator(value)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</Paper>
|
||||
</Grid>
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
|
||||
let model = this.model;
|
||||
let plugins = model.ui_plugins.get();
|
||||
|
||||
let selectedPlugin: UiPlugin | undefined = undefined;
|
||||
if (this.state.selected_uri) {
|
||||
let t = this.model.getUiPlugin(this.state.selected_uri);
|
||||
if (t) selectedPlugin = t;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
TransitionComponent={Transition}
|
||||
fullScreen={true}
|
||||
|
||||
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">
|
||||
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto"}}>
|
||||
<div style={{ display: "flex", flexDirection: "row", flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
|
||||
<IconButton onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography display="block" variant="h6" style={{ flex: "0 1 auto" }}>
|
||||
{ this.state.client_width > 520 ? "Select Plugin" : "" }
|
||||
</Typography>
|
||||
<div style={{ flex: "1 1 auto" }} />
|
||||
<Select defaultValue={this.state.filterType} key={this.state.filterType} onChange={(e) => { this.onFilterChange(e); }}
|
||||
style={{ flex: "0 0 160px" }}
|
||||
>
|
||||
{this.createFilterOptions()}
|
||||
</Select>
|
||||
<div style={{ flex: "0 0 auto", marginRight: 24, visibility: this.state.filterType === PluginType.Plugin? "hidden": "visible" }} >
|
||||
<IconButton onClick={() => { this.onClearFilter(); }}>
|
||||
<ClearIcon fontSize='small' style={{ opacity: 0.6 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers style={{ height: this.state.client_height - (this.state.client_width < NARROW_DISPLAY_THRESHOLD ? 288: 220),
|
||||
background: "#eee", display: "1 1 flex"}} >
|
||||
<Grid container justify="flex-start" spacing={1} style={{ background: "#EEE" }}>
|
||||
{
|
||||
this.filterPlugins(plugins)
|
||||
}
|
||||
|
||||
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
{ (this.state.client_width >= NARROW_DISPLAY_THRESHOLD)? (
|
||||
<DialogActions style={{flex: "0 0 auto"}} >
|
||||
<div className={classes.bottom}>
|
||||
<div style={{ display: "flex", justifyContent: "start", alignItems: "center", flex: "1 1 auto", height: "100%", overflow: "hidden" }} >
|
||||
<PluginInfoDialog plugin_uri={this.state.selected_uri ?? ""} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ position: "relative", float: "right", flex: "0 1 200px", width: 200, display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" 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}} />
|
||||
<Typography display='block' variant='body2' color="textPrimary" noWrap >
|
||||
{this.info_string(selectedPlugin)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div style={{flex: "1 1 1px"}}/>
|
||||
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
|
||||
<Button onClick={this.handleCancel} style={{ width: 120, height: 48 }} >Cancel</Button>
|
||||
<Button onClick={this.handleOk} color="secondary" disabled={selectedPlugin === null} style={{ width: 120, height: 48 }} >SELECT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
)}
|
||||
</Dialog>
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
export default LoadPluginDialog;
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
// Generated by https://quicktype.io
|
||||
//
|
||||
// To change quicktype's target language, run command:
|
||||
//
|
||||
// "Set quicktype target language"
|
||||
|
||||
import {PiPedalArgumentError} from "./PiPedalError";
|
||||
import Units from './Units';
|
||||
|
||||
interface Deserializable<T> {
|
||||
deserialize(input: any): T;
|
||||
}
|
||||
|
||||
|
||||
export class Port implements Deserializable<Port> {
|
||||
deserialize(input: any): Port {
|
||||
this.port_index = input.port_index;
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
this.min_value = input.min_value;
|
||||
this.max_value = input.max_value;
|
||||
this.default_value = input.default_value;
|
||||
this.scale_points = ScalePoint.deserialize_array(input.scale_points);
|
||||
this.is_input = input.is_input;
|
||||
this.is_output = input.is_output;
|
||||
this.is_control_port = input.is_control_port;
|
||||
this.is_audio_port = input.is_audio_port;
|
||||
this.is_atom_port = input.is_atom_port;
|
||||
this.is_valid = input.is_valid;
|
||||
this.supports_midi = input.supports_midi;
|
||||
this.port_group = input.port_group;
|
||||
return this;
|
||||
}
|
||||
|
||||
static EmptyPorts: Port[] = [];
|
||||
|
||||
static deserialize_array(input: any): Port[] {
|
||||
let result: Port[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new Port().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
port_index: number = -1;
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
min_value: number = 0;
|
||||
max_value: number = 1;
|
||||
default_value: number = 0.5;
|
||||
scale_points: ScalePoint[] = [];
|
||||
is_input: boolean = false;
|
||||
is_output: boolean = false
|
||||
is_control_port: boolean = false;
|
||||
is_audio_port: boolean = false;
|
||||
is_atom_port: boolean = false;
|
||||
is_valid: boolean = false;
|
||||
supports_midi: boolean = false;
|
||||
port_group: string = "";
|
||||
}
|
||||
|
||||
export class PortGroup {
|
||||
deserialize(input: any): PortGroup {
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any) : PortGroup[] {
|
||||
let result: PortGroup[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new PortGroup().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
};
|
||||
|
||||
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
|
||||
deserialize(input: any): Lv2Plugin
|
||||
{
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.plugin_class = input.plugin_class;
|
||||
this.supported_features = input.supported_features;
|
||||
this.required_features = input.required_features;
|
||||
this.optional_features = input.optional_features;
|
||||
this.author_name = input.author_name;
|
||||
this.author_homepage = input.author_homepage;
|
||||
this.comment = input.comment;
|
||||
this.ports= Port.deserialize_array(input.ports);
|
||||
this.port_groups = PortGroup.deserialize_array(input.port_groups);
|
||||
return this;
|
||||
}
|
||||
static EmptyFeatures: string[] = [];
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
plugin_class: string = "";
|
||||
supported_features: string[] = Lv2Plugin.EmptyFeatures;
|
||||
required_features: string[] = Lv2Plugin.EmptyFeatures;
|
||||
optional_features: string[] = Lv2Plugin.EmptyFeatures;
|
||||
author_name: string = "";
|
||||
author_homepage: string = "";
|
||||
comment: string = "";
|
||||
ports: Port[] = Port.EmptyPorts;
|
||||
port_groups: PortGroup[] = [];
|
||||
}
|
||||
|
||||
|
||||
export class ScalePoint implements Deserializable<ScalePoint> {
|
||||
deserialize(input: any): ScalePoint {
|
||||
this.value = input.value;
|
||||
this.label = input.label;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): ScalePoint[]
|
||||
{
|
||||
let result: ScalePoint[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new ScalePoint().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
value: number = 0;
|
||||
label: string = "";
|
||||
}
|
||||
|
||||
export enum PluginType {
|
||||
// Reserved types used in pedalboards.
|
||||
None="",
|
||||
InvalidPlugin= "InvalidPlugin",
|
||||
|
||||
Plugin = "Plugin",
|
||||
AllpassPlugin = "AllpassPlugin",
|
||||
AmplifierPlugin = "AmplifierPlugin",
|
||||
AnalyserPlugin = "AnalyserPlugin",
|
||||
BandpassPlugin = "BandpassPlugin",
|
||||
ChorusPlugin = "ChorusPlugin",
|
||||
CombPlugin = "CombPlugin",
|
||||
CompressorPlugin = "CompressorPlugin",
|
||||
ConstantPlugin = "ConstantPlugin",
|
||||
ConverterPlugin = "ConverterPlugin",
|
||||
DelayPlugin = "DelayPlugin",
|
||||
DistortionPlugin = "DistortionPlugin",
|
||||
DynamicsPlugin = "DynamicsPlugin",
|
||||
EQPlugin = "EQPlugin",
|
||||
EnvelopePlugin = "EnvelopePlugin",
|
||||
ExpanderPlugin = "ExpanderPlugin",
|
||||
FilterPlugin = "FilterPlugin",
|
||||
FlangerPlugin = "FlangerPlugin",
|
||||
FunctionPlugin = "FunctionPlugin",
|
||||
GatePlugin = "GatePlugin",
|
||||
GeneratorPlugin = "GeneratorPlugin",
|
||||
HighpassPlugin = "HighpassPlugin",
|
||||
InstrumentPlugin = "InstrumentPlugin",
|
||||
LimiterPlugin = "LimiterPlugin",
|
||||
LowpassPlugin = "LowpassPlugin",
|
||||
MixerPlugin = "MixerPlugin",
|
||||
ModulatorPlugin = "ModulatorPlugin",
|
||||
MultiEQPlugin = "MultiEQPlugin",
|
||||
OscillatorPlugin = "OscillatorPlugin",
|
||||
ParaEQPlugin = "ParaEQPlugin",
|
||||
PhaserPlugin = "PhaserPlugin",
|
||||
PitchPlugin = "PitchPlugin",
|
||||
ReverbPlugin = "ReverbPlugin",
|
||||
SimulatorPlugin = "SimulatorPlugin",
|
||||
SpatialPlugin = "SpatialPlugin",
|
||||
SpectralPlugin = "SpectralPlugin",
|
||||
UtilityPlugin = "UtilityPlugin",
|
||||
WaveshaperPlugin = "WaveshaperPlugin"
|
||||
}
|
||||
|
||||
|
||||
export class UiControl implements Deserializable<UiControl> {
|
||||
deserialize(input: any): UiControl
|
||||
{
|
||||
this.symbol = input.symbol;
|
||||
this.name = input.name;
|
||||
this.index = input.index;
|
||||
this.min_value = input.min_value;
|
||||
this.max_value = input.max_value;
|
||||
this.default_value = input.default_value;
|
||||
this.is_logarithmic = input.is_logarithmic;
|
||||
this.display_priority = input.display_priority;
|
||||
this.range_steps = input.range_steps;
|
||||
this.integer_property = input.integer_property;
|
||||
this.enumeration_property = input.enumeration_property;
|
||||
this.toggled_property = input.toggled_property;
|
||||
this.trigger = input.trigger;
|
||||
this.not_on_gui = input.not_on_gui;
|
||||
this.scale_points = ScalePoint.deserialize_array(input.scale_points);
|
||||
this.port_group = input.port_group;
|
||||
this.units = input.units as Units;
|
||||
return this;
|
||||
|
||||
}
|
||||
static deserialize_array(input: any): UiControl[] {
|
||||
let result: UiControl[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new UiControl().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
symbol: string = "";
|
||||
name: string = "";
|
||||
index: number = -1;
|
||||
min_value: number = 0;
|
||||
max_value: number = 1;
|
||||
default_value:number = 0.5;
|
||||
is_logarithmic: boolean = false;
|
||||
display_priority: number = -1;
|
||||
range_steps: number = 0;
|
||||
integer_property:boolean = false;
|
||||
enumeration_property: boolean = false;
|
||||
trigger: boolean = false;
|
||||
not_on_gui: boolean = false;
|
||||
toggled_property: boolean = false;
|
||||
scale_points: ScalePoint[] = [];
|
||||
port_group: string = "";
|
||||
units: Units = Units.none;
|
||||
|
||||
isOnOffSwitch() : boolean {
|
||||
if (this.isAbToggle()) return false;
|
||||
if (this.min_value !== 0 || this.max_value !== 1) return false;
|
||||
return (this.toggled_property || this.enumeration_property || this.integer_property);
|
||||
;
|
||||
}
|
||||
|
||||
isAbToggle(): boolean {
|
||||
if (this.min_value !== 0 || this.max_value !== 1) return false;
|
||||
return this.enumeration_property && this.scale_points.length === 2;
|
||||
}
|
||||
isSelect() : boolean {
|
||||
return this.enumeration_property && !this.isOnOffSwitch() && !this.isAbToggle();
|
||||
}
|
||||
|
||||
valueToRange(value: number): number {
|
||||
if (this.toggled_property) return value === 0 ? 0: 1;
|
||||
|
||||
if (this.integer_property || this.enumeration_property) {
|
||||
value = Math.round(value);
|
||||
}
|
||||
let range = (value - this.min_value) / (this.max_value - this.min_value);
|
||||
if (range > 1) range = 1;
|
||||
if (range < 0) range = 0;
|
||||
|
||||
if (this.range_steps !== 0) {
|
||||
range = Math.round(range*this.range_steps)/this.range_steps;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
rangeToValue(range: number) : number {
|
||||
if (range < 0) range = 0;
|
||||
if (range > 1) range = 1;
|
||||
|
||||
if (this.toggled_property) return range === 0? 0: 1;
|
||||
if (this.range_steps !== 0) {
|
||||
range = Math.round(range * this.range_steps) / this.range_steps;
|
||||
}
|
||||
let value = range * (this.max_value - this.min_value) + this.min_value;
|
||||
if (this.integer_property || this.enumeration_property) {
|
||||
value = Math.round(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
clampValue(value: number): number {
|
||||
return this.rangeToValue(this.valueToRange(value));
|
||||
}
|
||||
|
||||
formatValue(value: number): string {
|
||||
if (this.integer_property || this.enumeration_property) {
|
||||
value = Math.round(value);
|
||||
}
|
||||
if (this.enumeration_property) {
|
||||
for (let i = 0; i < this.scale_points.length; ++i) {
|
||||
let scale_point = this.scale_points[i];
|
||||
if (scale_point.value === value) {
|
||||
return scale_point.label;
|
||||
}
|
||||
}
|
||||
return "#invalid";
|
||||
} else if (this.integer_property) {
|
||||
return value.toFixed(0);
|
||||
} else {
|
||||
if (value >= 100 || value <= -100) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
if (value >= 10 || value <= -10) {
|
||||
return value.toFixed(1);
|
||||
}
|
||||
return value.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class UiPlugin implements Deserializable<UiPlugin> {
|
||||
deserialize(input: any): UiPlugin
|
||||
{
|
||||
this.uri = input.uri;
|
||||
this.name = input.name;
|
||||
this.plugin_type = input.plugin_type as PluginType;
|
||||
this.plugin_display_type = input.plugin_display_type;
|
||||
this.author_name = input.author_name;
|
||||
this.author_homepage = input.author_homepage;
|
||||
this.audio_inputs = input.audio_inputs;
|
||||
this.audio_outputs = input.audio_outputs;
|
||||
this.has_midi_input = input.has_midi_input;
|
||||
this.has_midi_output = input.has_midi_output;
|
||||
this.description = input.description;
|
||||
this.controls = UiControl.deserialize_array(input.controls);
|
||||
this.port_groups = PortGroup.deserialize_array(input.port_groups);
|
||||
return this;
|
||||
|
||||
}
|
||||
static deserialize_array(input: any): UiPlugin[] {
|
||||
let result: UiPlugin[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result[i] = new UiPlugin().deserialize(input[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getControl(key: string): UiControl | undefined {
|
||||
for (let i = 0; i < this.controls.length; ++i)
|
||||
{
|
||||
let control = this.controls[i];
|
||||
if (control.symbol === key)
|
||||
{
|
||||
return control;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
}
|
||||
getPortGroup(symbol: string): PortGroup
|
||||
{
|
||||
for (let i = 0; i < this.port_groups.length; ++i)
|
||||
{
|
||||
let port_group = this.port_groups[i];
|
||||
if (port_group.symbol === symbol)
|
||||
{
|
||||
return port_group;
|
||||
}
|
||||
}
|
||||
throw new PiPedalArgumentError("Port group not found.");
|
||||
}
|
||||
|
||||
uri: string = "";
|
||||
name: string = "";
|
||||
plugin_type: PluginType = PluginType.InvalidPlugin;
|
||||
plugin_display_type: string = "";
|
||||
author_name: string = "";
|
||||
author_homepage: string = "";
|
||||
audio_inputs: number = 0;
|
||||
audio_outputs: number = 0;
|
||||
has_midi_input: number = 0;
|
||||
has_midi_output: number = 0;
|
||||
description: string = "";
|
||||
controls: UiControl[] = [];
|
||||
port_groups: PortGroup[] = [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import {
|
||||
PedalBoard, PedalBoardItem, PedalBoardSplitItem, SplitType
|
||||
} from './PedalBoard';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import InputIcon from '@material-ui/icons/Input';
|
||||
import LoadPluginDialog from './LoadPluginDialog';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
|
||||
import PedalBoardView from './PedalBoardView';
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import PluginInfoDialog from './PluginInfoDialog';
|
||||
import { GetControlView } from './ControlViewFactory';
|
||||
import MidiBindingsDialog from './MidiBindingsDialog';
|
||||
import PluginPresetSelector from './PluginPresetSelector';
|
||||
|
||||
|
||||
const SPLIT_CONTROLBAR_THRESHHOLD = 650;
|
||||
|
||||
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) => createStyles({
|
||||
frame: {
|
||||
position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap",
|
||||
justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden"
|
||||
},
|
||||
pedalBoardScroll: {
|
||||
position: "relative", width: "100%",
|
||||
flex: "0 0 auto", overflow: "auto", maxHeight: 220
|
||||
},
|
||||
pedalBoardScrollSmall: {
|
||||
position: "relative", width: "100%",
|
||||
flex: "1 1 1px", overflow: "auto"
|
||||
},
|
||||
separator: {
|
||||
width: "100%", height: "1px", background: "#888", opacity: "0.5",
|
||||
flex: "0 0 1px"
|
||||
},
|
||||
|
||||
controlToolBar: {
|
||||
flex: "0 0 auto", width: "100%", height: 48
|
||||
},
|
||||
splitControlBar: {
|
||||
flex: "0 0 48px", width: "100%", paddingLeft: 24, paddingRight: 16, paddingBottom: 16
|
||||
},
|
||||
controlContent: {
|
||||
flex: "1 1 auto", width: "100%", overflowY: "auto", minHeight: 240
|
||||
},
|
||||
controlContentSmall: {
|
||||
flex: "0 0 162px", width: "100%", height: 162, overflowY: "hidden",
|
||||
},
|
||||
title: { fontSize: "1.3em", fontWeight: 700, marginRight: 8 },
|
||||
author: { fontWeight: 500, marginRight: 8 }
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface MainProps extends WithStyles<typeof styles> {
|
||||
hasTinyToolBar: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
interface MainState {
|
||||
selectedPedal: number;
|
||||
loadDialogOpen: boolean;
|
||||
pedalBoard: PedalBoard;
|
||||
addMenuAnchorEl: HTMLElement | null;
|
||||
splitControlBar: boolean;
|
||||
horizontalScrollLayout: boolean;
|
||||
showMidiBindingsDialog: boolean;
|
||||
screenHeight: number;
|
||||
};
|
||||
|
||||
|
||||
export const MainPage =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends ResizeResponsiveComponent<MainProps, MainState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MainProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.state = {
|
||||
selectedPedal: -1,
|
||||
loadDialogOpen: false,
|
||||
pedalBoard: this.model.pedalBoard.get(),
|
||||
addMenuAnchorEl: null,
|
||||
splitControlBar: this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD,
|
||||
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.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, presetName: string) {
|
||||
this.model.loadPluginPreset(instanceId, presetName);
|
||||
}
|
||||
onPedalBoardChanged(value: PedalBoard) {
|
||||
this.setState({ pedalBoard: value });
|
||||
}
|
||||
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);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.model.pedalBoard.removeOnChangedHandler(this.onPedalBoardChanged);
|
||||
super.componentWillUnmount();
|
||||
}
|
||||
updateResponsive() {
|
||||
this.setState({
|
||||
splitControlBar: this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD,
|
||||
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.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.setPedalBoardControlValue(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 });
|
||||
}
|
||||
|
||||
onLoadClick(e: SyntheticEvent) {
|
||||
this.setState({ loadDialogOpen: true });
|
||||
}
|
||||
|
||||
getPedalBoardItem(selectedId?: number): PedalBoardItem | null {
|
||||
if (selectedId === undefined) return null;
|
||||
|
||||
let pedalBoard = this.model.pedalBoard.get();
|
||||
if (!pedalBoard) return null;
|
||||
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.setPedalBoardControlValue(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 pluginUri = "";
|
||||
let presetsUri = "";
|
||||
if (pedalBoardItem) {
|
||||
if (pedalBoardItem.isEmpty()) {
|
||||
title = "";
|
||||
} else if (pedalBoardItem.isSplit()) {
|
||||
title = "Split";
|
||||
} else {
|
||||
let uiPlugin = this.model.getUiPlugin(pedalBoardItem.uri);
|
||||
if (uiPlugin == null) {
|
||||
title = "Missing Plugin";
|
||||
} else {
|
||||
title = uiPlugin.name;
|
||||
author = uiPlugin.author_name;
|
||||
presetsUri = uiPlugin.uri;
|
||||
if (uiPlugin.description.length > 20) {
|
||||
pluginUri = uiPlugin.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let classes = this.props.classes;
|
||||
return (
|
||||
<div style={{
|
||||
flex: "1 0 auto", overflow: "hidden", marginRight: 8,
|
||||
display: "flex", flexDirection: "row", flexWrap: "nowrap",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
<span className={classes.title}>{title}</span>
|
||||
</span>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
<span className={classes.author}>{author}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginInfoDialog plugin_uri={pluginUri} />
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<PluginPresetSelector pluginUri={presetsUri}
|
||||
onSelectPreset={(presetName) => this.handleSelectPluginPreset(pedalBoardItem!.instanceId, presetName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let pedalBoard = this.model.pedalBoard.get();
|
||||
let pedalBoardItem = this.getSelectedPedalBoardItem();
|
||||
let uiPlugin = null;
|
||||
let bypassVisible = false;
|
||||
let bypassChecked = false;
|
||||
let canDelete = false;
|
||||
let canAdd = false;
|
||||
let instanceId = -1;
|
||||
|
||||
if (pedalBoardItem) {
|
||||
canDelete = pedalBoard.canDeleteItem(pedalBoardItem.instanceId);
|
||||
instanceId = pedalBoardItem.instanceId;
|
||||
if (pedalBoardItem.isEmpty()) {
|
||||
canAdd = true;
|
||||
} else if (pedalBoardItem.isSplit()) {
|
||||
canAdd = true;
|
||||
} else {
|
||||
uiPlugin = this.model.getUiPlugin(pedalBoardItem.uri);
|
||||
canAdd = true;
|
||||
if (uiPlugin) {
|
||||
bypassVisible = true;
|
||||
bypassChecked = pedalBoardItem.isEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
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={uiPlugin?.uri ?? "#error"} selectedId={this.state.selectedPedal}
|
||||
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",
|
||||
width: "100%", height: 48, paddingLeft: 16, paddingRight: 16
|
||||
}} >
|
||||
<div style={{ flex: "0 0 auto", width: 80 }} >
|
||||
<div style={{ display: bypassVisible ? "block" : "none", width: 80 }} >
|
||||
<Switch checked={bypassChecked} onChange={this.handleEnableCurrentItemChanged} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(!this.state.splitControlBar) && this.titleBar(pedalBoardItem)
|
||||
}
|
||||
<div style={{ flex: "1 1 1px" }}>
|
||||
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto", display: canAdd ? "block" : "none", paddingRight: 8 }}>
|
||||
<IconButton onClick={(e) => { this.onAddClick(e) }} >
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="add-menu"
|
||||
anchorEl={this.state.addMenuAnchorEl}
|
||||
keepMounted
|
||||
open={Boolean(this.state.addMenuAnchorEl)}
|
||||
onClose={() => this.handleAddClose()}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
<MenuItem onClick={() => this.onInsertPedal(instanceId)}>Insert pedal</MenuItem>
|
||||
<MenuItem onClick={() => this.onAppendPedal(instanceId)}>Append pedal</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => this.onInsertSplit(instanceId)}>Insert split</MenuItem>
|
||||
<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) }} >
|
||||
<img src="/img/old_delete_outline_black_24dp.svg" alt="Delete" style={{ width: 24, height: 24, 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 || (this.getSelectedPedalBoardItem()?.isSplit() ?? true)}
|
||||
startIcon={<InputIcon />}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton onClick={(e) => { this.handleMidiConfiguration(instanceId); }}>
|
||||
<img src="img/ic_midi.svg" style={{ width: 24, height: 24, opacity: 0.6 }} alt="Midi configuration" />
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.splitControlBar && (
|
||||
<div className={classes.splitControlBar}>
|
||||
{
|
||||
this.titleBar(pedalBoardItem)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={horizontalScrollLayout ? classes.controlContentSmall : classes.controlContent}>
|
||||
{
|
||||
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}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default MainPage
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
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 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;
|
||||
|
||||
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 LATCH_CONTROL_TYPE: number = 0;
|
||||
static MOMENTARY_CONTROL_TYPE: number = 1;
|
||||
|
||||
switchControlType: number = MidiBinding.LATCH_CONTROL_TYPE;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Component } from 'react';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import Utility, { nullCast } from './Utility';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import MicNoneOutlinedIcon from '@material-ui/icons/MicNoneOutlined';
|
||||
import MicOutlinedIcon from '@material-ui/icons/MicOutlined';
|
||||
import IconButton from '@material-ui/core/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" }
|
||||
});
|
||||
|
||||
interface MidiBindingViewProps 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 MidiBindingViewState {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const MidiBindingView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<MidiBindingViewProps, MidiBindingViewState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MidiBindingViewProps) {
|
||||
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 value={control.value}>{control.displayName}</MenuItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let midiBinding = this.props.midiBinding;
|
||||
let pedalBoardItem = this.model.pedalBoard.get().getItem(this.props.instanceId);
|
||||
let uiPlugin = this.model.getUiPlugin(pedalBoardItem.uri);
|
||||
if (!uiPlugin) {
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
let canLatch: boolean = false;
|
||||
let canTrigger: boolean = false;
|
||||
let showLinearControlTypeSelect: boolean = false;
|
||||
let isBinaryControl: boolean = false;
|
||||
let showLinearRange: boolean = false;
|
||||
let canRotaryScale: boolean = false;
|
||||
if (this.props.midiBinding.symbol === "__bypass") {
|
||||
isBinaryControl = true;
|
||||
canLatch = midiBinding.bindingType !== MidiBinding.BINDING_TYPE_NONE;
|
||||
} else {
|
||||
let port = nullCast(uiPlugin!.getControl(this.props.midiBinding.symbol));
|
||||
isBinaryControl = (port.isAbToggle() || port.isOnOffSwitch());
|
||||
if (midiBinding.bindingType !== MidiBinding.BINDING_TYPE_NONE) {
|
||||
canLatch = isBinaryControl;
|
||||
canTrigger = port.trigger;
|
||||
showLinearControlTypeSelect = !(canLatch || canTrigger);
|
||||
showLinearRange = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.LINEAR_CONTROL_TYPE;
|
||||
canRotaryScale = showLinearControlTypeSelect && 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
|
||||
style={{ width: 80, }}
|
||||
onChange={(e, extra) => this.handleTypeChange(e, extra)}
|
||||
value={midiBinding.bindingType}
|
||||
>
|
||||
<MenuItem value={0}>None</MenuItem>
|
||||
{(isBinaryControl) && (
|
||||
<MenuItem value={1}>Note</MenuItem>
|
||||
)}
|
||||
<MenuItem value={2}>Control</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
|
||||
(
|
||||
<div className={classes.controlDiv2} >
|
||||
<Select
|
||||
style={{ width:80}}
|
||||
onChange={(e, extra) => this.handleNoteChange(e, extra)}
|
||||
value={midiBinding.note}
|
||||
|
||||
>
|
||||
{
|
||||
this.generateMidiSelects()
|
||||
}
|
||||
</Select>
|
||||
<IconButton onClick={()=> {
|
||||
if (this.props.listen)
|
||||
{
|
||||
this.props.onListen(-2, "", false)
|
||||
} else {
|
||||
this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false)
|
||||
}
|
||||
}}>
|
||||
{ this.props.listen ? (
|
||||
<MicOutlinedIcon />
|
||||
) : (
|
||||
<MicNoneOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) &&
|
||||
(
|
||||
<div className={classes.controlDiv2} >
|
||||
|
||||
<Select
|
||||
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)
|
||||
}
|
||||
}}>
|
||||
{ this.props.listen ? (
|
||||
<MicOutlinedIcon />
|
||||
) : (
|
||||
<MicNoneOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
((canLatch) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
onChange={(e, extra) => this.handleLatchControlTypeChange(e, extra)}
|
||||
value={midiBinding.switchControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.LATCH_CONTROL_TYPE}>Latch</MenuItem>
|
||||
<MenuItem value={MidiBinding.MOMENTARY_CONTROL_TYPE}>Momentary</MenuItem>
|
||||
</Select>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
(canTrigger) &&
|
||||
(
|
||||
<div className={classes.controlDiv2} >
|
||||
<Typography style={{paddingTop: 12, paddingBottom: 12}}>(Trigger)</Typography>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
{
|
||||
(showLinearControlTypeSelect) &&
|
||||
(
|
||||
<div className={classes.controlDiv} >
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
onChange={(e, extra) => this.handleLinearControlTypeChange(e, extra)}
|
||||
value={midiBinding.linearControlType}
|
||||
>
|
||||
<MenuItem value={MidiBinding.LATCH_CONTROL_TYPE}>Linear</MenuItem>
|
||||
<MenuItem value={MidiBinding.MOMENTARY_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>
|
||||
)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
export default MidiBindingView;
|
||||
@@ -0,0 +1,343 @@
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MidiBinding from './MidiBinding';
|
||||
import MidiBindingView from './MidiBindingView';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
},
|
||||
dialogTitle: {
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: "1 1 auto",
|
||||
},
|
||||
pluginTable: {
|
||||
border: "collapse",
|
||||
width: "100%",
|
||||
|
||||
},
|
||||
pluginHead: {
|
||||
borderTop: "1pt #DDD solid", paddingLeft: 22, paddingRight: 22
|
||||
},
|
||||
bindingTd: {
|
||||
verticalAlign: "top",
|
||||
paddingLeft: 12, paddingBottom: 8
|
||||
},
|
||||
nameTd: {
|
||||
paddingLeft: 48,
|
||||
verticalAlign: "top",
|
||||
paddingTop: 12
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export interface MidiBindingDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean,
|
||||
onClose: () => void
|
||||
};
|
||||
|
||||
export interface MidiBindingDialogState {
|
||||
listenInstanceId: number;
|
||||
listenSymbol: string;
|
||||
listenSnackbarOpen: boolean;
|
||||
|
||||
};
|
||||
|
||||
export const MidiBindingDialog =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends ResizeResponsiveComponent<MidiBindingDialogProps, MidiBindingDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: MidiBindingDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
listenInstanceId: -2,
|
||||
listenSymbol: "",
|
||||
listenSnackbarOpen: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
stateWasPopped: boolean = false;
|
||||
handlePopState(e: any): any {
|
||||
this.stateWasPopped = true;
|
||||
let shouldClose = (!e.state);
|
||||
if (shouldClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
handleClose() {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
listenTimeoutHandle?: NodeJS.Timeout;
|
||||
|
||||
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.listenHandle = undefined; // (one-shot event)
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
updateHooks(): void {
|
||||
let wantHooks = this.mounted && this.props.open;
|
||||
if (wantHooks !== this.hasHooks) {
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks) {
|
||||
this.stateWasPopped = false;
|
||||
window.addEventListener("popstate", this.handlePopState);
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
let state = history.state;
|
||||
if (!state) {
|
||||
state = {};
|
||||
}
|
||||
state.midiBindingDialog = true;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.pushState(
|
||||
state,
|
||||
"Preset MIDI Bindings",
|
||||
"#MidiBindingDialog"
|
||||
);
|
||||
} else {
|
||||
window.removeEventListener("popstate", this.handlePopState);
|
||||
if (!this.stateWasPopped) {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.back();
|
||||
}
|
||||
this.cancelListenForControl();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.updateHooks();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentDidMount();
|
||||
|
||||
this.mounted = false;
|
||||
this.updateHooks();
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateHooks();
|
||||
}
|
||||
handleItemChanged(instanceId: number, newBinding: MidiBinding) {
|
||||
this.model.setMidiBinding(instanceId, newBinding);
|
||||
}
|
||||
generateTable(): React.ReactNode[] {
|
||||
let classes = this.props.classes;
|
||||
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 plugin = this.model.getUiPlugin(item.uri);
|
||||
if (plugin) {
|
||||
result.push(
|
||||
<tr>
|
||||
<td colSpan={2} className={classes.pluginHead}>
|
||||
<Typography variant="caption" color="textSecondary" noWrap>
|
||||
{plugin.name}
|
||||
</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
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")}
|
||||
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];
|
||||
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}
|
||||
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, classes } = props;
|
||||
if (!open) {
|
||||
return (<div />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} fullWidth onClose={this.handleClose} aria-labelledby="Rename-dialog-title"
|
||||
fullScreen={true}
|
||||
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} >
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={this.handleClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography 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"
|
||||
/>
|
||||
</Dialog >
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default MidiBindingDialog;
|
||||
@@ -0,0 +1,131 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import Input from '@material-ui/core/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(styles, { withTheme: true })(
|
||||
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() {
|
||||
//let classes = this.props.classes;
|
||||
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}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default NumericInput;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
|
||||
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,40 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
import { withStyles, createStyles, WithStyles } from '@material-ui/core';
|
||||
import { Theme } from '@material-ui/core/styles/createMuiTheme';
|
||||
import { Lv2Plugin } from './Lv2Plugin'
|
||||
|
||||
|
||||
|
||||
const pedalStyles = ({ palette }: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface PedalProps extends WithStyles<typeof pedalStyles> {
|
||||
plugin: Lv2Plugin;
|
||||
children?: React.ReactChild | React.ReactChild[];
|
||||
|
||||
};
|
||||
|
||||
type PedalState = {
|
||||
|
||||
};
|
||||
|
||||
export const TemporaryDrawer = withStyles(pedalStyles)(
|
||||
class extends Component<PedalProps, PedalState>
|
||||
{
|
||||
state: PedalState;
|
||||
|
||||
constructor(props: PedalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
render() : ReactNode {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,570 @@
|
||||
import { PiPedalArgumentError } from './PiPedalError';
|
||||
import MidiBinding from './MidiBinding';
|
||||
|
||||
|
||||
const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/piddle/pedalboard#Split";
|
||||
const EMPTY_PEDALBOARD_ITEM_URI = "uri://two-play/piddle/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);
|
||||
|
||||
this.controlValues = ControlValue.deserializeArray(input.controlValues);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
throw new PiPedalArgumentError("Control not found.");
|
||||
}
|
||||
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[] = [];
|
||||
};
|
||||
|
||||
|
||||
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> {
|
||||
|
||||
deserialize(input: any): PedalBoard {
|
||||
this.name = input.name;
|
||||
this.items = PedalBoardItem.deserializeArray(input.items);
|
||||
this.nextInstanceId = input.nextInstanceId ?? -1;
|
||||
return this;
|
||||
}
|
||||
|
||||
clone(): PedalBoard {
|
||||
return new PedalBoard().deserialize(this);
|
||||
}
|
||||
name: string = "";
|
||||
items: PedalBoardItem[] = [];
|
||||
nextInstanceId: number = -1;
|
||||
|
||||
*itemsGenerator(): Generator<PedalBoardItem, void, undefined> {
|
||||
let it = itemGenerator_(this.items);
|
||||
while (true)
|
||||
{
|
||||
let v = it.next();
|
||||
if (v.done) break;
|
||||
yield v.value;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 = [];
|
||||
|
||||
}
|
||||
|
||||
_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.");
|
||||
}
|
||||
}
|
||||
_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,26 @@
|
||||
|
||||
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,269 @@
|
||||
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;
|
||||
|
||||
|
||||
|
||||
|
||||
class PiPedalSocket {
|
||||
onMessageReceived: MessageHandler;
|
||||
onError: ErrorHandler;
|
||||
onReconnecting: ReconnectingHandler;
|
||||
onReconnect: ReconnectHandler;
|
||||
|
||||
socket?: WebSocket;
|
||||
nextResponseCode: number = 0;
|
||||
url: string;
|
||||
retrying: boolean = false;
|
||||
retryCount: number = 0;
|
||||
retryDelay: number = 0;
|
||||
totalRetryDelay: number = 0;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
onMessageReceived: MessageHandler,
|
||||
onError: ErrorHandler,
|
||||
onReconnecting: ReconnectingHandler,
|
||||
onReconnect: ReconnectHandler) {
|
||||
this.url = url;
|
||||
this.onMessageReceived = onMessageReceived;
|
||||
this.onError = onError;
|
||||
this.onReconnecting = onReconnecting;
|
||||
this.onReconnect = onReconnect;
|
||||
}
|
||||
|
||||
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.onMessageReceived(header, body);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.onError) {
|
||||
this.onError("Invalid server response. " + error, error);
|
||||
} else {
|
||||
throw new PiPedalStateError("Invalid server response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleError(_event: Event): any {
|
||||
if (this.onError) {
|
||||
this.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection lost.");
|
||||
}
|
||||
}
|
||||
handleClose(_event: any): any {
|
||||
if (this.retrying) {
|
||||
// treat this as a fatal error.
|
||||
if (this.onError) {
|
||||
this.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection closed.");
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
this._discardReplyReservations();
|
||||
this.retrying = true;
|
||||
this.retryCount = 0;
|
||||
this.socket = undefined;
|
||||
this.retryDelay = 10000;
|
||||
this.totalRetryDelay = 0;
|
||||
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
|
||||
if (this.socket)
|
||||
{
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.onReconnecting(this.retryCount,MAX_RETRIES);
|
||||
++this.retryCount;
|
||||
|
||||
this.connectInternal_()
|
||||
.then((socket) => {
|
||||
this.socket = socket;
|
||||
this.retrying = false;
|
||||
this.onReconnect();
|
||||
})
|
||||
.catch(error => {
|
||||
if (this.totalRetryDelay === MAX_RETRY_TIME)
|
||||
{
|
||||
this.onError("Server connection lost.");
|
||||
return;
|
||||
} else {
|
||||
this.totalRetryDelay += this.retryDelay;
|
||||
Utility.delay(this.retryDelay).then(() => this.reconnect());
|
||||
this.retryDelay *= 2;
|
||||
if (this.retryDelay > 5000) this.retryDelay = 5000;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
} catch (ignored)
|
||||
{
|
||||
|
||||
}
|
||||
this.socket = undefined;
|
||||
}
|
||||
connectInternal_(): Promise<WebSocket> {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
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("Can't connect to server.");
|
||||
};
|
||||
ws.onerror = (event: Event) => {
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
reject("Can't connect to server.");
|
||||
};
|
||||
ws.onopen = (event: Event) => {
|
||||
ws.onerror = self.handleError.bind(self);
|
||||
ws.onclose = self.handleClose.bind(self);
|
||||
ws.onopen = null;
|
||||
resolve(ws);
|
||||
};
|
||||
});
|
||||
}
|
||||
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,63 @@
|
||||
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,743 @@
|
||||
import React, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { UiControl, ScalePoint } from './Lv2Plugin';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import Units from './Units';
|
||||
import Utility, {nullCast} from './Utility';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
|
||||
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 }
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
margin: "12px"
|
||||
},
|
||||
switchTrack: {
|
||||
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.type === 'light' ? 0.38 : 0.3
|
||||
},
|
||||
displayValue: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 4,
|
||||
textAlign: "center",
|
||||
background: "white",
|
||||
color: "#666",
|
||||
// zIndex: -1,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export interface PluginControlProps extends WithStyles<typeof styles> {
|
||||
uiControl?: UiControl;
|
||||
value: number;
|
||||
onPreviewChange?: (value: number) => void;
|
||||
onChange: (value: number) => void;
|
||||
theme: Theme;
|
||||
requestIMEEdit: (uiControl: UiControl, value: number) => void;
|
||||
};
|
||||
type PluginControlState = {
|
||||
error: boolean;
|
||||
};
|
||||
|
||||
const PluginControl =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<PluginControlProps, PluginControlState>
|
||||
{
|
||||
imgRef: React.RefObject<HTMLImageElement>;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
selectRef: React.RefObject<HTMLSelectElement>;
|
||||
|
||||
displayValueRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: PluginControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: false
|
||||
};
|
||||
|
||||
this.imgRef = React.createRef();
|
||||
this.inputRef = React.createRef();
|
||||
this.selectRef = React.createRef();
|
||||
this.displayValueRef = 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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (event.currentTarget) {
|
||||
event.currentTarget.value = this.formatValue(this.props.uiControl, result);
|
||||
}
|
||||
this.previewInputValue(result, true);
|
||||
this.inputRef.current.value = this.formatValue(this.props.uiControl, result); // 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<HTMLImageElement>): 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;
|
||||
|
||||
onTouchStart(e: TouchEvent<HTMLImageElement>) {
|
||||
}
|
||||
onTouchMove(e: TouchEvent<HTMLImageElement>) {
|
||||
// e.preventDefault();
|
||||
e.stopPropagation(); // cancels scroll!!!
|
||||
|
||||
}
|
||||
|
||||
capturedPointers: number[] = [];
|
||||
|
||||
onBodyPointerDownCapture(e_: any): any
|
||||
{
|
||||
let e = e_ as PointerEvent;
|
||||
if (this.isExtraTouch(e))
|
||||
{
|
||||
this.captureElement!.setPointerCapture(e.pointerId);
|
||||
this.capturedPointers.push(e.pointerId);
|
||||
++this.pointersDown;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pointerId: number = 0;
|
||||
pointerType: string = "";
|
||||
|
||||
isCapturedPointer(e: PointerEvent<HTMLImageElement>): 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?: HTMLElement = undefined;
|
||||
|
||||
onPointerDown(e: PointerEvent<HTMLImageElement>): void {
|
||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||
++this.pointersDown;
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.mouseDown = true;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
isExtraTouch(e: PointerEvent): boolean {
|
||||
return (this.mouseDown && this.pointerType === "touch" && e.pointerType === "touch" && e.pointerId !== this.pointerId);
|
||||
|
||||
}
|
||||
|
||||
onPointerLostCapture(e: PointerEvent<HTMLImageElement>) {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
--this.pointersDown;
|
||||
|
||||
|
||||
this.releaseCapture(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
updateRange(e: PointerEvent<HTMLImageElement>): 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;
|
||||
}
|
||||
onPointerUp(e: PointerEvent<HTMLImageElement>) {
|
||||
|
||||
if (this.isCapturedPointer(e)) {
|
||||
--this.pointersDown;
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
let dRange = this.updateRange(e)
|
||||
this.previewRange(dRange, true);
|
||||
|
||||
this.releaseCapture(e);
|
||||
|
||||
} else {
|
||||
--this.pointersDown;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
releaseCapture(e: PointerEvent<HTMLImageElement>)
|
||||
{
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
onPointerMove(e: PointerEvent<HTMLImageElement>): void {
|
||||
if (this.isCapturedPointer(e)) {
|
||||
e.preventDefault();
|
||||
let dRange = this.updateRange(e)
|
||||
this.previewRange(dRange, false);
|
||||
}
|
||||
}
|
||||
|
||||
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.formatValue(this.props.uiControl, 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}
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (control.isAbToggle()) {
|
||||
// unchecked color is not gray.
|
||||
return (
|
||||
<Switch checked={value !== 0}
|
||||
onChange={(event) => {
|
||||
this.onCheckChanged(event.target.checked);
|
||||
}}
|
||||
classes={{
|
||||
track: this.props.classes.switchTrack
|
||||
}}
|
||||
style={{ color: this.props.theme.palette.secondary.main }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
ref={this.selectRef}
|
||||
value={value}
|
||||
onChange={this.onSelectChanged}
|
||||
inputProps={{
|
||||
name: control.name,
|
||||
id: 'id' + control.symbol,
|
||||
style: { fontSize: FONT_SIZE }
|
||||
}}
|
||||
style={{ marginLeft: 4, marginRight: 4, width: 140 }}
|
||||
>
|
||||
{control.scale_points.map((scale_point: ScalePoint) => (
|
||||
<MenuItem value={scale_point.value}>{scale_point.label}</MenuItem>
|
||||
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
formatDisplayValue(uiControl: UiControl | undefined, value: number): string {
|
||||
if (!uiControl) return "";
|
||||
for (let i = 0; i < uiControl.scale_points.length; ++i)
|
||||
{
|
||||
let scalePoint = uiControl.scale_points[i];
|
||||
if (scalePoint.value === value)
|
||||
{
|
||||
return scalePoint.label;
|
||||
}
|
||||
}
|
||||
let text = this.formatValue(uiControl, value);
|
||||
|
||||
switch (uiControl.units) {
|
||||
case Units.bpm:
|
||||
text += "bpm";
|
||||
break;
|
||||
case Units.cent:
|
||||
text += "cents";
|
||||
break;
|
||||
case Units.cm:
|
||||
text += "cm";
|
||||
break;
|
||||
case Units.db:
|
||||
text += "dB";
|
||||
break;
|
||||
case Units.hz:
|
||||
text += "Hz";
|
||||
break;
|
||||
case Units.khz:
|
||||
text += "kHz";
|
||||
break;
|
||||
case Units.km:
|
||||
text += "km";
|
||||
break;
|
||||
case Units.m:
|
||||
text += "m";
|
||||
break;
|
||||
case Units.mhz:
|
||||
text += "MHz";
|
||||
break;
|
||||
case Units.min:
|
||||
text += "min";
|
||||
break;
|
||||
case Units.ms:
|
||||
text += "ms";
|
||||
break;
|
||||
case Units.pc:
|
||||
text += "%";
|
||||
break;
|
||||
case Units.s:
|
||||
text += "s";
|
||||
break;
|
||||
// Midinote: not handled.
|
||||
// semitone12TET not handled.
|
||||
|
||||
|
||||
|
||||
}
|
||||
return text;
|
||||
|
||||
|
||||
}
|
||||
formatValue(uiControl: UiControl | undefined, value: number): string {
|
||||
if (!uiControl) return "";
|
||||
return uiControl.formatValue(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)
|
||||
{
|
||||
range = Math.log(value/uiControl.min_value)/Math.log(uiControl.max_value/uiControl.min_value);
|
||||
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 (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.is_logarithmic)
|
||||
{
|
||||
value = uiControl.min_value*Math.pow(uiControl.max_value/uiControl.min_value,range);
|
||||
} 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() {
|
||||
let classes = this.props.classes;
|
||||
let t = this.props.uiControl;
|
||||
if (!t) {
|
||||
return (<div>#Error</div>);
|
||||
|
||||
}
|
||||
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();
|
||||
|
||||
if (isAbSwitch) {
|
||||
switchText = control.scale_points[0].value === value ? control.scale_points[0].label : control.scale_points[1].label;
|
||||
}
|
||||
|
||||
let item_width = isSelect ? 160 : 80;
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: item_width, margin: 8 }}>
|
||||
{/* TITLE SECTION */}
|
||||
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8,marginLeft: isSelect? 16: 0, marginRight: 0 }}>
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%",
|
||||
textAlign: isSelect ? "left" : "center"
|
||||
}}> {control.name}</Typography>
|
||||
</div>
|
||||
{/* CONTROL SECTION */}
|
||||
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
{(isSelect || isAbSwitch || isOnOffSwitch) ? (
|
||||
this.makeSelect(control, value)
|
||||
) : (
|
||||
<div style={{ flex: "0 1 auto" }}>
|
||||
<img ref={this.imgRef} src="img/fx_dial.svg"
|
||||
style={{ overscrollBehavior: "none", touchAction: "none", width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform() }}
|
||||
draggable="true"
|
||||
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
|
||||
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
|
||||
alt={control.name}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* LABEL/EDIT SECTION*/}
|
||||
<div style={{ flex: "0 0 auto", position: "relative", width: 60 }}>
|
||||
{(!(isSelect || isOnOffSwitch)) &&
|
||||
(
|
||||
(isAbSwitch) ? (
|
||||
<Typography variant="caption" display="block" noWrap style={{
|
||||
width: "100%", textAlign: "center"
|
||||
}}> {switchText} </Typography>
|
||||
) : (
|
||||
<div>
|
||||
<Input key={value}
|
||||
type="number"
|
||||
defaultValue={this.formatValue(control, 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<HTMLImageElement> e): boolean
|
||||
{
|
||||
return e.pointerId === this.pointerId
|
||||
&& e.pointerType === this.pointerType;
|
||||
}
|
||||
*/
|
||||
touchPointerId: number = 0;
|
||||
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default PluginControl;
|
||||
@@ -0,0 +1,488 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { UiPlugin, UiControl } 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 GxTunerControl from './GxTunerControl';
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import FullScreenIME from './FullScreenIME';
|
||||
|
||||
|
||||
export const StandardItemSize = { width: 80, height: 110 };
|
||||
|
||||
|
||||
const GXTUNER_URI = "http://guitarix.sourceforge.net/plugins/gxtuner#tuner";
|
||||
|
||||
const LANDSCAPE_HEIGHT_BREAK = 500;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
display: "block",
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "0px",
|
||||
height: "100%",
|
||||
overflowX: "auto",
|
||||
overflowY: "auto"
|
||||
},
|
||||
vuMeterL: {
|
||||
position: "fixed",
|
||||
paddingLeft: 12,
|
||||
paddingRight: 4,
|
||||
paddingBottom: 24,
|
||||
left: 0,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
vuMeterR: {
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
marginRight: 22,
|
||||
paddingLeft: 4,
|
||||
paddingBottom: 24,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
vuMeterRLandscape: {
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
paddingRight: 22,
|
||||
paddingLeft: 12,
|
||||
paddingBottom: 24,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
|
||||
normalGrid: {
|
||||
position: "relative",
|
||||
paddingLeft: 22,
|
||||
paddingRight: 34,
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start",
|
||||
|
||||
|
||||
},
|
||||
landscapeGrid: {
|
||||
paddingLeft: 40,
|
||||
// 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",
|
||||
flex: "0 0 auto"
|
||||
},
|
||||
portgroupControlPadding: {
|
||||
flex: "0 0 auto",
|
||||
marginTop: 12,
|
||||
marginBottom: 8
|
||||
|
||||
},
|
||||
controlPadding: {
|
||||
flex: "0 0 auto",
|
||||
marginTop: 0,
|
||||
marginBottom: 0
|
||||
|
||||
},
|
||||
portGroup: {
|
||||
marginLeft: 8,
|
||||
marginTop: 0,
|
||||
marginRight: 8,
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
borderRadius: 10,
|
||||
elevation: 12,
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
flex: "0 1 auto",
|
||||
},
|
||||
portGroupLandscape: {
|
||||
marginLeft: 8,
|
||||
marginTop: 0,
|
||||
marginRight: 8,
|
||||
marginBottom: 12,
|
||||
position: "relative",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
border: "2pt #AAA solid",
|
||||
borderRadius: 10,
|
||||
elevation: 12,
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "nowrap",
|
||||
flex: "0 0 auto"
|
||||
},
|
||||
portGroupTitle: {
|
||||
position: "absolute",
|
||||
top: -10,
|
||||
background: "white",
|
||||
marginLeft: 20,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8
|
||||
},
|
||||
portGroupControls: {
|
||||
display: "flex",
|
||||
flexDirection: "row", flexWrap: "wrap",
|
||||
paddingTop: 6,
|
||||
paddingBottom: 8
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
export class ControlGroup {
|
||||
constructor(name: string, controls: ReactNode[])
|
||||
{
|
||||
this.name = name;
|
||||
this.controls = controls;
|
||||
}
|
||||
name: string;
|
||||
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;
|
||||
};
|
||||
|
||||
const PluginControlView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
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
|
||||
|
||||
}
|
||||
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.setPedalBoardControlValue(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.not_on_gui) {
|
||||
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
|
||||
|
||||
});
|
||||
}
|
||||
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
|
||||
let symbol = uiControl.symbol;
|
||||
|
||||
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 uiControl={uiControl} value={controlValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(controlValue!.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)}
|
||||
|
||||
/>
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
getStandardControlNodes(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes {
|
||||
let result: ControlNodes = [];
|
||||
|
||||
for (let i = 0; i < plugin.controls.length; ++i) {
|
||||
let pluginControl = plugin.controls[i];
|
||||
if (!pluginControl.not_on_gui) {
|
||||
if (pluginControl.port_group !== "") {
|
||||
let portGroup = pluginControl.port_group;
|
||||
let groupControls: ReactNode[] = [];
|
||||
groupControls.push(
|
||||
this.makeStandardControl(pluginControl, controlValues)
|
||||
);
|
||||
while (i + 1 < plugin.controls.length && plugin.controls[i + 1].port_group === portGroup) {
|
||||
++i;
|
||||
pluginControl = plugin.controls[i];
|
||||
if (!pluginControl.not_on_gui) {
|
||||
groupControls.push(
|
||||
this.makeStandardControl(pluginControl, controlValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
result.push(
|
||||
new ControlGroup(plugin.getPortGroup(portGroup).name, groupControls)
|
||||
)
|
||||
} else {
|
||||
result.push(
|
||||
this.makeStandardControl(pluginControl, controlValues)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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.");
|
||||
}
|
||||
|
||||
getTunerControls(plugin: UiPlugin, controlValues: ControlValue[]): ControlNodes {
|
||||
// display standard version of just a few controls.
|
||||
let shortList: ControlValue[] = [];
|
||||
// for (let i = 0; i < controlValues.length; ++i)
|
||||
// {
|
||||
// shortList.push(controlValues[i]);
|
||||
// }
|
||||
shortList.push(this.getControl(controlValues, "REFFREQ"));
|
||||
shortList.push(this.getControl(controlValues, "THRESHOLD"));
|
||||
|
||||
|
||||
let controls = shortList.map((controlValue) => (
|
||||
<PluginControl uiControl={plugin.getControl(controlValue.key)} value={controlValue.value}
|
||||
onChange={(value: number) => { this.onValueChanged(controlValue.key, value) }}
|
||||
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue.key, value) }}
|
||||
requestIMEEdit={(uiControl, value) => this.requestImeEdit(uiControl, value)}
|
||||
/>
|
||||
));
|
||||
let tunerControl = (<GxTunerControl instanceId={this.props.instanceId} />);
|
||||
let result: ControlNodes = [];
|
||||
result.push(tunerControl);
|
||||
|
||||
for (let i = 0; i < controls.length; ++i)
|
||||
{
|
||||
result.push(controls[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
onImeValueChange(key: string, value: number) {
|
||||
this.model.setPedalBoardControlValue(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;
|
||||
}
|
||||
|
||||
controlNodesToNodes(nodes: (ReactNode | ControlGroup)[]): ReactNode[] {
|
||||
let classes = this.props.classes;
|
||||
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 className={classes.controlPadding}>
|
||||
{ item }
|
||||
</div>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
result.push((
|
||||
<div className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
|
||||
<div className={classes.portGroupTitle}>
|
||||
<Typography variant="caption">{controlGroup.name}</Typography>
|
||||
</div>
|
||||
<div className={classes.portGroupControls} >
|
||||
{
|
||||
controls
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
} else {
|
||||
result.push((
|
||||
<div className={ hasGroups ? classes.portgroupControlPadding: classes.controlPadding } >
|
||||
{node as ReactNode}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
render(): ReactNode {
|
||||
let classes = this.props.classes;
|
||||
let pedalBoardItem = this.model.pedalBoard.get().getItem(this.props.instanceId);
|
||||
|
||||
if (!pedalBoardItem)
|
||||
return (<div className={classes.frame} ></div>);
|
||||
|
||||
let controlValues = pedalBoardItem.controlValues;
|
||||
|
||||
|
||||
let plugin: UiPlugin = nullCast(this.model.getUiPlugin(pedalBoardItem.uri));
|
||||
|
||||
|
||||
controlValues = this.filterNotOnGui(controlValues, plugin);
|
||||
|
||||
|
||||
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
|
||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||
let controlNodes: ControlNodes;
|
||||
|
||||
if (plugin.uri === GXTUNER_URI) {
|
||||
controlNodes = this.getTunerControls(plugin, controlValues);
|
||||
} else {
|
||||
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);
|
||||
|
||||
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} >
|
||||
{
|
||||
nodes
|
||||
}
|
||||
<div style={{ flex: "0 0 40px", width: 40, height: 1 }} />
|
||||
</div>
|
||||
<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 >
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default PluginControlView;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
import { PluginType } from './Lv2Plugin';
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
icon: {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
margin: "0px",
|
||||
opacity: "0.6"
|
||||
},
|
||||
});
|
||||
|
||||
export interface PluginIconProps extends WithStyles<typeof styles> {
|
||||
size?: number;
|
||||
offsetY?: number;
|
||||
pluginType: PluginType;
|
||||
pluginUri: string;
|
||||
}
|
||||
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.HighpassPlugin:
|
||||
return "fx_highpass.svg";
|
||||
case PluginType.ParaEQPlugin:
|
||||
case PluginType.LowpassPlugin:
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
var colors: string[] = [
|
||||
"#14003d",
|
||||
"#630651",
|
||||
"#005010",
|
||||
"#281431",
|
||||
"#5E0012",
|
||||
"#401222",
|
||||
"#3B1502"
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function colorFromHash(uri: string) {
|
||||
let hash = hashString(uri);
|
||||
|
||||
return colors[hash % colors.length];
|
||||
|
||||
}
|
||||
|
||||
function hashString(str: string): number {
|
||||
var hash = 0, i, chr;
|
||||
for (i = 0; i < str.length; i++) {
|
||||
chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
if (hash < 0) return -hash;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function SelectIcon(plugin_type: PluginType, itemUri: string) {
|
||||
let icon = SelectBaseIcon(plugin_type);
|
||||
return PiPedalModelFactory.getInstance().svgImgUrl(icon);
|
||||
}
|
||||
|
||||
const PluginIcon = withStyles(styles)((props: PluginIconProps) => {
|
||||
const { classes, pluginUri, pluginType } = props;
|
||||
|
||||
let size: number = 24;
|
||||
if (props.size) size = props.size;
|
||||
let topVal: number = (props.offsetY ?? 0);
|
||||
|
||||
return (
|
||||
<img src={SelectIcon(pluginType, pluginUri)} className={classes.icon} style={{ width: size, height: size, color: "#F88", position: "relative", top: topVal }} alt={pluginType as string}
|
||||
/>);
|
||||
|
||||
// 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>
|
||||
// );
|
||||
});
|
||||
|
||||
export default PluginIcon;
|
||||
@@ -0,0 +1,218 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import MuiDialogTitle from '@material-ui/core/DialogTitle';
|
||||
import MuiDialogContent from '@material-ui/core/DialogContent';
|
||||
import MuiDialogActions from '@material-ui/core/DialogActions';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { PiPedalModelFactory } from "./PiPedalModel";
|
||||
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
|
||||
import { UiPlugin, UiControl } from './Lv2Plugin';
|
||||
import PluginIcon from './PluginIcon';
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
});
|
||||
|
||||
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((theme: Theme) => ({
|
||||
root: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}))(MuiDialogContent);
|
||||
|
||||
const PluginInfoDialogActions = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
margin: 0,
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}))(MuiDialogActions);
|
||||
|
||||
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[]) {
|
||||
return (
|
||||
<Grid container direction="row" justify="flex-start" alignItems="flex-start" spacing={1} style={{ paddingLeft: "24px" }}>
|
||||
{
|
||||
controls.map((control) => (
|
||||
<Grid item spacing={2} xs={6} sm={4} key={control.symbol} >
|
||||
<Typography variant="body2">
|
||||
{control.name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
const PluginInfoDialog = withStyles(styles)((props: PluginInfoProps) => {
|
||||
|
||||
let model = PiPedalModelFactory.getInstance();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
let { classes, plugin_uri } = 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 !== "") ? "block" : "none" }} onClick={handleClickOpen}
|
||||
>
|
||||
<InfoOutlinedIcon />
|
||||
</IconButton>
|
||||
{open && (
|
||||
<Dialog onClose={handleClose} open={open} fullWidth >
|
||||
<MuiDialogTitle >
|
||||
<div style={{ display: "flex", flexDirection: "row", alignItems: "start", flexWrap: "nowrap" }}>
|
||||
<div style={{ flex: "0 0 auto", marginRight: 16 }}>
|
||||
<PluginIcon pluginType={plugin.plugin_type} pluginUri={plugin.uri} 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" }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</MuiDialogTitle>
|
||||
<PluginInfoDialogContent dividers style={{ width: "100%", maxHeight: "80%", overflowX: "hidden" }}>
|
||||
<Typography gutterBottom>
|
||||
Author:
|
||||
{(plugin.author_homepage !== "")
|
||||
? <a href={plugin.author_homepage} target="_blank" rel="noreferrer">{plugin.author_name}</a>
|
||||
: (
|
||||
plugin.author_name
|
||||
)
|
||||
}
|
||||
</Typography>
|
||||
<Typography gutterBottom>
|
||||
{ioDescription(plugin)}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" gutterBottom style={{ paddingTop: "1em" }}>
|
||||
Controls:
|
||||
</Typography>
|
||||
{
|
||||
makeControls(plugin.controls)
|
||||
}
|
||||
<Typography gutterBottom style={{ paddingTop: "1em" }}>
|
||||
Description:
|
||||
</Typography>
|
||||
{
|
||||
(plugin.description !== "") && makeParagraphs(plugin.description)
|
||||
}
|
||||
</PluginInfoDialogContent>
|
||||
<PluginInfoDialogActions>
|
||||
<Button autoFocus onClick={handleClose} color="primary" style={{ width: "130px" }}>
|
||||
OK
|
||||
</Button>
|
||||
</PluginInfoDialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginInfoDialog;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
export default class PluginPreset {
|
||||
deserialize(input: any): PluginPreset {
|
||||
this.presetUri = input.presetUri;
|
||||
this.name = input.name;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any) : PluginPreset[] {
|
||||
let result: PluginPreset[] = [];
|
||||
for (let i = 0; i < input.length; ++i)
|
||||
{
|
||||
result.push(new PluginPreset().deserialize(input[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
presetUri: string = "";
|
||||
name: string = "";
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import PresetDialog from './PresetDialog';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import RenameDialog from './RenameDialog'
|
||||
import PluginPreset from './PluginPreset';
|
||||
|
||||
|
||||
interface PluginPresetSelectorProps extends WithStyles<typeof styles> {
|
||||
pluginUri?: string;
|
||||
|
||||
onSelectPreset: (presetName: string) => void;
|
||||
};
|
||||
|
||||
|
||||
interface PluginPresetSelectorState {
|
||||
presets: PluginPreset[] | null;
|
||||
enabled: boolean;
|
||||
showPresetsDialog: boolean;
|
||||
showEditPresetsDialog: boolean;
|
||||
presetsMenuAnchorRef: HTMLElement | null;
|
||||
|
||||
renameDialogOpen: boolean;
|
||||
renameDialogDefaultName: string;
|
||||
renameDialogActionName: string;
|
||||
renameDialogOnOk?: (name: string) => void;
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
|
||||
const PluginPresetSelector =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<PluginPresetSelectorProps, PluginPresetSelectorState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: PluginPresetSelectorProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
presets: null,
|
||||
enabled: false,
|
||||
showPresetsDialog: false,
|
||||
showEditPresetsDialog: false,
|
||||
presetsMenuAnchorRef: null,
|
||||
renameDialogOpen: false,
|
||||
renameDialogDefaultName: "",
|
||||
renameDialogActionName: "",
|
||||
renameDialogOnOk: undefined
|
||||
|
||||
};
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
|
||||
}
|
||||
|
||||
handleLoadPluginPreset(presetName: string)
|
||||
{
|
||||
this.handlePresetsMenuClose();
|
||||
this.props.onSelectPreset(presetName);
|
||||
}
|
||||
|
||||
handlePresetMenuClick(e: SyntheticEvent): void {
|
||||
this.setState({ presetsMenuAnchorRef: (e.currentTarget as HTMLElement) });
|
||||
}
|
||||
handlePresetsMenuClose(): void {
|
||||
this.setState({ presetsMenuAnchorRef: null });
|
||||
}
|
||||
|
||||
handlePresetsMenuSave(e: SyntheticEvent): void {
|
||||
this.handlePresetsMenuClose();
|
||||
e.stopPropagation();
|
||||
|
||||
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) => {
|
||||
// s'fine. dealt with by updates, but we do need error handling.
|
||||
})
|
||||
.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);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
currentUri?: string = "";
|
||||
componentDidUpdate()
|
||||
{
|
||||
if (this.currentUri !== this.props.pluginUri)
|
||||
{
|
||||
this.currentUri = this.props.pluginUri;
|
||||
if (!this.props.pluginUri)
|
||||
{
|
||||
this.setState({presets: null});
|
||||
} else {
|
||||
this.setState({presets: null});
|
||||
let captureUri = this.currentUri;
|
||||
this.model.getPluginPresets(this.props.pluginUri)
|
||||
.then((presets: PluginPreset[]) => {
|
||||
if (captureUri === this.currentUri)
|
||||
{
|
||||
this.setState({presets: presets});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (captureUri === this.currentUri)
|
||||
{
|
||||
this.setState({presets: null});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
handleSave() {
|
||||
this.model.saveCurrentPreset();
|
||||
}
|
||||
|
||||
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() {
|
||||
//let classes = this.props.classes;
|
||||
//let classes = this.props.classes;
|
||||
if ((!this.props.pluginUri) || (!this.state.presets) ||this.state.presets.length === 0)
|
||||
{
|
||||
return (<div/>);
|
||||
}
|
||||
return (
|
||||
<div >
|
||||
<IconButton onClick={(e)=> this.handlePresetMenuClick(e)}>
|
||||
<img src="img/ic_pluginpreset.svg" style={{ width: 24, height: 24,opacity: 0.6 }} alt="Plugin Presets" />
|
||||
</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}
|
||||
}
|
||||
}
|
||||
|
||||
>
|
||||
<Typography variant="caption" color="textSecondary" style={{marginLeft: 16,marginTop:4,marginBottom: 4}}>Plugin presets</Typography>
|
||||
{
|
||||
this.state.presets.map((preset) => {
|
||||
return (<MenuItem onClick={(e) => this.handleLoadPluginPreset(preset.presetUri)}>{preset.name}</MenuItem>);
|
||||
})
|
||||
}
|
||||
{/*
|
||||
<Divider />
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save plugin preset</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save plugin preset as...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
|
||||
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
|
||||
*/}
|
||||
|
||||
</Menu>
|
||||
<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)} />
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
export default PluginPresetSelector;
|
||||
@@ -0,0 +1,450 @@
|
||||
import React, { SyntheticEvent,Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
|
||||
import Button from "@material-ui/core/Button";
|
||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import UploadDialog from './UploadDialog';
|
||||
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import RenameDialog from './RenameDialog';
|
||||
|
||||
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;
|
||||
openUploadDialog: boolean;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
},
|
||||
dialogActionBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0,
|
||||
background: "black"
|
||||
},
|
||||
dialogTitle: {
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
},
|
||||
itemFrame: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
width: "100%",
|
||||
height: "56px",
|
||||
alignItems: "center",
|
||||
textAlign: "left",
|
||||
justifyContent: "center",
|
||||
paddingLeft: 8
|
||||
},
|
||||
iconFrame: {
|
||||
flex: "0 0 auto",
|
||||
|
||||
},
|
||||
itemIcon: {
|
||||
width: 24, height: 24, margin: 12, opacity: 0.6
|
||||
},
|
||||
itemLabel: {
|
||||
flex: "1 1 1px",
|
||||
marginLeft: 8
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ActionBarTransition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="right" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
const PresetDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
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,
|
||||
openUploadDialog: 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.model.presets.get().selectedInstanceId);
|
||||
}
|
||||
handleUploadPreset() {
|
||||
this.handleMoreClose();
|
||||
this.setState({ openUploadDialog: 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;
|
||||
let classes = this.props.classes;
|
||||
let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId;
|
||||
return (
|
||||
<div key={presetEntry.instanceId} style={{ background: "white" }} >
|
||||
|
||||
<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="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() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
|
||||
let defaultSelectedIndex = this.getSelectedIndex();
|
||||
|
||||
return (
|
||||
<Dialog fullScreen open={this.props.show}
|
||||
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}>
|
||||
<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>
|
||||
<img src="img/file_download_black_24dp.svg" style={{ width: 24, height: 24, opacity: 0.6 }} alt="" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Download preset
|
||||
</ListItemText>
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { this.handleUploadPreset() }}>
|
||||
<ListItemIcon>
|
||||
<img src="img/file_upload_black_24dp.svg" style={{ width: 24, height: 24, opacity: 0.6 }} alt="" />
|
||||
</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>
|
||||
<UploadDialog
|
||||
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) }
|
||||
uploadAfter={this.state.selectedItem}
|
||||
open={this.state.openUploadDialog}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
|
||||
</Dialog >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
export default PresetDialog;
|
||||
@@ -0,0 +1,355 @@
|
||||
import { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
|
||||
import SaveIconOutline from '@material-ui/icons/Save';
|
||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import PresetDialog from './PresetDialog';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import RenameDialog from './RenameDialog'
|
||||
import Select from '@material-ui/core/Select';
|
||||
import UploadDialog from './UploadDialog';
|
||||
|
||||
|
||||
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;
|
||||
openUploadDialog: boolean;
|
||||
|
||||
};
|
||||
|
||||
|
||||
const selectColor = "white";
|
||||
|
||||
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(styles, { withTheme: true })(
|
||||
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,
|
||||
openUploadDialog: 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({ openUploadDialog: 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) => {
|
||||
// s'fine. dealt with by updates, but we do need error handling.
|
||||
})
|
||||
.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);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
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() {
|
||||
//let classes = this.props.classes;
|
||||
let presets = this.state.presets;
|
||||
let classes = this.props.classes;
|
||||
return (
|
||||
<div style={{ marginLeft: 12, display: "flex", flexDirection: "row", justifyContent: "left", flexWrap: "nowrap", alignItems: "center", height: "100%", position: "relative" }}>
|
||||
<div style={{ flex: "1 1 auto", minWidth: 60, maxWidth: 300, position: "relative", paddingRight: 12 }} >
|
||||
{true ? (
|
||||
<Select
|
||||
className={classes.select}
|
||||
style={{ width: "100%", position: "relative", top: 0, color: "white" }} disabled={!this.state.enabled}
|
||||
onChange={(e, extra) => this.handleChange(e, extra)}
|
||||
onClose={(e) => this.handleSelectClose(e)}
|
||||
displayEmpty
|
||||
value={presets.selectedInstanceId}
|
||||
inputProps={{
|
||||
classes: { icon: classes.icon },
|
||||
'aria-label': "Select preset"
|
||||
}
|
||||
}
|
||||
>
|
||||
{
|
||||
presets.presets.map((preset) => {
|
||||
return (
|
||||
<MenuItem value={preset.instanceId} >
|
||||
{(presets.presetChanged && preset.instanceId === presets.selectedInstanceId)
|
||||
? (preset.name + "*")
|
||||
: (preset.name)
|
||||
}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
) : (
|
||||
<ButtonBase style={{ width: "100%", textAlign: "left", borderBottom: "1px white solid", position: "relative", top: 2 }} disabled={!this.state.enabled}
|
||||
onClick={(e) => { this.showPresetDialog(true); }}
|
||||
>
|
||||
<div style={{ width: "100%", textAlign: "left", display: "flex", flexWrap: "nowrap", flexDirection: "row", overflow: "hidden" }}>
|
||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, overflow: "hidden" }}>
|
||||
<Typography noWrap >
|
||||
{this.state.presets.getSelectedText()}
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 1px" }} />
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<ArrowDropDownIcon style={{ opacity: 0.75 }} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton color="inherit" style={{ flex: "0 0 auto", opacity: this.state.presets.presetChanged ? 1.0 : 0.0 }}
|
||||
onClick={(e) => { this.handleSave(); }} >
|
||||
<SaveIconOutline style={{ opacity: 0.75 }} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div style={{ flex: "0 0 auto" }}>
|
||||
<IconButton color="inherit" style={{ flex: "0 0 auto" }} onClick={(e) => this.handlePresetMenuClick(e)}>
|
||||
<MoreVertIcon style={{ opacity: 0.75 }} />
|
||||
</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>
|
||||
<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)} />
|
||||
<UploadDialog onUploaded={(instanceId) => { this.model.loadPreset(instanceId); }}
|
||||
open={this.state.openUploadDialog}
|
||||
uploadAfter={-1}
|
||||
onClose={() => { this.setState({ openUploadDialog: false }) }} />
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
export default PresetSelector;
|
||||
@@ -0,0 +1,70 @@
|
||||
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,156 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import { nullCast } from './Utility';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
|
||||
|
||||
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>;
|
||||
|
||||
constructor(props: RenameDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
};
|
||||
this.refText = React.createRef<HTMLInputElement>();
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
stateWasPopped: boolean = false;
|
||||
handlePopState(e: any): any {
|
||||
this.stateWasPopped = true;
|
||||
let shouldClose = (!e.state || !e.state.renameDialog);
|
||||
if (shouldClose)
|
||||
{
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
updateHooks() : void {
|
||||
let wantHooks = this.mounted && this.props.open;
|
||||
if (wantHooks !== this.hasHooks)
|
||||
{
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks)
|
||||
{
|
||||
this.stateWasPopped = false;
|
||||
window.addEventListener("popstate",this.handlePopState);
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
let state = history.state;
|
||||
if (!state)
|
||||
{
|
||||
state = {};
|
||||
}
|
||||
state.renameDialog = true;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.pushState(
|
||||
state,
|
||||
this.props.acceptActionName,
|
||||
"#RenameDialog"
|
||||
);
|
||||
} else {
|
||||
window.removeEventListener("popstate",this.handlePopState);
|
||||
if (!this.stateWasPopped)
|
||||
{
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void
|
||||
{
|
||||
this.setState({fullScreen: height < 200})
|
||||
}
|
||||
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.updateHooks();
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
super.componentDidMount();
|
||||
this.mounted = false;
|
||||
this.updateHooks();
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
this.updateHooks();
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let { open, defaultName, acceptActionName, onClose, onOk } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
let text = nullCast(this.refText.current).value;
|
||||
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 (
|
||||
<Dialog open={open} fullWidth onClose={handleClose} aria-labelledby="Rename-dialog-title" style={{}}
|
||||
fullScreen={this.state.fullScreen}
|
||||
>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
defaultValue={defaultName}
|
||||
inputRef={this.refText}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="secondary" >
|
||||
{acceptActionName}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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,154 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
|
||||
|
||||
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 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 => {
|
||||
onClose(currentSelection);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
|
||||
<DialogTitle id="simple-dialog-title">Select Channels</DialogTitle>
|
||||
<List>
|
||||
{availableChannels.map((channel) => (
|
||||
<ListItem button >
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel} />
|
||||
}
|
||||
label={channel}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary" disabled={currentSelection.length === 0 || currentSelection.length > 2} >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
const handleClose = () => {
|
||||
onClose(selectedChannels);
|
||||
};
|
||||
|
||||
const handleListItemClick = (value: string) => {
|
||||
switch (value) {
|
||||
case "Stereo":
|
||||
onClose(availableChannels);
|
||||
break;
|
||||
case "Left":
|
||||
onClose([availableChannels[0]]);
|
||||
break;
|
||||
case "Right":
|
||||
onClose([availableChannels[1]]);
|
||||
break;
|
||||
case "None":
|
||||
default:
|
||||
onClose([availableChannels[1]]);
|
||||
}
|
||||
};
|
||||
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 (
|
||||
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
|
||||
|
||||
<List style={{ marginLeft: 0, marginRight: 0}}>
|
||||
<ListItem button onClick={() => handleListItemClick("Stereo")} key={"Stereo"} selected={selectionKey === "Stereo"} >
|
||||
<ListItemText primary={"Stereo"} />
|
||||
</ListItem>
|
||||
<ListItem button onClick={() => handleListItemClick("Left")} key={"LeftChannel"} selected={selectionKey === "Left"} >
|
||||
<ListItemText primary={"Mono (left channel only)"} />
|
||||
</ListItem>
|
||||
<ListItem button onClick={() => handleListItemClick("Right")} key={"RightChannel"} selected={selectionKey === "Right"}>
|
||||
<ListItemText primary={"Mono (right channel only"} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectChannelsDialog;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Component, SyntheticEvent } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
|
||||
const styles = ({ palette }: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
hover: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "#000000",
|
||||
opacity: 0.1
|
||||
},
|
||||
selected: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: palette.action.active,
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface HoverProps extends WithStyles<typeof styles> {
|
||||
selected: boolean;
|
||||
showHover?: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
interface HoverState {
|
||||
hovering: boolean;
|
||||
};
|
||||
|
||||
|
||||
export const SelectHoverBackground =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<HoverProps, HoverState>
|
||||
{
|
||||
constructor(props: HoverProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hovering: false
|
||||
};
|
||||
|
||||
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||
|
||||
}
|
||||
|
||||
handleMouseEnter(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(true);
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseLeave(e: SyntheticEvent): void {
|
||||
if (this.props.showHover??true)
|
||||
{
|
||||
this.setHovering(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
setHovering(value: boolean): void {
|
||||
if (this.state.hovering !== value) {
|
||||
this.setState({ hovering: value });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let isSelected = this.props.selected;
|
||||
let hovering = this.state.hovering && (this.props.showHover??true);
|
||||
return (<div className={classes.frame}
|
||||
onMouseEnter={(e) => { this.handleMouseEnter(e); }} onMouseLeave={(e) => { this.handleMouseLeave(e) }}
|
||||
>
|
||||
<div className={classes.selected} style={{ display: (isSelected ? "block" : "none") }} />
|
||||
<div className={classes.hover} style={{ display: (hovering ? "block" : "none") }} />
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default SelectHoverBackground;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
|
||||
|
||||
|
||||
export interface SelectMidiChannelsDialogProps {
|
||||
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 SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
||||
//const classes = useStyles();
|
||||
const { onClose, selectedChannels, availableChannels, open } = props;
|
||||
const [currentSelection, setCurrentSelection] = useState(selectedChannels);
|
||||
|
||||
|
||||
|
||||
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 => {
|
||||
onClose(currentSelection);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
|
||||
<DialogTitle id="simple-dialog-title">Select Channels</DialogTitle>
|
||||
<List>
|
||||
{availableChannels.map((channel) => (
|
||||
<ListItem button >
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel} />
|
||||
}
|
||||
label={channel}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
|
||||
)}
|
||||
</List>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary" >
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMidiChannelsDialog;
|
||||
@@ -0,0 +1,497 @@
|
||||
import React, { SyntheticEvent, Component } from 'react';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import { TransitionProps } from '@material-ui/core/transitions/transition';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import JackConfiguration, { JackChannelSelection } from './Jack';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import SelectChannelsDialog from './SelectChannelsDialog';
|
||||
import SelectMidiChannelsDialog from './SelectMidiChannelsDialog';
|
||||
import SelectHoverBackground from './SelectHoverBackground';
|
||||
import JackServerSettings from './JackServerSettings';
|
||||
import JackServerSettingsDialog from './JackServerSettingsDialog';
|
||||
import JackHostStatus from './JackHostStatus';
|
||||
|
||||
|
||||
|
||||
interface SettingsDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
|
||||
};
|
||||
|
||||
interface SettingsDialogState {
|
||||
jackConfiguration: JackConfiguration;
|
||||
jackSettings: JackChannelSelection;
|
||||
jackServerSettings: JackServerSettings;
|
||||
jackStatus?: JackHostStatus;
|
||||
|
||||
showInputSelectDialog: boolean;
|
||||
showOutputSelectDialog: boolean;
|
||||
showMidiSelectDialog: boolean;
|
||||
showJackServerSettingsDialog: boolean;
|
||||
shuttingDown: boolean;
|
||||
restarting: boolean;
|
||||
};
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
dialogAppBar: {
|
||||
position: 'relative',
|
||||
top: 0, left: 0
|
||||
},
|
||||
dialogTitle: {
|
||||
marginLeft: theme.spacing(2),
|
||||
flex: 1,
|
||||
},
|
||||
sectionHead: {
|
||||
marginLeft: 24,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 12
|
||||
|
||||
},
|
||||
textBlock: {
|
||||
marginLeft: 24,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 0,
|
||||
opacity: 0.95
|
||||
|
||||
},
|
||||
textBlockIndented: {
|
||||
marginLeft: 40,
|
||||
marginRight: 24,
|
||||
marginTop: 16,
|
||||
paddingBottom: 0,
|
||||
opacity: 0.95
|
||||
|
||||
},
|
||||
|
||||
setting: {
|
||||
minHeight: 64,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
paddingLeft: 22,
|
||||
paddingRight: 22
|
||||
},
|
||||
primaryItem: {
|
||||
|
||||
},
|
||||
secondaryItem: {
|
||||
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
|
||||
const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
|
||||
class extends Component<SettingsDialogProps, SettingsDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
|
||||
|
||||
constructor(props: SettingsDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.state = {
|
||||
jackServerSettings: this.model.jackServerSettings.get(),
|
||||
jackConfiguration: this.model.jackConfiguration.get(),
|
||||
jackStatus: undefined,
|
||||
jackSettings: this.model.jackSettings.get(),
|
||||
showInputSelectDialog: false,
|
||||
showOutputSelectDialog: false,
|
||||
showMidiSelectDialog: false,
|
||||
showJackServerSettingsDialog: false,
|
||||
shuttingDown: false,
|
||||
restarting: false
|
||||
|
||||
|
||||
};
|
||||
this.handleJackConfigurationChanged = this.handleJackConfigurationChanged.bind(this);
|
||||
this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this);
|
||||
this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this);
|
||||
|
||||
}
|
||||
|
||||
|
||||
handleJackSettingsChanged(): void {
|
||||
this.setState({
|
||||
jackSettings: this.model.jackSettings.get()
|
||||
});
|
||||
}
|
||||
handleJackServerSettingsChanged(): void {
|
||||
this.setState({
|
||||
jackServerSettings: this.model.jackServerSettings.get()
|
||||
});
|
||||
}
|
||||
|
||||
handleJackConfigurationChanged(): void {
|
||||
this.setState({
|
||||
jackConfiguration: this.model.jackConfiguration.get()
|
||||
});
|
||||
}
|
||||
|
||||
mounted: boolean = false;
|
||||
active: boolean = false;
|
||||
|
||||
timerHandle?: NodeJS.Timeout;
|
||||
|
||||
tick() {
|
||||
this.model.getJackStatus()
|
||||
.then((jackStatus) =>
|
||||
this.setState(
|
||||
{
|
||||
jackStatus: jackStatus
|
||||
})
|
||||
)
|
||||
.catch((error) => { });
|
||||
}
|
||||
|
||||
updateActive() {
|
||||
let active = this.mounted && this.props.open;
|
||||
if (active !== this.active) {
|
||||
this.active = active;
|
||||
if (active) {
|
||||
this.model.jackSettings.addOnChangedHandler(this.handleJackSettingsChanged);
|
||||
this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged);
|
||||
this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged);
|
||||
this.model.getJackStatus()
|
||||
.then((jackStatus) =>
|
||||
this.setState(
|
||||
{
|
||||
jackStatus: jackStatus
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
this.setState(
|
||||
{
|
||||
jackStatus: undefined
|
||||
})
|
||||
});
|
||||
this.handleJackConfigurationChanged();
|
||||
this.handleJackSettingsChanged();
|
||||
this.handleJackServerSettingsChanged();
|
||||
this.timerHandle = setInterval(() => this.tick(), 1000);
|
||||
} else {
|
||||
if (this.timerHandle) {
|
||||
clearInterval(this.timerHandle);
|
||||
}
|
||||
this.model.jackConfiguration.removeOnChangedHandler(this.handleJackConfigurationChanged);
|
||||
this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged);
|
||||
this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.updateActive();
|
||||
// scroll selected item into view.
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.updateActive();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
this.updateActive();
|
||||
}
|
||||
|
||||
handleDialogClose(e: SyntheticEvent) {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
handleMidiSelection() {
|
||||
this.setState({
|
||||
showMidiSelectDialog: true
|
||||
})
|
||||
}
|
||||
handleJackServerSettings() {
|
||||
this.setState({
|
||||
showJackServerSettingsDialog: true,
|
||||
});
|
||||
}
|
||||
|
||||
handleInputSelection() {
|
||||
this.setState({
|
||||
showInputSelectDialog: true,
|
||||
showOutputSelectDialog: false
|
||||
});
|
||||
}
|
||||
|
||||
handleOutputSelection() {
|
||||
this.setState({
|
||||
showInputSelectDialog: false,
|
||||
showOutputSelectDialog: true
|
||||
});
|
||||
|
||||
}
|
||||
handleSelectMidiDialogResult(channels: string[] | null): void {
|
||||
if (channels) {
|
||||
let newSelection = this.state.jackSettings.clone();
|
||||
newSelection.inputMidiPorts = channels;
|
||||
this.model.setJackSettings(newSelection);
|
||||
}
|
||||
this.setState({
|
||||
showMidiSelectDialog: false,
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleSelectChannelsDialogResult(channels: string[] | null): void {
|
||||
if (channels) {
|
||||
let newSelection = this.state.jackSettings.clone();
|
||||
if (this.state.showInputSelectDialog) {
|
||||
newSelection.inputAudioPorts = channels;
|
||||
} else {
|
||||
newSelection.outputAudioPorts = channels;
|
||||
}
|
||||
this.setState({
|
||||
jackSettings: newSelection,
|
||||
showInputSelectDialog: false,
|
||||
showOutputSelectDialog: false
|
||||
|
||||
});
|
||||
this.model.setJackSettings(newSelection);
|
||||
} else {
|
||||
this.setState({
|
||||
showInputSelectDialog: false,
|
||||
showOutputSelectDialog: false
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
midiSummary(): string {
|
||||
let ports = this.state.jackSettings.inputMidiPorts;
|
||||
if (ports.length === 0) return "Disabled";
|
||||
if (ports.length === 1) return ports[0];
|
||||
return ports.length + " channels";
|
||||
}
|
||||
|
||||
handleRestart() {
|
||||
this.model.restart()
|
||||
.then(() => {
|
||||
this.setState({ restarting: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
this.model.shutdown()
|
||||
.then(() => {
|
||||
this.setState({ shuttingDown: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
let isConfigValid = this.state.jackConfiguration.isValid;
|
||||
let selectedChannels: string[] = this.state.showInputSelectDialog ? this.state.jackSettings.inputAudioPorts : this.state.jackSettings.outputAudioPorts;
|
||||
let disableShutdown = this.state.shuttingDown || this.state.restarting;
|
||||
|
||||
return (
|
||||
<Dialog fullScreen open={this.props.open}
|
||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}>
|
||||
|
||||
<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.handleDialogClose} aria-label="back"
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.dialogTitle}>
|
||||
Settings
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
<div style={{
|
||||
flex: "1 1 auto", position: "relative", overflow: "hidden",
|
||||
overflowX: "hidden", overflowY: "auto", marginTop: 22
|
||||
}}
|
||||
>
|
||||
{!isConfigValid && (
|
||||
<div>
|
||||
<Typography className={classes.sectionHead} display="block" variant="caption" color="error">
|
||||
Error
|
||||
</Typography>
|
||||
<Typography className={classes.textBlock} display="block" variant="body2" >
|
||||
The server encounter the following problem:
|
||||
</Typography>
|
||||
<Typography className={classes.textBlockIndented} display="block" variant="body2" >
|
||||
{this.state.jackConfiguration.errorState}
|
||||
</Typography>
|
||||
<Typography className={classes.textBlock} display="block" variant="body2">
|
||||
Please get the Jack Audio Server running properly on the server before attempting to configure audio.
|
||||
</Typography>
|
||||
<Divider style={{ marginTop: 16, marginBottom: 16 }} />
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
|
||||
<div style={{ opacity: isConfigValid ? 1.0 : 0.6 }}>
|
||||
<Typography className={classes.sectionHead} display="block" variant="caption" color="secondary">
|
||||
AUDIO
|
||||
</Typography>
|
||||
<div style={{ paddingLeft: 48, position: "relative", top: -12 }}>
|
||||
{JackHostStatus.getDisplayView("Status:\u00A0", this.state.jackStatus)}
|
||||
</div>
|
||||
<ButtonBase className={classes.setting} onClick={() => this.handleJackServerSettings()}
|
||||
disabled={!(isConfigValid && this.state.jackServerSettings.valid)}
|
||||
style={{ opacity: !(isConfigValid && this.state.jackServerSettings.valid) ? 0.6 : 1.0 }}
|
||||
>
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography display="block" variant="body2" noWrap>Jack Server Settings</Typography>
|
||||
<Typography display="block" variant="caption" noWrap color="textSecondary">{this.state.jackServerSettings.getSummaryText()}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
<JackServerSettingsDialog
|
||||
open={this.state.showJackServerSettingsDialog}
|
||||
jackServerSettings={this.state.jackServerSettings}
|
||||
onClose={() => this.setState({ showJackServerSettingsDialog: false })}
|
||||
onApply={(sampleRate, bufferSize, numberOfBuffers) => {
|
||||
this.setState({ showJackServerSettingsDialog: false });
|
||||
this.model.setJackServerSettings(new JackServerSettings(sampleRate, bufferSize, numberOfBuffers));
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<ButtonBase className={classes.setting} onClick={() => this.handleInputSelection()} disabled={!isConfigValid}
|
||||
style={{ opacity: !isConfigValid ? 0.6 : 1.0 }}
|
||||
|
||||
>
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography display="block" variant="body2" noWrap>Input Channels</Typography>
|
||||
<Typography display="block" variant="caption" color="textSecondary" noWrap>{this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
<ButtonBase className={classes.setting} onClick={() => this.handleOutputSelection()} disabled={!isConfigValid}
|
||||
style={{ opacity: !isConfigValid ? 0.6 : 1.0 }}
|
||||
>
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography display="block" variant="body2" noWrap>Output channels</Typography>
|
||||
<Typography display="block" variant="caption" color="textSecondary" noWrap>{this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
<Divider />
|
||||
<div >
|
||||
<Typography className={classes.sectionHead} display="block" variant="caption" color="secondary">MIDI</Typography>
|
||||
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiSelection()} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Select MIDI Input Channels</Typography>
|
||||
|
||||
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>{this.midiSummary()}</Typography>
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div >
|
||||
<Typography className={classes.sectionHead} display="block" variant="caption" color="secondary">SYSTEM</Typography>
|
||||
<ButtonBase className={classes.setting} disabled={!isConfigValid || disableShutdown}
|
||||
onClick={() => this.handleRestart()} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
{
|
||||
this.state.restarting ? (
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" color="textSecondary" noWrap>Restarting...</Typography>
|
||||
) : (
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Restart</Typography>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase className={classes.setting} disabled={!isConfigValid || disableShutdown}
|
||||
onClick={() => this.handleShutdown()} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<div style={{ width: "100%" }}>
|
||||
{
|
||||
this.state.shuttingDown ? (
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" color="textSecondary" noWrap>Shutting down...</Typography>
|
||||
) : (
|
||||
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Shut down</Typography>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
</ButtonBase>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(this.state.showInputSelectDialog || this.state.showOutputSelectDialog) &&
|
||||
(
|
||||
<SelectChannelsDialog open={this.state.showInputSelectDialog || this.state.showOutputSelectDialog}
|
||||
onClose={(selectedChannels: string[] | null) => this.handleSelectChannelsDialogResult(selectedChannels)}
|
||||
selectedChannels={selectedChannels}
|
||||
availableChannels={this.state.showInputSelectDialog ? this.state.jackConfiguration.inputAudioPorts : this.state.jackConfiguration.outputAudioPorts}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
{
|
||||
(this.state.showMidiSelectDialog) &&
|
||||
(
|
||||
<SelectMidiChannelsDialog open={this.state.showMidiSelectDialog}
|
||||
onClose={(selectedChannels: string[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
|
||||
selectedChannels={this.state.jackSettings.inputMidiPorts}
|
||||
availableChannels={this.state.jackConfiguration.inputMidiPorts}
|
||||
/>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
</Dialog >
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
export default SettingsDialog;
|
||||
@@ -0,0 +1,262 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
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';
|
||||
|
||||
|
||||
const LANDSCAPE_HEIGHT_BREAK = 500;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
display: "block",
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
flexWrap: "nowrap",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "0px",
|
||||
height: "100%",
|
||||
overflowX: "auto",
|
||||
overflowY: "auto"
|
||||
},
|
||||
vuMeterL: {
|
||||
position: "fixed",
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
left: 0,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
vuMeterR: {
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
marginRight: 22,
|
||||
paddingLeft: 12,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
vuMeterRLandscape: {
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
paddingRight: 22,
|
||||
paddingLeft: 12,
|
||||
background: "white",
|
||||
zIndex: 3
|
||||
|
||||
},
|
||||
|
||||
normalGrid: {
|
||||
position: "relative",
|
||||
paddingLeft: 40,
|
||||
paddingRight: 40,
|
||||
flex: "1 1 auto",
|
||||
display: "flex", flexDirection: "row", flexWrap: "wrap",
|
||||
justifyContent: "flex-start", alignItems: "flex_start"
|
||||
|
||||
},
|
||||
landscapeGrid: {
|
||||
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> {
|
||||
theme: Theme;
|
||||
instanceId: number;
|
||||
item: PedalBoardSplitItem;
|
||||
};
|
||||
type SplitControlViewState = {
|
||||
landscapeGrid: boolean;
|
||||
};
|
||||
|
||||
const SplitControlView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
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.setPedalBoardControlValue(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 {
|
||||
let classes = this.props.classes;
|
||||
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 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 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 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 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 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 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 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 >
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
export default SplitControlView;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,21 @@
|
||||
|
||||
|
||||
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,32 @@
|
||||
import StringBuilder from './StringBuilder';
|
||||
|
||||
class SvgPathBuilder {
|
||||
_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(' ');
|
||||
|
||||
}
|
||||
closePath(): SvgPathBuilder {
|
||||
this._sb.append("Z ");
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
toString(): string { return this._sb.toString(); }
|
||||
}
|
||||
|
||||
export default SvgPathBuilder;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withStyles, createStyles, WithStyles } from '@material-ui/core';
|
||||
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import IconButton from '@material-ui/core/Toolbar';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
|
||||
import { Theme } from '@material-ui/core/styles/createMuiTheme';
|
||||
|
||||
|
||||
const drawerStyles = ({ palette }: Theme) => createStyles({
|
||||
list: {
|
||||
width: 250,
|
||||
},
|
||||
fullList: {
|
||||
width: 'auto',
|
||||
},
|
||||
|
||||
drawer_header: {
|
||||
color: 'white',
|
||||
background: palette.primary.main,
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
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.ReactChild | React.ReactChild[];
|
||||
|
||||
};
|
||||
type DrawerState = {
|
||||
is_open: boolean;
|
||||
}
|
||||
|
||||
export const TemporaryDrawer = withStyles(drawerStyles)(
|
||||
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 } = 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(); }}
|
||||
>
|
||||
<Toolbar className={classes.drawer_header} style={{backgroundImage: 'url("img/ic_drawer.svg")' }} >
|
||||
<IconButton style={{ marginLeft: -24 }}
|
||||
>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
||||
import { PedalBoardItem } from './PedalBoard';
|
||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
||||
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface ToobCabSimProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalBoardItem;
|
||||
|
||||
};
|
||||
interface ToobCabSimState {
|
||||
|
||||
};
|
||||
|
||||
const ToobCabSimView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends React.Component<ToobCabSimProps, ToobCabSimState>
|
||||
implements ControlViewCustomization
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
customizationId: number = 1;
|
||||
|
||||
constructor(props: ToobCabSimProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
controls.splice(0,0,
|
||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||
);
|
||||
return controls;
|
||||
}
|
||||
render() {
|
||||
return (<PluginControlView
|
||||
instanceId={this.props.instanceId}
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
class ToobCabSimViewFactory implements IControlViewFactory {
|
||||
uri: string = "http://two-play.com/plugins/toob-cab-sim";
|
||||
|
||||
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
|
||||
return (<ToobCabSimView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
export default ToobCabSimViewFactory;
|
||||
@@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
import { PiPedalModelFactory, PiPedalModel, State } from "./PiPedalModel";
|
||||
import { StandardItemSize } from './PluginControlView';
|
||||
import Utility from './Utility';
|
||||
import SvgPathBuilder from './SvgPathBuilder';
|
||||
|
||||
|
||||
const FREQUENCY_RESPONSE_VECTOR_URI = "http://two-play.com/plugins/toob#frequencyResponseVector";
|
||||
|
||||
const PLOT_WIDTH = StandardItemSize.width*2-16;
|
||||
const PLOT_HEIGHT = StandardItemSize.height-12;
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
frame: {
|
||||
width: PLOT_WIDTH, height: PLOT_HEIGHT, background: "#444",
|
||||
borderRadius: 6,
|
||||
marginTop: 12, marginLeft: 8, marginRight: 8,
|
||||
boxShadow: "1px 4px 8px #000 inset"
|
||||
},
|
||||
});
|
||||
|
||||
interface ToobFrequencyResponseProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
|
||||
};
|
||||
interface ToobFrequencyResponseState {
|
||||
path: string;
|
||||
};
|
||||
|
||||
const ToobFrequencyResponseView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends React.Component<ToobFrequencyResponseProps, ToobFrequencyResponseState>
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
customizationId: number = 1;
|
||||
pathRef: React.RefObject<SVGPathElement>;
|
||||
|
||||
constructor(props: ToobFrequencyResponseProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
path: ""
|
||||
};
|
||||
|
||||
this.pathRef = React.createRef();
|
||||
this.onPedalBoardChanged = this.onPedalBoardChanged.bind(this);
|
||||
this.onStateChanged = this.onStateChanged.bind(this);
|
||||
}
|
||||
|
||||
onStateChanged()
|
||||
{
|
||||
if (this.model.state.get() === State.Ready)
|
||||
{
|
||||
this.requestDeferred = false;
|
||||
this.requestOutstanding = false;
|
||||
this.updateFrequencyResponse(); // after a reconnect.
|
||||
}
|
||||
}
|
||||
onPedalBoardChanged()
|
||||
{
|
||||
this.updateFrequencyResponse();
|
||||
}
|
||||
componentDidMount()
|
||||
{
|
||||
this.model.state.addOnChangedHandler(this.onStateChanged);
|
||||
this.model.pedalBoard.addOnChangedHandler(this.onPedalBoardChanged);
|
||||
this.updateFrequencyResponse();
|
||||
}
|
||||
componentWillUnmount()
|
||||
{
|
||||
this.model.state.removeOnChangedHandler(this.onStateChanged);
|
||||
this.model.pedalBoard.removeOnChangedHandler(this.onPedalBoardChanged);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
//this.updateFrequencyResponse();
|
||||
}
|
||||
requestOutstanding: boolean = false;
|
||||
requestDeferred: boolean = false;
|
||||
|
||||
dbMinOpt: number = -35;
|
||||
dbMaxOpt: number = 5;
|
||||
dbTickSpacingOpt: number = 10;
|
||||
|
||||
|
||||
MIN_DB_AF: number = Math.pow(10,-192/20);
|
||||
fMin: number = 30;
|
||||
fMax: number = 20000;
|
||||
logMin: number = Math.log(this.fMin);
|
||||
logMax: number = Math.log(this.fMax);
|
||||
|
||||
// Size of the SVG element.
|
||||
xMin: number = 0;
|
||||
xMax: number = PLOT_WIDTH+4;
|
||||
yMin: number = 0;
|
||||
yMax: number = PLOT_HEIGHT;
|
||||
dbMin: number = this.dbMaxOpt; // deliberately reversed to flip up and down.
|
||||
dbMax: number = this.dbMinOpt;
|
||||
dbTickSpacing: number = this.dbTickSpacingOpt;
|
||||
|
||||
toX(frequency: number): number
|
||||
{
|
||||
var logV = Math.log(frequency);
|
||||
return (this.xMax-this.xMin) * (logV-this.logMin)/(this.logMax-this.logMin) + this.xMin;
|
||||
|
||||
}
|
||||
toY(value: number): number {
|
||||
value = Math.abs(value);
|
||||
|
||||
var db;
|
||||
if (value < this.MIN_DB_AF) {
|
||||
db = -192.0;
|
||||
} else {
|
||||
db = 20*Math.log10(value);
|
||||
}
|
||||
var y = (db-this.dbMin)/(this.dbMax-this.dbMin)*(this.yMax-this.yMin) + this.yMin;
|
||||
return y;
|
||||
}
|
||||
|
||||
onFrequencyResponseUpdated(data: number[]) {
|
||||
let pathBuilder = new SvgPathBuilder();
|
||||
if (data.length > 2)
|
||||
{
|
||||
pathBuilder.moveTo(this.toX(data[0]),this.toY(data[1]))
|
||||
for (let i = 2; i < data.length; i += 2)
|
||||
{
|
||||
pathBuilder.lineTo(this.toX(data[i]),this.toY(data[i+1]));
|
||||
}
|
||||
}
|
||||
this.currentPath = pathBuilder.toString();
|
||||
this.setState({path: this.currentPath});
|
||||
|
||||
}
|
||||
|
||||
updateFrequencyResponse() {
|
||||
if (this.requestOutstanding) { // throttling.
|
||||
this.requestDeferred = true;
|
||||
return;
|
||||
}
|
||||
this.requestOutstanding = true;
|
||||
this.model.getLv2Parameter<number[]>(this.props.instanceId, FREQUENCY_RESPONSE_VECTOR_URI)
|
||||
.then((data) => {
|
||||
this.onFrequencyResponseUpdated(data);
|
||||
if (this.requestDeferred) {
|
||||
Utility.delay(10) // take breath
|
||||
.then(
|
||||
() => {
|
||||
this.requestOutstanding = false;
|
||||
this.requestDeferred = false;
|
||||
this.updateFrequencyResponse();
|
||||
}
|
||||
|
||||
);
|
||||
} else {
|
||||
this.requestOutstanding = false;
|
||||
}
|
||||
}).catch(error => {
|
||||
// assume the connection was lost. We'll get saved by a reconnect.
|
||||
this.requestOutstanding = false;
|
||||
this.requestDeferred = false;
|
||||
});
|
||||
}
|
||||
|
||||
currentPath: string = "";
|
||||
|
||||
gridLine(x0: number, y0: number, x1: number, y1: number): React.ReactNode {
|
||||
return (<line x1={x0} y1={y0} x2={x1} y2={y1} fill='none' stroke="#FFF" strokeWidth="0.25" opacity="1" />);
|
||||
}
|
||||
grid(): React.ReactNode[] {
|
||||
let result: React.ReactNode[] = [];
|
||||
|
||||
|
||||
for (var db = Math.ceil(this.dbMax/this.dbTickSpacing)*this.dbTickSpacing; db < this.dbMin; db += this.dbTickSpacing )
|
||||
{
|
||||
var y = (db-this.dbMin)/(this.dbMax-this.dbMin)*(this.yMax-this.yMin);
|
||||
result.push(
|
||||
this.gridLine(this.xMin,y,this.xMax,y)
|
||||
);
|
||||
}
|
||||
let decade0 = Math.pow(10,Math.floor(Math.log10(this.fMin)));
|
||||
for (var decade = decade0; decade < this.fMax; decade *= 10)
|
||||
{
|
||||
for (var i = 0; i < 10; ++i)
|
||||
{
|
||||
var f = decade*i;
|
||||
if (f > this.fMin && f < this.fMax)
|
||||
{
|
||||
var x = this.toX(f);
|
||||
result.push(this.gridLine(x,this.yMin,x,this.yMax));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
return (
|
||||
<div className={classes.frame} >
|
||||
<svg width={PLOT_WIDTH} height={PLOT_HEIGHT} viewBox={"0 0 " + PLOT_WIDTH + " " + PLOT_HEIGHT} stroke="#0F8" fill="none" strokeWidth="2.5" opacity="0.6">
|
||||
{ this.grid() }
|
||||
<path d={this.state.path} ref={this.pathRef} />
|
||||
</svg>
|
||||
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
export default ToobFrequencyResponseView;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
||||
import { PedalBoardItem } from './PedalBoard';
|
||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
||||
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface ToobInputStageProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalBoardItem;
|
||||
|
||||
};
|
||||
interface ToobInputStageState {
|
||||
|
||||
};
|
||||
|
||||
const ToobInputStageView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends React.Component<ToobInputStageProps, ToobInputStageState>
|
||||
implements ControlViewCustomization
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
customizationId: number = 1;
|
||||
|
||||
constructor(props: ToobInputStageProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
let group = controls[1] as ControlGroup;
|
||||
group.controls.splice(0,0,
|
||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||
);
|
||||
return controls;
|
||||
}
|
||||
render() {
|
||||
return (<PluginControlView
|
||||
instanceId={this.props.instanceId}
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
class ToobInputStageViewFactory implements IControlViewFactory {
|
||||
uri: string = "http://two-play.com/plugins/toob-input_stage";
|
||||
|
||||
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
|
||||
return (<ToobInputStageView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
export default ToobInputStageViewFactory;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
|
||||
import IControlViewFactory from './IControlViewFactory';
|
||||
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
||||
import { PedalBoardItem } from './PedalBoard';
|
||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
||||
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||
|
||||
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
});
|
||||
|
||||
interface ToobToneStackProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
item: PedalBoardItem;
|
||||
|
||||
};
|
||||
interface ToobToneStackState {
|
||||
|
||||
};
|
||||
|
||||
const ToobToneStackView =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends React.Component<ToobToneStackProps, ToobToneStackState>
|
||||
implements ControlViewCustomization
|
||||
{
|
||||
model: PiPedalModel;
|
||||
|
||||
customizationId: number = 1;
|
||||
|
||||
constructor(props: ToobToneStackProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
ModifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
||||
{
|
||||
controls.splice(0,0,
|
||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||
);
|
||||
return controls;
|
||||
}
|
||||
render() {
|
||||
return (<PluginControlView
|
||||
instanceId={this.props.instanceId}
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
|
||||
class ToobToneStackViewFactory implements IControlViewFactory {
|
||||
uri: string = "http://two-play.com/plugins/toob-tone-stack";
|
||||
|
||||
Create(model: PiPedalModel, pedalBoardItem: PedalBoardItem): React.ReactNode {
|
||||
return (<ToobToneStackView instanceId={pedalBoardItem.instanceId} item={pedalBoardItem} />);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
export default ToobToneStackViewFactory;
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
|
||||
|
||||
enum Units {
|
||||
none = "none",
|
||||
unknown = "unknown",
|
||||
bar = "bar",
|
||||
beat = "beat",
|
||||
bpm = "bpm",
|
||||
cent = "cent",
|
||||
cm = "cm",
|
||||
db = "db",
|
||||
hz = "hz",
|
||||
khz = "khz",
|
||||
km = "km",
|
||||
m = "m",
|
||||
mhz = "mhz",
|
||||
midiNote = "midiNote",
|
||||
min = "min",
|
||||
ms = "ms",
|
||||
pc = "pc",
|
||||
s = "s",
|
||||
semitone12TET = "semitone12TET"
|
||||
};
|
||||
|
||||
export default Units;
|
||||
@@ -0,0 +1,221 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
|
||||
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
|
||||
const PRESET_EXTENSION = ".piPreset";
|
||||
const BANK_EXTENSION = ".piBank";
|
||||
|
||||
export interface UploadDialogProps {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
uploadAfter: number,
|
||||
onUploaded: (instanceId: number) => void
|
||||
|
||||
};
|
||||
|
||||
export interface UploadDialogState {
|
||||
fullScreen: boolean;
|
||||
};
|
||||
|
||||
export default class UploadDialog extends ResizeResponsiveComponent<UploadDialogProps, UploadDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
|
||||
constructor(props: UploadDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fullScreen: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.handlePopState = this.handlePopState.bind(this);
|
||||
}
|
||||
mounted: boolean = false;
|
||||
|
||||
hasHooks: boolean = false;
|
||||
|
||||
stateWasPopped: boolean = false;
|
||||
handlePopState(e: any): any {
|
||||
this.stateWasPopped = true;
|
||||
let shouldClose = (!e.state || !e.state.UploadDialog);
|
||||
if (shouldClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
updateHooks(): void {
|
||||
let wantHooks = this.mounted && this.props.open;
|
||||
if (wantHooks !== this.hasHooks) {
|
||||
this.hasHooks = wantHooks;
|
||||
|
||||
if (this.hasHooks) {
|
||||
this.stateWasPopped = false;
|
||||
window.addEventListener("popstate", this.handlePopState);
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
let state = history.state;
|
||||
if (!state) {
|
||||
state = {};
|
||||
}
|
||||
state.UploadDialog = true;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.pushState(
|
||||
state,
|
||||
"Upload",
|
||||
"#UploadDialog"
|
||||
);
|
||||
} else {
|
||||
window.removeEventListener("popstate", this.handlePopState);
|
||||
if (!this.stateWasPopped) {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
onWindowSizeChanged(width: number, height: number): void {
|
||||
this.setState({ fullScreen: height < 200 })
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.updateHooks();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = false;
|
||||
this.updateHooks();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateHooks();
|
||||
}
|
||||
|
||||
handleClose(): void {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
handleOk(): void {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
async uploadFiles(fileList: FileList)
|
||||
{
|
||||
try {
|
||||
let uploadAfter = this.props.uploadAfter;
|
||||
for (let i = 0; i < fileList.length; ++i)
|
||||
{
|
||||
uploadAfter = await this.model.uploadPreset(fileList[i],uploadAfter);
|
||||
}
|
||||
this.props.onUploaded(uploadAfter);
|
||||
} catch(error) {
|
||||
this.model.showAlert(error);
|
||||
};
|
||||
this.props.onClose();
|
||||
}
|
||||
handleDrop(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.background = "";
|
||||
|
||||
if (this.wantsTransfer(e.dataTransfer)) {
|
||||
this.uploadFiles(e.dataTransfer.files)
|
||||
.then(() => {
|
||||
this.handleClose();
|
||||
})
|
||||
.catch((error)=> {
|
||||
this.handleClose();
|
||||
this.model.showAlert(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getFileExtension(name: string): string {
|
||||
let pos = name.lastIndexOf('.');
|
||||
if (pos !== -1) {
|
||||
return name.substring(pos);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
wantsTransfer(fileList: DataTransfer): boolean {
|
||||
if (fileList.files.length === 0) return false;
|
||||
|
||||
for (let i = 0; i < fileList.files.length; ++i) {
|
||||
let file = fileList.files[i];
|
||||
let extension = this.getFileExtension(file.name);
|
||||
if (extension !== PRESET_EXTENSION && extension !== BANK_EXTENSION) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
handleDragOver(e: React.DragEvent<HTMLDivElement>) {
|
||||
e.currentTarget.style.background = "#D8D8D8";
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handleButtonSelect(e: React.ChangeEvent<HTMLInputElement>)
|
||||
{
|
||||
if (e.currentTarget.files)
|
||||
{
|
||||
this.uploadFiles(e.currentTarget.files);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={this.props.open} fullWidth onClose={() => this.handleClose()} style={{}}
|
||||
fullScreen={this.state.fullScreen}
|
||||
>
|
||||
<DialogTitle>Upload preset</DialogTitle>
|
||||
<DialogContent>
|
||||
<div style={{
|
||||
width: "100%", height: 100, marginBottom: 0,
|
||||
border: "2px dashed #CCC",
|
||||
borderRadius: "10px",
|
||||
fontFamily: "Roboto",
|
||||
padding: 20
|
||||
}}
|
||||
onDragEnter={(e) => { this.handleDragOver(e); }}
|
||||
onDragLeave={(e) => { e.currentTarget.style.background = ""; e.preventDefault(); }}
|
||||
onDragOver={(e) => { this.handleDragOver(e); }}
|
||||
onDrop={(e) => {
|
||||
this.handleDrop(e);
|
||||
}}
|
||||
>
|
||||
<Typography noWrap color="textSecondary" align="center" variant="caption" style={{verticalAlign: "middle"}} >Drop files here</Typography>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.handleClose()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
component="label" color="secondary" style={{width: 120}}
|
||||
>
|
||||
Select File
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept=".piPreset"
|
||||
multiple
|
||||
onChange={(e)=> this.handleButtonSelect(e)}
|
||||
/>
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { PiPedalArgumentError } from './PiPedalError';
|
||||
|
||||
export function nullCast<T>(value: T | null | undefined): T {
|
||||
if (!value) throw new PiPedalArgumentError("Unexpected null value.");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
const FLAT = "\u266D";
|
||||
|
||||
|
||||
const LONG_PRESS_TIME_MS = 500; // current android long-press.
|
||||
|
||||
export function getLongPressTimeMs(): number { return LONG_PRESS_TIME_MS; }
|
||||
|
||||
export interface ControlEntry {
|
||||
value: number;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
const Utility = class {
|
||||
static isTouchDevice(): boolean {
|
||||
return navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
static hasIMEKeyboard(): boolean {
|
||||
if (/Mobi|Android/i.test(navigator.userAgent)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static delay(ms: number): Promise<void> {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
static NOTE_DEGREES: string[] =
|
||||
[
|
||||
"C", "C#", "D", "E" + FLAT, "E", "F", "F#", "G", "A" + FLAT, "A", "B" + FLAT, "B"
|
||||
];
|
||||
static midiNoteName(noteNumber: number): string {
|
||||
let n = noteNumber - 24;
|
||||
let octave = Math.floor(n / 12);
|
||||
let iNote = n % 12;
|
||||
if (iNote < 0) iNote += 12;
|
||||
|
||||
return Utility.NOTE_DEGREES[iNote] + octave;
|
||||
|
||||
|
||||
}
|
||||
|
||||
static validMidiControllers: ControlEntry[] = [
|
||||
{ value: 1, displayName: "CC-1 Mod Wheel" },
|
||||
{ value: 2, displayName: "CC-2 Breath controller" },
|
||||
{ value: 3, displayName: "CC-3" },
|
||||
{ value: 4, displayName: "CC-4 Foot Pedal" },
|
||||
{ value: 5, displayName: "CC-5 Portamento Time" },
|
||||
{ value: 7, displayName: "CC-7 Volume" },
|
||||
{ value: 8, displayName: "CC-8 Balance" },
|
||||
{ value: 10, displayName: "CC-10 Pan position" },
|
||||
{ value: 11, displayName: "CC-11 Expression" },
|
||||
{ value: 12, displayName: "CC-12 Effect Control 1" },
|
||||
{ value: 13, displayName: "CC-13 Effect Control 2" },
|
||||
{ value: 14, displayName: "CC-14" },
|
||||
{ value: 15, displayName: "CC-15" },
|
||||
{ value: 16, displayName: "CC-16" },
|
||||
{ value: 17, displayName: "CC-17" },
|
||||
{ value: 18, displayName: "CC-18" },
|
||||
{ value: 19, displayName: "CC-19" },
|
||||
{ value: 20, displayName: "CC-20" },
|
||||
{ value: 21, displayName: "CC-21" },
|
||||
{ value: 22, displayName: "CC-22" },
|
||||
{ value: 23, displayName: "CC-23" },
|
||||
{ value: 24, displayName: "CC-24" },
|
||||
{ value: 25, displayName: "CC-25" },
|
||||
{ value: 26, displayName: "CC-26" },
|
||||
{ value: 27, displayName: "CC-27" },
|
||||
{ value: 28, displayName: "CC-28" },
|
||||
{ value: 29, displayName: "CC-29" },
|
||||
{ value: 30, displayName: "CC-30" },
|
||||
{ value: 31, displayName: "CC-31" },
|
||||
// Control LSB.
|
||||
{ value: 64, displayName: "CC-64 Hold Pedal" },
|
||||
{ value: 65, displayName: "CC-65 Portamento" },
|
||||
{ value: 66, displayName: "CC-66 Sustenuto Pedal" },
|
||||
{ value: 67, displayName: "CC-67 Soft Pedal" },
|
||||
{ value: 68, displayName: "CC-68 Legato Pedal" },
|
||||
{ value: 69, displayName: "CC-69 Hold 2 Pedal" },
|
||||
{ value: 70, displayName: "CC-70 Sound Variation" },
|
||||
{ value: 71, displayName: "CC-71 Resonance" },
|
||||
{ value: 72, displayName: "CC-72 Sound Release Time" },
|
||||
{ value: 73, displayName: "CC-73 Sound Attack Time" },
|
||||
{ value: 74, displayName: "CC-74 Frequency Cutoff" },
|
||||
{ value: 75, displayName: "CC-75" },
|
||||
{ value: 76, displayName: "CC-76" },
|
||||
{ value: 77, displayName: "CC-77" },
|
||||
{ value: 78, displayName: "CC-78" },
|
||||
{ value: 79, displayName: "CC-79" },
|
||||
{ value: 80, displayName: "CC-80 Decay" },
|
||||
{ value: 81, displayName: "CC-81" },
|
||||
{ value: 82, displayName: "CC-82" },
|
||||
{ value: 83, displayName: "CC-83" },
|
||||
{ value: 91, displayName: "CC-91 Reverb Level" },
|
||||
{ value: 92, displayName: "CC-92 Tremolo Level" },
|
||||
{ value: 93, displayName: "CC-93 Chorus Level" },
|
||||
{ value: 94, displayName: "CC-94 Detune" },
|
||||
{ value: 95, displayName: "CC-95 Phaser Level" },
|
||||
// RPN/DATA controls
|
||||
{ value: 102, displayName: "CC-102" },
|
||||
{ value: 103, displayName: "CC-103" },
|
||||
{ value: 104, displayName: "CC-104" },
|
||||
{ value: 105, displayName: "CC-105" },
|
||||
{ value: 106, displayName: "CC-106" },
|
||||
{ value: 107, displayName: "CC-107" },
|
||||
{ value: 108, displayName: "CC-108" },
|
||||
{ value: 109, displayName: "CC-109" },
|
||||
{ value: 110, displayName: "CC-110" },
|
||||
{ value: 111, displayName: "CC-111" },
|
||||
{ value: 112, displayName: "CC-112" },
|
||||
{ value: 113, displayName: "CC-113" },
|
||||
{ value: 114, displayName: "CC-114" },
|
||||
{ value: 115, displayName: "CC-115" },
|
||||
{ value: 116, displayName: "CC-116" },
|
||||
{ value: 117, displayName: "CC-117" },
|
||||
{ value: 118, displayName: "CC-118" },
|
||||
{ value: 119, displayName: "CC-119" },
|
||||
{ value: 120, displayName: "CC-120 All Sound Off" },
|
||||
{ value: 121, displayName: "CC-121 All Controllers Off" },
|
||||
{ value: 122, displayName: "CC-122 Local Keyboard" },
|
||||
{ value: 123, displayName: "CC-123 All Notes Off" },
|
||||
{ value: 124, displayName: "CC-124 Omni Mode Off" },
|
||||
{ value: 125, displayName: "CC-125 Omni Mode On" },
|
||||
{ value: 126, displayName: "CC-126 Mono " },
|
||||
{ value: 127, displayName: "CC-127 Poly" },
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
export default Utility;
|
||||
@@ -0,0 +1,399 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiPedalModel, PiPedalModelFactory, VuUpdateInfo, VuSubscriptionHandle } from './PiPedalModel';
|
||||
|
||||
|
||||
const DISPLAY_HEIGHT = 110;
|
||||
const BORDER_THICKNESS = 2;
|
||||
const TELLTALE_HEIGHT = 4;
|
||||
const CHANNEL_WIDTH = 4;
|
||||
const TELLTALE_HOLD_MS = 2000;
|
||||
const TELLTALE_DECAY_RATE = -20 / 1000.0; // -20db in one second.
|
||||
|
||||
|
||||
const INTERIOR_DISPLAY_HEIGHT = DISPLAY_HEIGHT - BORDER_THICKNESS * 2;
|
||||
|
||||
|
||||
const MIN_DB = -35;
|
||||
const MAX_DB = 6;
|
||||
const DB_YELLOW = -12;
|
||||
|
||||
|
||||
|
||||
interface VuChannelData {
|
||||
value: number;
|
||||
redDiv: HTMLDivElement;
|
||||
yellowDiv: HTMLDivElement;
|
||||
greenDiv: HTMLDivElement;
|
||||
telltaleDiv: HTMLDivElement;
|
||||
|
||||
};
|
||||
|
||||
class TelltaleState {
|
||||
telltaleDb: number = MIN_DB;
|
||||
lastTelltaleTime: number = 0;
|
||||
telltaleHoldTime: number = 0;
|
||||
|
||||
reset() : void {
|
||||
this.telltaleHoldTime = 0;
|
||||
this.lastTelltaleTime = 0;
|
||||
this.telltaleDb = MIN_DB;
|
||||
}
|
||||
getValue(currentDb: number): number {
|
||||
let t = new Date().getTime();
|
||||
|
||||
let holdValue: number;
|
||||
if (t < this.telltaleHoldTime) {
|
||||
holdValue = this.telltaleDb;
|
||||
} else {
|
||||
if (this.telltaleHoldTime === 0) {
|
||||
holdValue = MIN_DB;
|
||||
} else {
|
||||
holdValue = this.telltaleDb + (t - this.telltaleHoldTime) * TELLTALE_DECAY_RATE;
|
||||
if (holdValue < MIN_DB) {
|
||||
this.telltaleDb = holdValue = MIN_DB;
|
||||
this.telltaleHoldTime = t + 100 * 1000; // wait for a long time.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDb > holdValue) {
|
||||
this.telltaleDb = currentDb;
|
||||
this.telltaleHoldTime = t + TELLTALE_HOLD_MS;
|
||||
return currentDb;
|
||||
} else {
|
||||
return holdValue;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const LEFT_X = BORDER_THICKNESS;
|
||||
const RIGHT_X = BORDER_THICKNESS + + CHANNEL_WIDTH + BORDER_THICKNESS;
|
||||
|
||||
const styles = ({ palette }: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
width: CHANNEL_WIDTH + 2*BORDER_THICKNESS,
|
||||
borderTop: BORDER_THICKNESS + "px black solid",
|
||||
borderBottom: BORDER_THICKNESS + "px black solid",
|
||||
height: DISPLAY_HEIGHT,
|
||||
background: "black",
|
||||
overflow: "hidden",
|
||||
marginLeft: (CHANNEL_WIDTH+BORDER_THICKNESS)/2,
|
||||
marginRight: (CHANNEL_WIDTH+BORDER_THICKNESS)/2
|
||||
},
|
||||
frameStereo: {
|
||||
position: "relative",
|
||||
width: 2*CHANNEL_WIDTH + 3*BORDER_THICKNESS,
|
||||
borderTop: BORDER_THICKNESS + "px black solid",
|
||||
borderBottom: BORDER_THICKNESS + "px black solid",
|
||||
height: DISPLAY_HEIGHT,
|
||||
background: "black",
|
||||
overflow: "hidden",
|
||||
},
|
||||
redFrameL: {
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#F00"
|
||||
},
|
||||
yellowFrameL: {
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#CC0"
|
||||
},
|
||||
greenFrameL: {
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#0C2"
|
||||
},
|
||||
indicatorL: {
|
||||
position: "absolute",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: TELLTALE_HEIGHT + "px",
|
||||
top: "100%",
|
||||
background: "#F00"
|
||||
|
||||
},
|
||||
redFrameR: {
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#F00"
|
||||
},
|
||||
yellowFrameR: {
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#CC0"
|
||||
},
|
||||
greenFrameR: {
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#0C2"
|
||||
},
|
||||
indicatorR: {
|
||||
position: "absolute",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: TELLTALE_HEIGHT + "px",
|
||||
top: "100%",
|
||||
background: "#F00"
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface VuMeterProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
display: "input" | "output";
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
interface VuMeterState {
|
||||
isStereo: boolean;
|
||||
};
|
||||
|
||||
function aToDb(value: number): number {
|
||||
if (value === 0) return -96;
|
||||
return Math.log10(Math.abs(value)) * 20;
|
||||
}
|
||||
|
||||
function dbToY(db: number): number {
|
||||
if (db < MIN_DB) db = MIN_DB;
|
||||
if (db > MAX_DB) db = MAX_DB;
|
||||
|
||||
let y = INTERIOR_DISPLAY_HEIGHT - (db - MIN_DB) / (MAX_DB - MIN_DB) * INTERIOR_DISPLAY_HEIGHT;;
|
||||
return y;
|
||||
}
|
||||
|
||||
export const VuMeter =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<VuMeterProps, VuMeterState>
|
||||
{
|
||||
divRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
model: PiPedalModel;
|
||||
|
||||
yZero: number;
|
||||
yYellow: number;
|
||||
|
||||
constructor(props: VuMeterProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isStereo: false
|
||||
};
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
this.yZero = dbToY(0);
|
||||
this.yYellow = dbToY(DB_YELLOW);
|
||||
|
||||
this.onVuChanged = this.onVuChanged.bind(this);
|
||||
|
||||
}
|
||||
|
||||
|
||||
telltaleStateL: TelltaleState = new TelltaleState();
|
||||
telltaleStateR: TelltaleState = new TelltaleState();
|
||||
|
||||
vuInfo?: VuUpdateInfo;
|
||||
requesttedStereoState: boolean = false;
|
||||
|
||||
onVuChanged(vuInfo: VuUpdateInfo): void {
|
||||
let value: number;
|
||||
let valueR: number;
|
||||
|
||||
this.vuInfo = vuInfo;
|
||||
|
||||
|
||||
let isStereo: boolean;
|
||||
|
||||
if (this.props.display === "input") {
|
||||
value = vuInfo.inputMaxValueL;
|
||||
valueR = vuInfo.inputMaxValueR;
|
||||
isStereo = vuInfo.isStereoInput;
|
||||
} else {
|
||||
isStereo = vuInfo.isStereoOutput;
|
||||
value = vuInfo.outputMaxValueL;
|
||||
valueR = vuInfo.outputMaxValueR;
|
||||
}
|
||||
if (this.state.isStereo !== isStereo)
|
||||
{
|
||||
if (isStereo !== this.requesttedStereoState) // guard against overrunning the layout engine.
|
||||
{
|
||||
this.requesttedStereoState = isStereo;
|
||||
|
||||
this.setState({
|
||||
isStereo: isStereo
|
||||
});
|
||||
return; // we can't procede without stereo layout.
|
||||
}
|
||||
}
|
||||
|
||||
let childNodes = this.divRef.current!.childNodes;
|
||||
|
||||
let vuData: VuChannelData = {
|
||||
value: value,
|
||||
redDiv: childNodes[0] as HTMLDivElement,
|
||||
yellowDiv: childNodes[1] as HTMLDivElement,
|
||||
greenDiv: childNodes[2] as HTMLDivElement,
|
||||
telltaleDiv: childNodes[3] as HTMLDivElement
|
||||
};
|
||||
this.updateChannel(vuData,this.telltaleStateL);
|
||||
if (this.state.isStereo) {
|
||||
vuData = {
|
||||
value: valueR,
|
||||
redDiv: childNodes[4] as HTMLDivElement,
|
||||
yellowDiv: childNodes[5] as HTMLDivElement,
|
||||
greenDiv: childNodes[6] as HTMLDivElement,
|
||||
telltaleDiv: childNodes[7] as HTMLDivElement
|
||||
};
|
||||
this.updateChannel(vuData, this.telltaleStateR);
|
||||
}
|
||||
}
|
||||
|
||||
resetTelltales() {
|
||||
this.telltaleStateL.reset();
|
||||
this.telltaleStateR.reset();
|
||||
}
|
||||
updateChannel(vuData: VuChannelData, telltaleState: TelltaleState) {
|
||||
let db = aToDb(vuData.value);
|
||||
if (db > MAX_DB) db = MAX_DB;
|
||||
if (db < MIN_DB) db = MIN_DB;
|
||||
|
||||
let y = dbToY(db);
|
||||
let INVISIBLE_Y = INTERIOR_DISPLAY_HEIGHT + "px";
|
||||
if (y >= this.yYellow) {
|
||||
// green only.
|
||||
vuData.greenDiv.style.top = y + "px";
|
||||
vuData.yellowDiv.style.top = INVISIBLE_Y;
|
||||
vuData.redDiv.style.top = INVISIBLE_Y;
|
||||
} else {
|
||||
vuData.greenDiv.style.top = this.yYellow + "px";
|
||||
if (y >= this.yZero) {
|
||||
vuData.yellowDiv.style.top = y + "px";
|
||||
vuData.redDiv.style.top = INVISIBLE_Y;
|
||||
} else {
|
||||
vuData.yellowDiv.style.top = this.yZero + "px";
|
||||
vuData.redDiv.style.top = y + "px";
|
||||
}
|
||||
}
|
||||
let dbTelltale = telltaleState.getValue(db);
|
||||
let yTelltale = dbToY(dbTelltale);
|
||||
|
||||
if (yTelltale < this.yZero) {
|
||||
vuData.telltaleDiv.style.background = "#F00";
|
||||
} else if (yTelltale < this.yYellow) {
|
||||
vuData.telltaleDiv.style.background = "#CC0";
|
||||
} else {
|
||||
vuData.telltaleDiv.style.background = "#0F2";
|
||||
}
|
||||
if (yTelltale > INTERIOR_DISPLAY_HEIGHT - TELLTALE_HEIGHT) {
|
||||
// non-zero and zero are different displays.
|
||||
// zero: nothing.. tiny: show the telltale.
|
||||
if (vuData.value !== 0) {
|
||||
yTelltale = INTERIOR_DISPLAY_HEIGHT - TELLTALE_HEIGHT;
|
||||
} else {
|
||||
yTelltale = INTERIOR_DISPLAY_HEIGHT;
|
||||
}
|
||||
}
|
||||
vuData.telltaleDiv.style.top = yTelltale + "px";
|
||||
|
||||
|
||||
}
|
||||
subscriptionHandle?: VuSubscriptionHandle;
|
||||
|
||||
subscribedInstanceId?: number;
|
||||
|
||||
addVuSubscription() {
|
||||
this.removeVuSubscription();
|
||||
if (this.props.instanceId !== -1) {
|
||||
this.subscribedInstanceId = this.props.instanceId;
|
||||
this.subscriptionHandle = this.model.addVuSubscription(this.props.instanceId, this.onVuChanged);
|
||||
this.resetTelltales();
|
||||
}
|
||||
}
|
||||
removeVuSubscription() {
|
||||
if (this.subscriptionHandle) {
|
||||
this.model.removeVuSubscription(this.subscriptionHandle);
|
||||
this.subscriptionHandle = undefined;
|
||||
this.subscribedInstanceId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
if (this.subscribedInstanceId)
|
||||
{
|
||||
if (this.props.instanceId !== this.subscribedInstanceId)
|
||||
{
|
||||
this.removeVuSubscription();
|
||||
this.addVuSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
if (!this.state.isStereo) {
|
||||
return (<div className={classes.frame} ref={this.divRef}
|
||||
>
|
||||
<div className={classes.redFrameL} />
|
||||
<div className={classes.yellowFrameL} />
|
||||
<div className={classes.greenFrameL} />
|
||||
<div className={classes.indicatorL} />
|
||||
</div>);
|
||||
} else {
|
||||
return (<div className={classes.frameStereo} ref={this.divRef}
|
||||
>
|
||||
<div className={classes.redFrameL} />
|
||||
<div className={classes.yellowFrameL} />
|
||||
<div className={classes.greenFrameL} />
|
||||
<div className={classes.indicatorL} />
|
||||
|
||||
<div className={classes.redFrameR} />
|
||||
<div className={classes.yellowFrameR} />
|
||||
<div className={classes.greenFrameR} />
|
||||
<div className={classes.indicatorR} />
|
||||
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
this.addVuSubscription();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.removeVuSubscription();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default VuMeter;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
import React, { Component } from 'react';
|
||||
import { createStyles, withStyles, WithStyles, Theme } from '@material-ui/core/styles';
|
||||
import { PiddleModel, PiddleModelFactory, VuUpdateInfo, VuSubscriptionHandle } from './PiddleModel';
|
||||
|
||||
|
||||
const DISPLAY_HEIGHT = 110;
|
||||
const BORDER_THICKNESS = 2;
|
||||
const TELLTALE_HEIGHT = 4;
|
||||
const CHANNEL_WIDTH = 4;
|
||||
const TELLTALE_HOLD_MS = 2000;
|
||||
const TELLTALE_DECAY_RATE = -20 / 1000.0; // -20db in one second.
|
||||
|
||||
|
||||
const INTERIOR_DISPLAY_HEIGHT = DISPLAY_HEIGHT - BORDER_THICKNESS * 2;
|
||||
|
||||
|
||||
const MIN_DB = -35;
|
||||
const MAX_DB = 6;
|
||||
const DB_YELLOW = -12;
|
||||
|
||||
|
||||
|
||||
interface VuChannelData {
|
||||
value: number;
|
||||
redDiv: HTMLDivElement;
|
||||
yellowDiv: HTMLDivElement;
|
||||
greenDiv: HTMLDivElement;
|
||||
telltaleDiv: HTMLDivElement;
|
||||
|
||||
};
|
||||
|
||||
class TelltaleState {
|
||||
telltaleDb: number = MIN_DB;
|
||||
lastTelltaleTime: number = 0;
|
||||
telltaleHoldTime: number = 0;
|
||||
|
||||
reset() : void {
|
||||
this.telltaleHoldTime = 0;
|
||||
this.lastTelltaleTime = 0;
|
||||
this.telltaleDb = MIN_DB;
|
||||
}
|
||||
getValue(currentDb: number): number {
|
||||
let t = new Date().getTime();
|
||||
|
||||
let holdValue: number;
|
||||
if (t < this.telltaleHoldTime) {
|
||||
holdValue = this.telltaleDb;
|
||||
} else {
|
||||
if (this.telltaleHoldTime === 0) {
|
||||
holdValue = MIN_DB;
|
||||
} else {
|
||||
holdValue = this.telltaleDb + (t - this.telltaleHoldTime) * TELLTALE_DECAY_RATE;
|
||||
if (holdValue < MIN_DB) {
|
||||
this.telltaleDb = holdValue = MIN_DB;
|
||||
this.telltaleHoldTime = t + 100 * 1000; // wait for a long time.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDb > holdValue) {
|
||||
this.telltaleDb = currentDb;
|
||||
this.telltaleHoldTime = t + TELLTALE_HOLD_MS;
|
||||
return currentDb;
|
||||
} else {
|
||||
return holdValue;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const LEFT_X = BORDER_THICKNESS;
|
||||
const RIGHT_X = BORDER_THICKNESS + + CHANNEL_WIDTH + BORDER_THICKNESS;
|
||||
|
||||
const styles = ({ palette }: Theme) => createStyles({
|
||||
frame: {
|
||||
position: "relative",
|
||||
width: CHANNEL_WIDTH + 2*BORDER_THICKNESS,
|
||||
borderTop: BORDER_THICKNESS + "px black solid",
|
||||
borderBottom: BORDER_THICKNESS + "px black solid",
|
||||
height: DISPLAY_HEIGHT,
|
||||
background: "black",
|
||||
overflow: "hidden",
|
||||
marginLeft: (CHANNEL_WIDTH+BORDER_THICKNESS)/2,
|
||||
marginRight: (CHANNEL_WIDTH+BORDER_THICKNESS)/2
|
||||
},
|
||||
frameStereo: {
|
||||
position: "relative",
|
||||
width: 2*CHANNEL_WIDTH + 3*BORDER_THICKNESS,
|
||||
borderTop: BORDER_THICKNESS + "px black solid",
|
||||
borderBottom: BORDER_THICKNESS + "px black solid",
|
||||
height: DISPLAY_HEIGHT,
|
||||
background: "black",
|
||||
overflow: "hidden",
|
||||
},
|
||||
redFrameL: {
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#F00"
|
||||
},
|
||||
yellowFrameL: {
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#CC0"
|
||||
},
|
||||
greenFrameL: {
|
||||
top: "100%",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#0C2"
|
||||
},
|
||||
indicatorL: {
|
||||
position: "absolute",
|
||||
left: LEFT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: TELLTALE_HEIGHT + "px",
|
||||
top: "100%",
|
||||
background: "#F00"
|
||||
|
||||
},
|
||||
redFrameR: {
|
||||
position: "absolute",
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#F00"
|
||||
},
|
||||
yellowFrameR: {
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#CC0"
|
||||
},
|
||||
greenFrameR: {
|
||||
top: "100%",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
position: "absolute",
|
||||
height: DISPLAY_HEIGHT - 2,
|
||||
background: "#0C2"
|
||||
},
|
||||
indicatorR: {
|
||||
position: "absolute",
|
||||
left: RIGHT_X,
|
||||
width: CHANNEL_WIDTH,
|
||||
height: TELLTALE_HEIGHT + "px",
|
||||
top: "100%",
|
||||
background: "#F00"
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface VuMeterProps extends WithStyles<typeof styles> {
|
||||
instanceId: number;
|
||||
display: "input" | "output";
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
interface VuMeterState {
|
||||
isStereo: boolean;
|
||||
};
|
||||
|
||||
function aToDb(value: number): number {
|
||||
if (value === 0) return -96;
|
||||
return Math.log10(Math.abs(value)) * 20;
|
||||
}
|
||||
|
||||
function dbToY(db: number): number {
|
||||
if (db < MIN_DB) db = MIN_DB;
|
||||
if (db > MAX_DB) db = MAX_DB;
|
||||
|
||||
let y = INTERIOR_DISPLAY_HEIGHT - (db - MIN_DB) / (MAX_DB - MIN_DB) * INTERIOR_DISPLAY_HEIGHT;;
|
||||
return y;
|
||||
}
|
||||
|
||||
export const VuMeter =
|
||||
withStyles(styles, { withTheme: true })(
|
||||
class extends Component<VuMeterProps, VuMeterState>
|
||||
{
|
||||
divRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
model: PiddleModel;
|
||||
|
||||
yZero: number;
|
||||
yYellow: number;
|
||||
|
||||
constructor(props: VuMeterProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isStereo: false
|
||||
};
|
||||
this.model = PiddleModelFactory.getInstance();
|
||||
|
||||
this.yZero = dbToY(0);
|
||||
this.yYellow = dbToY(DB_YELLOW);
|
||||
|
||||
this.onVuChanged = this.onVuChanged.bind(this);
|
||||
|
||||
}
|
||||
|
||||
|
||||
telltaleStateL: TelltaleState = new TelltaleState();
|
||||
telltaleStateR: TelltaleState = new TelltaleState();
|
||||
|
||||
vuInfo?: VuUpdateInfo;
|
||||
requesttedStereoState: boolean = false;
|
||||
|
||||
onVuChanged(vuInfo: VuUpdateInfo): void {
|
||||
let value: number;
|
||||
let valueR: number;
|
||||
|
||||
this.vuInfo = vuInfo;
|
||||
|
||||
|
||||
let isStereo: boolean;
|
||||
|
||||
if (this.props.display === "input") {
|
||||
value = vuInfo.inputMaxValueL;
|
||||
valueR = vuInfo.inputMaxValueR;
|
||||
isStereo = vuInfo.isStereoInput;
|
||||
} else {
|
||||
isStereo = vuInfo.isStereoOutput;
|
||||
value = vuInfo.outputMaxValueL;
|
||||
valueR = vuInfo.outputMaxValueR;
|
||||
}
|
||||
if (this.state.isStereo !== isStereo)
|
||||
{
|
||||
if (isStereo !== this.requesttedStereoState) // guard against overrunning the layout engine.
|
||||
{
|
||||
this.requesttedStereoState = isStereo;
|
||||
|
||||
this.setState({
|
||||
isStereo: isStereo
|
||||
});
|
||||
return; // we can't procede without stereo layout.
|
||||
}
|
||||
}
|
||||
|
||||
let childNodes = this.divRef.current!.childNodes;
|
||||
|
||||
let vuData: VuChannelData = {
|
||||
value: value,
|
||||
redDiv: childNodes[0] as HTMLDivElement,
|
||||
yellowDiv: childNodes[1] as HTMLDivElement,
|
||||
greenDiv: childNodes[2] as HTMLDivElement,
|
||||
telltaleDiv: childNodes[3] as HTMLDivElement
|
||||
};
|
||||
this.updateChannel(vuData,this.telltaleStateL);
|
||||
if (this.state.isStereo) {
|
||||
vuData = {
|
||||
value: valueR,
|
||||
redDiv: childNodes[4] as HTMLDivElement,
|
||||
yellowDiv: childNodes[5] as HTMLDivElement,
|
||||
greenDiv: childNodes[6] as HTMLDivElement,
|
||||
telltaleDiv: childNodes[7] as HTMLDivElement
|
||||
};
|
||||
this.updateChannel(vuData, this.telltaleStateR);
|
||||
}
|
||||
}
|
||||
|
||||
resetTelltales() {
|
||||
this.telltaleStateL.reset();
|
||||
this.telltaleStateR.reset();
|
||||
}
|
||||
updateChannel(vuData: VuChannelData, telltaleState: TelltaleState) {
|
||||
let db = aToDb(vuData.value);
|
||||
if (db > MAX_DB) db = MAX_DB;
|
||||
if (db < MIN_DB) db = MIN_DB;
|
||||
|
||||
let y = dbToY(db);
|
||||
let INVISIBLE_Y = INTERIOR_DISPLAY_HEIGHT + "px";
|
||||
if (y >= this.yYellow) {
|
||||
// green only.
|
||||
vuData.greenDiv.style.top = y + "px";
|
||||
vuData.yellowDiv.style.top = INVISIBLE_Y;
|
||||
vuData.redDiv.style.top = INVISIBLE_Y;
|
||||
} else {
|
||||
vuData.greenDiv.style.top = this.yYellow + "px";
|
||||
if (y >= this.yZero) {
|
||||
vuData.yellowDiv.style.top = y + "px";
|
||||
vuData.redDiv.style.top = INVISIBLE_Y;
|
||||
} else {
|
||||
vuData.yellowDiv.style.top = this.yZero + "px";
|
||||
vuData.redDiv.style.top = y + "px";
|
||||
}
|
||||
}
|
||||
let dbTelltale = telltaleState.getValue(db);
|
||||
let yTelltale = dbToY(dbTelltale);
|
||||
|
||||
if (yTelltale < this.yZero) {
|
||||
vuData.telltaleDiv.style.background = "#F00";
|
||||
} else if (yTelltale < this.yYellow) {
|
||||
vuData.telltaleDiv.style.background = "#CC0";
|
||||
} else {
|
||||
vuData.telltaleDiv.style.background = "#0F2";
|
||||
}
|
||||
if (yTelltale > INTERIOR_DISPLAY_HEIGHT - TELLTALE_HEIGHT) {
|
||||
// non-zero and zero are different displays.
|
||||
// zero: nothing.. tiny: show the telltale.
|
||||
if (vuData.value !== 0) {
|
||||
yTelltale = INTERIOR_DISPLAY_HEIGHT - TELLTALE_HEIGHT;
|
||||
} else {
|
||||
yTelltale = INTERIOR_DISPLAY_HEIGHT;
|
||||
}
|
||||
}
|
||||
vuData.telltaleDiv.style.top = yTelltale + "px";
|
||||
|
||||
|
||||
}
|
||||
subscriptionHandle?: VuSubscriptionHandle;
|
||||
|
||||
subscribedInstanceId?: number;
|
||||
|
||||
addVuSubscription() {
|
||||
this.removeVuSubscription();
|
||||
if (this.props.instanceId !== -1) {
|
||||
this.subscribedInstanceId = this.props.instanceId;
|
||||
this.subscriptionHandle = this.model.addVuSubscription(this.props.instanceId, this.onVuChanged);
|
||||
this.resetTelltales();
|
||||
}
|
||||
}
|
||||
removeVuSubscription() {
|
||||
if (this.subscriptionHandle) {
|
||||
this.model.removeVuSubscription(this.subscriptionHandle);
|
||||
this.subscriptionHandle = undefined;
|
||||
this.subscribedInstanceId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate()
|
||||
{
|
||||
if (this.subscribedInstanceId)
|
||||
{
|
||||
if (this.props.instanceId !== this.subscribedInstanceId)
|
||||
{
|
||||
this.removeVuSubscription();
|
||||
this.addVuSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let classes = this.props.classes;
|
||||
|
||||
if (!this.state.isStereo) {
|
||||
return (<div className={classes.frame} ref={this.divRef}
|
||||
>
|
||||
<div className={classes.redFrameL} />
|
||||
<div className={classes.yellowFrameL} />
|
||||
<div className={classes.greenFrameL} />
|
||||
<div className={classes.indicatorL} />
|
||||
</div>);
|
||||
} else {
|
||||
return (<div className={classes.frameStereo} ref={this.divRef}
|
||||
>
|
||||
<div className={classes.redFrameL} />
|
||||
<div className={classes.yellowFrameL} />
|
||||
<div className={classes.greenFrameL} />
|
||||
<div className={classes.indicatorL} />
|
||||
|
||||
<div className={classes.redFrameR} />
|
||||
<div className={classes.yellowFrameR} />
|
||||
<div className={classes.greenFrameR} />
|
||||
<div className={classes.indicatorR} />
|
||||
|
||||
</div>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
this.addVuSubscription();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.removeVuSubscription();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default VuMeter;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user