diff --git a/src/Banks.hpp b/src/Banks.hpp index 47f6803..4522426 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -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(); } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index ea45f4b..831de12 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -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 &presetInstanceIds) { std::lock_guard 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 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 &presets) +{ + std::lock_guard lock(mutex); + uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets); + return lastAdded; + +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 91491cd..897f524 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -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 &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 RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); + int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 01b0df9..a3bf46b 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -46,6 +46,17 @@ using namespace std; using namespace pipedal; +class CopyPresetsToBankBody { + public: + int64_t bankInstanceId_; + std::vector 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 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()); diff --git a/src/Storage.cpp b/src/Storage.cpp index 005f509..b6979ad 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -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 &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 presetsSet { presets.begin(), presets.end()}; + + std::set 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 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 &presetInstanceIds) { - int64_t newSelection = currentBank.deletePreset(presetId); + int64_t newSelection = currentBank.selectedPreset(); + for (auto presetId: presetInstanceIds) { + newSelection = currentBank.deletePreset(presetId); + } SaveCurrentBank(); return newSelection; } diff --git a/src/Storage.hpp b/src/Storage.hpp index 03ce8da..765bc6f 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -162,7 +162,7 @@ public: bool LoadPreset(int64_t presetId); - int64_t DeletePreset(int64_t presetId); + int64_t DeletePresets(const std::vector& 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 RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); + int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); }; diff --git a/todo.txt b/todo.txt index dcd4fe9..8423926 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,3 @@ -Did we fix Toob Tremolo stereo harmonic? ls diff --git a/vite/install.sh b/vite/install.sh new file mode 100755 index 0000000..3fbedb2 --- /dev/null +++ b/vite/install.sh @@ -0,0 +1,4 @@ +#!/bin/sudo /bin/bash +rm -rf /etc/pipedal/react +mkdir -p /etc/pipedal/react +cp -r dist/* /etc/pipedal/react/ diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index b8a60f5..202c11a 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -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' diff --git a/vite/src/pipedal/CopyPresetsToBankDialog.tsx b/vite/src/pipedal/CopyPresetsToBankDialog.tsx new file mode 100644 index 0000000..07c9442 --- /dev/null +++ b/vite/src/pipedal/CopyPresetsToBankDialog.tsx @@ -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 { + + 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 ( + { }} + > +
+ {!this.state.fullScreen && ( + + + { onClose(); }} + > + + + + + Copy Presets to Bank + + + + + )} + + + Bank + + + + + + + +
+
+ ); + } +} diff --git a/vite/src/pipedal/DraggableGrid.tsx b/vite/src/pipedal/DraggableGrid.tsx index 25a15b6..b402b38 100644 --- a/vite/src/pipedal/DraggableGrid.tsx +++ b/vite/src/pipedal/DraggableGrid.tsx @@ -82,7 +82,7 @@ export interface DraggableGridProps defaultSelectedIndex?: number; - onLongPress?: (itemIndex: number) => void; + onLongPress?: (event: PointerEvent, 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): 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; diff --git a/vite/src/pipedal/ImportPresetFromBankDialog.tsx b/vite/src/pipedal/ImportPresetFromBankDialog.tsx index 0dffc1f..8327a8b 100644 --- a/vite/src/pipedal/ImportPresetFromBankDialog.tsx +++ b/vite/src/pipedal/ImportPresetFromBankDialog.tsx @@ -158,44 +158,78 @@ export default class ImportPresetFromBankDialog extends ResizeResponsiveComponen onEnterKey={() => { }} >
- {!this.state.fullScreen && ( - - - { onClose(); }} - > - - + {!this.state.fullScreen ? ( + <> + + + { onClose(); }} + > + + + + + Import Presets from Bank + + + + + + Bank + + + + + ) : ( +
+ { onClose(); }} + > + + + +
+ Bank + + +
+
- - Import Presets from Bank - -
-
)} - - Bank - - - {this.state.bankPresets?.map((presetEntry) => { @@ -242,7 +276,7 @@ export default class ImportPresetFromBankDialog extends ResizeResponsiveComponen
- + ); } } diff --git a/vite/src/pipedal/PerformanceView.tsx b/vite/src/pipedal/PerformanceView.tsx index 00fa270..c360e1d 100644 --- a/vite/src/pipedal/PerformanceView.tsx +++ b/vite/src/pipedal/PerformanceView.tsx @@ -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"; diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 39c2356..88c2882 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -2192,8 +2192,8 @@ export class PiPedalModel //implements PiPedalModel } - deletePresetItem(instanceId: number): Promise { - return nullCast(this.webSocket).request("deletePresetItem", instanceId); + deletePresetItems(instanceIds: Set): Promise { + return nullCast(this.webSocket).request("deletePresetItems", Array.from(instanceIds)); } deleteBankItem(instanceId: number): Promise { @@ -3549,6 +3549,9 @@ export class PiPedalModel //implements PiPedalModel importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise { return nullCast(this.webSocket).request("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets}); } + copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise { + return nullCast(this.webSocket).request("copyPresetsToBank", {bankInstanceId: bankInstanceId, presets: presets}); + } }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/PresetDialog.tsx b/vite/src/pipedal/PresetDialog.tsx index f4b4f72..857a819 100644 --- a/vite/src/pipedal/PresetDialog.tsx +++ b/vite/src/pipedal/PresetDialog.tsx @@ -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 { show: boolean; - isEditDialog: boolean; onDialogClose: () => void; }; @@ -67,13 +70,16 @@ interface PresetDialogProps extends WithStyles { interface PresetDialogState { presets: PresetIndex; - showActionBar: boolean; + multiSelect: boolean; + showTouchActionBar: boolean; + currentItem: number; selectedItems: Set; 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 { + class extends ResizeResponsiveComponent { 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(), + selectedItems: new Set([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) { + this.setState({ + multiSelect: instanceIds.size > 1 + }) + + } + setSelections(instanceIds: Set) { + this.setState({ + selectedItems: instanceIds, + }); + this.handleSelectionUpdated(instanceIds); } setSelection(instanceId: number) { - this.setState({ + let selectedItems = new Set([instanceId]); + this.setState({ currentItem: instanceId, - selectedItems: new Set([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([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, instanceId: number): void { + e.stopPropagation(); + if (e.ctrlKey || this.state.showTouchActionBar) { + let selectedItems = new Set(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(); + + 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 (
- - this.handleItemClick(presetEntry.instanceId)} + this.handleItemClick(e, presetEntry.instanceId)} + style={{ height: 48 }} > - -
-
- -
-
- - {presetEntry.name} - -
-
-
+ + + + + +
); @@ -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([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([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 ( { this.handleDialogClose() }} TransitionComponent={Transition} @@ -395,37 +468,37 @@ const PresetDialog = withStyles( >
- + - { this.handleCloseActionBar(e); }} > - + - - Presets + + {this.state.selectedItems.size.toString() + " items selected"} - this.showActionBar(true)} > - + + { this.handleDeleteClick(); }} > + Delete - { e.stopPropagation(); e.preventDefault(); }} > - {(!this.props.isEditDialog) ? ( - this.showActionBar(false)} aria-label="close"> - - - ) : ( - - - - - )} - + + + + Presets {(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" }}> - - - { this.setState({ renameOpen: false }) }} - onOk={(text: string) => { - this.handleRenameOk(text); - } - } - /> - this.handleDeleteClick()} > - Delete - + {this.state.selectedItems.size === 1 && ( + + )} + {this.state.selectedItems.size === 1 && ( + + + )} + {this.state.renameOpen && ( + { this.setState({ renameOpen: false }) }} + onOk={(text: string) => { + this.handleRenameOk(text); + } + } + /> + )} + {this.state.selectedItems.size === 1 && ( + this.handleDeleteClick()} + style={{ opacity: this.state.selectedItems.size !== 1 ? 0.5 : 1.0 }} + > + Delete + + )} { this.onMoreClick(e) }} > @@ -466,15 +552,21 @@ const PresetDialog = withStyles( onClose={() => this.handleMoreClose()} TransitionComponent={Fade} > - { this.handleDownloadPreset(); }} > - - - - - Copy to bank... - + {this.state.selectedItems.size !== 0 && ( + { + this.handleMoreClose(); + this.handleCopyPresetsToBank(); + }} + > + + + + + Copy to bank... + - + + )} { this.handleImportPresetsFromBank(); }} > @@ -486,15 +578,17 @@ const PresetDialog = withStyles( - { this.handleDownloadPreset(); }} > - - - - - Download preset - + {this.state.selectedItems.size !== 0 && ( + { this.handleDownloadPreset(); }} > + + + + + Download preset + - + + )} { this.handleUploadPreset() }}> @@ -516,12 +610,16 @@ const PresetDialog = withStyles(
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 && ( - this.setState({ importOpen: false })} - onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)} - /> - )} + {this.state.importOpen && ( + this.setState({ importOpen: false })} + onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)} + /> + )} + {this.state.copyToOpen && ( + this.setState({ copyToOpen: false })} + onOk={(bankInstanceId) => this.handleCopyToBankDialogOk(bankInstanceId)} + /> + )} diff --git a/vite/src/pipedal/PresetSelector.tsx b/vite/src/pipedal/PresetSelector.tsx index 18b055a..acb0579 100644 --- a/vite/src/pipedal/PresetSelector.tsx +++ b/vite/src/pipedal/PresetSelector.tsx @@ -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 { @@ -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 { - + class extends ResizeResponsiveComponent { + 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 = this.handlePresetMenuClick(e)} + onClick={(e) => this.handlePresetsMenuClick(e)} size="large" > @@ -381,10 +405,24 @@ const PresetSelector = TransitionComponent={Fade} > this.handlePresetsMenuSave(e)}>Save preset - this.handlePresetsMenuSaveAs(e)}>Save preset as... - this.handlePresetsMenuRename(e)}>Rename... - this.handlePresetsMenuImport(e)}>Import from bank... - this.handlePresetsMenuNew(e)}>New... + + {this.state.compactHorizontalLayoutMenu ? ( + this.handlePresetsSubmenuClick(e)} + style={{ display: 'flex', flexFlow: "row nowrap", alignItems: 'center', justifyContent: 'space-between' }} + > + Edit + + + ) + :( + [ + ( this.handlePresetsMenuSaveAs(e)}>Save preset as...), + ( this.handlePresetsMenuRename(e)}>Rename...), + ( this.handlePresetsMenuImport(e)}>Import from bank...), + ( this.handlePresetsMenuNew(e)}>New...), + ] + + )} { this.handleDownloadPreset(e); }} >Download preset { this.handleUploadPreset(e) }}>Upload preset @@ -392,8 +430,24 @@ const PresetSelector = this.handleMenuEditPresets()}>Manage presets...
- this.handleDialogClose()} - /> + + {/* Submenu */} + this.handlePresetsSubmenuClose()} + anchorOrigin={{ vertical: 'top', horizontal: 'right' }} + anchorPosition={{left: 48, top: 0}} + anchorEl={this.state.presetsSubmenuAnchorRef} + open={this.state.presetsSubmenuAnchorRef !== null} > + this.handlePresetsMenuSaveAs(e)}>Save preset as... + this.handlePresetsMenuRename(e)}>Rename... + this.handlePresetsMenuImport(e)}>Import from bank... + this.handlePresetsMenuNew(e)}>New... + + + + {this.state.showPresetsDialog&& ( + this.handleDialogClose()} + /> + )} {this.state.saveAsDialogOpen && (