diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index a4e8e6e..ec29dd1 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -290,7 +290,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) int id = presetCombo.getSelectedId(); if (id >= 1) { - proc.loadKit(id - 1); + proc.loadPreset(id - 1); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); } @@ -298,7 +298,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) addAndMakeVisible(presetCombo); presetPrevBtn.onClick = [this] { - proc.prevKit(); + proc.prevPreset(); refreshPresetCombo(); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); @@ -306,13 +306,36 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) addAndMakeVisible(presetPrevBtn); presetNextBtn.onClick = [this] { - proc.nextKit(); + proc.nextPreset(); refreshPresetCombo(); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); }; addAndMakeVisible(presetNextBtn); + presetSaveBtn.onClick = [this] + { + auto* aw = new juce::AlertWindow("Save Preset", "Enter a name for this preset:", + juce::MessageBoxIconType::NoIcon); + aw->addTextEditor("name", "", "Preset name"); + aw->addButton("Save", 1, juce::KeyPress(juce::KeyPress::returnKey)); + aw->addButton("Cancel", 0, juce::KeyPress(juce::KeyPress::escapeKey)); + aw->enterModalState(true, juce::ModalCallbackFunction::create( + [this, aw](int result) + { + if (result == 1) + { + auto name = aw->getTextEditor("name")->getText().trim(); + if (name.isNotEmpty()) + { + proc.saveUserPreset(name); + refreshPresetCombo(); + } + } + }), true); + }; + addAndMakeVisible(presetSaveBtn); + scaleCombo.addItemList({"100%", "125%", "150%", "175%", "200%"}, 1); scaleCombo.setSelectedId(3, dontSendNotification); scaleCombo.setColour(ComboBox::backgroundColourId, Colour(0xFF333333)); @@ -449,10 +472,10 @@ void TrommelkisteEditor::updateFileLabel(int idx) void TrommelkisteEditor::refreshPresetCombo() { presetCombo.clear(dontSendNotification); - const int n = proc.getNumKits(); + const int n = proc.getNumPresets(); for (int i = 0; i < n; ++i) - presetCombo.addItem(proc.getKitList()[i].name, i + 1); - presetCombo.setSelectedId(proc.currentKitIndex + 1, dontSendNotification); + presetCombo.addItem(proc.getPresetName(i), i + 1); + presetCombo.setSelectedId(proc.currentPresetIndex + 1, dontSendNotification); } void TrommelkisteEditor::paint(Graphics& g) @@ -606,7 +629,8 @@ void TrommelkisteEditor::resized() presetPrevBtn.setBounds(scaled(150), scaled(14), scaled(20), scaled(20)); presetCombo.setBounds(scaled(172), scaled(14), scaled(100), scaled(20)); presetNextBtn.setBounds(scaled(274), scaled(14), scaled(20), scaled(20)); - scaleCombo.setBounds(scaled(298), scaled(14), scaled(52), scaled(20)); + presetSaveBtn.setBounds(scaled(296), scaled(14), scaled(48), scaled(20)); + scaleCombo.setBounds(scaled(348), scaled(14), scaled(52), scaled(20)); // Header knobs const int hk = HDR_KNOB; diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index c69ee23..9ab62e5 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -42,6 +42,12 @@ public: void mouseDown(const juce::MouseEvent&) override; void timerCallback() override; + bool isInterestedInFileDrag(const juce::StringArray& files) override; + void fileDragEnter(const juce::StringArray& files, int x, int y) override; + void fileDragMove(const juce::StringArray& files, int x, int y) override; + void fileDragExit(const juce::StringArray& files) override; + void filesDropped(const juce::StringArray& files, int x, int y) override; + private: TrommelkisteProcessor& proc; TrommelkisteLookAndFeel lnf; @@ -78,6 +84,7 @@ private: juce::ComboBox presetCombo; juce::TextButton presetPrevBtn{"<"}; juce::TextButton presetNextBtn{">"}; + juce::TextButton presetSaveBtn{"SAVE"}; // Global effect controls in header KnobSlider globalRingFreq, globalDestruction; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index a401c5d..e86cbbe 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -13,6 +13,7 @@ TrommelkisteProcessor::TrommelkisteProcessor() formatManager.registerBasicFormats(); scanSamples(); loadKit(0); + refreshUserPresets(); } TrommelkisteProcessor::~TrommelkisteProcessor() {} @@ -605,19 +606,12 @@ static const TrommelkisteProcessor::Kit kits[] = { const TrommelkisteProcessor::Kit* TrommelkisteProcessor::getKitList() { return kits; } int TrommelkisteProcessor::getNumKits() const { return 4; } -juce::String TrommelkisteProcessor::getCurrentKitName() const -{ - if (currentKitIndex >= 0 && currentKitIndex < getNumKits()) - return kits[currentKitIndex].name; - return {}; -} void TrommelkisteProcessor::loadKit(int index) { if (index < 0 || index >= getNumKits()) return; - currentKitIndex = index; const auto& kit = kits[index]; for (int t = 0; t < NUM_TRACKS; ++t) @@ -647,18 +641,161 @@ void TrommelkisteProcessor::loadKit(int index) } } -void TrommelkisteProcessor::nextKit() +juce::String TrommelkisteProcessor::getPresetName(int index) const { - int idx = currentKitIndex + 1; - if (idx >= getNumKits()) idx = 0; - loadKit(idx); + if (index < 0 || index >= getNumPresets()) + return {}; + if (index < getNumKits()) + return kits[index].name; + return userPresets[index - getNumKits()].name; } -void TrommelkisteProcessor::prevKit() +void TrommelkisteProcessor::loadPreset(int index) { - int idx = currentKitIndex - 1; - if (idx < 0) idx = getNumKits() - 1; - loadKit(idx); + if (index < 0 || index >= getNumPresets()) + return; + + currentPresetIndex = index; + if (index < getNumKits()) + loadKit(index); + else + loadUserPreset(index - getNumKits()); +} + +void TrommelkisteProcessor::nextPreset() +{ + const int n = getNumPresets(); + if (n <= 0) return; + loadPreset((currentPresetIndex + 1) % n); +} + +void TrommelkisteProcessor::prevPreset() +{ + const int n = getNumPresets(); + if (n <= 0) return; + loadPreset((currentPresetIndex + n - 1) % n); +} + +// ── User presets ─────────────────────────────────────────────── + +static juce::File getUserPresetDirectory() +{ + return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) + .getChildFile("Trommelkiste").getChildFile("Presets"); +} + +void TrommelkisteProcessor::refreshUserPresets() +{ + userPresets.clear(); + + auto dir = getUserPresetDirectory(); + if (! dir.isDirectory()) + return; + + juce::Array files = + dir.findChildFiles(juce::File::findFiles, false, "*.trommelpreset"); + files.sort(); + + for (const auto& f : files) + { + auto xml = juce::XmlDocument::parse(f); + if (xml == nullptr) + continue; + + UserPreset p; + p.name = xml->getStringAttribute("name"); + if (p.name.isEmpty()) + p.name = f.getFileNameWithoutExtension(); + p.file = f; + userPresets.add(p); + } +} + +void TrommelkisteProcessor::saveUserPreset(const juce::String& name) +{ + auto dir = getUserPresetDirectory(); + dir.createDirectory(); + + juce::String safeName = name; + safeName = safeName.replaceCharacter('\\', '_').replaceCharacter('/', '_'); + auto file = dir.getChildFile(safeName + ".trommelpreset"); + + auto state = apvts.copyState(); + state.setProperty("name", name, nullptr); + + juce::ValueTree samples("Samples"); + for (int i = 0; i < NUM_TRACKS; ++i) + { + juce::ValueTree tr("Track" + juce::String(i)); + tr.setProperty("path", tracks[i].filePath, nullptr); + tr.setProperty("sampleIndex", currentSampleIndex[i], nullptr); + if (currentSampleIndex[i] >= 0 && currentSampleIndex[i] < trackSamples[i].size()) + tr.setProperty("sampleName", trackSamples[i][currentSampleIndex[i]].name, nullptr); + samples.appendChild(tr, nullptr); + } + state.appendChild(samples, nullptr); + + std::unique_ptr xml(state.createXml()); + if (! xml->writeToFile(file, {})) + return; + + refreshUserPresets(); + + for (int i = 0; i < userPresets.size(); ++i) + { + if (userPresets[i].file == file) + { + currentPresetIndex = getNumKits() + i; + break; + } + } +} + +void TrommelkisteProcessor::loadUserPreset(int index) +{ + if (index < 0 || index >= userPresets.size()) + return; + + std::unique_ptr xml = juce::XmlDocument::parse(userPresets[index].file); + if (xml == nullptr) + return; + + auto state = juce::ValueTree::fromXml(*xml); + apvts.replaceState(state); + + auto samples = state.getChildWithName("Samples"); + if (samples.isValid()) + { + for (int i = 0; i < NUM_TRACKS; ++i) + { + auto tr = samples.getChildWithName("Track" + juce::String(i)); + if (! tr.isValid()) + continue; + + auto path = tr.getProperty("path", "").toString(); + auto sampleName = tr.getProperty("sampleName", "").toString(); + + if (path.isNotEmpty()) + { + loadSample(i, juce::File(path)); + currentSampleIndex[i] = (int)tr.getProperty("sampleIndex", -1); + } + else if (sampleName.isNotEmpty()) + { + int found = -1; + for (int s = 0; s < trackSamples[i].size(); ++s) + { + if (trackSamples[i][s].name.equalsIgnoreCase(sampleName)) + { + found = s; + break; + } + } + if (found >= 0) + loadSampleByIndex(i, found); + } + } + } } juce::AudioProcessorEditor* TrommelkisteProcessor::createEditor() @@ -670,7 +807,7 @@ void TrommelkisteProcessor::getStateInformation(juce::MemoryBlock& destData) { auto state = apvts.copyState(); - state.setProperty("kitIndex", currentKitIndex, nullptr); + state.setProperty("presetIndex", currentPresetIndex, nullptr); juce::ValueTree samples("Samples"); for (int i = 0; i < NUM_TRACKS; ++i) @@ -697,7 +834,8 @@ void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInByte auto state = juce::ValueTree::fromXml(*xml); apvts.replaceState(state); - currentKitIndex = (int)state.getProperty("kitIndex", 0); + currentPresetIndex = (int)state.getProperty("presetIndex", + state.getProperty("kitIndex", 0)); auto samples = state.getChildWithName("Samples"); if (samples.isValid()) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 47f3f14..419c65c 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -78,14 +78,29 @@ public: const char* samples[NUM_TRACKS]; }; - int currentKitIndex = 0; + int currentPresetIndex = 0; void loadKit(int index); - void nextKit(); - void prevKit(); int getNumKits() const; - juce::String getCurrentKitName() const; static const Kit* getKitList(); + // User presets (saved to disk, shown in the preset menu) + struct UserPreset + { + juce::String name; + juce::File file; + }; + + juce::Array userPresets; + void saveUserPreset(const juce::String& name); + void loadUserPreset(int index); + void refreshUserPresets(); + + int getNumPresets() const { return getNumKits() + userPresets.size(); } + juce::String getPresetName(int index) const; + void loadPreset(int index); + void nextPreset(); + void prevPreset(); + private: struct Voice {