524 lines
24 KiB
TypeScript
524 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 |