Copy prests between banks; preset multi-select.

This commit is contained in:
Robin E. R. Davies
2025-09-21 15:08:55 -04:00
parent b25e90df7b
commit 6105dff931
16 changed files with 672 additions and 208 deletions
+5 -1
View File
@@ -228,7 +228,11 @@ namespace pipedal
{
newSelection = presets_[i]->instanceId();
}
else if (presets_.size() > 1)
else if (i == presets_.size() && i != 0) {
newSelection = presets_[i-1]->instanceId();
}
else if (presets_.size() >= 1)
{
newSelection = presets_[0]->instanceId();
}
+10 -5
View File
@@ -1174,12 +1174,12 @@ int64_t PiPedalModel::DeleteBank(int64_t clientId, int64_t instanceId)
return newSelection;
}
int64_t PiPedalModel::DeletePreset(int64_t clientId, int64_t instanceId)
int64_t PiPedalModel::DeletePresets(int64_t clientId, const std::vector<int64_t> &presetInstanceIds)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t oldSelection = storage.GetCurrentPresetId();
int64_t newSelection = storage.DeletePreset(instanceId);
this->FirePresetsChanged(clientId); // fire now.
int64_t newSelection = storage.DeletePresets(presetInstanceIds);
this->FirePresetsChanged(clientId); // fire BEFORE we load a new preset.
if (oldSelection != newSelection)
{
this->LoadPreset(
@@ -3270,10 +3270,15 @@ int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::v
{
std::lock_guard<std::recursive_mutex> lock(mutex);
uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets);
if (lastAdded != -1) {
}
FirePresetsChanged(-1);
return lastAdded;
}
int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets);
return lastAdded;
}
+2 -1
View File
@@ -385,7 +385,7 @@ namespace pipedal
void LoadPreset(int64_t clientId, int64_t instanceId);
bool UpdatePresets(int64_t clientId, const PresetIndex &presets);
void UpdatePluginPresets(const PluginUiPresets &pluginPresets);
int64_t DeletePreset(int64_t clientId, int64_t instanceId);
int64_t DeletePresets(int64_t clientId, const std::vector<int64_t> &presetInstanceIds);
bool RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name);
int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex);
uint64_t CopyPluginPreset(const std::string &pluginUri, uint64_t presetId);
@@ -496,6 +496,7 @@ namespace pipedal
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
};
} // namespace pipedal.
+22 -5
View File
@@ -46,6 +46,17 @@
using namespace std;
using namespace pipedal;
class CopyPresetsToBankBody {
public:
int64_t bankInstanceId_;
std::vector<int64_t> presets_;
DECLARE_JSON_MAP(CopyPresetsToBankBody);
};
JSON_MAP_BEGIN(CopyPresetsToBankBody)
JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId)
JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets)
JSON_MAP_END()
class ImportPresetsFromBankBody {
public:
@@ -1470,12 +1481,12 @@ public:
model.RequestShutdown(true);
this->Reply(replyTo, "restart");
}
else if (message == "deletePresetItem")
else if (message == "deletePresetItems")
{
int64_t instanceId = 0;
pReader->read(&instanceId);
int64_t result = model.DeletePreset(this->clientId, instanceId);
this->Reply(replyTo, "deletePresetItem", result);
std::vector<int64_t> items;
pReader->read(&items);
int64_t result = model.DeletePresets(this->clientId, items);
this->Reply(replyTo, "deletePresetItems", result);
}
else if (message == "deleteBankItem")
{
@@ -1831,6 +1842,12 @@ public:
auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_);
this->Reply(replyTo,"importPresetsFromBank",result);
}
else if (message == "copyPresetsToBank") {
CopyPresetsToBankBody args;
pReader->read(&args);
auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_);
this->Reply(replyTo,"copyPresetsToBank",result);
}
else
{
Lv2Log::error("Unknown message received: %s", message.c_str());
+34 -2
View File
@@ -764,6 +764,35 @@ int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector
SaveCurrentBank();
return lastPresetId;
}
int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets)
{
if (bankIndex.selectedBank() == bankInstanceId) {
throw std::runtime_error("Can't copy to self.");
}
auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId);
BankFile bankFile;
LoadBankFile(indexEntry.name(),&bankFile);
std::set<int64_t> presetsSet { presets.begin(), presets.end()};
std::set<std::string> existingNames;
for (auto&preset: bankFile.presets()) {
existingNames.insert(preset->preset().name());
}
for (auto &presetEntry: this->currentBank.presets()) {
if (presetsSet.contains(presetEntry->instanceId())) {
std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames);
existingNames.insert(uniqueName);
Pedalboard t = presetEntry->preset();
t.name(uniqueName);
bankFile.addPreset(t);
}
}
SaveBankFile(indexEntry.name(),bankFile);
return -1;
}
std::vector<PresetIndexEntry> Storage::RequestBankPresets(int64_t bankInstanceId)
{
@@ -852,9 +881,12 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const
throw PiPedalException("Not found.");
}
int64_t Storage::DeletePreset(int64_t presetId)
int64_t Storage::DeletePresets(const std::vector<int64_t> &presetInstanceIds)
{
int64_t newSelection = currentBank.deletePreset(presetId);
int64_t newSelection = currentBank.selectedPreset();
for (auto presetId: presetInstanceIds) {
newSelection = currentBank.deletePreset(presetId);
}
SaveCurrentBank();
return newSelection;
}
+2 -1
View File
@@ -162,7 +162,7 @@ public:
bool LoadPreset(int64_t presetId);
int64_t DeletePreset(int64_t presetId);
int64_t DeletePresets(const std::vector<int64_t>& presetInstanceIds);
bool RenamePreset(int64_t presetId, const std::string&name);
int64_t CopyPreset(int64_t fromId, int64_t toId = -1);
int64_t CreateNewPreset();
@@ -271,6 +271,7 @@ public:
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
};
-1
View File
@@ -1,4 +1,3 @@
Did we fix Toob Tremolo stereo harmonic?
ls
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sudo /bin/bash
rm -rf /etc/pipedal/react
mkdir -p /etc/pipedal/react
cp -r dist/* /etc/pipedal/react/
+17
View File
@@ -39,6 +39,12 @@ declare module '@mui/material/styles' {
mainBackground?: React.CSSProperties['color'];
toolbarColor?: React.CSSProperties['color'];
}
interface Palette {
actionBar: Palette['primary'];
}
interface PaletteOptions {
actionBar: PaletteOptions['primary'];
}
}
@@ -143,7 +149,12 @@ const theme = createTheme(
},
secondary: {
main: "#FF6060"
},
actionBar: {
main: '#130b22ff',
contrastText: '#FFFFFF'
}
},
mainBackground: "#222",
toolbarColor: '#222'
@@ -209,8 +220,14 @@ const theme = createTheme(
},
secondary: {
main: "#FF6060"
},
actionBar: {
main: '#130b22ff',
contrastText: '#FFFFFF'
}
},
mainBackground: "#FFFFFF",
toolbarColor: '#FFFFFF'
@@ -0,0 +1,180 @@
// Copyright (c) Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Toolbar from "@mui/material/Toolbar";
import IconButtonEx from "./IconButtonEx";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Typography from "@mui/material/Typography";
import Select from '@mui/material/Select';
import Button from '@mui/material/Button';
import InputLabel from '@mui/material/InputLabel';
import DialogEx from './DialogEx';
import DialogTitle from '@mui/material/DialogTitle';
import MenuItem from '@mui/material/MenuItem';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { PiPedalModel } from './PiPedalModel';
import { BankIndexEntry } from './Banks';
export interface CopyPresetsToBankDialogProps {
open: boolean,
onOk: (bankInstanceId: number) => void,
onClose: () => void
};
export interface CopyPresetsToBankDialogState {
selectedBank: number;
banks: BankIndexEntry[];
fullScreen: boolean;
};
function setSavedDefaultBankSelection(bankInstanceId: number) {
localStorage.setItem('pipedal_copyTo_defaultBankSelection', bankInstanceId.toString());
}
function getSavedDefaultBankSelection() {
let defaultValue = localStorage.getItem('pipedal_copyTo_defaultBankSelection');
if (defaultValue) {
return parseInt(defaultValue);
}
return -1;
}
export default class CopyPresetsToBankDialog extends ResizeResponsiveComponent<CopyPresetsToBankDialogProps, CopyPresetsToBankDialogState> {
model: PiPedalModel;
constructor(props: CopyPresetsToBankDialogProps) {
super(props);
this.model = PiPedalModel.getInstance();
let banks = this.getBanks();
let selectedBank = this.getDefaultBankSelection(banks);
this.state = {
banks: banks,
selectedBank: selectedBank,
fullScreen: false,
};
}
private getBanks() {
let bankIndex = this.model.banks.get();
let result: BankIndexEntry[] = [];
for (let entry of bankIndex.entries) {
if (entry.instanceId === bankIndex.selectedBank) continue;
result.push(entry);
}
return result;
}
private getDefaultBankSelection(banks: BankIndexEntry[]) {
if (banks.length === 0) return -1;
let defaultSelection = getSavedDefaultBankSelection();
for (let bank of banks) {
if (bank.instanceId === defaultSelection) return defaultSelection;
}
return banks[0].instanceId;
}
componentDidMount() {
super.componentDidMount();
}
componentWillUnmount() {
super.componentWillUnmount();
}
componentDidUpdate() {
}
handleBankChanged(bankId: number) {
setSavedDefaultBankSelection(bankId);
this.setState({ selectedBank: bankId });
}
handleOk() {
this.props.onOk(this.state.selectedBank);
}
render() {
let props = this.props;
let { open, onClose } = props;
const handleClose = () => {
onClose();
};
return (
<DialogEx tag="importPreset" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<div style={{ display: "flex", flexFlow: "column nowrap", height: this.state.fullScreen ? "100vh" : undefined }}>
{!this.state.fullScreen && (
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Copy Presets to Bank
</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
<DialogActions style={{ flex: "0 0 auto" }}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button
onClick={() => { this.handleOk(); }} variant="dialogPrimary"
disabled={false}
>
OK
</Button>
</DialogActions>
</div>
</DialogEx>
);
}
}
+14 -4
View File
@@ -82,7 +82,7 @@ export interface DraggableGridProps
defaultSelectedIndex?: number;
onLongPress?: (itemIndex: number) => void;
onLongPress?: (event: PointerEvent<HTMLDivElement>, itemIndex: number) => void;
canDrag?: boolean;
@@ -209,7 +209,7 @@ const DraggableGrid =
}
bringSelectedItemIntoView() {
if (this.state.indexToSelect && this.state.indexToSelect >= 0)
if (this.state.indexToSelect !== undefined && this.state.indexToSelect >= 0)
{
let grid = this.refGrid.current;
if (grid)
@@ -295,6 +295,7 @@ const DraggableGrid =
startClientX: number = 0;
startClientY: number = 0;
dragStarted: boolean = false;
dragThresholdWasExceeded: boolean = false;
savedIndex: string = "";
savedBackground: string = "";
lastPointerDown: number = 0;
@@ -529,6 +530,11 @@ const DraggableGrid =
if (this.startIndex !== this.currentIndex) {
this.props.moveElement(this.startIndex, this.currentIndex);
}
if (!this.dragThresholdWasExceeded) {
if (this.props.onLongPress) {
this.props.onLongPress(e,this.startIndex);
}
}
}
this.cancelDrag();
}
@@ -693,6 +699,7 @@ const DraggableGrid =
///window.addEventListener("touchstart",this.handleTouchStart, {passive: false});
this.dragStarted = false;
this.dragThresholdWasExceeded = false;
this.startX = e.clientX;
this.startY = e.clientY;
this.startClientX = e.clientX;
@@ -756,8 +763,11 @@ const DraggableGrid =
handlePointerMove(e: PointerEvent<HTMLDivElement>): boolean {
if (this.isCapturedPointer(e)) {
if (!this.dragStarted && this.dragThresholdExceeded(e)) {
this.startDrag();
if (this.dragThresholdExceeded(e)) {
if (!this.dragStarted) {
this.startDrag();
}
this.dragThresholdWasExceeded = true;
}
if (this.dragStarted) {
this.lastX = e.clientX;
+69 -35
View File
@@ -158,44 +158,78 @@ export default class ImportPresetFromBankDialog extends ResizeResponsiveComponen
onEnterKey={() => { }}
>
<div style={{ display: "flex", flexFlow: "column nowrap", height: this.state.fullScreen ? "100vh" : "80vh" }}>
{!this.state.fullScreen && (
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
{!this.state.fullScreen ? (
<>
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Import Presets from Bank
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
</>
) : (
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "top", flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<div style={{ marginLeft: 24, display: "flex", flex: "0 0 auto", flexFlow: "column nowrap", justifyContent: "start" }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</div>
</div>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Import Presets from Bank
</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
<Divider />
<List sx={{ flex: "1 1 auto", width: '100%', bgColor: 'background.paper', overflowX: 'auto' }}>
{this.state.bankPresets?.map((presetEntry) => {
@@ -242,7 +276,7 @@ export default class ImportPresetFromBankDialog extends ResizeResponsiveComponen
</Button>
</DialogActions>
</div>
</DialogEx>
</DialogEx >
);
}
}
-2
View File
@@ -30,7 +30,6 @@ import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
import AppBar from '@mui/material/AppBar';
import SnapshotPanel from './SnapshotPanel';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
@@ -250,7 +249,6 @@ export const PerformanceView =
render() {
const classes = withStyles.getClasses(this.props);
let wrapSelects = this.state.wrapSelects;
let presets = this.state.presets;
let banks = this.state.banks;
let appBarIconSize: "large" | undefined = this.state.largeAppBar ? undefined : "large";
+5 -2
View File
@@ -2192,8 +2192,8 @@ export class PiPedalModel //implements PiPedalModel
}
deletePresetItem(instanceId: number): Promise<number> {
return nullCast(this.webSocket).request<number>("deletePresetItem", instanceId);
deletePresetItems(instanceIds: Set<number>): Promise<number> {
return nullCast(this.webSocket).request<number>("deletePresetItems", Array.from(instanceIds));
}
deleteBankItem(instanceId: number): Promise<number> {
@@ -3549,6 +3549,9 @@ export class PiPedalModel //implements PiPedalModel
importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise<number> {
return nullCast(this.webSocket).request<number>("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets});
}
copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise<number> {
return nullCast(this.webSocket).request<number>("copyPresetsToBank", {bankInstanceId: bankInstanceId, presets: presets});
}
};
let instance: PiPedalModel | undefined = undefined;
+242 -137
View File
@@ -17,8 +17,9 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React, { SyntheticEvent, Component } from 'react';
import React, { SyntheticEvent } from 'react';
import ImportPresetFromBankDialog from './ImportPresetFromBankDialog';
import CopyPresetsToBankDialog from './CopyPresetsToBankDialog';
import Icon from '@mui/material/Icon';
import Divider from '@mui/material/Divider';
import { css } from '@emotion/react';
@@ -27,23 +28,22 @@ import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
import Button from '@mui/material/Button';
import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
import CloseIcon from '@mui/icons-material/Close';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade';
import UploadPresetDialog from './UploadPresetDialog';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import SelectHoverBackground from './SelectHoverBackground';
import CloseIcon from '@mui/icons-material/Close';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import RenameDialog from './RenameDialog';
import Slide, { SlideProps } from '@mui/material/Slide';
@@ -56,10 +56,13 @@ import { withStyles } from "tss-react/mui";
import DownloadIcon from './svg/file_download_black_24dp.svg?react';
import UploadIcon from './svg/file_upload_black_24dp.svg?react';
// function isTouchUi() {
// return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// }
interface PresetDialogProps extends WithStyles<typeof styles> {
show: boolean;
isEditDialog: boolean;
onDialogClose: () => void;
};
@@ -67,13 +70,16 @@ interface PresetDialogProps extends WithStyles<typeof styles> {
interface PresetDialogState {
presets: PresetIndex;
showActionBar: boolean;
multiSelect: boolean;
showTouchActionBar: boolean;
currentItem: number;
selectedItems: Set<number>;
renameOpen: boolean;
importOpen: boolean;
copyToOpen: boolean;
moreMenuAnchorEl: HTMLElement | null;
openUploadPresetDialog: boolean;
@@ -93,7 +99,8 @@ const styles = (theme: Theme) => createStyles({
dialogActionBar: css({
position: 'relative',
top: 0, left: 0,
background: "black"
background: theme.palette.actionBar.main,
color: theme.palette.actionBar.contrastText
}),
dialogTitle: css({
marginLeft: theme.spacing(2),
@@ -111,7 +118,7 @@ const styles = (theme: Theme) => createStyles({
alignItems: "center",
textAlign: "left",
justifyContent: "center",
paddingLeft: 8
paddibomngLeft: 8
}),
iconFrame: css({
flex: "0 0 auto",
@@ -136,7 +143,7 @@ const Transition = React.forwardRef(function Transition(
const PresetDialog = withStyles(
class extends Component<PresetDialogProps, PresetDialogState> {
class extends ResizeResponsiveComponent<PresetDialogProps, PresetDialogState> {
model: PiPedalModel;
@@ -150,32 +157,49 @@ const PresetDialog = withStyles(
let presets = this.model.presets.get();
this.state = {
presets: presets,
showActionBar: false,
multiSelect: false,
showTouchActionBar: false,
currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>(),
selectedItems: new Set<number>([presets.selectedInstanceId]),
renameOpen: false,
importOpen: false,
copyToOpen: false,
moreMenuAnchorEl: null,
openUploadPresetDialog: false
};
this.handlePresetsChanged = this.handlePresetsChanged.bind(this);
}
onWindowSizeChanged(width: number, height: number) {
super.onWindowSizeChanged(width, height);
}
handleSelectionUpdated(instanceIds: Set<number>) {
this.setState({
multiSelect: instanceIds.size > 1
})
}
setSelections(instanceIds: Set<number>) {
this.setState({
selectedItems: instanceIds,
});
this.handleSelectionUpdated(instanceIds);
}
setSelection(instanceId: number) {
this.setState({
let selectedItems = new Set<number>([instanceId]);
this.setState({
currentItem: instanceId,
selectedItems: new Set<number>([instanceId])
});
selectedItems: selectedItems
});
this.handleSelectionUpdated(selectedItems);
}
selectItemAtIndex(index: number) {
let instanceId = this.state.presets.presets[index].instanceId;
this.setSelection(instanceId);
}
isEditMode() {
return this.state.showActionBar || this.props.isEditDialog;
}
onMoreClick(e: SyntheticEvent): void {
this.setState({ moreMenuAnchorEl: e.currentTarget as HTMLElement })
@@ -203,9 +227,10 @@ const PresetDialog = withStyles(
{
// if we don't have a valid selection, then use the current preset.
if (this.state.presets.getItem(this.state.currentItem) == null) {
this.setState({ presets: presets, currentItem: presets.selectedInstanceId,
this.setState({
presets: presets, currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>([presets.selectedInstanceId])
});
});
} else {
this.setState({ presets: presets });
}
@@ -222,8 +247,10 @@ const PresetDialog = withStyles(
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged);
}
getSelectedIndex() {
let instanceId = this.isEditMode() ? this.state.currentItem : this.state.presets.selectedInstanceId;
getSelectedIndex(instanceId?: number): number {
if (instanceId === undefined) {
instanceId = this.state.currentItem;
}
let presets = this.state.presets;
for (let i = 0; i < presets.presets.length; ++i) {
if (presets.presets[i].instanceId === instanceId) return i;
@@ -232,33 +259,59 @@ const PresetDialog = withStyles(
}
handleDeleteClick() {
if (this.state.currentItem === -1) return;
let currentItem = this.state.currentItem;
if (currentItem !== -1) {
this.model.deletePresetItem(currentItem)
.then((currentItem: number) => {
this.setSelection(currentItem);
})
.catch((error) => {
this.model.showAlert(error);
});
if (this.state.selectedItems.size === 0) {
return;
}
this.model.deletePresetItems(this.state.selectedItems)
.then((currentItem: number) => {
this.setSelection(currentItem);
})
.catch((error) => {
this.model.showAlert(error);
});
}
handleDialogClose() {
this.props.onDialogClose();
}
handleItemClick(instanceId: number): void {
if (this.isEditMode()) {
this.setSelection(instanceId);
handleItemClick(e: React.MouseEvent<HTMLElement>, instanceId: number): void {
e.stopPropagation();
if (e.ctrlKey || this.state.showTouchActionBar) {
let selectedItems = new Set<number>(this.state.selectedItems);
if (selectedItems.has(instanceId)) {
selectedItems.delete(instanceId);
this.setSelections(selectedItems);
} else {
selectedItems.add(instanceId);
this.setState({
selectedItems: selectedItems,
currentItem: instanceId
});
this.handleSelectionUpdated(selectedItems);
}
} else if (e.shiftKey) {
let presets = this.state.presets;
let startIndex = this.getSelectedIndex();
let endIndex = this.getSelectedIndex(instanceId);
if (startIndex === -1 || endIndex === -1) return; // should not happen.
let selectedItems = new Set<number>();
if (endIndex < startIndex) {
let t = startIndex;
startIndex = endIndex;
endIndex = t;
}
for (let i = startIndex; i <= endIndex; ++i) {
selectedItems.add(presets.presets[i].instanceId);
}
this.setSelections(selectedItems);
} else {
this.model.loadPreset(instanceId);
this.props.onDialogClose();
this.setSelection(instanceId);
}
}
showActionBar(show: boolean): void {
this.setState({ showActionBar: show });
showTouchActionBar(show: boolean): void {
this.setState({ showTouchActionBar: show });
}
@@ -266,29 +319,23 @@ const PresetDialog = withStyles(
mapElement(el: any): React.ReactNode {
let presetEntry = el as PresetIndexEntry;
const classes = withStyles.getClasses(this.props);
let selected =
this.isEditMode() ? this.state.selectedItems.has(presetEntry.instanceId) :
(presetEntry.instanceId === this.state.presets.selectedInstanceId);
let selected = this.state.selectedItems.has(presetEntry.instanceId);
return (
<div key={presetEntry.instanceId} id={"psetdlgItem_" + presetEntry.instanceId} className="itemBackground">
<ButtonBase style={{ width: "100%", height: 48 }}
onClick={() => this.handleItemClick(presetEntry.instanceId)}
<ListItemButton
selected={selected}
onClick={(e) => this.handleItemClick(e, presetEntry.instanceId)}
style={{ height: 48 }}
>
<SelectHoverBackground selected={selected} showHover={true} />
<div className={classes.itemFrame}>
<div className={classes.iconFrame}>
<img
src={isDarkMode() ? "img/ic_presets_white.svg" : "img/ic_presets.svg"}
className={classes.itemIcon} alt="" />
</div>
<div className={classes.itemLabel}>
<Typography>
{presetEntry.name}
</Typography>
</div>
</div>
</ButtonBase>
<ListItemIcon>
<img
src={isDarkMode() ? "img/ic_presets_white.svg" : "img/ic_presets.svg"}
className={classes.itemIcon} alt="" />
</ListItemIcon>
<ListItemText primary={presetEntry.name}
/>
</ListItemButton>
</div>
);
@@ -343,10 +390,10 @@ const PresetDialog = withStyles(
if (!item) return;
this.model.duplicatePreset(this.state.currentItem)
.then((newId) => {
this.setState({
this.setState({
currentItem: newId,
selectedItems: new Set<number>([newId])
});
});
}).catch((error) => {
this.onError(error);
});
@@ -356,8 +403,15 @@ const PresetDialog = withStyles(
this.model?.showAlert(error);
}
handleImportPresetsFromBank()
{
handleCloseActionBar(e: SyntheticEvent) {
e.stopPropagation();
this.setState({
showTouchActionBar: false,
selectedItems: new Set<number>([this.state.currentItem]),
multiSelect: false
});
}
handleImportPresetsFromBank() {
this.handleMoreClose();
this.setState({ importOpen: true });
}
@@ -372,21 +426,40 @@ const PresetDialog = withStyles(
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
},0);
}, 0);
}
})
.catch((error) => {
this.model.showAlert(error);
});
}
handleCopyToBankDialogOk(bankInstanceId: number): void {
this.setState({ copyToOpen: false });
let selectedItems: number[] = [];
for (let i = 0; i < this.state.presets.presets.length; ++i) {
let preset = this.state.presets.presets[i];
if (this.state.selectedItems.has(preset.instanceId)) {
selectedItems.push(preset.instanceId);
}
}
this.model.copyPresetsToBank(bankInstanceId, selectedItems)
.then((instanceId) => {
})
.catch((error) => {
this.model.showAlert(error);
});
}
handleCopyPresetsToBank() {
this.handleMoreClose();
this.setState({ copyToOpen: true });
}
render() {
const classes = withStyles.getClasses(this.props);
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar;
let defaultSelectedIndex = this.getSelectedIndex();
const showActionBar = this.state.multiSelect || this.state.showTouchActionBar;
return (
<DialogEx tag="preset" fullScreen open={this.props.show}
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
@@ -395,37 +468,37 @@ const PresetDialog = withStyles(
>
<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" }} >
<AppBar className={classes.dialogActionBar} style={{ display: showActionBar ? "block" : "none" }}
>
<Toolbar>
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
<IconButtonEx tooltip="Back" edge="start" color="inherit" aria-label="back"
onClick={(e) => { this.handleCloseActionBar(e); }}
>
<ArrowBackIcon />
<CloseIcon />
</IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle}>
Presets
<Typography variant="h6" className={classes.dialogTitle} noWrap>
{this.state.selectedItems.size.toString() + " items selected"}
</Typography>
<IconButtonEx tooltip="Edit" color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
<Button color="inherit"
onClick={() => {
this.handleCopyPresetsToBank();
}} >
Copy to
</Button>
<IconButtonEx tooltip="Delete" 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 }} />
</IconButtonEx>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
<AppBar className={classes.dialogAppBar} style={{ display: showActionBar ? "none" : "block" }}
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButtonEx>
) : (
<IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
)}
<Typography variant="h6" className={classes.dialogTitle}>
<IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle} noWrap>
Presets
</Typography>
{(this.state.presets.getItem(this.state.currentItem) != null)
@@ -435,26 +508,39 @@ const PresetDialog = withStyles(
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center"
}}>
<Button color="inherit" onClick={(e) => this.handleCopy()}>
Copy
</Button>
<Button color="inherit" onClick={() => this.handleRenameClick()}>
Rename
</Button>
<RenameDialog
title="Rename"
open={this.state.renameOpen}
defaultName={this.getSelectedName()}
acceptActionName={"Rename"}
onClose={() => { this.setState({ renameOpen: false }) }}
onOk={(text: string) => {
this.handleRenameOk(text);
}
}
/>
<IconButtonEx tooltip="Delete" 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 }} />
</IconButtonEx>
{this.state.selectedItems.size === 1 && (
<Button color="inherit" onClick={(e) => this.handleCopy()}
>
Copy
</Button>
)}
{this.state.selectedItems.size === 1 && (
<Button color="inherit" onClick={() => this.handleRenameClick()}
>
Rename
</Button>
)}
{this.state.renameOpen && (
<RenameDialog
title="Rename"
open={this.state.renameOpen}
defaultName={this.getSelectedName()}
acceptActionName={"Rename"}
onClose={() => { this.setState({ renameOpen: false }) }}
onOk={(text: string) => {
this.handleRenameOk(text);
}
}
/>
)}
{this.state.selectedItems.size === 1 && (
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()}
style={{ opacity: this.state.selectedItems.size !== 1 ? 0.5 : 1.0 }}
>
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButtonEx>
)}
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButtonEx>
@@ -466,15 +552,21 @@ const PresetDialog = withStyles(
onClose={() => this.handleMoreClose()}
TransitionComponent={Fade}
>
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText>
Copy to bank...
</ListItemText>
{this.state.selectedItems.size !== 0 && (
<MenuItem onClick={() => {
this.handleMoreClose();
this.handleCopyPresetsToBank();
}}
>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText>
Copy to bank...
</ListItemText>
</MenuItem>
</MenuItem>
)}
<MenuItem onClick={() => { this.handleImportPresetsFromBank(); }} >
<ListItemIcon>
<Icon />
@@ -486,15 +578,17 @@ const PresetDialog = withStyles(
</MenuItem>
<Divider />
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon>
<DownloadIcon className={classes.listIcon} />
</ListItemIcon>
<ListItemText>
Download preset
</ListItemText>
{this.state.selectedItems.size !== 0 && (
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon>
<DownloadIcon className={classes.listIcon} />
</ListItemIcon>
<ListItemText>
Download preset
</ListItemText>
</MenuItem>
</MenuItem>
)}
<MenuItem onClick={() => { this.handleUploadPreset() }}>
<ListItemIcon>
<UploadIcon className={classes.listIcon} />
@@ -516,12 +610,16 @@ const PresetDialog = withStyles(
</div>
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
<DraggableGrid
onLongPress={(item) => this.showActionBar(true)}
canDrag={this.isEditMode()}
onLongPress={(e,item) => {
if (e.pointerType === "touch")
{
this.showTouchActionBar(true);
}
}}
canDrag={!this.state.multiSelect && !this.state.showTouchActionBar}
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) => {
@@ -536,16 +634,23 @@ const PresetDialog = withStyles(
extension='.piPreset'
uploadPage='uploadPreset'
onUploaded={(instanceId) => this.setSelection(instanceId)}
uploadAfter={this.state.currentItem }
uploadAfter={this.state.currentItem}
open={this.state.openUploadPresetDialog}
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
{this.state.importOpen && (
<ImportPresetFromBankDialog
open={this.state.importOpen}
onClose={() => this.setState({ importOpen: false })}
onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)}
/>
)}
{this.state.importOpen && (
<ImportPresetFromBankDialog
open={this.state.importOpen}
onClose={() => this.setState({ importOpen: false })}
onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)}
/>
)}
{this.state.copyToOpen && (
<CopyPresetsToBankDialog
open={this.state.copyToOpen}
onClose={() => this.setState({ copyToOpen: false })}
onOk={(bankInstanceId) => this.handleCopyToBankDialogOk(bankInstanceId)}
/>
)}
</DialogEx>
+66 -12
View File
@@ -17,7 +17,8 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { SyntheticEvent, Component } from 'react';
import { SyntheticEvent } from 'react';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import IconButtonEx from './IconButtonEx';
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
import SaveIconOutline from '@mui/icons-material/Save';
@@ -39,6 +40,7 @@ import ImportPresetFromBankDialog from './ImportPresetFromBankDialog';
import Select from '@mui/material/Select';
import UploadPresetDialog from './UploadPresetDialog';
import { isDarkMode } from './DarkMode';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
interface PresetSelectorProps extends WithStyles<typeof styles> {
@@ -51,6 +53,8 @@ interface PresetSelectorState {
showPresetsDialog: boolean;
showEditPresetsDialog: boolean;
presetsMenuAnchorRef: HTMLElement | null;
presetsSubmenuAnchorRef: HTMLElement | null;
compactHorizontalLayoutMenu: boolean;
saveAsDialogOpen: boolean;
importDialogOpen: boolean;
@@ -87,8 +91,8 @@ const styles = (theme: Theme) => createStyles({
const PresetSelector =
withStyles(
class extends Component<PresetSelectorProps, PresetSelectorState> {
class extends ResizeResponsiveComponent<PresetSelectorProps, PresetSelectorState> {
private MENU_THRESHOLD: number = 570;
model: PiPedalModel;
constructor(props: PresetSelectorProps) {
@@ -98,9 +102,11 @@ const PresetSelector =
presets:
this.model.presets.get(),
enabled: false,
compactHorizontalLayoutMenu: this.windowSize.height < this.MENU_THRESHOLD,
showPresetsDialog: false,
showEditPresetsDialog: false,
presetsMenuAnchorRef: null,
presetsSubmenuAnchorRef: null,
renameDialogOpen: false,
saveAsDialogOpen: false,
importDialogOpen: false,
@@ -117,12 +123,28 @@ const PresetSelector =
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this);
}
onWindowSizeChanged(width: number, height: number): void {
super.onWindowSizeChanged(width,height);
this.setState({ compactHorizontalLayoutMenu: height < this.MENU_THRESHOLD });
}
handlePresetMenuClick(event: SyntheticEvent): void {
handlePresetsMenuClick(event: SyntheticEvent): void {
this.setState({ presetsMenuAnchorRef: (event.currentTarget as HTMLElement) });
}
handlePresetsSubmenuClick(event: SyntheticEvent): void {
this.setState({ presetsSubmenuAnchorRef: (event.currentTarget as HTMLElement) });
}
handlePresetsMenuClose(): void {
this.setState({ presetsMenuAnchorRef: null });
this.setState({
presetsMenuAnchorRef: null,
presetsSubmenuAnchorRef: null,
});
}
handlePresetsSubmenuClose(): void {
this.setState({
presetsSubmenuAnchorRef: null,
});
}
handlePresetsMenuImport(e: SyntheticEvent): void {
@@ -284,11 +306,13 @@ const PresetSelector =
this.updatePresetState();
}
componentDidMount() {
super.componentDidMount();
this.model.presets.addOnChangedHandler(this.handlePresetsChanged);
this.updatePresetState();
}
componentWillUnmount() {
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged)
super.componentWillUnmount();
}
showPresetDialog(show: boolean) {
this.setState({
@@ -368,7 +392,7 @@ const PresetSelector =
<IconButtonEx
tooltip="More..."
style={{ flex: "0 0 auto", color: "#FFFFFF" }}
onClick={(e) => this.handlePresetMenuClick(e)}
onClick={(e) => this.handlePresetsMenuClick(e)}
size="large"
>
<MoreVertIcon style={{ opacity: 0.75 }} color="inherit" />
@@ -381,10 +405,24 @@ const PresetSelector =
TransitionComponent={Fade}
>
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
{this.state.compactHorizontalLayoutMenu ? (
<MenuItem key="a" onClick={(e) => this.handlePresetsSubmenuClick(e)}
style={{ display: 'flex', flexFlow: "row nowrap", alignItems: 'center', justifyContent: 'space-between' }}
>
Edit
<ArrowRightIcon/>
</MenuItem>
)
:(
[
(<MenuItem key="b" onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>),
(<MenuItem key="c" onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>),
(<MenuItem key="d" onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>),
(<MenuItem key="e" onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>),
]
)}
<Divider />
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
<MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem>
@@ -392,8 +430,24 @@ const PresetSelector =
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
</Menu>
</div>
<PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()}
/>
{/* Submenu */}
<Menu key="b" onClose={() => this.handlePresetsSubmenuClose()}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
anchorPosition={{left: 48, top: 0}}
anchorEl={this.state.presetsSubmenuAnchorRef}
open={this.state.presetsSubmenuAnchorRef !== null} >
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
</Menu>
{this.state.showPresetsDialog&& (
<PresetDialog show={this.state.showPresetsDialog} onDialogClose={() => this.handleDialogClose()}
/>
)}
{this.state.saveAsDialogOpen && (
<SavePresetAsDialog open={this.state.saveAsDialogOpen}
defaultName={presets.getItem(presets.selectedInstanceId)?.name ?? "My Preset"}