From b25e90df7b06b7caeb8ae1a573baaef8ba5854ef Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 20 Sep 2025 07:27:51 -0400 Subject: [PATCH] Import Prests from Bank --- .vscode/launch.json | 3 +- src/Banks.hpp | 570 +++++++++--------- src/PiLatencyMain.cpp | 88 ++- src/PiPedalModel.cpp | 24 +- src/PiPedalModel.hpp | 5 +- src/PiPedalSocket.cpp | 38 +- src/Storage.cpp | 150 ++++- src/Storage.hpp | 6 +- todo.txt | 7 +- .../pipedal/ImportPresetFromBankDialog.tsx | 248 ++++++++ vite/src/pipedal/PiPedalModel.tsx | 14 +- vite/src/pipedal/PresetDialog.tsx | 174 ++++-- vite/src/pipedal/PresetSelector.tsx | 90 ++- vite/src/pipedal/RenameDialog.tsx | 9 + vite/src/pipedal/SavePresetAsDialog.tsx | 189 ++++++ 15 files changed, 1215 insertions(+), 400 deletions(-) create mode 100644 vite/src/pipedal/ImportPresetFromBankDialog.tsx create mode 100644 vite/src/pipedal/SavePresetAsDialog.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index f149c6a..c6e4187 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,8 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", "args": [ - "--gplot-downloads" + "hw:M2", + "hw:M2" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/src/Banks.hpp b/src/Banks.hpp index e8d25e8..47f6803 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -23,316 +23,344 @@ #include "Pedalboard.hpp" #include "PiPedalException.hpp" -namespace pipedal { +namespace pipedal +{ -#define GETTER_SETTER_REF(name) \ - decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} \ +#define GETTER_SETTER_REF(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } \ void name(const decltype(name##_) &value) { name##_ = value; } -#define GETTER_SETTER_VEC(name) \ - decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} +#define GETTER_SETTER_VEC(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } - -#define GETTER_SETTER(name) \ - decltype(name##_) name() const { return name##_;} \ +#define GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ void name(decltype(name##_) value) { name##_ = value; } + class PresetIndexEntry + { + int64_t instanceId_ = 0; + std::string name_; -class PresetIndexEntry { - int64_t instanceId_ = 0; - std::string name_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER(name); - DECLARE_JSON_MAP(PresetIndexEntry); -}; - -class PresetIndex { - int64_t selectedInstanceId_ = -1; - bool presetChanged_ = false; - std::vector presets_; -public: - bool GetPresetName(int64_t instanceId, std::string*pResult) { - for (size_t i = 0; i < presets_.size(); ++i) + public: + PresetIndexEntry() {} + PresetIndexEntry(uint64_t instanceId, const std::string &name) + : instanceId_(instanceId), name_(name) { - if (presets_[i].instanceId() == instanceId) + } + + GETTER_SETTER(instanceId); + GETTER_SETTER(name); + DECLARE_JSON_MAP(PresetIndexEntry); + }; + + class PresetIndex + { + int64_t selectedInstanceId_ = -1; + bool presetChanged_ = false; + std::vector presets_; + + public: + bool GetPresetName(int64_t instanceId, std::string *pResult) + { + for (size_t i = 0; i < presets_.size(); ++i) { - *pResult = presets_[i].name(); - return true; - } - } - return false; - } - GETTER_SETTER(selectedInstanceId); - GETTER_SETTER_VEC(presets); - GETTER_SETTER(presetChanged); - - DECLARE_JSON_MAP(PresetIndex); -}; - - -class BankFileEntry { - int64_t instanceId_; - Pedalboard preset_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER_REF(preset); - - DECLARE_JSON_MAP(BankFileEntry); -}; -class BankFile { - std::string name_; - int64_t nextInstanceId_ = 0; - int64_t selectedPreset_ = -1; - std::vector> presets_; - -public: - GETTER_SETTER(name); - GETTER_SETTER(nextInstanceId); - GETTER_SETTER(selectedPreset); - GETTER_SETTER_VEC(presets); - - void clear() - { - nextInstanceId_ = 0; - presets_.clear(); - selectedPreset_ = -1; - } - - void move(size_t from, size_t to) - { - if (from >= this->presets_.size()) { - throw std::invalid_argument("Argument out of range."); - } - if (to >= this->presets_.size()) { - throw std::invalid_argument("Argument out of range."); - } - std::unique_ptr t = std::move(this->presets_[from]); - presets_.erase(presets_.begin()+from); - presets_.insert(presets_.begin()+to,std::move(t)); - } - void updateNextIndex() { - int64_t t = 0; - for (size_t i = 0; i < this->presets_.size(); ++i) - { - int64_t instanceId = this->presets_[i]->instanceId(); - if (instanceId > t) t = instanceId; - - } - this->nextInstanceId_ = t; - } - int64_t addPreset(const Pedalboard&preset, int64_t afterItem = -1) - { - if (hasName(preset.name())) - { - throw PiPedalStateException("A preset by that name already exists."); - } - std::unique_ptr entry = std::make_unique(); - entry->instanceId(++nextInstanceId_); - entry->preset(preset); - int64_t instanceId = entry->instanceId(); - if (afterItem == -1) - { - this->presets_.push_back(std::move(entry)); - } else { - for (auto it = this->presets_.begin(); it != this->presets_.end(); ++it) - { - if ((*it)->instanceId() == afterItem) + if (presets_[i].instanceId() == instanceId) { - ++it; - this->presets_.insert(it,std::move(entry)); - break; + *pResult = presets_[i].name(); + return true; } } + return false; } - return instanceId; - } - bool hasItem(int64_t instanceId) - { - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->instanceId() == instanceId) - { - return true; - } - } - return false; - } - bool hasName(const std::string &name) { - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->preset().name() == name) - { - return true; - } - } - return false; - } - bool renamePreset(int64_t instanceId, const std::string&name) - { - if (hasName(name)) - { - throw PiPedalStateException("A preset by that name already exists."); - } - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->instanceId() ==instanceId) - { - presets_[i]->preset().name(name); - return true; - } - } - return false; + GETTER_SETTER(selectedInstanceId); + GETTER_SETTER_VEC(presets); + GETTER_SETTER(presetChanged); - } + DECLARE_JSON_MAP(PresetIndex); + }; - BankFileEntry &getItem(int64_t instanceId) + class BankFileEntry { - for (size_t i = 0; i < presets_.size(); ++i) + int64_t instanceId_; + Pedalboard preset_; + + public: + GETTER_SETTER(instanceId); + GETTER_SETTER_REF(preset); + + DECLARE_JSON_MAP(BankFileEntry); + }; + class BankFile + { + std::string name_; + int64_t nextInstanceId_ = 0; + int64_t selectedPreset_ = -1; + std::vector> presets_; + + public: + GETTER_SETTER(name); + GETTER_SETTER(nextInstanceId); + GETTER_SETTER(selectedPreset); + GETTER_SETTER_VEC(presets); + + void clear() { - if (presets_[i]->instanceId() == instanceId) - { - return *(presets_[i].get()); - } + nextInstanceId_ = 0; + presets_.clear(); + selectedPreset_ = -1; } - throw PiPedalArgumentException("Instance not found."); - } - int64_t deletePreset(int64_t instanceId) { - for (size_t i = 0; i < presets_.size(); ++i) + + void move(size_t from, size_t to) { - if (presets_[i]->instanceId() == instanceId) + if (from >= this->presets_.size()) { - presets_.erase(presets_.begin()+i); - int64_t newSelection; - if (i < presets_.size()) + throw std::invalid_argument("Argument out of range."); + } + if (to >= this->presets_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + std::unique_ptr t = std::move(this->presets_[from]); + presets_.erase(presets_.begin() + from); + presets_.insert(presets_.begin() + to, std::move(t)); + } + void updateNextIndex() + { + int64_t t = 0; + for (size_t i = 0; i < this->presets_.size(); ++i) + { + int64_t instanceId = this->presets_[i]->instanceId(); + if (instanceId > t) + t = instanceId; + } + this->nextInstanceId_ = t; + } + int64_t addPreset(const Pedalboard &preset, int64_t afterItem = -1) + { + if (hasName(preset.name())) + { + throw PiPedalStateException("A preset by that name already exists."); + } + std::unique_ptr entry = std::make_unique(); + entry->instanceId(++nextInstanceId_); + entry->preset(preset); + int64_t instanceId = entry->instanceId(); + if (afterItem == -1) + { + this->presets_.push_back(std::move(entry)); + } + else + { + for (auto it = this->presets_.begin(); it != this->presets_.end(); ++it) { - newSelection = presets_[i]->instanceId(); - } else if (presets_.size() > 1) { - newSelection = presets_[0]->instanceId(); - } else { - // zero length? We can never have a zero-length bank. - // Add a default preset and make it the selected preset. - Pedalboard pedalboard = Pedalboard::MakeDefault(); - this->addPreset(pedalboard); - newSelection = presets_[0]->instanceId(); + if ((*it)->instanceId() == afterItem) + { + ++it; + this->presets_.insert(it, std::move(entry)); + break; + } } - if (instanceId == this->selectedPreset_) + } + return instanceId; + } + bool hasItem(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) { - this->selectedPreset_ = newSelection; + return true; } - return newSelection; } + return false; } - throw PiPedalStateException("Preset not found."); - } - - DECLARE_JSON_MAP(BankFile); -}; - -class BankIndexEntry { - int64_t instanceId_ = 0; - std::string name_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER_REF(name); - - DECLARE_JSON_MAP(BankIndexEntry); -}; - -class BankIndex { - int64_t selectedBank_ = 0; - int64_t nextInstanceId_ = 0; - std::vector entries_; -public: - GETTER_SETTER(selectedBank); - GETTER_SETTER_VEC(entries); - - DECLARE_JSON_MAP(BankIndex); - - bool hasName(const std::string&name) const { - for (size_t i = 0; i < this->entries_.size(); ++i) + bool hasName(const std::string &name) { - if (this->entries_[i].name() == name) return true; - } - return false; - } - - void move(size_t from, size_t to) - { - if (from >= this->entries_.size()) { - throw std::invalid_argument("Argument out of range."); - } - if (to >= this->entries_.size()) { - throw std::invalid_argument("Argument out of range."); - } - BankIndexEntry t = std::move(this->entries_[from]); - entries_.erase(entries_.begin()+from); - entries_.insert(entries_.begin()+to,std::move(t)); - } - - void clear() { - entries_.clear(); - selectedBank_ = 0; - } - int64_t addBank(int64_t afterId,const std::string& name) - { - BankIndexEntry bank; - bank.name(name); - bank.instanceId(++nextInstanceId_); - - for (size_t i = 0; i < this->entries_.size(); ++i) - { - if (entries_[i].instanceId() == afterId) + for (size_t i = 0; i < presets_.size(); ++i) { - entries_.insert(entries_.begin()+(i+1), bank); - return bank.instanceId(); + if (presets_[i]->preset().name() == name) + { + return true; + } } + return false; } - // else at the end. - entries_.push_back(bank); - return bank.instanceId(); - } - BankIndexEntry* getEntryByName(const std::string &name) - { - for (size_t i = 0; i < entries_.size(); ++i) + bool renamePreset(int64_t instanceId, const std::string &name) { - if (entries_[i].name() == name) + if (hasName(name)) { - return &entries_[i]; + throw PiPedalStateException("A preset by that name already exists."); } - } - return nullptr; - } - BankIndexEntry&getBankIndexEntry(int64_t instanceId) - { - for (size_t i = 0; i < entries_.size(); ++i) - { - if (entries_[i].instanceId() == instanceId) + for (size_t i = 0; i < presets_.size(); ++i) { - return entries_[i]; + if (presets_[i]->instanceId() == instanceId) + { + presets_[i]->preset().name(name); + return true; + } } + return false; } - throw PiPedalArgumentException("Bank not found."); - } - const BankIndexEntry&getBankIndexEntry(int64_t instanceId) const - { - for (size_t i = 0; i < entries_.size(); ++i) - { - if (entries_[i].instanceId() == instanceId) - { - return entries_[i]; - } - } - throw PiPedalArgumentException("Bank not found."); - } -}; + BankFileEntry &getItem(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) + { + return *(presets_[i].get()); + } + } + throw PiPedalArgumentException("Instance not found."); + } + int64_t deletePreset(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) + { + presets_.erase(presets_.begin() + i); + int64_t newSelection; + if (i < presets_.size()) + { + newSelection = presets_[i]->instanceId(); + } + else if (presets_.size() > 1) + { + newSelection = presets_[0]->instanceId(); + } + else + { + // zero length? We can never have a zero-length bank. + // Add a default preset and make it the selected preset. + Pedalboard pedalboard = Pedalboard::MakeDefault(); + this->addPreset(pedalboard); + newSelection = presets_[0]->instanceId(); + } + if (instanceId == this->selectedPreset_) + { + this->selectedPreset_ = newSelection; + } + return newSelection; + } + } + throw PiPedalStateException("Preset not found."); + } + + DECLARE_JSON_MAP(BankFile); + }; + + class BankIndexEntry + { + int64_t instanceId_ = 0; + std::string name_; + + public: + GETTER_SETTER(instanceId); + GETTER_SETTER_REF(name); + + DECLARE_JSON_MAP(BankIndexEntry); + }; + + class BankIndex + { + int64_t selectedBank_ = 0; + int64_t nextInstanceId_ = 0; + std::vector entries_; + + public: + GETTER_SETTER(selectedBank); + GETTER_SETTER_VEC(entries); + + DECLARE_JSON_MAP(BankIndex); + + bool hasName(const std::string &name) const + { + for (size_t i = 0; i < this->entries_.size(); ++i) + { + if (this->entries_[i].name() == name) + return true; + } + return false; + } + + void move(size_t from, size_t to) + { + if (from >= this->entries_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + if (to >= this->entries_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + BankIndexEntry t = std::move(this->entries_[from]); + entries_.erase(entries_.begin() + from); + entries_.insert(entries_.begin() + to, std::move(t)); + } + + void clear() + { + entries_.clear(); + selectedBank_ = 0; + } + int64_t addBank(int64_t afterId, const std::string &name) + { + BankIndexEntry bank; + bank.name(name); + bank.instanceId(++nextInstanceId_); + + for (size_t i = 0; i < this->entries_.size(); ++i) + { + if (entries_[i].instanceId() == afterId) + { + entries_.insert(entries_.begin() + (i + 1), bank); + return bank.instanceId(); + } + } + // else at the end. + entries_.push_back(bank); + return bank.instanceId(); + } + BankIndexEntry *getEntryByName(const std::string &name) + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].name() == name) + { + return &entries_[i]; + } + } + return nullptr; + } + BankIndexEntry &getBankIndexEntry(int64_t instanceId) + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].instanceId() == instanceId) + { + return entries_[i]; + } + } + throw PiPedalArgumentException("Bank not found."); + } + const BankIndexEntry &getBankIndexEntry(int64_t instanceId) const + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].instanceId() == instanceId) + { + return entries_[i]; + } + } + throw PiPedalArgumentException("Bank not found."); + } + }; #undef GETTER_SETTER #undef GETTER_SETTER_VEC #undef GETTER_SETTER_REF } - - diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index e1616ac..01887e5 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -33,6 +33,7 @@ #include #include #include +#include using namespace pipedal; @@ -59,7 +60,7 @@ void PrintHelp() pp << Indent(0) << "Syntax\n\n"; pp << Indent(2) << "pipedal_latency_test [] []\n\n"; pp << "where is the name of an ALSA capture device and is the name of a playback device. " - "If is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; + "If is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; pp << Indent(0) << "Options\n\n"; pp << Indent(15); @@ -85,13 +86,13 @@ void PrintHelp() << "PiPedal Latency Tester measures internal buffer delays as well as operating system and " << "signal delays in hardware peripherals. Latency figures will therefore be somewhat higher than " << "most reported latency figures which typically only include internal buffer delays.\n\n"; - - pp - << "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some " - "stress on the audio system.\n\n" - << "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n" - << Indent(6) << "sudo systemctl stop pipedald\n\n"; + pp + << "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some " + "stress on the audio system.\n\n" + + << "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n" + << Indent(6) << "sudo systemctl stop pipedald\n\n"; pp << Indent(0) << "Examples\n\n"; pp << Indent(2) << "pipedal_latency_test --list\n\n"; @@ -123,7 +124,6 @@ void ListDevices() } } - using ChannelsT = std::vector; class AlsaTester : private AudioDriverHost @@ -169,12 +169,13 @@ public: delete[] inputBuffers; delete[] outputBuffers; } - std::vector SelectChannels(const std::vector&available, const std::vector& selection) + std::vector SelectChannels(const std::vector &available, const std::vector &selection) { - if (selection.size() == 0) return available; + if (selection.size() == 0) + return available; std::vector result; - for (int sel: selection) + for (int sel : selection) { if (sel < 0 || sel >= available.size()) { @@ -195,14 +196,13 @@ public: JackConfiguration jackConfiguration; jackConfiguration.AlsaInitialize(serverSettings); - auto & availableInputs = jackConfiguration.inputAudioPorts(); - auto & availableOutputs = jackConfiguration.outputAudioPorts(); - + auto &availableInputs = jackConfiguration.inputAudioPorts(); + auto &availableOutputs = jackConfiguration.outputAudioPorts(); std::vector inputAudioPorts, outputAudioPorts; - inputAudioPorts = SelectChannels(availableInputs,this->inputChannels); - outputAudioPorts = SelectChannels(availableOutputs,this->outputChannels); + inputAudioPorts = SelectChannels(availableInputs, this->inputChannels); + outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels); JackChannelSelection channelSelection( inputAudioPorts, outputAudioPorts, @@ -360,7 +360,6 @@ public: } virtual void OnAudioTerminated() { - } virtual void OnProcess(size_t nFrames) { @@ -413,7 +412,7 @@ TestResult RunLatencyTest( const ChannelsT &outputChannels, uint32_t sampleRate, int bufferSize, int buffers) { - AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers); + AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers); return tester.Test(); } @@ -432,6 +431,28 @@ static std::string overheadDisplay(float value) return s.str(); } +static bool testRealtimePriorityPrivileges() +{ + + struct sched_param currentParam; + int currentPolicy = sched_getscheduler(0); + if (currentPolicy == -1) { + return false; + } + if (sched_getparam(0, ¤tParam) != 0) { + return false; + } + struct sched_param param; + param.sched_priority = 85; + if (sched_setscheduler(0, SCHED_RR, ¶m) != 0) + { + return false; + } + // Restore normal priority + sched_setscheduler(0, currentPolicy, ¤tParam); + return true; +} + void RunLatencyTest( const std::string &inputDeviceId, const std::string &outputDeviceId, @@ -439,8 +460,20 @@ void RunLatencyTest( const ChannelsT &outputChannels, uint32_t sampleRate) { + PrettyPrinter pp; - pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n"; + + if (!testRealtimePriorityPrivileges()) + { + pp << "Unable to enable realtime scheduling. Add your user id to the pipedal_d group to fix this problem:" << "\n\n"; + pp.AddIndent(4); + pp << "sudo usermod -a -G pipedal_d " << "\n\n"; + pp.AddIndent(-4); + pp << "You will need to log out or reboot your system in order for the change to take effect.\n\n"; + throw std::runtime_error("Unable to set relatime thread priority."); + } + + pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n"; const int SIZE_COLUMN_WIDTH = 8; const int BUFFERS_COLUMN_WIDTH = 20; @@ -467,7 +500,7 @@ void RunLatencyTest( for (auto bufferCount : bufferCounts) { - auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount); + auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, bufferCount); pp.Column(column); column += BUFFERS_COLUMN_WIDTH; @@ -495,8 +528,7 @@ void RunLatencyTest( } } - -ChannelsT ParseChannels(const std::string&channels) +ChannelsT ParseChannels(const std::string &channels) { ChannelsT result; std::stringstream s(channels); @@ -504,9 +536,11 @@ ChannelsT ParseChannels(const std::string&channels) while (true) { int c = s.peek(); - if (c == -1) break; + if (c == -1) + break; - if (c == ',') { + if (c == ',') + { s.get(); c = s.peek(); } @@ -518,14 +552,12 @@ ChannelsT ParseChannels(const std::string&channels) while (s.peek() >= '0' && s.peek() <= '9') { c = s.get(); - v = v*10 + c-'0'; + v = v * 10 + c - '0'; } result.push_back(v); } return result; - - } int main(int argc, const char **argv) @@ -567,7 +599,7 @@ std: inputChannels = ParseChannels(strInputChannels); outputChannels = ParseChannels(strOutputChannels); - std::string inDev = parser.Arguments()[0]; + std::string inDev = parser.Arguments()[0]; std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev; RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate); } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index ff3625f..ea45f4b 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -887,7 +887,7 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string & return presetId; } -int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId) +int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId,const std::string &name, int64_t saveAfterInstanceId) { std::lock_guard guard{mutex}; @@ -897,7 +897,7 @@ int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &n UpdateVst3Settings(pedalboard); pedalboard.name(name); - int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId); + int64_t result = storage.SaveCurrentPresetAs(pedalboard, bankInstanceId, name, saveAfterInstanceId); FirePresetsChanged(clientId); return result; } @@ -3257,3 +3257,23 @@ bool PiPedalModel::HasTone3000Auth() const { return storage.GetTone3000Auth() != ""; } + +std::vector PiPedalModel::RequestBankPresets(int64_t bankInstanceId) +{ + std::lock_guard lock(mutex); + + return storage.RequestBankPresets(bankInstanceId); + +} + +int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets) +{ + std::lock_guard lock(mutex); + uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets); + if (lastAdded != -1) { + + } + FirePresetsChanged(-1); + return lastAdded; + +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 012a49e..91491cd 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -378,9 +378,10 @@ namespace pipedal int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1); void UploadPluginPresets(const PluginPresets &pluginPresets); void SaveCurrentPreset(int64_t clientId); - int64_t SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId = -1); + int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name); + void LoadPreset(int64_t clientId, int64_t instanceId); bool UpdatePresets(int64_t clientId, const PresetIndex &presets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets); @@ -493,6 +494,8 @@ namespace pipedal void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); + std::vector RequestBankPresets(int64_t bankInstanceId); + int64_t ImportPresetsFromBank(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 600b17b..01b0df9 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -46,6 +46,27 @@ using namespace std; using namespace pipedal; + +class ImportPresetsFromBankBody { + public: + int64_t bankInstanceId_; + std::vector presets_; + DECLARE_JSON_MAP(ImportPresetsFromBankBody); +}; +JSON_MAP_BEGIN(ImportPresetsFromBankBody) +JSON_MAP_REFERENCE(ImportPresetsFromBankBody, bankInstanceId) +JSON_MAP_REFERENCE(ImportPresetsFromBankBody, presets) +JSON_MAP_END() + +class RequestBankPresetsBody { +public: + int64_t bankInstanceId_; + DECLARE_JSON_MAP(RequestBankPresetsBody); +}; +JSON_MAP_BEGIN(RequestBankPresetsBody) +JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId) +JSON_MAP_END() + class PathPatchPropertyChangedBody { public: @@ -336,6 +357,7 @@ class SaveCurrentPresetAsBody { public: int64_t clientId_ = -1; + int64_t bankInstanceId_ = -1; std::string name_; int64_t saveAfterInstanceId_ = -1; @@ -343,6 +365,7 @@ public: }; JSON_MAP_BEGIN(SaveCurrentPresetAsBody) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId) +JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, bankInstanceId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId) JSON_MAP_END() @@ -1314,7 +1337,7 @@ public: { SaveCurrentPresetAsBody body; pReader->read(&body); - int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.name_, body.saveAfterInstanceId_); + int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_,body.name_, body.saveAfterInstanceId_); Reply(replyTo, "saveCurrentPresetsAs", result); } else if (message == "setSelectedPedalboardPlugin") @@ -1794,6 +1817,19 @@ public: { std::vector result = model.GetAlsaSequencerPorts(); this->Reply(replyTo,"getAlsaSequencerPorts", result); + } else if (message == "requestBankPresets") { + + RequestBankPresetsBody args; + pReader->read(&args); + auto result = this->model.RequestBankPresets(args.bankInstanceId_); + this->Reply(replyTo,"requestBankPresets",result); + + } + else if (message == "importPresetsFromBank") { + ImportPresetsFromBankBody args; + pReader->read(&args); + auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_); + this->Reply(replyTo,"importPresetsFromBank",result); } else { diff --git a/src/Storage.cpp b/src/Storage.cpp index d5b65b1..005f509 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -225,7 +225,6 @@ static void CopyDirectory(const std::filesystem::path &source, const std::filesy } } - void Storage::MaybeCopyDefaultPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -239,13 +238,17 @@ void Storage::MaybeCopyDefaultPresets() } static void removeFileNoThrow(const std::filesystem::path &path) -{ - try { +{ + try + { fs::remove(path); - } catch (const std::exception&) + } + catch (const std::exception &) { } } + + void Storage::UpgradeFactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -520,7 +523,6 @@ void Storage::SavePluginPresetIndex() } } - void Storage::SaveBankIndex() { pipedal::ofstream_synced os; @@ -653,17 +655,133 @@ void Storage::SaveCurrentPreset(const Pedalboard &pedalboard) item.preset(pedalboard); SaveCurrentBank(); } -int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, const std::string &name, int64_t saveAfterInstanceId) +int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId) { Pedalboard newPedalboard = pedalboard; newPedalboard.name(name); - int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); - currentBank.selectedPreset(newInstanceId); - SaveCurrentBank(); - return newInstanceId; + if (bankInstanceId == this->bankIndex.selectedBank()) + { + int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); + currentBank.selectedPreset(newInstanceId); + SaveCurrentBank(); + return newInstanceId; + } + else + { + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + try + { + BankFile bankFile; + LoadBankFile(indexEntry.name(), &(bankFile)); + int64_t newInstanceId = bankFile.addPreset(newPedalboard, -1); + SaveBankFile(indexEntry.name(), bankFile); + return -1; + } + catch (const std::exception &e) + { + throw std::logic_error(SS("Bank file corrupted. " << e.what() << "(" << GetBankFileName(indexEntry.name()) << ")")); + } + } } +static std::string stripNumericSuffix(const std::string &name) { + // remove (digit*) from the end of the string. + // Find the last '(' character + size_t pos = name.find_last_of('('); + if (pos == std::string::npos) { + return name; + } + + // Check if everything between '(' and ')' is digits + size_t len = name.length()-1; + if (name[len] != ')') + { + return name; + } + bool allDigits = true; + for (size_t i = pos + 1; i < len - 1; ++i) { + if (name[i] < '0' || name[i] > '9') { + allDigits = false; + break; + } + } + + if (!allDigits) { + return name; + } + + // Remove trailing spaces before the '(' + while (pos > 0 && name[pos - 1] == ' ') { + --pos; + } + + return name.substr(0, pos); +} +static std::string makeUniqueName(const std::string &name, const std::set&existingNames) +{ + if (!existingNames.contains(name)) + { + return name; + } + std::string baseName = stripNumericSuffix(name); + + size_t i = 2; + while (true) { + std::string newName = SS(baseName << " (" << i << ")"); + if (!existingNames.contains(newName)) { + return newName; + } + ++i; + } +} +int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets) +{ + if (bankIndex.selectedBank() == bankInstanceId) { + throw std::runtime_error("Can't import to self."); + } + std::set presetsSet { presets.begin(), presets.end()}; + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + std::set existingNames; + + for (auto&preset: this->currentBank.presets()) { + existingNames.insert(preset->preset().name()); + } + BankFile bankFile; + LoadBankFile(indexEntry.name(),&bankFile); + int64_t lastPresetId = -1; + for (auto &presetEntry: bankFile.presets()) { + if (presetsSet.contains(presetEntry->instanceId())) { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + existingNames.insert(uniqueName); + Pedalboard t = presetEntry->preset(); + t.name(uniqueName); + lastPresetId = this->currentBank.addPreset(t); + } + } + SaveCurrentBank(); + return lastPresetId; +} + +std::vector Storage::RequestBankPresets(int64_t bankInstanceId) +{ + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + std::vector result; + BankFile bankFile; + LoadBankFile(indexEntry.name(),&bankFile); + for (auto &preset: bankFile.presets()) { + result.push_back( + PresetIndexEntry(preset->instanceId(),preset->preset().name()) + ); + } + return result; +} + + + void Storage::SetPresetIndex(const PresetIndex &presets) { // painful because we must move unique_ptrs. @@ -826,25 +944,30 @@ int64_t Storage::CreateNewPreset() } } newPedalboard.name(name); - return this->currentBank.addPreset(newPedalboard, -1); + auto t = this->currentBank.addPreset(newPedalboard, -1); + SaveCurrentBank(); + return t; } int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &fromItem = this->currentBank.getItem(fromId); + int64_t result; if (toId == -1) { Pedalboard newPedalboard = fromItem.preset(); std::string name = GetPresetCopyName(fromItem.preset().name()); newPedalboard.name(name); - return this->currentBank.addPreset(newPedalboard, fromId); + result = this->currentBank.addPreset(newPedalboard, fromId); } else { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); - return toId; + result = toId; } + SaveCurrentBank(); + return result; } void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection) @@ -2861,6 +2984,7 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson) return v.to_string(); } + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index e31212c..03ce8da 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -149,7 +149,7 @@ public: int64_t GetBankByMidiBankNumber(uint8_t bankNumber); const Pedalboard& GetCurrentPreset(); void SaveCurrentPreset(const Pedalboard&pedalboard); - int64_t SaveCurrentPresetAs(const Pedalboard&pedalboard, const std::string&namne,int64_t saveAfterInstanceId = -1); + int64_t SaveCurrentPresetAs(const Pedalboard&pedalboard, int64_t bankInstanceId,const std::string&namne,int64_t saveAfterInstanceId = -1); int64_t GetCurrentPresetId() const; void GetPresetIndex(PresetIndex*pResult); @@ -268,6 +268,10 @@ public: void LoadTone3000Auth(); void SetTone3000Auth(const std::string&apiKey); std::string GetTone3000Auth() const; + + std::vector RequestBankPresets(int64_t bankInstanceId); + int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); + }; diff --git a/todo.txt b/todo.txt index 0cdcf93..dcd4fe9 100644 --- a/todo.txt +++ b/todo.txt @@ -1,9 +1,4 @@ - -double select on double-click/ok in file dialog. -sudo apt install libbz2-dev - -README, license files in file browser. - +Did we fix Toob Tremolo stereo harmonic? ls diff --git a/vite/src/pipedal/ImportPresetFromBankDialog.tsx b/vite/src/pipedal/ImportPresetFromBankDialog.tsx new file mode 100644 index 0000000..0dffc1f --- /dev/null +++ b/vite/src/pipedal/ImportPresetFromBankDialog.tsx @@ -0,0 +1,248 @@ +// 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 Divider from '@mui/material/Divider'; +import Toolbar from "@mui/material/Toolbar"; +import IconButtonEx from "./IconButtonEx"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import Typography from "@mui/material/Typography"; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Checkbox from '@mui/material/Checkbox'; +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, PresetIndexEntry } from './PiPedalModel'; +import { BankIndexEntry } from './Banks'; + + +export interface ImportPresetFromBankDialogProps { + open: boolean, + onOk: (bankInstanceId: number, presets: number[]) => void, + onClose: () => void +}; + +export interface ImportPresetFromBankDialogState { + selectedBank: number; + banks: BankIndexEntry[]; + bankPresets: PresetIndexEntry[] | null; + selectedPresets: number[]; + + fullScreen: boolean; +}; + +function setSavedDefaultBankSelection(bankInstanceId: number) { + localStorage.setItem('pipedal_defaultBankSelection', bankInstanceId.toString()); +} +function getSavedDefaultBankSelection() { + let defaultValue = localStorage.getItem('pipedal_defaultBankSelection'); + if (defaultValue) { + return parseInt(defaultValue); + } + return -1; +} +export default class ImportPresetFromBankDialog extends ResizeResponsiveComponent { + + model: PiPedalModel; + constructor(props: ImportPresetFromBankDialogProps) { + super(props); + this.model = PiPedalModel.getInstance(); + let banks = this.getBanks(); + let selectedBank = this.getDefaultBankSelection(banks); + this.state = { + banks: banks, + selectedBank: selectedBank, + fullScreen: this.windowSize.height < 550, + bankPresets: null, + selectedPresets: [] + + + }; + } + 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; + } + + private mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + this.setState({ fullScreen: height < 550 }) + } + + + requestPresets(bankInstanceId: number) { + this.model.requestBankPresets(bankInstanceId).then((presets) => { + if (!this.mounted) return; + this.setState({ bankPresets: presets, selectedPresets: [] }); + }).catch((error) => { + this.model.showAlert("Error requesting presets: " + error); + }); + } + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + this.requestPresets(this.state.selectedBank); + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() { + } + checkForIllegalCharacters(filename: string) { + } + + handleBankChanged(bankId: number) { + setSavedDefaultBankSelection(bankId); + this.setState({ selectedBank: bankId }); + this.requestPresets(bankId); + } + + handleOk() { + if (this.state.selectedPresets.length === 0) return; + this.props.onOk(this.state.selectedBank, this.state.selectedPresets); + } + + render() { + let props = this.props; + let { open, onClose } = props; + + const handleClose = () => { + onClose(); + }; + return ( + { }} + > +
+ {!this.state.fullScreen && ( + + + { onClose(); }} + > + + + + + Import Presets from Bank + + + + + )} + + + Bank + + + + + + {this.state.bankPresets?.map((presetEntry) => { + const labelId = `checkbox-list-label-${presetEntry.instanceId}`; + + return ( + { + let selectedPresets = this.state.selectedPresets; + let index = selectedPresets.indexOf(presetEntry.instanceId); + if (index === -1) { + selectedPresets.push(presetEntry.instanceId); + } else { + selectedPresets.splice(index, 1); + } + this.setState({ selectedPresets: [...selectedPresets] }); + }}> + + + + + + ); + })} + + + + + + + +
+
+ ); + } +} diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 38e6b2b..39c2356 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -2049,13 +2049,14 @@ export class PiPedalModel //implements PiPedalModel } - saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise { + saveCurrentPresetAs(bankInstanceId: number, newName: string, saveAfterInstanceId = -1): Promise { // default behaviour is to save after the currently selected preset. if (saveAfterInstanceId === -1) { saveAfterInstanceId = this.presets.get().selectedInstanceId; } let request: any = { clientId: this.clientId, + bankInstanceId: bankInstanceId, name: newName, saveAfterInstanceId: saveAfterInstanceId @@ -2064,7 +2065,9 @@ export class PiPedalModel //implements PiPedalModel return nullCast(this.webSocket) .request("saveCurrentPresetAs", request) .then((newPresetId) => { - this.loadPreset(newPresetId); + if (bankInstanceId === this.banks.get().selectedBank) { + this.loadPreset(newPresetId); + } return newPresetId; }); } @@ -3539,6 +3542,13 @@ export class PiPedalModel //implements PiPedalModel this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId }); } + requestBankPresets(bankInstanceId: number): Promise { + return nullCast(this.webSocket).request("requestBankPresets", {bankInstanceId: bankInstanceId}); + } + + importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise { + return nullCast(this.webSocket).request("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets}); + } }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/PresetDialog.tsx b/vite/src/pipedal/PresetDialog.tsx index 935ce03..f4b4f72 100644 --- a/vite/src/pipedal/PresetDialog.tsx +++ b/vite/src/pipedal/PresetDialog.tsx @@ -17,9 +17,12 @@ // 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, Component } from 'react'; +import ImportPresetFromBankDialog from './ImportPresetFromBankDialog'; +import Icon from '@mui/material/Icon'; +import Divider from '@mui/material/Divider'; import { css } from '@emotion/react'; -import {isDarkMode} from './DarkMode'; +import { isDarkMode } from './DarkMode'; import IconButtonEx from './IconButtonEx'; import Typography from '@mui/material/Typography'; import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel'; @@ -43,8 +46,8 @@ 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'; -import {createStyles} from './WithStyles'; +import Slide, { SlideProps } from '@mui/material/Slide'; +import { createStyles } from './WithStyles'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; @@ -66,9 +69,11 @@ interface PresetDialogState { showActionBar: boolean; - selectedItem: number; + currentItem: number; + selectedItems: Set; renameOpen: boolean; + importOpen: boolean; moreMenuAnchorEl: HTMLElement | null; openUploadPresetDialog: boolean; @@ -79,7 +84,7 @@ interface PresetDialogState { const styles = (theme: Theme) => createStyles({ listIcon: css({ - width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary + width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary }), dialogAppBar: css({ position: 'relative', @@ -146,19 +151,27 @@ const PresetDialog = withStyles( this.state = { presets: presets, showActionBar: false, - selectedItem: presets.selectedInstanceId, + currentItem: presets.selectedInstanceId, + selectedItems: new Set(), renameOpen: false, + importOpen: false, moreMenuAnchorEl: null, openUploadPresetDialog: false }; this.handlePresetsChanged = this.handlePresetsChanged.bind(this); - + + } + setSelection(instanceId: number) { + this.setState({ + currentItem: instanceId, + selectedItems: new Set([instanceId]) + }); } selectItemAtIndex(index: number) { let instanceId = this.state.presets.presets[index].instanceId; - this.setState({ selectedItem: instanceId }); + this.setSelection(instanceId); } isEditMode() { return this.state.showActionBar || this.props.isEditDialog; @@ -170,7 +183,7 @@ const PresetDialog = withStyles( handleDownloadPreset() { this.handleMoreClose(); - this.model.download("downloadPreset", this.state.selectedItem); + this.model.download("downloadPreset", this.state.currentItem); } handleUploadPreset() { this.handleMoreClose(); @@ -189,8 +202,10 @@ const PresetDialog = withStyles( if (!presets.areEqual(this.state.presets, false)) // avoid a bunch of peculiar effects if we update while a drag is in progress { // if we don't have a valid selection, then use the current preset. - if (this.state.presets.getItem(this.state.selectedItem) == null) { - this.setState({ presets: presets, selectedItem: presets.selectedInstanceId }); + if (this.state.presets.getItem(this.state.currentItem) == null) { + this.setState({ presets: presets, currentItem: presets.selectedInstanceId, + selectedItems: new Set([presets.selectedInstanceId]) + }); } else { this.setState({ presets: presets }); } @@ -208,7 +223,7 @@ const PresetDialog = withStyles( } getSelectedIndex() { - let instanceId = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId; + let instanceId = this.isEditMode() ? this.state.currentItem : this.state.presets.selectedInstanceId; let presets = this.state.presets; for (let i = 0; i < presets.presets.length; ++i) { if (presets.presets[i].instanceId === instanceId) return i; @@ -217,12 +232,12 @@ const PresetDialog = withStyles( } handleDeleteClick() { - if (!this.state.selectedItem) return; - let selectedItem = this.state.selectedItem; - if (selectedItem !== -1) { - this.model.deletePresetItem(selectedItem) - .then((selectedItem: number) => { - this.setState({ selectedItem: selectedItem }); + 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); @@ -236,7 +251,7 @@ const PresetDialog = withStyles( handleItemClick(instanceId: number): void { if (this.isEditMode()) { - this.setState({ selectedItem: instanceId }); + this.setSelection(instanceId); } else { this.model.loadPreset(instanceId); this.props.onDialogClose(); @@ -251,19 +266,21 @@ const PresetDialog = withStyles( mapElement(el: any): React.ReactNode { let presetEntry = el as PresetIndexEntry; const classes = withStyles.getClasses(this.props); - let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId; + let selected = + this.isEditMode() ? this.state.selectedItems.has(presetEntry.instanceId) : + (presetEntry.instanceId === this.state.presets.selectedInstanceId); return ( -
+
this.handleItemClick(presetEntry.instanceId)} > - +
- +
@@ -287,30 +304,33 @@ const PresetDialog = withStyles( moveElement(from: number, to: number): void { let newPresets = this.state.presets.clone(); newPresets.movePreset(from, to); + let toInstanceId = newPresets.presets[to].instanceId; this.setState({ presets: newPresets, - selectedItem: newPresets.presets[to].instanceId + currentItem: toInstanceId, + selectedItems: new Set([toInstanceId]) + }); this.updateServerPresets(newPresets); } getSelectedName(): string { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (item) return item.name; return ""; } handleRenameClick() { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (item) { this.setState({ renameOpen: true }); } } handleRenameOk(text: string) { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (!item) return; if (item.name !== text) { - this.model.renamePresetItem(this.state.selectedItem, text) + this.model.renamePresetItem(this.state.currentItem, text) .catch((error) => { this.onError(error); }); @@ -319,11 +339,14 @@ const PresetDialog = withStyles( this.setState({ renameOpen: false }); } handleCopy() { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (!item) return; - this.model.duplicatePreset(this.state.selectedItem) + this.model.duplicatePreset(this.state.currentItem) .then((newId) => { - this.setState({ selectedItem: newId }); + this.setState({ + currentItem: newId, + selectedItems: new Set([newId]) + }); }).catch((error) => { this.onError(error); }); @@ -333,6 +356,29 @@ const PresetDialog = withStyles( this.model?.showAlert(error); } + handleImportPresetsFromBank() + { + this.handleMoreClose(); + this.setState({ importOpen: true }); + } + handleImportDialogOk(bankInstanceId: number, presets: number[]): void { + this.setState({ importOpen: false }); + this.model.importPresetsFromBank(bankInstanceId, presets) + .then((instanceId) => { + if (instanceId !== -1) { + this.setSelection(instanceId); + setTimeout(() => { + let el = document.getElementById("psetdlgItem_" + instanceId); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + } + },0); + } + }) + .catch((error) => { + this.model.showAlert(error); + }); + } render() { @@ -344,9 +390,9 @@ const PresetDialog = withStyles( return ( { this.handleDialogClose() }} TransitionComponent={Transition} - style={{userSelect: "none"}} - onEnterKey={()=>{}} - > + style={{ userSelect: "none" }} + onEnterKey={() => { }} + >
@@ -359,7 +405,7 @@ const PresetDialog = withStyles( Presets - this.showActionBar(true)} > + this.showActionBar(true)} > @@ -382,12 +428,13 @@ const PresetDialog = withStyles( Presets - {(this.state.presets.getItem(this.state.selectedItem) != null) + {(this.state.presets.getItem(this.state.currentItem) != null) && ( -
+
@@ -419,6 +466,26 @@ const PresetDialog = withStyles( onClose={() => this.handleMoreClose()} TransitionComponent={Fade} > + { this.handleDownloadPreset(); }} > + + + + + Copy to bank... + + + + { this.handleImportPresetsFromBank(); }} > + + + + + Import presets from bank... + + + + + { this.handleDownloadPreset(); }} > @@ -464,14 +531,21 @@ const PresetDialog = withStyles(
- this.setState({selectedItem: instanceId}) } - uploadAfter={this.state.selectedItem} - open={this.state.openUploadPresetDialog} - onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> + this.setSelection(instanceId)} + 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)} + /> + )} diff --git a/vite/src/pipedal/PresetSelector.tsx b/vite/src/pipedal/PresetSelector.tsx index a903f79..18b055a 100644 --- a/vite/src/pipedal/PresetSelector.tsx +++ b/vite/src/pipedal/PresetSelector.tsx @@ -25,7 +25,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; import { withStyles } from "tss-react/mui"; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import PresetDialog from './PresetDialog'; import Menu from '@mui/material/Menu'; @@ -33,9 +33,12 @@ import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; import Divider from '@mui/material/Divider'; import RenameDialog from './RenameDialog' +import SavePresetAsDialog from './SavePresetAsDialog'; +import ImportPresetFromBankDialog from './ImportPresetFromBankDialog'; + import Select from '@mui/material/Select'; import UploadPresetDialog from './UploadPresetDialog'; -import {isDarkMode} from './DarkMode'; +import { isDarkMode } from './DarkMode'; interface PresetSelectorProps extends WithStyles { @@ -49,6 +52,9 @@ interface PresetSelectorState { showEditPresetsDialog: boolean; presetsMenuAnchorRef: HTMLElement | null; + saveAsDialogOpen: boolean; + importDialogOpen: boolean; + renameDialogOpen: boolean; renameDialogTitle: string; renameDialogDefaultName: string; @@ -59,7 +65,7 @@ interface PresetSelectorState { } -const selectColor = isDarkMode()? "#888": "#FFFFFF"; +const selectColor = isDarkMode() ? "#888" : "#FFFFFF"; const styles = (theme: Theme) => createStyles({ select: { // fu fu fu.Overrides for white selector on dark background. @@ -96,6 +102,8 @@ const PresetSelector = showEditPresetsDialog: false, presetsMenuAnchorRef: null, renameDialogOpen: false, + saveAsDialogOpen: false, + importDialogOpen: false, renameDialogTitle: "", renameDialogDefaultName: "", renameDialogActionName: "", @@ -117,6 +125,12 @@ const PresetSelector = this.setState({ presetsMenuAnchorRef: null }); } + handlePresetsMenuImport(e: SyntheticEvent): void { + this.handlePresetsMenuClose(); + e.stopPropagation(); + this.setState({ importDialogOpen: true }); + } + handleDownloadPreset(e: SyntheticEvent) { this.handlePresetsMenuClose(); e.preventDefault(); @@ -144,21 +158,8 @@ const PresetSelector = let currentPresets = this.model.presets.get(); let item = currentPresets.getItem(currentPresets.selectedInstanceId); if (item == null) return; - let name = item.name; - - this.renameDialogOpen(name, "Save Preset As", "OK") - .then((newName) => { - return this.model.saveCurrentPresetAs(newName); - }) - .then((newInstanceId) => { - - }) - .catch((error) => { - this.showError(error); - }) - ; - + this.setState({ saveAsDialogOpen: true }); } handlePresetsMenuRename(e: SyntheticEvent): void { this.handlePresetsMenuClose(); @@ -198,7 +199,7 @@ const PresetSelector = showError(error: string) { this.model.showAlert(error); } - renameDialogOpen(defaultText: string, title: string,acceptButtonText: string): Promise { + renameDialogOpen(defaultText: string, title: string, acceptButtonText: string): Promise { let result = new Promise( (resolve, reject) => { this.setState( @@ -219,6 +220,30 @@ const PresetSelector = return result; } + handleSaveAsDialogOk(bankInstanceId: number, name: string): void { + this.setState({ saveAsDialogOpen: false }); + + this.model.saveCurrentPresetAs(bankInstanceId, name) + .then((instanceId) => { + this.model.loadPreset(instanceId); + }) + .catch((error) => { + this.showError(error); + }); + } + handleImportDialogOk(bankInstanceId: number, presets: number[]): void { + this.setState({ importDialogOpen: false }); + this.model.importPresetsFromBank(bankInstanceId, presets) + .then((instanceId) => { + if (instanceId !== -1) { + this.model.loadPreset(instanceId); + } + }) + .catch((error) => { + this.showError(error); + }); + } + handleRenameDialogClose(): void { this.setState({ renameDialogOpen: false, @@ -318,7 +343,7 @@ const PresetSelector = onChange={(e, extra) => this.handleChange(e, extra)} onClose={(e) => this.handleSelectClose(e)} displayEmpty - value={presets.selectedInstanceId === 0? '' : presets.selectedInstanceId} + value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId} inputProps={{ classes: { icon: classes.icon }, 'aria-label': "Select preset" @@ -339,13 +364,13 @@ const PresetSelector = }
-
+
this.handlePresetMenuClick(e)} size="large" - > + > 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.handleDownloadPreset(e); }} >Download preset @@ -366,7 +392,23 @@ const PresetSelector = this.handleMenuEditPresets()}>Manage presets...
- this.handleDialogClose()} /> + this.handleDialogClose()} + /> + {this.state.saveAsDialogOpen && ( + { this.setState({ saveAsDialogOpen: false }) }} + onOk={(bankInstanceId, name) => { + this.handleSaveAsDialogOk(bankInstanceId, name); + }} /> + )} + {this.state.importDialogOpen && ( + { this.setState({ importDialogOpen: false }) }} + onOk={(bankInstanceId, presets) => { + this.handleImportDialogOk(bankInstanceId, presets); + }} /> + )} void, + onClose: () => void +}; + +export interface SavePresetAsDialogState { + selectedBank: number; + banks: BankIndex; + + fullScreen: boolean; +}; + +function isTouchUi() +{ + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +} +export default class SavePresetAsDialog extends ResizeResponsiveComponent { + + refText: React.RefObject; + + model: PiPedalModel; + constructor(props: SavePresetAsDialogProps) { + super(props); + this.model = PiPedalModel.getInstance(); + this.state = { + banks: this.model.banks.get(), + selectedBank: this.model.banks.get().selectedBank, + fullScreen: false + + }; + this.refText = React.createRef(); + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void + { + this.setState({fullScreen: height < 200}) + } + + + componentDidMount() + { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() + { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() + { + } + checkForIllegalCharacters(filename: string) { + } + + render() { + let props = this.props; + let { open, defaultName, onClose, onOk } = props; + + const handleClose = () => { + onClose(); + }; + + const handleOk = () => { + let text = nullCast(this.refText.current).value; + text = text.trim(); + try { + this.checkForIllegalCharacters(text); + } catch (e:any) + { + let model:PiPedalModel = PiPedalModelFactory.getInstance(); + model.showAlert(e.toString()); + return; + } + if (text.length === 0) return; + onOk(this.state.selectedBank,text); + } + const handleKeyDown = (event: React.KeyboardEvent): void => { + // 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleOk(); + } + }; + return ( + {}} + > + + Save Preset As + + + + Bank + + + + + + + + + + + ); + } +}