diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index 4255ec8..222b837 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -49,12 +49,6 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP listBox.setModel(&listBoxModel); addAndMakeVisible(listBox); - // Add preset buttons and set up callbacks - addAndMakeVisible(loadPresetButton); - addAndMakeVisible(savePresetButton); - loadPresetButton.onClick = [this] { loadPresetClicked(); }; - savePresetButton.onClick = [this] { savePresetClicked(); }; - setTextLabelPosition(juce::Justification::centred); } @@ -67,12 +61,6 @@ void EffectsComponent::resized() { auto area = getLocalBounds(); auto titleBar = area.removeFromTop(30); titleBar.removeFromLeft(100); - - // Position preset buttons near the top - // auto topArea = area.removeFromTop(25); - // Position the preset buttons below the title bar on the right - savePresetButton.setBounds(titleBar.removeFromRight(50)); - loadPresetButton.setBounds(titleBar.removeFromRight(50 + 5)); // Add 5px spacing randomiseButton.setBounds(titleBar.removeFromLeft(20)); area = area.reduced(20); @@ -86,42 +74,3 @@ void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { itemData.resetData(); listBox.updateContent(); } - -// Add implementations for button handlers -void EffectsComponent::loadPresetClicked() -{ - presetChooser = std::make_unique("Load OsciPreset", - juce::File::getSpecialLocation(juce::File::userDocumentsDirectory), - "*.ospreset"); - auto chooserFlags = juce::FileBrowserComponent::openMode | - juce::FileBrowserComponent::canSelectFiles; - - presetChooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) - { - auto file = fc.getResult(); - if (file != juce::File{}) - audioProcessor.loadPreset(file); - }); -} - -void EffectsComponent::savePresetClicked() -{ - presetChooser = std::make_unique("Save OsciPreset", - juce::File::getSpecialLocation(juce::File::userDocumentsDirectory), - "*.ospreset"); - auto chooserFlags = juce::FileBrowserComponent::saveMode | - juce::FileBrowserComponent::canSelectFiles; - - presetChooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) - { - auto file = fc.getResult(); - if (file != juce::File{}) - { - // Ensure the file has the correct extension - if (!file.hasFileExtension(".ospreset")) - file = file.withFileExtension(".ospreset"); - - audioProcessor.savePreset(file); - } - }); -} diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 9741819..3177bae 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -33,12 +33,14 @@ private: EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false); - juce::TextButton loadPresetButton { "Load" }; - juce::TextButton savePresetButton { "Save" }; - std::unique_ptr presetChooser; + // Remove preset buttons and chooser + // juce::TextButton loadPresetButton { "Load" }; + // juce::TextButton savePresetButton { "Save" }; + // std::unique_ptr presetChooser; - void loadPresetClicked(); - void savePresetClicked(); + // Remove preset click handlers + // void loadPresetClicked(); + // void savePresetClicked(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectsComponent) }; \ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 5cb015d..8902ded 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -2,7 +2,7 @@ #include "PluginEditor.h" #include "CustomStandaloneFilterWindow.h" -OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioProcessor& p) : CommonPluginEditor(p, "osci-render", "osci", 1100, 750), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white) { +OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioProcessor& p) : CommonPluginEditor(p, "osci-render", "osci", 1100, 750), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white), presetComponent(p), mainComponent(p, *this) { #if !SOSCI_FEATURES addAndMakeVisible(upgradeButton); upgradeButton.onClick = [this] { @@ -82,6 +82,9 @@ OscirenderAudioProcessorEditor::OscirenderAudioProcessorEditor(OscirenderAudioPr #endif initialiseMenuBar(model); + + addAndMakeVisible(presetComponent); + addAndMakeVisible(mainComponent); } OscirenderAudioProcessorEditor::~OscirenderAudioProcessorEditor() { @@ -170,7 +173,22 @@ void OscirenderAudioProcessorEditor::resized() { if (audioProcessor.visualiserParameters.visualiserFullScreen->getBoolValue()) { visualiser.setBounds(area); + presetComponent.setVisible(false); + settings.setVisible(false); + resizerBar.setVisible(false); + luaResizerBar.setVisible(false); + lua.setVisible(false); + console.setVisible(false); + collapseButton.setVisible(false); + setCodeEditorVisible(false); + if (!usingNativeMenuBar) menuBar.setVisible(false); + volume.setVisible(false); return; + } else { + presetComponent.setVisible(true); + settings.setVisible(true); + if (!usingNativeMenuBar) menuBar.setVisible(true); + volume.setVisible(true); } if (!usingNativeMenuBar) { @@ -186,8 +204,11 @@ void OscirenderAudioProcessorEditor::resized() { auto volumeArea = area.removeFromLeft(30); volume.setBounds(volumeArea.withSizeKeepingCentre(volumeArea.getWidth(), juce::jmin(volumeArea.getHeight(), 300))); area.removeFromLeft(3); - bool editorVisible = false; + auto presetHeight = 160; + presetComponent.setBounds(area.removeFromTop(presetHeight)); + + bool editorVisible = false; { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); @@ -208,9 +229,7 @@ void OscirenderAudioProcessorEditor::resized() { juce::Component* columns[] = { &dummy, &resizerBar, &dummy2 }; - // offsetting the y position by -1 and the height by +1 is a hack to fix a bug where the code editor - // doesn't draw up to the edges of the menu bar above. - layout.layOutComponents(columns, 3, area.getX(), area.getY() - 1, area.getWidth(), area.getHeight() + 1, false, true); + layout.layOutComponents(columns, 3, area.getX(), area.getY() -1 , area.getWidth(), area.getHeight() + 1, false, true); auto dummyBounds = dummy.getBounds(); collapseButton.setBounds(dummyBounds.removeFromRight(20)); area = dummyBounds; @@ -245,14 +264,18 @@ void OscirenderAudioProcessorEditor::resized() { collapseButton.setVisible(ableToEditFile); - if (index < codeEditors.size()) { - codeEditors[index]->setVisible(fileOpen); + if (!fileOpen) { + if (index < codeEditors.size()) codeEditors[index]->setVisible(false); + resizerBar.setVisible(false); + console.setVisible(false); + luaResizerBar.setVisible(false); + lua.setVisible(false); + } else { + resizerBar.setVisible(true); + console.setVisible(luaFileOpen); + luaResizerBar.setVisible(luaFileOpen); + lua.setVisible(luaFileOpen); } - resizerBar.setVisible(fileOpen); - - console.setVisible(luaFileOpen); - luaResizerBar.setVisible(luaFileOpen); - lua.setVisible(luaFileOpen); } if (editorVisible) { @@ -269,8 +292,6 @@ void OscirenderAudioProcessorEditor::resized() { audioProcessor.setProperty("codeEditorLayoutPreferredSize", layout.getItemCurrentRelativeSize(0)); audioProcessor.setProperty("luaLayoutPreferredSize", luaLayout.getItemCurrentRelativeSize(0)); - - repaint(); } void OscirenderAudioProcessorEditor::addCodeEditor(int index) { @@ -297,9 +318,7 @@ void OscirenderAudioProcessorEditor::addCodeEditor(int index) { codeDocuments.insert(codeDocuments.begin() + index, codeDocument); codeEditors.insert(codeEditors.begin() + index, editor); addChildComponent(*editor); - // I need to disable accessibility otherwise it doesn't work! Appears to be a JUCE issue, very annoying! editor->setAccessible(false); - // listen for changes to the code editor codeDocument->addListener(this); editor->getEditor().setColourScheme(colourScheme); } @@ -313,7 +332,6 @@ void OscirenderAudioProcessorEditor::removeCodeEditor(int index) { // parsersLock AND effectsLock must be locked before calling this function void OscirenderAudioProcessorEditor::updateCodeEditor(bool binaryFile, bool shouldOpenEditor) { - // check if any code editors are visible bool visible = shouldOpenEditor; if (!visible) { for (int i = 0; i < codeEditors.size(); i++) { @@ -338,10 +356,6 @@ void OscirenderAudioProcessorEditor::updateCodeEditor(bool binaryFile, bool shou codeEditors[i]->setVisible(false); } codeEditors[index]->setVisible(true); - // used so that codeDocumentTextInserted and codeDocumentTextDeleted know whether the parserLock - // is held by the message thread or not. We hold the lock in this function, but not when the - // code document is updated by the user editing text. Since both functions are called by the - // message thread, this is safe. updatingDocumentsWithParserLock = true; if (index == 0) { codeEditors[index]->getEditor().loadContent(audioProcessor.customEffect->getCode()); @@ -380,8 +394,8 @@ void OscirenderAudioProcessorEditor::changeListenerCallback(juce::ChangeBroadcas repaint(); } else if (source == &audioProcessor.fileChangeBroadcaster) { juce::SpinLock::ScopedLockType lock(audioProcessor.parsersLock); - // triggered when the audioProcessor changes the current file (e.g. to Blender) settings.fileUpdated(audioProcessor.getCurrentFileName()); + mainComponent.updateFileLabel(); } } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 14a47b1..3144f2a 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -10,6 +10,8 @@ #include "components/LuaConsole.h" #include "visualiser/VisualiserSettings.h" #include "CommonPluginEditor.h" +#include "MainComponent.h" +#include "PresetComponent.h" class OscirenderAudioProcessorEditor : public CommonPluginEditor, private juce::CodeDocument::Listener, public juce::AsyncUpdater, public juce::ChangeListener, public juce::FileDragAndDropTarget { public: @@ -84,5 +86,8 @@ public: void mouseDown(const juce::MouseEvent& event) override; void mouseMove(const juce::MouseEvent& event) override; + PresetComponent presetComponent; + MainComponent mainComponent; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscirenderAudioProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 05a5cd8..8557c02 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -850,30 +850,36 @@ void OscirenderAudioProcessor::envelopeChanged(EnvelopeComponent* changedEnvelop } } -void OscirenderAudioProcessor::savePreset(juce::File file) { - juce::SpinLock::ScopedLockType lock(effectsLock); // Lock to safely access effects & parameters +// Rename and add metadata parameters +void OscirenderAudioProcessor::saveScene(juce::File file, const juce::String& author, const juce::String& collection, const juce::String& presetName, const juce::String& notes) { + // Lock required resources (effects and potentially parsers/files) + juce::SpinLock::ScopedLockType lockEffects(effectsLock); + juce::SpinLock::ScopedLockType lockParsers(parsersLock); - std::unique_ptr xml = std::make_unique("OsciPreset"); - xml->setAttribute("version", ProjectInfo::versionString); // Use the project version + std::unique_ptr xml = std::make_unique("OsciScene"); // Root element name changed + xml->setAttribute("version", ProjectInfo::versionString); - // --- Save Effects --- + // --- Save Metadata --- + xml->setAttribute("author", author); + xml->setAttribute("collection", collection); + xml->setAttribute("presetName", presetName); + // Use a child element for potentially longer notes + auto* notesXml = xml->createNewChildElement("Notes"); + notesXml->addTextElement(notes); + + // --- Save Effects (same as before) --- auto effectsXml = xml->createNewChildElement("Effects"); - // Save toggleable effects (includes state like enabled, parameter values, LFOs) for (auto& effect : toggleableEffects) { effect->save(effectsXml->createNewChildElement("Effect")); } - // Save permanent effects (e.g., frequency parameter) for (auto& effect : permanentEffects) { - // Only save effects relevant to sound generation/preset? Or save all for consistency? - // Let's save all permanent ones for now, mirroring getStateInformation. effect->save(effectsXml->createNewChildElement("Effect")); } - // Save Lua slider effects for (auto& effect : luaEffects) { effect->save(effectsXml->createNewChildElement("Effect")); } - // --- Save ADSR Parameters --- + // --- Save ADSR Parameters (same as before) --- auto adsrXml = xml->createNewChildElement("ADSR"); attackTime->save(adsrXml->createNewChildElement("Parameter")); attackLevel->save(adsrXml->createNewChildElement("Parameter")); @@ -884,106 +890,149 @@ void OscirenderAudioProcessor::savePreset(juce::File file) { releaseTime->save(adsrXml->createNewChildElement("Parameter")); releaseShape->save(adsrXml->createNewChildElement("Parameter")); - // --- Save Synth Parameters --- + // --- Save Synth Parameters (same as before) --- auto synthParamsXml = xml->createNewChildElement("SynthParameters"); - voices->save(synthParamsXml->createNewChildElement("Parameter")); // Save number of voices + voices->save(synthParamsXml->createNewChildElement("Parameter")); - // --- Save Lua Code --- + // --- Save Lua Code (same as before) --- auto customFunctionXml = xml->createNewChildElement("CustomFunction"); customFunctionXml->addTextElement(juce::Base64::toBase64(customEffect->getCode())); - // --- Write to File --- - if (!xml->writeToFile(file, {})) { - // Handle error - maybe log it or show an alert? - DBG("Error writing preset file: " + file.getFullPathName()); - // For now, just log an error message in debug builds. - // In a real application, you might want to show a juce::AlertWindow. + // --- Save Current Visual File Info --- + xml->setAttribute("currentFileIndex", currentFile.load()); + if (currentFile.load() >= 0 && currentFile.load() < fileNames.size()) { + auto currentFileXml = xml->createNewChildElement("CurrentVisualFile"); + currentFileXml->setAttribute("name", fileNames[currentFile.load()]); + auto base64 = fileBlocks[currentFile.load()]->toBase64Encoding(); + currentFileXml->addTextElement(base64); + } + + // --- Save other relevant parameters (e.g., Main Settings, 3D Settings) --- + // Example: Saving perspective parameters + auto perspectiveParamsXml = xml->createNewChildElement("PerspectiveParameters"); + perspective->parameters[0]->save(perspectiveParamsXml->createNewChildElement("Parameter")); // Strength + perspective->parameters[1]->save(perspectiveParamsXml->createNewChildElement("Parameter")); // Focal Length + + // Example: Saving animation parameters + auto animParamsXml = xml->createNewChildElement("AnimationParameters"); + animateFrames->save(animParamsXml->createNewChildElement("Parameter")); + animationSyncBPM->save(animParamsXml->createNewChildElement("Parameter")); + animationRate->save(animParamsXml->createNewChildElement("Parameter")); + animationOffset->save(animParamsXml->createNewChildElement("Parameter")); + + // Add saving for other parameter groups as needed... + + // --- Write to File (Ensure .osscene extension) --- + juce::File sceneFile = file; + if (!sceneFile.hasFileExtension(".osscene")) + sceneFile = sceneFile.withFileExtension(".osscene"); + + if (!xml->writeToFile(sceneFile, {})) { + DBG("Error writing scene file: " + sceneFile.getFullPathName()); } } -void OscirenderAudioProcessor::loadPreset(juce::File file) { +// Change return type and populate/return SceneMetadata struct +SceneMetadata OscirenderAudioProcessor::loadScene(juce::File file) { + // Keep existing preset loading logic for now + // TODO: Expand this to load the full scene state (visual file, metadata, other params) std::unique_ptr xml = juce::XmlDocument::parse(file); + + SceneMetadata loadedMetadata; // Create struct instance if (xml == nullptr) { - DBG("Error parsing preset file: " + file.getFullPathName()); - // Optionally show an AlertWindow here - return; + DBG("Error parsing scene file: " + file.getFullPathName()); + // Return empty metadata on error + return loadedMetadata; } - if (!xml->hasTagName("OsciPreset")) { - DBG("Invalid preset file format: " + file.getFullPathName()); - // Optionally show an AlertWindow here - return; + // Check for OsciScene or fallback to OsciPreset for backward compatibility? + if (!xml->hasTagName("OsciScene") && !xml->hasTagName("OsciPreset")) { + DBG("Invalid scene/preset file format: " + file.getFullPathName()); + // Return empty metadata on error + return loadedMetadata; + } + bool isSceneFile = xml->hasTagName("OsciScene"); + + juce::SpinLock::ScopedLockType lockEffects(effectsLock); + juce::SpinLock::ScopedLockType lockParsers(parsersLock); + + // --- Load Metadata (if it's a scene file) and populate struct --- + if (isSceneFile) { + loadedMetadata.author = xml->getStringAttribute("author", ""); + loadedMetadata.collection = xml->getStringAttribute("collection", ""); + loadedMetadata.presetName = xml->getStringAttribute("presetName", ""); + auto* notesXml = xml->getChildByName("Notes"); + if (notesXml != nullptr) { + loadedMetadata.notes = notesXml->getAllSubText(); + } + // Log statements can be removed if desired + DBG("Loading Scene Metadata:"); + DBG(" Author: " + loadedMetadata.author); + DBG(" Collection: " + loadedMetadata.collection); + DBG(" Preset Name: " + loadedMetadata.presetName); + DBG(" Notes: " + loadedMetadata.notes); } - // Lock effects/parameters while loading - juce::SpinLock::ScopedLockType lock(effectsLock); - - // --- Load Effects --- + // --- Load Effects --- auto effectsXml = xml->getChildByName("Effects"); if (effectsXml != nullptr) { - // Iterate through all saved Effect elements for (auto* effectXml : effectsXml->getChildIterator()) { if (effectXml->hasTagName("Effect")) { - // Find the matching effect in our processor's lists by ID auto effectId = effectXml->getStringAttribute("id"); - auto effect = getEffect(effectId); // Assuming getEffect searches all relevant lists (toggleable, permanent, lua) + auto effect = getEffect(effectId); if (effect != nullptr) { effect->load(effectXml); } else { - DBG("Preset loading: Could not find effect with ID: " + effectId); + DBG("Scene loading: Could not find effect with ID: " + effectId); } } } - updateEffectPrecedence(); // Re-sort toggleable effects based on loaded precedence + updateEffectPrecedence(); } - // --- Load ADSR Parameters --- + // --- Load ADSR Parameters --- auto adsrXml = xml->getChildByName("ADSR"); if (adsrXml != nullptr) { for (auto* parameterXml : adsrXml->getChildIterator()) { if (parameterXml->hasTagName("Parameter")) { auto paramId = parameterXml->getStringAttribute("id"); - auto parameter = getFloatParameter(paramId); // Assumes ADSR params are FloatParameters + auto parameter = getFloatParameter(paramId); if (parameter != nullptr) { parameter->load(parameterXml); } else { - DBG("Preset loading: Could not find ADSR parameter with ID: " + paramId); + DBG("Scene loading: Could not find ADSR parameter with ID: " + paramId); } } } - // Update the internal adsrEnv object after loading parameters adsrEnv = Env::adsr( attackTime->getValueUnnormalised(), decayTime->getValueUnnormalised(), sustainLevel->getValueUnnormalised(), releaseTime->getValueUnnormalised(), - attackLevel->getValueUnnormalised(), // Assuming attackLevel corresponds to env's 'peakLevel' + attackLevel->getValueUnnormalised(), std::vector{ attackShape->getValueUnnormalised(), decayShape->getValueUnnormalised(), releaseShape->getValueUnnormalised() } ); } - // --- Load Synth Parameters --- + // --- Load Synth Parameters --- auto synthParamsXml = xml->getChildByName("SynthParameters"); if (synthParamsXml != nullptr) { for (auto* parameterXml : synthParamsXml->getChildIterator()) { if (parameterXml->hasTagName("Parameter")) { auto paramId = parameterXml->getStringAttribute("id"); - // Check if it's the 'voices' parameter if (paramId == voices->getParameterID()) { voices->load(parameterXml); - // Trigger parameterValueChanged to update synth voices parameterValueChanged(voices->getParameterIndex(), voices->getValue()); - } - // Add checks for other synth parameters if needed + } else { - DBG("Preset loading: Could not find Synth parameter with ID: " + paramId); + DBG("Scene loading: Could not find Synth parameter with ID: " + paramId); } } } } - // --- Load Lua Code --- + // --- Load Lua Code --- auto customFunctionXml = xml->getChildByName("CustomFunction"); if (customFunctionXml != nullptr) { auto base64Code = customFunctionXml->getAllSubText(); @@ -991,14 +1040,93 @@ void OscirenderAudioProcessor::loadPreset(juce::File file) { if (juce::Base64::convertFromBase64(stream, base64Code)) { customEffect->updateCode(stream.toString()); } else { - DBG("Preset loading: Failed to decode Base64 Lua code."); + DBG("Scene loading: Failed to decode Base64 Lua code."); } } - // --- Notify UI/Listeners --- - // Use MessageManagerLock as sendChangeMessage needs to be called from the message thread + // --- Load Current Visual File Info (if it's a scene file) --- + if (isSceneFile) { + int loadedFileIndex = xml->getIntAttribute("currentFileIndex", -1); + auto* currentFileXml = xml->getChildByName("CurrentVisualFile"); + if (currentFileXml != nullptr && loadedFileIndex != -1) { + auto fileName = currentFileXml->getStringAttribute("name"); + auto base64Data = currentFileXml->getAllSubText(); + std::shared_ptr fileBlock = std::make_shared(); + if (fileBlock->fromBase64Encoding(base64Data)) { + // Need to find if this file already exists or add it + // This requires more careful handling to avoid duplicates + // and potentially clearing existing files first. + // For now, let's just try adding it. If it needs replacing, + // the logic gets more complex. + + // A simpler approach might be to clear all existing files first? + // Clear existing files (Careful! This deletes user's loaded files) + // int numFiles = fileBlocks.size(); + // for (int i = numFiles - 1; i >= 0; --i) { + // removeFile(i); + // } + // Then add the saved file: + // addFile(fileName, fileBlock); + // changeCurrentFile(0); // Assuming it becomes the first file + + DBG("Scene loading: Visual file found - Name: " + fileName + ", Index: " + juce::String(loadedFileIndex)); + DBG("Scene loading: TODO - Implement proper visual file loading logic (clearing/adding/selecting)"); + } else { + DBG("Scene loading: Failed to decode Base64 visual file data."); + } + } else if (loadedFileIndex == -1) { + // If scene saved with no visual file selected, clear current file? + // changeCurrentFile(-1); + DBG("Scene loading: TODO - Implement logic for loading scene with no visual file selected."); + } + } + + // --- Load other parameters (if it's a scene file) --- + if (isSceneFile) { + // Example: Loading perspective parameters + auto perspectiveParamsXml = xml->getChildByName("PerspectiveParameters"); + if (perspectiveParamsXml != nullptr) { + for (auto* parameterXml : perspectiveParamsXml->getChildIterator()) { + if (parameterXml->hasTagName("Parameter")) { + auto paramId = parameterXml->getStringAttribute("id"); + auto parameter = getFloatParameter(paramId); // Assuming perspective params are float + if (parameter != nullptr) { + parameter->load(parameterXml); + } else { + DBG("Scene loading: Could not find Perspective parameter with ID: " + paramId); + } + } + } + } + + // Example: Loading animation parameters + auto animParamsXml = xml->getChildByName("AnimationParameters"); + if (animParamsXml != nullptr) { + for (auto* parameterXml : animParamsXml->getChildIterator()) { + if (parameterXml->hasTagName("Parameter")) { + auto paramId = parameterXml->getStringAttribute("id"); + // Need to check boolean and float params separately + auto boolParam = getBooleanParameter(paramId); + auto floatParam = getFloatParameter(paramId); + if (boolParam != nullptr) { + boolParam->load(parameterXml); + } else if (floatParam != nullptr) { + floatParam->load(parameterXml); + } else { + DBG("Scene loading: Could not find Animation parameter with ID: " + paramId); + } + } + } + } + // Add loading for other parameter groups as needed... + } + + // --- Notify UI/Listeners --- juce::MessageManagerLock mmlock; broadcaster.sendChangeMessage(); + + // Return the populated metadata struct + return loadedMetadata; } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index f8777b5..ae12de8 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -31,6 +31,14 @@ #include "CommonPluginProcessor.h" #include "MidiAlwaysEnabled.h" +// Define a struct to hold scene metadata +struct SceneMetadata { + juce::String author; + juce::String collection; + juce::String presetName; + juce::String notes; +}; + //============================================================================== /** */ @@ -207,8 +215,8 @@ public: void removeErrorListener(ErrorListener* listener); void notifyErrorListeners(int lineNumber, juce::String id, juce::String error); - void savePreset(juce::File file); - void loadPreset(juce::File file); + void saveScene(juce::File file, const juce::String& author, const juce::String& collection, const juce::String& presetName, const juce::String& notes); + SceneMetadata loadScene(juce::File file); private: std::atomic prevMidiEnabled = isMidiAlwaysEnabled() || !midiEnabled->getBoolValue(); diff --git a/Source/PresetComponent.cpp b/Source/PresetComponent.cpp new file mode 100644 index 0000000..c97a755 --- /dev/null +++ b/Source/PresetComponent.cpp @@ -0,0 +1,150 @@ +#include "PresetComponent.h" +#include "PluginProcessor.h" // Include processor again if needed (usually just in .h) + +//============================================================================== +PresetComponent::PresetComponent(OscirenderAudioProcessor& p) : processor(p) +{ + // Setup Labels + authorLabel.setText("Author:", juce::dontSendNotification); + collectionLabel.setText("Collection:", juce::dontSendNotification); + presetNameLabel.setText("Scene Name:", juce::dontSendNotification); + notesLabel.setText("Notes:", juce::dontSendNotification); + + // Setup TextEditors + presetNameEditor.setJustification(juce::Justification::centredLeft); + authorEditor.setJustification(juce::Justification::centredLeft); + collectionEditor.setJustification(juce::Justification::centredLeft); + notesEditor.setMultiLine(true); + notesEditor.setReturnKeyStartsNewLine(true); + + // Make components visible + addAndMakeVisible(authorLabel); + addAndMakeVisible(authorEditor); + addAndMakeVisible(collectionLabel); + addAndMakeVisible(collectionEditor); + addAndMakeVisible(presetNameLabel); + addAndMakeVisible(presetNameEditor); + addAndMakeVisible(notesLabel); + addAndMakeVisible(notesEditor); + addAndMakeVisible(loadSceneButton); + addAndMakeVisible(saveSceneButton); + + // Setup button callbacks + loadSceneButton.onClick = [this] { loadSceneClicked(); }; + saveSceneButton.onClick = [this] { saveSceneClicked(); }; + + // TODO: Need a way to get initial/loaded metadata to populate fields. + // Maybe processor needs getter methods or PresetComponent becomes a ChangeListener? +} + +PresetComponent::~PresetComponent() +{ + // Destructor logic if needed +} + +void PresetComponent::paint (juce::Graphics& g) +{ + // Optional: Add background painting or borders + // g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); + // g.setColour (juce::Colours::grey); + // g.drawRect (getLocalBounds(), 1); +} + +void PresetComponent::resized() +{ + auto bounds = getLocalBounds().reduced(10); // Add some padding + + auto topRow = bounds.removeFromTop(25); + loadSceneButton.setBounds(topRow.removeFromLeft(100)); + saveSceneButton.setBounds(topRow.removeFromLeft(100).translated(105, 0)); + + bounds.removeFromTop(10); // Spacing + + auto metadataRowHeight = 25; + auto labelWidth = 80; + auto editorWidth = bounds.getWidth() - labelWidth - 5; // 5px spacing + + auto nameRow = bounds.removeFromTop(metadataRowHeight); + presetNameLabel.setBounds(nameRow.removeFromLeft(labelWidth)); + presetNameEditor.setBounds(nameRow.withX(nameRow.getX() + 5).withWidth(editorWidth)); + + bounds.removeFromTop(5); // Spacing + + auto authorRow = bounds.removeFromTop(metadataRowHeight); + authorLabel.setBounds(authorRow.removeFromLeft(labelWidth)); + authorEditor.setBounds(authorRow.withX(authorRow.getX() + 5).withWidth(editorWidth)); + + bounds.removeFromTop(5); // Spacing + + auto collectionRow = bounds.removeFromTop(metadataRowHeight); + collectionLabel.setBounds(collectionRow.removeFromLeft(labelWidth)); + collectionEditor.setBounds(collectionRow.withX(collectionRow.getX() + 5).withWidth(editorWidth)); + + bounds.removeFromTop(5); // Spacing + + notesLabel.setBounds(bounds.removeFromTop(metadataRowHeight)); // Label takes full width temporarily + notesLabel.setJustificationType(juce::Justification::topLeft); // Align label top-left + bounds.removeFromTop(5); // Spacing + notesEditor.setBounds(bounds); // Notes editor takes remaining space +} + +void PresetComponent::loadSceneClicked() +{ + sceneChooser = std::make_unique("Load OsciScene File", + juce::File::getSpecialLocation(juce::File::userDocumentsDirectory), + "*.osscene"); // Use new extension + auto chooserFlags = juce::FileBrowserComponent::openMode | + juce::FileBrowserComponent::canSelectFiles; + + sceneChooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (file != juce::File{}) + { + // Call loadScene and capture the returned metadata + SceneMetadata loadedData = processor.loadScene(file); + + // Update the text fields using the returned metadata + updateMetadataFields(loadedData.author, loadedData.collection, loadedData.presetName, loadedData.notes); + + // The DBG message is no longer a TODO + DBG("PresetComponent: Updated metadata fields after loading scene."); + } + }); +} + +void PresetComponent::saveSceneClicked() +{ + sceneChooser = std::make_unique("Save OsciScene File", + juce::File::getSpecialLocation(juce::File::userDocumentsDirectory), + "*.osscene", // Use new extension + true); // useNativeBox = true + + auto chooserFlags = juce::FileBrowserComponent::saveMode | + juce::FileBrowserComponent::canSelectFiles; + + sceneChooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (file != juce::File{}) + { + // Get metadata from TextEditors + auto author = authorEditor.getText(); + auto collection = collectionEditor.getText(); + auto presetName = presetNameEditor.getText(); + auto notes = notesEditor.getText(); + + // Call the processor's save function + processor.saveScene(file, author, collection, presetName, notes); + } + }); +} + +// Method implementation to update text fields +void PresetComponent::updateMetadataFields(const juce::String& author, const juce::String& collection, const juce::String& presetName, const juce::String& notes) +{ + authorEditor.setText(author, juce::dontSendNotification); + collectionEditor.setText(collection, juce::dontSendNotification); + presetNameEditor.setText(presetName, juce::dontSendNotification); + notesEditor.setText(notes, juce::dontSendNotification); +} diff --git a/Source/PresetComponent.h b/Source/PresetComponent.h new file mode 100644 index 0000000..fc5041b --- /dev/null +++ b/Source/PresetComponent.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include "PluginProcessor.h" // Include processor header + +/* + Component for handling scene loading, saving, and metadata. +*/ +class PresetComponent : public juce::Component +{ +public: + PresetComponent(OscirenderAudioProcessor& processor); + ~PresetComponent() override; + + void paint (juce::Graphics&) override; + void resized() override; + + // Method to update text fields after loading a scene + void updateMetadataFields(const juce::String& author, const juce::String& collection, const juce::String& presetName, const juce::String& notes); + +private: + OscirenderAudioProcessor& processor; // Reference to the processor + + // Metadata Labels and TextEditors + juce::Label authorLabel; + juce::TextEditor authorEditor; + juce::Label collectionLabel; + juce::TextEditor collectionEditor; + juce::Label presetNameLabel; + juce::TextEditor presetNameEditor; + juce::Label notesLabel; + juce::TextEditor notesEditor; // Multi-line for notes + + // Load/Save Buttons + juce::TextButton loadSceneButton { "Load Scene" }; + juce::TextButton saveSceneButton { "Save Scene" }; + + // File Chooser + std::unique_ptr sceneChooser; + + // Button Handlers + void loadSceneClicked(); + void saveSceneClicked(); + + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PresetComponent) +}; \ No newline at end of file diff --git a/osci-render-midi.jucer b/osci-render-midi.jucer index 8c83436..90b9e20 100644 --- a/osci-render-midi.jucer +++ b/osci-render-midi.jucer @@ -206,6 +206,12 @@ + + + +