From fcdb1939ee2a3cb8c8893935f040056f1b2b6983 Mon Sep 17 00:00:00 2001 From: Armin Date: Wed, 15 Jul 2026 23:48:41 +0200 Subject: [PATCH] Make patch LCD clickable to open .cfl load/save file menu --- Source/PluginEditor.cpp | 103 ++++++++++++++++++++++++++++++++++++++++ Source/PluginEditor.h | 4 ++ 2 files changed, 107 insertions(+) diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 7323353..17afe68 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -529,6 +529,7 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p) addAndMakeVisible(presetButton); addAndMakeVisible(patchLCD); + patchLCD.onClick = [this]() { showFileMenu(); }; auto setupArrow = [&](juce::TextButton& btn) { btn.setColour(juce::TextButton::buttonColourId, juce::Colour(0xcc2a2a2a)); @@ -703,6 +704,108 @@ void MainContentComponent::showPresetMenu() { }); } +void MainContentComponent::showFileMenu() { + juce::PopupMenu menu; + menu.addItem(1, "Load Preset..."); + menu.addItem(2, "Save Preset..."); + menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&patchLCD), + [this](int result) { + if (result == 1) loadPresetFile(); + else if (result == 2) savePresetFile(); + }); +} + +static const char* kCflTag = "ChromaFlockPatch"; +#include "Version.h" +static const juce::String kCflVersion = CHROMAFLOCK_VERSION; + +juce::String MainContentComponent::currentPresetName() const { + auto& presets = processorRef.presetManager.getPresets(); + if (currentPresetIndex >= 0 && currentPresetIndex < static_cast(presets.size())) + return presets[currentPresetIndex].name; + return patchLCD.getText(); +} + +void MainContentComponent::savePresetFile() { + auto* chooser = new juce::FileChooser("Save ChromaFlock Preset", + juce::File::getSpecialLocation(juce::File::userHomeDirectory) + .getChildFile("chromaflock-" + currentPresetName().replaceCharacter(' ', '-') + ".cfl"), + "*.cfl"); + + chooser->launchAsync(juce::FileBrowserComponent::saveMode + | juce::FileBrowserComponent::canSelectFiles, + [this, chooser](const juce::FileChooser& c) { + std::unique_ptr deleter(chooser); + auto file = c.getResult(); + if (file == juce::File()) + return; + if (file.getFileExtension().toLowerCase() != ".cfl") + file = file.withFileExtension(".cfl"); + + auto state = processorRef.apvts.copyState(); + std::unique_ptr presetXml(state.createXml()); + + std::unique_ptr root(new juce::XmlElement(kCflTag)); + root->setAttribute("version", kCflVersion); + root->setAttribute("generator", "ChromaFlock"); + root->addChildElement(presetXml.release()); + + if (!root->writeTo(file)) + juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, + "Save Failed", "Could not write preset file:\n" + file.getFullPathName(), this); + else + patchLCD.setText(file.getFileNameWithoutExtension()); + }); +} + +void MainContentComponent::loadPresetFile() { + auto* chooser = new juce::FileChooser("Load ChromaFlock Preset", + juce::File::getSpecialLocation(juce::File::userHomeDirectory), + "*.cfl"); + + chooser->launchAsync(juce::FileBrowserComponent::openMode + | juce::FileBrowserComponent::canSelectFiles, + [this, chooser](const juce::FileChooser& c) { + std::unique_ptr deleter(chooser); + auto file = c.getResult(); + if (file == juce::File() || !file.existsAsFile()) + return; + + std::unique_ptr root(juce::XmlDocument::parse(file)); + if (root == nullptr || !root->hasTagName(kCflTag)) { + juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, + "Invalid File", "This is not a valid ChromaFlock (.cfl) preset file.", this); + return; + } + + auto fileVersion = root->getStringAttribute("version", ""); + if (fileVersion != kCflVersion) { + juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, + "Version Mismatch", + "This preset was saved with ChromaFlock " + (fileVersion.isEmpty() ? "" : fileVersion) + + ", but you are running ChromaFlock " + kCflVersion + + ".\nThe preset may not sound as intended.", this); + } + + auto* presetXml = root->getChildByName(processorRef.apvts.state.getType()); + if (presetXml == nullptr) { + juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, + "Invalid File", "The preset file does not contain ChromaFlock parameter data.", this); + return; + } + + processorRef.fadeOutActiveVoices(0.25f); + processorRef.apvts.replaceState(juce::ValueTree::fromXml(*presetXml)); + + currentPresetIndex = -1; + patchLCD.setText(file.getFileNameWithoutExtension()); + + setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox); + setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox); + setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox); + }); +} + void MainContentComponent::applyCurrentPreset() { auto& presets = processorRef.presetManager.getPresets(); if (presets.empty()) return; diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index c352733..5e61217 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -207,6 +207,10 @@ private: const juce::String& beatId, juce::ComboBox& syncBox); void showPresetMenu(); + void showFileMenu(); + void loadPresetFile(); + void savePresetFile(); + juce::String currentPresetName() const; void applyCurrentPreset(); void randomizeParameters();