Import Prests from Bank

This commit is contained in:
Robin E. R. Davies
2025-09-20 07:27:51 -04:00
parent 9470699527
commit b25e90df7b
15 changed files with 1215 additions and 400 deletions
+2 -1
View File
@@ -22,7 +22,8 @@
// Resolved by CMake Tools: // Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}", "program": "${command:cmake.launchTargetPath}",
"args": [ "args": [
"--gplot-downloads" "hw:M2",
"hw:M2"
], ],
"stopAtEntry": false, "stopAtEntry": false,
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
+299 -271
View File
@@ -23,316 +23,344 @@
#include "Pedalboard.hpp" #include "Pedalboard.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
namespace pipedal { namespace pipedal
{
#define GETTER_SETTER_REF(name) \ #define GETTER_SETTER_REF(name) \
decltype(name##_)& name() { return name##_;} \ decltype(name##_) &name() { return name##_; } \
const decltype(name##_)& name() const { return name##_;} \ const decltype(name##_) &name() const { return name##_; } \
void name(const decltype(name##_) &value) { name##_ = value; } void name(const decltype(name##_) &value) { name##_ = value; }
#define GETTER_SETTER_VEC(name) \ #define GETTER_SETTER_VEC(name) \
decltype(name##_)& name() { return name##_;} \ decltype(name##_) &name() { return name##_; } \
const decltype(name##_)& name() const { return name##_;} const decltype(name##_) &name() const { return name##_; }
#define GETTER_SETTER(name) \
#define GETTER_SETTER(name) \ decltype(name##_) name() const { return name##_; } \
decltype(name##_) name() const { return name##_;} \
void name(decltype(name##_) value) { name##_ = value; } void name(decltype(name##_) value) { name##_ = value; }
class PresetIndexEntry
{
int64_t instanceId_ = 0;
std::string name_;
class PresetIndexEntry { public:
int64_t instanceId_ = 0; PresetIndexEntry() {}
std::string name_; PresetIndexEntry(uint64_t instanceId, const std::string &name)
public: : instanceId_(instanceId), name_(name)
GETTER_SETTER(instanceId);
GETTER_SETTER(name);
DECLARE_JSON_MAP(PresetIndexEntry);
};
class PresetIndex {
int64_t selectedInstanceId_ = -1;
bool presetChanged_ = false;
std::vector<PresetIndexEntry> presets_;
public:
bool GetPresetName(int64_t instanceId, std::string*pResult) {
for (size_t i = 0; i < presets_.size(); ++i)
{ {
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<PresetIndexEntry> presets_;
public:
bool GetPresetName(int64_t instanceId, std::string *pResult)
{
for (size_t i = 0; i < presets_.size(); ++i)
{ {
*pResult = presets_[i].name(); if (presets_[i].instanceId() == instanceId)
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<std::unique_ptr<BankFileEntry>> 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<BankFileEntry> 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<BankFileEntry> entry = std::make_unique<BankFileEntry>();
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)
{ {
++it; *pResult = presets_[i].name();
this->presets_.insert(it,std::move(entry)); return true;
break;
} }
} }
return false;
} }
return instanceId; GETTER_SETTER(selectedInstanceId);
} GETTER_SETTER_VEC(presets);
bool hasItem(int64_t instanceId) GETTER_SETTER(presetChanged);
{
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;
} 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<std::unique_ptr<BankFileEntry>> presets_;
public:
GETTER_SETTER(name);
GETTER_SETTER(nextInstanceId);
GETTER_SETTER(selectedPreset);
GETTER_SETTER_VEC(presets);
void clear()
{ {
if (presets_[i]->instanceId() == instanceId) nextInstanceId_ = 0;
{ presets_.clear();
return *(presets_[i].get()); selectedPreset_ = -1;
}
} }
throw PiPedalArgumentException("Instance not found.");
} void move(size_t from, size_t to)
int64_t deletePreset(int64_t instanceId) {
for (size_t i = 0; i < presets_.size(); ++i)
{ {
if (presets_[i]->instanceId() == instanceId) if (from >= this->presets_.size())
{ {
presets_.erase(presets_.begin()+i); throw std::invalid_argument("Argument out of range.");
int64_t newSelection; }
if (i < presets_.size()) if (to >= this->presets_.size())
{
throw std::invalid_argument("Argument out of range.");
}
std::unique_ptr<BankFileEntry> 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<BankFileEntry> entry = std::make_unique<BankFileEntry>();
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(); if ((*it)->instanceId() == afterItem)
} else if (presets_.size() > 1) { {
newSelection = presets_[0]->instanceId(); ++it;
} else { this->presets_.insert(it, std::move(entry));
// zero length? We can never have a zero-length bank. break;
// 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_) }
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."); bool hasName(const std::string &name)
}
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<BankIndexEntry> 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; for (size_t i = 0; i < presets_.size(); ++i)
}
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); if (presets_[i]->preset().name() == name)
return bank.instanceId(); {
return true;
}
} }
return false;
} }
// else at the end. bool renamePreset(int64_t instanceId, const std::string &name)
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) if (hasName(name))
{ {
return &entries_[i]; throw PiPedalStateException("A preset by that name already exists.");
} }
} for (size_t i = 0; i < presets_.size(); ++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]; 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<BankIndexEntry> 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
#undef GETTER_SETTER_VEC #undef GETTER_SETTER_VEC
#undef GETTER_SETTER_REF #undef GETTER_SETTER_REF
} }
+60 -28
View File
@@ -33,6 +33,7 @@
#include <iomanip> #include <iomanip>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <sched.h>
using namespace pipedal; using namespace pipedal;
@@ -59,7 +60,7 @@ void PrintHelp()
pp << Indent(0) << "Syntax\n\n"; pp << Indent(0) << "Syntax\n\n";
pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n"; pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n";
pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. " pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. "
"If <output-device> is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; "If <output-device> 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(0) << "Options\n\n";
pp << Indent(15); pp << Indent(15);
@@ -85,13 +86,13 @@ void PrintHelp()
<< "PiPedal Latency Tester measures internal buffer delays as well as operating system and " << "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 " << "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"; << "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" pp
<< Indent(6) << "sudo systemctl stop pipedald\n\n"; << "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(0) << "Examples\n\n";
pp << Indent(2) << "pipedal_latency_test --list\n\n"; pp << Indent(2) << "pipedal_latency_test --list\n\n";
@@ -123,7 +124,6 @@ void ListDevices()
} }
} }
using ChannelsT = std::vector<int>; using ChannelsT = std::vector<int>;
class AlsaTester : private AudioDriverHost class AlsaTester : private AudioDriverHost
@@ -169,12 +169,13 @@ public:
delete[] inputBuffers; delete[] inputBuffers;
delete[] outputBuffers; delete[] outputBuffers;
} }
std::vector<std::string> SelectChannels(const std::vector<std::string>&available, const std::vector<int>& selection) std::vector<std::string> SelectChannels(const std::vector<std::string> &available, const std::vector<int> &selection)
{ {
if (selection.size() == 0) return available; if (selection.size() == 0)
return available;
std::vector<std::string> result; std::vector<std::string> result;
for (int sel: selection) for (int sel : selection)
{ {
if (sel < 0 || sel >= available.size()) if (sel < 0 || sel >= available.size())
{ {
@@ -195,14 +196,13 @@ public:
JackConfiguration jackConfiguration; JackConfiguration jackConfiguration;
jackConfiguration.AlsaInitialize(serverSettings); jackConfiguration.AlsaInitialize(serverSettings);
auto & availableInputs = jackConfiguration.inputAudioPorts(); auto &availableInputs = jackConfiguration.inputAudioPorts();
auto & availableOutputs = jackConfiguration.outputAudioPorts(); auto &availableOutputs = jackConfiguration.outputAudioPorts();
std::vector<std::string> inputAudioPorts, outputAudioPorts; std::vector<std::string> inputAudioPorts, outputAudioPorts;
inputAudioPorts = SelectChannels(availableInputs,this->inputChannels); inputAudioPorts = SelectChannels(availableInputs, this->inputChannels);
outputAudioPorts = SelectChannels(availableOutputs,this->outputChannels); outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels);
JackChannelSelection channelSelection( JackChannelSelection channelSelection(
inputAudioPorts, outputAudioPorts, inputAudioPorts, outputAudioPorts,
@@ -360,7 +360,6 @@ public:
} }
virtual void OnAudioTerminated() virtual void OnAudioTerminated()
{ {
} }
virtual void OnProcess(size_t nFrames) virtual void OnProcess(size_t nFrames)
{ {
@@ -413,7 +412,7 @@ TestResult RunLatencyTest(
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate, int bufferSize, int buffers) 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(); return tester.Test();
} }
@@ -432,6 +431,28 @@ static std::string overheadDisplay(float value)
return s.str(); return s.str();
} }
static bool testRealtimePriorityPrivileges()
{
struct sched_param currentParam;
int currentPolicy = sched_getscheduler(0);
if (currentPolicy == -1) {
return false;
}
if (sched_getparam(0, &currentParam) != 0) {
return false;
}
struct sched_param param;
param.sched_priority = 85;
if (sched_setscheduler(0, SCHED_RR, &param) != 0)
{
return false;
}
// Restore normal priority
sched_setscheduler(0, currentPolicy, &currentParam);
return true;
}
void RunLatencyTest( void RunLatencyTest(
const std::string &inputDeviceId, const std::string &inputDeviceId,
const std::string &outputDeviceId, const std::string &outputDeviceId,
@@ -439,8 +460,20 @@ void RunLatencyTest(
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate) uint32_t sampleRate)
{ {
PrettyPrinter pp; 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 <your user id>" << "\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 SIZE_COLUMN_WIDTH = 8;
const int BUFFERS_COLUMN_WIDTH = 20; const int BUFFERS_COLUMN_WIDTH = 20;
@@ -467,7 +500,7 @@ void RunLatencyTest(
for (auto bufferCount : bufferCounts) 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); pp.Column(column);
column += BUFFERS_COLUMN_WIDTH; column += BUFFERS_COLUMN_WIDTH;
@@ -495,8 +528,7 @@ void RunLatencyTest(
} }
} }
ChannelsT ParseChannels(const std::string &channels)
ChannelsT ParseChannels(const std::string&channels)
{ {
ChannelsT result; ChannelsT result;
std::stringstream s(channels); std::stringstream s(channels);
@@ -504,9 +536,11 @@ ChannelsT ParseChannels(const std::string&channels)
while (true) while (true)
{ {
int c = s.peek(); int c = s.peek();
if (c == -1) break; if (c == -1)
break;
if (c == ',') { if (c == ',')
{
s.get(); s.get();
c = s.peek(); c = s.peek();
} }
@@ -518,14 +552,12 @@ ChannelsT ParseChannels(const std::string&channels)
while (s.peek() >= '0' && s.peek() <= '9') while (s.peek() >= '0' && s.peek() <= '9')
{ {
c = s.get(); c = s.get();
v = v*10 + c-'0'; v = v * 10 + c - '0';
} }
result.push_back(v); result.push_back(v);
} }
return result; return result;
} }
int main(int argc, const char **argv) int main(int argc, const char **argv)
@@ -567,7 +599,7 @@ std:
inputChannels = ParseChannels(strInputChannels); inputChannels = ParseChannels(strInputChannels);
outputChannels = ParseChannels(strOutputChannels); 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; std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev;
RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate); RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate);
} }
+22 -2
View File
@@ -887,7 +887,7 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string &
return presetId; 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<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
@@ -897,7 +897,7 @@ int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &n
UpdateVst3Settings(pedalboard); UpdateVst3Settings(pedalboard);
pedalboard.name(name); pedalboard.name(name);
int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId); int64_t result = storage.SaveCurrentPresetAs(pedalboard, bankInstanceId, name, saveAfterInstanceId);
FirePresetsChanged(clientId); FirePresetsChanged(clientId);
return result; return result;
} }
@@ -3257,3 +3257,23 @@ bool PiPedalModel::HasTone3000Auth() const
{ {
return storage.GetTone3000Auth() != ""; return storage.GetTone3000Auth() != "";
} }
std::vector<PresetIndexEntry> PiPedalModel::RequestBankPresets(int64_t bankInstanceId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.RequestBankPresets(bankInstanceId);
}
int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets);
if (lastAdded != -1) {
}
FirePresetsChanged(-1);
return lastAdded;
}
+4 -1
View File
@@ -378,9 +378,10 @@ namespace pipedal
int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1); int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1);
void UploadPluginPresets(const PluginPresets &pluginPresets); void UploadPluginPresets(const PluginPresets &pluginPresets);
void SaveCurrentPreset(int64_t clientId); 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); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name);
void LoadPreset(int64_t clientId, int64_t instanceId); void LoadPreset(int64_t clientId, int64_t instanceId);
bool UpdatePresets(int64_t clientId, const PresetIndex &presets); bool UpdatePresets(int64_t clientId, const PresetIndex &presets);
void UpdatePluginPresets(const PluginUiPresets &pluginPresets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets);
@@ -493,6 +494,8 @@ namespace pipedal
void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId);
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
}; };
} // namespace pipedal. } // namespace pipedal.
+37 -1
View File
@@ -46,6 +46,27 @@
using namespace std; using namespace std;
using namespace pipedal; using namespace pipedal;
class ImportPresetsFromBankBody {
public:
int64_t bankInstanceId_;
std::vector<int64_t> 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 class PathPatchPropertyChangedBody
{ {
public: public:
@@ -336,6 +357,7 @@ class SaveCurrentPresetAsBody
{ {
public: public:
int64_t clientId_ = -1; int64_t clientId_ = -1;
int64_t bankInstanceId_ = -1;
std::string name_; std::string name_;
int64_t saveAfterInstanceId_ = -1; int64_t saveAfterInstanceId_ = -1;
@@ -343,6 +365,7 @@ public:
}; };
JSON_MAP_BEGIN(SaveCurrentPresetAsBody) JSON_MAP_BEGIN(SaveCurrentPresetAsBody)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, bankInstanceId)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId)
JSON_MAP_END() JSON_MAP_END()
@@ -1314,7 +1337,7 @@ public:
{ {
SaveCurrentPresetAsBody body; SaveCurrentPresetAsBody body;
pReader->read(&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); Reply(replyTo, "saveCurrentPresetsAs", result);
} }
else if (message == "setSelectedPedalboardPlugin") else if (message == "setSelectedPedalboardPlugin")
@@ -1794,6 +1817,19 @@ public:
{ {
std::vector<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts(); std::vector<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts();
this->Reply(replyTo,"getAlsaSequencerPorts", result); 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 else
{ {
+137 -13
View File
@@ -225,7 +225,6 @@ static void CopyDirectory(const std::filesystem::path &source, const std::filesy
} }
} }
void Storage::MaybeCopyDefaultPresets() void Storage::MaybeCopyDefaultPresets()
{ {
auto presetsDirectory = this->GetPresetsDirectory(); auto presetsDirectory = this->GetPresetsDirectory();
@@ -239,13 +238,17 @@ void Storage::MaybeCopyDefaultPresets()
} }
static void removeFileNoThrow(const std::filesystem::path &path) static void removeFileNoThrow(const std::filesystem::path &path)
{ {
try { try
{
fs::remove(path); fs::remove(path);
} catch (const std::exception&) }
catch (const std::exception &)
{ {
} }
} }
void Storage::UpgradeFactoryPresets() void Storage::UpgradeFactoryPresets()
{ {
auto presetsDirectory = this->GetPresetsDirectory(); auto presetsDirectory = this->GetPresetsDirectory();
@@ -520,7 +523,6 @@ void Storage::SavePluginPresetIndex()
} }
} }
void Storage::SaveBankIndex() void Storage::SaveBankIndex()
{ {
pipedal::ofstream_synced os; pipedal::ofstream_synced os;
@@ -653,17 +655,133 @@ void Storage::SaveCurrentPreset(const Pedalboard &pedalboard)
item.preset(pedalboard); item.preset(pedalboard);
SaveCurrentBank(); 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; Pedalboard newPedalboard = pedalboard;
newPedalboard.name(name); newPedalboard.name(name);
int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); if (bankInstanceId == this->bankIndex.selectedBank())
currentBank.selectedPreset(newInstanceId); {
SaveCurrentBank(); int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId);
return newInstanceId; 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<std::string>&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<int64_t> &presets)
{
if (bankIndex.selectedBank() == bankInstanceId) {
throw std::runtime_error("Can't import to self.");
}
std::set<int64_t> presetsSet { presets.begin(), presets.end()};
auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId);
std::set<std::string> 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<PresetIndexEntry> Storage::RequestBankPresets(int64_t bankInstanceId)
{
auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId);
std::vector<PresetIndexEntry> 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) void Storage::SetPresetIndex(const PresetIndex &presets)
{ {
// painful because we must move unique_ptrs. // painful because we must move unique_ptrs.
@@ -826,25 +944,30 @@ int64_t Storage::CreateNewPreset()
} }
} }
newPedalboard.name(name); 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) int64_t Storage::CopyPreset(int64_t fromId, int64_t toId)
{ {
auto &fromItem = this->currentBank.getItem(fromId); auto &fromItem = this->currentBank.getItem(fromId);
int64_t result;
if (toId == -1) if (toId == -1)
{ {
Pedalboard newPedalboard = fromItem.preset(); Pedalboard newPedalboard = fromItem.preset();
std::string name = GetPresetCopyName(fromItem.preset().name()); std::string name = GetPresetCopyName(fromItem.preset().name());
newPedalboard.name(name); newPedalboard.name(name);
return this->currentBank.addPreset(newPedalboard, fromId); result = this->currentBank.addPreset(newPedalboard, fromId);
} }
else else
{ {
auto &toItem = this->currentBank.getItem(toId); auto &toItem = this->currentBank.getItem(toId);
toItem.preset(fromItem.preset()); toItem.preset(fromItem.preset());
return toId; result = toId;
} }
SaveCurrentBank();
return result;
} }
void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection) void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection)
@@ -2861,6 +2984,7 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson)
return v.to_string(); return v.to_string();
} }
JSON_MAP_BEGIN(UserSettings) JSON_MAP_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
+5 -1
View File
@@ -149,7 +149,7 @@ public:
int64_t GetBankByMidiBankNumber(uint8_t bankNumber); int64_t GetBankByMidiBankNumber(uint8_t bankNumber);
const Pedalboard& GetCurrentPreset(); const Pedalboard& GetCurrentPreset();
void SaveCurrentPreset(const Pedalboard&pedalboard); 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; int64_t GetCurrentPresetId() const;
void GetPresetIndex(PresetIndex*pResult); void GetPresetIndex(PresetIndex*pResult);
@@ -268,6 +268,10 @@ public:
void LoadTone3000Auth(); void LoadTone3000Auth();
void SetTone3000Auth(const std::string&apiKey); void SetTone3000Auth(const std::string&apiKey);
std::string GetTone3000Auth() const; std::string GetTone3000Auth() const;
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
}; };
+1 -6
View File
@@ -1,9 +1,4 @@
Did we fix Toob Tremolo stereo harmonic?
double select on double-click/ok in file dialog.
sudo apt install libbz2-dev
README, license files in file browser.
ls ls
@@ -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<ImportPresetFromBankDialogProps, ImportPresetFromBankDialogState> {
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 (
<DialogEx tag="importPreset" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<div style={{ display: "flex", flexFlow: "column nowrap", height: this.state.fullScreen ? "100vh" : "80vh" }}>
{!this.state.fullScreen && (
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Import Presets from Bank
</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
<Divider />
<List sx={{ flex: "1 1 auto", width: '100%', bgColor: 'background.paper', overflowX: 'auto' }}>
{this.state.bankPresets?.map((presetEntry) => {
const labelId = `checkbox-list-label-${presetEntry.instanceId}`;
return (
<ListItem key={presetEntry.instanceId}
onClick={() => {
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] });
}}>
<ListItemIcon style={{ marginLeft: 16 }}>
<Checkbox
edge="start"
checked={this.state.selectedPresets.indexOf(presetEntry.instanceId) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={presetEntry.name} />
</ListItem>
);
})}
</List>
<Divider />
<DialogActions style={{ flex: "0 0 auto" }}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button
onClick={() => { this.handleOk(); }} variant="dialogPrimary"
disabled={this.state.selectedPresets.length === 0}
style={{ opacity: this.state.selectedPresets.length === 0 ? 0.5 : 1.0 }}
>
OK
</Button>
</DialogActions>
</div>
</DialogEx>
);
}
}
+12 -2
View File
@@ -2049,13 +2049,14 @@ export class PiPedalModel //implements PiPedalModel
} }
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> { saveCurrentPresetAs(bankInstanceId: number, newName: string, saveAfterInstanceId = -1): Promise<number> {
// default behaviour is to save after the currently selected preset. // default behaviour is to save after the currently selected preset.
if (saveAfterInstanceId === -1) { if (saveAfterInstanceId === -1) {
saveAfterInstanceId = this.presets.get().selectedInstanceId; saveAfterInstanceId = this.presets.get().selectedInstanceId;
} }
let request: any = { let request: any = {
clientId: this.clientId, clientId: this.clientId,
bankInstanceId: bankInstanceId,
name: newName, name: newName,
saveAfterInstanceId: saveAfterInstanceId saveAfterInstanceId: saveAfterInstanceId
@@ -2064,7 +2065,9 @@ export class PiPedalModel //implements PiPedalModel
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<number>("saveCurrentPresetAs", request) .request<number>("saveCurrentPresetAs", request)
.then((newPresetId) => { .then((newPresetId) => {
this.loadPreset(newPresetId); if (bankInstanceId === this.banks.get().selectedBank) {
this.loadPreset(newPresetId);
}
return newPresetId; return newPresetId;
}); });
} }
@@ -3539,6 +3542,13 @@ export class PiPedalModel //implements PiPedalModel
this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId }); this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId });
} }
requestBankPresets(bankInstanceId: number): Promise<PresetIndexEntry[]> {
return nullCast(this.webSocket).request<PresetIndexEntry[]>("requestBankPresets", {bankInstanceId: bankInstanceId});
}
importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise<number> {
return nullCast(this.webSocket).request<number>("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets});
}
}; };
let instance: PiPedalModel | undefined = undefined; let instance: PiPedalModel | undefined = undefined;
+124 -50
View File
@@ -17,9 +17,12 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // 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. // 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 { css } from '@emotion/react';
import {isDarkMode} from './DarkMode'; import { isDarkMode } from './DarkMode';
import IconButtonEx from './IconButtonEx'; import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel'; 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 EditIcon from '@mui/icons-material/Edit';
import RenameDialog from './RenameDialog'; import RenameDialog from './RenameDialog';
import Slide, {SlideProps} from '@mui/material/Slide'; import Slide, { SlideProps } from '@mui/material/Slide';
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
@@ -66,9 +69,11 @@ interface PresetDialogState {
showActionBar: boolean; showActionBar: boolean;
selectedItem: number; currentItem: number;
selectedItems: Set<number>;
renameOpen: boolean; renameOpen: boolean;
importOpen: boolean;
moreMenuAnchorEl: HTMLElement | null; moreMenuAnchorEl: HTMLElement | null;
openUploadPresetDialog: boolean; openUploadPresetDialog: boolean;
@@ -79,7 +84,7 @@ interface PresetDialogState {
const styles = (theme: Theme) => createStyles({ const styles = (theme: Theme) => createStyles({
listIcon: css({ 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({ dialogAppBar: css({
position: 'relative', position: 'relative',
@@ -146,19 +151,27 @@ const PresetDialog = withStyles(
this.state = { this.state = {
presets: presets, presets: presets,
showActionBar: false, showActionBar: false,
selectedItem: presets.selectedInstanceId, currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>(),
renameOpen: false, renameOpen: false,
importOpen: false,
moreMenuAnchorEl: null, moreMenuAnchorEl: null,
openUploadPresetDialog: false openUploadPresetDialog: false
}; };
this.handlePresetsChanged = this.handlePresetsChanged.bind(this); this.handlePresetsChanged = this.handlePresetsChanged.bind(this);
}
setSelection(instanceId: number) {
this.setState({
currentItem: instanceId,
selectedItems: new Set<number>([instanceId])
});
} }
selectItemAtIndex(index: number) { selectItemAtIndex(index: number) {
let instanceId = this.state.presets.presets[index].instanceId; let instanceId = this.state.presets.presets[index].instanceId;
this.setState({ selectedItem: instanceId }); this.setSelection(instanceId);
} }
isEditMode() { isEditMode() {
return this.state.showActionBar || this.props.isEditDialog; return this.state.showActionBar || this.props.isEditDialog;
@@ -170,7 +183,7 @@ const PresetDialog = withStyles(
handleDownloadPreset() { handleDownloadPreset() {
this.handleMoreClose(); this.handleMoreClose();
this.model.download("downloadPreset", this.state.selectedItem); this.model.download("downloadPreset", this.state.currentItem);
} }
handleUploadPreset() { handleUploadPreset() {
this.handleMoreClose(); 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 (!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 we don't have a valid selection, then use the current preset.
if (this.state.presets.getItem(this.state.selectedItem) == null) { if (this.state.presets.getItem(this.state.currentItem) == null) {
this.setState({ presets: presets, selectedItem: presets.selectedInstanceId }); this.setState({ presets: presets, currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>([presets.selectedInstanceId])
});
} else { } else {
this.setState({ presets: presets }); this.setState({ presets: presets });
} }
@@ -208,7 +223,7 @@ const PresetDialog = withStyles(
} }
getSelectedIndex() { 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; let presets = this.state.presets;
for (let i = 0; i < presets.presets.length; ++i) { for (let i = 0; i < presets.presets.length; ++i) {
if (presets.presets[i].instanceId === instanceId) return i; if (presets.presets[i].instanceId === instanceId) return i;
@@ -217,12 +232,12 @@ const PresetDialog = withStyles(
} }
handleDeleteClick() { handleDeleteClick() {
if (!this.state.selectedItem) return; if (this.state.currentItem === -1) return;
let selectedItem = this.state.selectedItem; let currentItem = this.state.currentItem;
if (selectedItem !== -1) { if (currentItem !== -1) {
this.model.deletePresetItem(selectedItem) this.model.deletePresetItem(currentItem)
.then((selectedItem: number) => { .then((currentItem: number) => {
this.setState({ selectedItem: selectedItem }); this.setSelection(currentItem);
}) })
.catch((error) => { .catch((error) => {
this.model.showAlert(error); this.model.showAlert(error);
@@ -236,7 +251,7 @@ const PresetDialog = withStyles(
handleItemClick(instanceId: number): void { handleItemClick(instanceId: number): void {
if (this.isEditMode()) { if (this.isEditMode()) {
this.setState({ selectedItem: instanceId }); this.setSelection(instanceId);
} else { } else {
this.model.loadPreset(instanceId); this.model.loadPreset(instanceId);
this.props.onDialogClose(); this.props.onDialogClose();
@@ -251,19 +266,21 @@ const PresetDialog = withStyles(
mapElement(el: any): React.ReactNode { mapElement(el: any): React.ReactNode {
let presetEntry = el as PresetIndexEntry; let presetEntry = el as PresetIndexEntry;
const classes = withStyles.getClasses(this.props); 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 ( return (
<div key={presetEntry.instanceId} className="itemBackground"> <div key={presetEntry.instanceId} id={"psetdlgItem_" + presetEntry.instanceId} className="itemBackground">
<ButtonBase style={{ width: "100%", height: 48 }} <ButtonBase style={{ width: "100%", height: 48 }}
onClick={() => this.handleItemClick(presetEntry.instanceId)} onClick={() => this.handleItemClick(presetEntry.instanceId)}
> >
<SelectHoverBackground selected={presetEntry.instanceId === selectedItem} showHover={true} /> <SelectHoverBackground selected={selected} showHover={true} />
<div className={classes.itemFrame}> <div className={classes.itemFrame}>
<div className={classes.iconFrame}> <div className={classes.iconFrame}>
<img <img
src={isDarkMode()? "img/ic_presets_white.svg": "img/ic_presets.svg"} src={isDarkMode() ? "img/ic_presets_white.svg" : "img/ic_presets.svg"}
className={classes.itemIcon} alt="" /> className={classes.itemIcon} alt="" />
</div> </div>
<div className={classes.itemLabel}> <div className={classes.itemLabel}>
<Typography> <Typography>
@@ -287,30 +304,33 @@ const PresetDialog = withStyles(
moveElement(from: number, to: number): void { moveElement(from: number, to: number): void {
let newPresets = this.state.presets.clone(); let newPresets = this.state.presets.clone();
newPresets.movePreset(from, to); newPresets.movePreset(from, to);
let toInstanceId = newPresets.presets[to].instanceId;
this.setState({ this.setState({
presets: newPresets, presets: newPresets,
selectedItem: newPresets.presets[to].instanceId currentItem: toInstanceId,
selectedItems: new Set<number>([toInstanceId])
}); });
this.updateServerPresets(newPresets); this.updateServerPresets(newPresets);
} }
getSelectedName(): string { 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; if (item) return item.name;
return ""; return "";
} }
handleRenameClick() { handleRenameClick() {
let item = this.state.presets.getItem(this.state.selectedItem); let item = this.state.presets.getItem(this.state.currentItem);
if (item) { if (item) {
this.setState({ renameOpen: true }); this.setState({ renameOpen: true });
} }
} }
handleRenameOk(text: string) { 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) return;
if (item.name !== text) { if (item.name !== text) {
this.model.renamePresetItem(this.state.selectedItem, text) this.model.renamePresetItem(this.state.currentItem, text)
.catch((error) => { .catch((error) => {
this.onError(error); this.onError(error);
}); });
@@ -319,11 +339,14 @@ const PresetDialog = withStyles(
this.setState({ renameOpen: false }); this.setState({ renameOpen: false });
} }
handleCopy() { handleCopy() {
let item = this.state.presets.getItem(this.state.selectedItem); let item = this.state.presets.getItem(this.state.currentItem);
if (!item) return; if (!item) return;
this.model.duplicatePreset(this.state.selectedItem) this.model.duplicatePreset(this.state.currentItem)
.then((newId) => { .then((newId) => {
this.setState({ selectedItem: newId }); this.setState({
currentItem: newId,
selectedItems: new Set<number>([newId])
});
}).catch((error) => { }).catch((error) => {
this.onError(error); this.onError(error);
}); });
@@ -333,6 +356,29 @@ const PresetDialog = withStyles(
this.model?.showAlert(error); 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() { render() {
@@ -344,9 +390,9 @@ const PresetDialog = withStyles(
return ( return (
<DialogEx tag="preset" fullScreen open={this.props.show} <DialogEx tag="preset" fullScreen open={this.props.show}
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition} onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
style={{userSelect: "none"}} style={{ userSelect: "none" }}
onEnterKey={()=>{}} onEnterKey={() => { }}
> >
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}> <div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} > <AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
@@ -359,7 +405,7 @@ const PresetDialog = withStyles(
<Typography variant="h6" className={classes.dialogTitle}> <Typography variant="h6" className={classes.dialogTitle}>
Presets Presets
</Typography> </Typography>
<IconButtonEx tooltip="Edit"color="inherit" onClick={(e) => this.showActionBar(true)} > <IconButtonEx tooltip="Edit" color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon /> <EditIcon />
</IconButtonEx> </IconButtonEx>
</Toolbar> </Toolbar>
@@ -382,12 +428,13 @@ const PresetDialog = withStyles(
<Typography variant="h6" className={classes.dialogTitle}> <Typography variant="h6" className={classes.dialogTitle}>
Presets Presets
</Typography> </Typography>
{(this.state.presets.getItem(this.state.selectedItem) != null) {(this.state.presets.getItem(this.state.currentItem) != null)
&& ( && (
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center" <div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center"
}}>
}}>
<Button color="inherit" onClick={(e) => this.handleCopy()}> <Button color="inherit" onClick={(e) => this.handleCopy()}>
Copy Copy
</Button> </Button>
@@ -419,6 +466,26 @@ const PresetDialog = withStyles(
onClose={() => this.handleMoreClose()} onClose={() => this.handleMoreClose()}
TransitionComponent={Fade} TransitionComponent={Fade}
> >
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText>
Copy to bank...
</ListItemText>
</MenuItem>
<MenuItem onClick={() => { this.handleImportPresetsFromBank(); }} >
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText>
Import presets from bank...
</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={() => { this.handleDownloadPreset(); }} > <MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon> <ListItemIcon>
<DownloadIcon className={classes.listIcon} /> <DownloadIcon className={classes.listIcon} />
@@ -464,14 +531,21 @@ const PresetDialog = withStyles(
</DraggableGrid> </DraggableGrid>
</div> </div>
</div> </div>
<UploadPresetDialog <UploadPresetDialog
title='Upload preset' title='Upload preset'
extension='.piPreset' extension='.piPreset'
uploadPage='uploadPreset' uploadPage='uploadPreset'
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) } onUploaded={(instanceId) => this.setSelection(instanceId)}
uploadAfter={this.state.selectedItem} uploadAfter={this.state.currentItem }
open={this.state.openUploadPresetDialog} open={this.state.openUploadPresetDialog}
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
{this.state.importOpen && (
<ImportPresetFromBankDialog
open={this.state.importOpen}
onClose={() => this.setState({ importOpen: false })}
onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)}
/>
)}
</DialogEx> </DialogEx>
+66 -24
View File
@@ -25,7 +25,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui"; import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import PresetDialog from './PresetDialog'; import PresetDialog from './PresetDialog';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
@@ -33,9 +33,12 @@ import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade'; import Fade from '@mui/material/Fade';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import RenameDialog from './RenameDialog' import RenameDialog from './RenameDialog'
import SavePresetAsDialog from './SavePresetAsDialog';
import ImportPresetFromBankDialog from './ImportPresetFromBankDialog';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import UploadPresetDialog from './UploadPresetDialog'; import UploadPresetDialog from './UploadPresetDialog';
import {isDarkMode} from './DarkMode'; import { isDarkMode } from './DarkMode';
interface PresetSelectorProps extends WithStyles<typeof styles> { interface PresetSelectorProps extends WithStyles<typeof styles> {
@@ -49,6 +52,9 @@ interface PresetSelectorState {
showEditPresetsDialog: boolean; showEditPresetsDialog: boolean;
presetsMenuAnchorRef: HTMLElement | null; presetsMenuAnchorRef: HTMLElement | null;
saveAsDialogOpen: boolean;
importDialogOpen: boolean;
renameDialogOpen: boolean; renameDialogOpen: boolean;
renameDialogTitle: string; renameDialogTitle: string;
renameDialogDefaultName: string; renameDialogDefaultName: string;
@@ -59,7 +65,7 @@ interface PresetSelectorState {
} }
const selectColor = isDarkMode()? "#888": "#FFFFFF"; const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
const styles = (theme: Theme) => createStyles({ const styles = (theme: Theme) => createStyles({
select: { // fu fu fu.Overrides for white selector on dark background. select: { // fu fu fu.Overrides for white selector on dark background.
@@ -96,6 +102,8 @@ const PresetSelector =
showEditPresetsDialog: false, showEditPresetsDialog: false,
presetsMenuAnchorRef: null, presetsMenuAnchorRef: null,
renameDialogOpen: false, renameDialogOpen: false,
saveAsDialogOpen: false,
importDialogOpen: false,
renameDialogTitle: "", renameDialogTitle: "",
renameDialogDefaultName: "", renameDialogDefaultName: "",
renameDialogActionName: "", renameDialogActionName: "",
@@ -117,6 +125,12 @@ const PresetSelector =
this.setState({ presetsMenuAnchorRef: null }); this.setState({ presetsMenuAnchorRef: null });
} }
handlePresetsMenuImport(e: SyntheticEvent): void {
this.handlePresetsMenuClose();
e.stopPropagation();
this.setState({ importDialogOpen: true });
}
handleDownloadPreset(e: SyntheticEvent) { handleDownloadPreset(e: SyntheticEvent) {
this.handlePresetsMenuClose(); this.handlePresetsMenuClose();
e.preventDefault(); e.preventDefault();
@@ -144,21 +158,8 @@ const PresetSelector =
let currentPresets = this.model.presets.get(); let currentPresets = this.model.presets.get();
let item = currentPresets.getItem(currentPresets.selectedInstanceId); let item = currentPresets.getItem(currentPresets.selectedInstanceId);
if (item == null) return; 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 { handlePresetsMenuRename(e: SyntheticEvent): void {
this.handlePresetsMenuClose(); this.handlePresetsMenuClose();
@@ -198,7 +199,7 @@ const PresetSelector =
showError(error: string) { showError(error: string) {
this.model.showAlert(error); this.model.showAlert(error);
} }
renameDialogOpen(defaultText: string, title: string,acceptButtonText: string): Promise<string> { renameDialogOpen(defaultText: string, title: string, acceptButtonText: string): Promise<string> {
let result = new Promise<string>( let result = new Promise<string>(
(resolve, reject) => { (resolve, reject) => {
this.setState( this.setState(
@@ -219,6 +220,30 @@ const PresetSelector =
return result; 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 { handleRenameDialogClose(): void {
this.setState({ this.setState({
renameDialogOpen: false, renameDialogOpen: false,
@@ -318,7 +343,7 @@ const PresetSelector =
onChange={(e, extra) => this.handleChange(e, extra)} onChange={(e, extra) => this.handleChange(e, extra)}
onClose={(e) => this.handleSelectClose(e)} onClose={(e) => this.handleSelectClose(e)}
displayEmpty displayEmpty
value={presets.selectedInstanceId === 0? '' : presets.selectedInstanceId} value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
inputProps={{ inputProps={{
classes: { icon: classes.icon }, classes: { icon: classes.icon },
'aria-label': "Select preset" 'aria-label': "Select preset"
@@ -339,13 +364,13 @@ const PresetSelector =
} }
</Select> </Select>
</div> </div>
<div style={{ flex: "0 0 auto"}}> <div style={{ flex: "0 0 auto" }}>
<IconButtonEx <IconButtonEx
tooltip="More..." tooltip="More..."
style={{ flex: "0 0 auto", color: "#FFFFFF" }} style={{ flex: "0 0 auto", color: "#FFFFFF" }}
onClick={(e) => this.handlePresetMenuClick(e)} onClick={(e) => this.handlePresetMenuClick(e)}
size="large" size="large"
> >
<MoreVertIcon style={{ opacity: 0.75 }} color="inherit" /> <MoreVertIcon style={{ opacity: 0.75 }} color="inherit" />
</IconButtonEx> </IconButtonEx>
<Menu <Menu
@@ -358,6 +383,7 @@ const PresetSelector =
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem> <MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem> <MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem> <MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem> <MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
<Divider /> <Divider />
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem> <MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
@@ -366,7 +392,23 @@ const PresetSelector =
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem> <MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
</Menu> </Menu>
</div> </div>
<PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()} /> <PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()}
/>
{this.state.saveAsDialogOpen && (
<SavePresetAsDialog open={this.state.saveAsDialogOpen}
defaultName={presets.getItem(presets.selectedInstanceId)?.name ?? "My Preset"}
onClose={() => { this.setState({ saveAsDialogOpen: false }) }}
onOk={(bankInstanceId, name) => {
this.handleSaveAsDialogOk(bankInstanceId, name);
}} />
)}
{this.state.importDialogOpen && (
<ImportPresetFromBankDialog open={this.state.importDialogOpen}
onClose={() => { this.setState({ importDialogOpen: false }) }}
onOk={(bankInstanceId, presets) => {
this.handleImportDialogOk(bankInstanceId, presets);
}} />
)}
<RenameDialog open={this.state.renameDialogOpen} <RenameDialog open={this.state.renameDialogOpen}
title={this.state.renameDialogTitle} title={this.state.renameDialogTitle}
defaultName={this.state.renameDialogDefaultName} defaultName={this.state.renameDialogDefaultName}
@@ -387,6 +429,6 @@ const PresetSelector =
} }
}, },
styles); styles);
export default PresetSelector; export default PresetSelector;
+9
View File
@@ -136,7 +136,16 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
autoFocus={!isTouchUi()} autoFocus={!isTouchUi()}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
variant="standard" variant="standard"
autoComplete="off"
autoCorrect="on"
autoCapitalize="on"
spellCheck={false}
slotProps={{ slotProps={{
inputLabel: {
shrink: true
},
input: { input: {
style: { scrollMargin: 24 } style: { scrollMargin: 24 }
} }
+189
View File
@@ -0,0 +1,189 @@
// 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 React from 'react';
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 { nullCast } from './Utility';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
//import TextFieldEx from './TextFieldEx';
import TextField from '@mui/material/TextField';
import { BankIndex } from './Banks';
export interface SavePresetAsDialogProps {
open: boolean,
defaultName: string,
onOk: (bankInstanceId: number, text: string) => 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<SavePresetAsDialogProps, SavePresetAsDialogState> {
refText: React.RefObject<HTMLInputElement|null>;
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<HTMLInputElement>();
}
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<HTMLDivElement>): void => {
// 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleOk();
}
};
return (
<DialogEx tag="savePresetAs" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{userSelect: "none"}}
onEnterKey={()=>{}}
>
<DialogTitle>
Save Preset As
</DialogTitle>
<DialogContent >
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 16 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{marginBottom: 16}}
onChange={(e) => this.setState({ selectedBank: e.target.value as number })}>
{this.state.banks.entries.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
<TextField
autoFocus={!isTouchUi()}
onKeyDown={handleKeyDown}
autoComplete="off"
autoCorrect="on"
autoCapitalize="on"
spellCheck={false}
variant="standard"
slotProps={{
inputLabel: {
shrink: true
},
input: {
style: { scrollMargin: 24 }
}
}}
id="name"
type="text"
label={"Name"}
fullWidth
defaultValue={defaultName}
inputRef={this.refText}
/>
</DialogContent>
<DialogActions style={{flexShrink: 1}}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button onClick={handleOk} variant="dialogPrimary" >
OK
</Button>
</DialogActions>
</DialogEx>
);
}
}