Files
op-pedal/react/src/MainPage.tsx
T
2022-04-23 07:27:39 -04:00

525 lines
24 KiB
TypeScript

// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { SyntheticEvent } from 'react';
import { Theme } from '@mui/material/styles';
import { WithStyles } from '@mui/styles';
import createStyles from '@mui/styles/createStyles';
import withStyles from '@mui/styles/withStyles';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import {
PedalBoard, PedalBoardItem, PedalBoardSplitItem, SplitType
} from './PedalBoard';
import Button from '@mui/material/Button';
import InputIcon from '@mui/icons-material/Input';
import LoadPluginDialog from './LoadPluginDialog';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import PedalBoardView from './PedalBoardView';
import { PiPedalStateError } from './PiPedalError';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade';
import Divider from '@mui/material/Divider';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import PluginInfoDialog from './PluginInfoDialog';
import { GetControlView } from './ControlViewFactory';
import MidiBindingsDialog from './MidiBindingsDialog';
import PluginPresetSelector from './PluginPresetSelector';
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.1em", 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();
let pedalboard = this.model.pedalBoard.get();
let selectedPedal = pedalboard.getFirstSelectableItem();
this.state = {
selectedPedal: selectedPedal,
loadDialogOpen: false,
pedalBoard: pedalboard,
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, presetInstanceId: number) {
this.model.loadPluginPreset(instanceId, presetInstanceId);
}
onPedalBoardChanged(value: PedalBoard) {
let selectedItem = -1;
if (value.hasItem(this.state.selectedPedal))
{
selectedItem = this.state.selectedPedal;
} else {
selectedItem = value.getFirstSelectableItem();
}
this.setState({
pedalBoard: value,
selectedPedal: selectedItem
});
}
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 = "";
let missing = false;
if (pedalBoardItem) {
if (pedalBoardItem.isEmpty()) {
title = "";
} else if (pedalBoardItem.isSplit()) {
title = "Split";
} else {
let uiPlugin = this.model.getUiPlugin(pedalBoardItem.uri);
if (!uiPlugin) {
missing = true;
title = pedalBoardItem?.pluginName ?? "Missing plugin";
} else {
title = uiPlugin.name;
author = uiPlugin.author_name;
presetsUri = uiPlugin.uri;
if (uiPlugin.description.length > 20) {
pluginUri = uiPlugin.uri;
}
}
}
}
let classes = this.props.classes;
if (missing)
{
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", color: "#800000" }}>
<span className={classes.title}>{title}</span>
</span>
</div>
</div>
);
} else {
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", verticalAlign: "center" }}>
<PluginInfoDialog plugin_uri={pluginUri} />
</div>
<div style={{ flex: "0 0 auto" }}>
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalBoardItem?.instanceId ?? 0}
/>
</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;
let missing = false;
let pluginUri = "#error";
if (pedalBoardItem) {
canDelete = pedalBoard.canDeleteItem(pedalBoardItem.instanceId);
instanceId = pedalBoardItem.instanceId;
if (pedalBoardItem.isEmpty()) {
canAdd = true;
} else if (pedalBoardItem.isSplit()) {
canAdd = true;
} else {
pluginUri = pedalBoardItem.uri;
uiPlugin = this.model.getUiPlugin(pluginUri);
canAdd = true;
if (uiPlugin) {
bypassVisible = true;
bypassChecked = pedalBoardItem.isEnabled;
} else {
missing = true;
}
}
}
let horizontalScrollLayout = this.state.horizontalScrollLayout;
return (
<div className={classes.frame}>
<div id="pedalBoardScroll" className={horizontalScrollLayout ? classes.pedalBoardScrollSmall : classes.pedalBoardScroll}
style={{ maxHeight: horizontalScrollLayout ? undefined : this.state.screenHeight / 2 }}>
<PedalBoardView key={pluginUri} selectedId={this.state.selectedPedal}
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 color="secondary" 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) }} size="large">
<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) }}
size="large">
<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 />}
style={{ borderRadius: 24, paddingLeft: 18, paddingRight: 18, textTransform: "none"}}
>
Load
</Button>
</div>
<div style={{ flex: "0 0 auto" }}>
<IconButton
onClick={(e) => { this.handleMidiConfiguration(instanceId); }}
size="large">
<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}>
{
missing ? (
<div style={{marginLeft: 100,marginTop: 20}}>
<Typography variant="body1" paragraph={true}>Error: Plugin is not installed.</Typography>
<Typography variant="body2" paragraph={true}>{pluginUri}</Typography>
</div>
):
(
GetControlView(pedalBoardItem)
)
}
</div>
<MidiBindingsDialog open={this.state.showMidiBindingsDialog}
onClose={()=> this.setState({showMidiBindingsDialog: false} ) }
/>
{
(this.state.loadDialogOpen) && (
<LoadPluginDialog open={this.state.loadDialogOpen} uri={this.getSelectedUri()}
onOk={this.onLoadOk} onCancel={this.onLoadCancel}
/>
)
}
</div>
);
}
}
);
export default MainPage