From 3fc9fa208b2cc2629d15d17d82c057e689ca4542 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Sat, 9 Aug 2025 15:32:40 +0100 Subject: [PATCH] Add new 'add effect' button and toggle between the list of effects and the effects grid --- Source/EffectsComponent.cpp | 54 ++++++++++++++++++- Source/EffectsComponent.h | 3 ++ Source/PluginProcessor.cpp | 7 ++- Source/components/EffectTypeGridComponent.cpp | 6 +++ Source/components/EffectTypeGridComponent.h | 2 + Source/components/EffectTypeItemComponent.cpp | 39 +++++--------- Source/components/EffectTypeItemComponent.h | 8 +-- Source/components/EffectsListComponent.cpp | 21 +++++++- Source/components/EffectsListComponent.h | 49 ++++++++++++----- Source/components/HoverAnimationMixin.cpp | 27 ++++------ Source/components/HoverAnimationMixin.h | 25 ++++----- modules/osci_render_core | 2 +- 12 files changed, 164 insertions(+), 79 deletions(-) diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index 69b9148b..f35adedf 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -33,8 +33,55 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP audioProcessor.broadcaster.addChangeListener(this); } + // Wire list model to notify when user wants to add + itemData.onAddNewEffectRequested = [this]() { + showingGrid = true; + if (grid) + grid->setVisible(true); + listBox.setVisible(false); + resized(); + repaint(); + }; + + // Start with grid visible by default + showingGrid = true; + grid = std::make_unique(audioProcessor); + grid->onEffectSelected = [this](const juce::String& effectId) { + DBG("Effect selected from grid: " + effectId); + // Mark the chosen effect as selected and enabled (no instance creation for now) + { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + for (auto& eff : audioProcessor.toggleableEffects) { + if (eff->getId() == effectId) { + eff->markSelectable(true); + break; + } + } + } + // Refresh list content + itemData.resetData(); + listBox.updateContent(); + showingGrid = false; + listBox.setVisible(true); + if (grid) + grid->setVisible(false); + resized(); + repaint(); + }; + grid->onCanceled = [this]() { + // If canceled while default grid, just show list + showingGrid = false; + listBox.setVisible(true); + if (grid) + grid->setVisible(false); + resized(); + repaint(); + }; + listBox.setModel(&listBoxModel); addAndMakeVisible(listBox); + addAndMakeVisible(*grid); + listBox.setVisible(false); // grid shown first } EffectsComponent::~EffectsComponent() { @@ -52,7 +99,12 @@ void EffectsComponent::resized() { frequency.setBounds(area.removeFromTop(30)); area.removeFromTop(6); - listBox.setBounds(area); + if (showingGrid) { + if (grid) + grid->setBounds(area); + } else { + listBox.setBounds(area); + } } void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 16c917f8..20d5cc0e 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -6,6 +6,7 @@ #include "PluginProcessor.h" #include "components/DraggableListBox.h" #include "components/EffectsListComponent.h" +#include "components/EffectTypeGridComponent.h" class OscirenderAudioProcessorEditor; class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener { @@ -26,6 +27,8 @@ private: AudioEffectListBoxItemData itemData; EffectsListBoxModel listBoxModel; DraggableListBox listBox; + std::unique_ptr grid; + bool showingGrid = true; // show grid by default EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index ffba9beb..88fa132f 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -162,6 +162,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse for (int i = 0; i < toggleableEffects.size(); i++) { auto effect = toggleableEffects[i]; + effect->markSelectable(false); + booleanParameters.push_back(effect->selected); + effect->selected->setValueNotifyingHost(false); effect->markEnableable(false); booleanParameters.push_back(effect->enabled); effect->enabled->setValueNotifyingHost(false); @@ -630,7 +633,9 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju juce::SpinLock::ScopedLockType lock2(effectsLock); if (volume > EPSILON) { for (auto& effect : toggleableEffects) { - if (effect->enabled->getValue()) { + bool isEnabled = effect->enabled != nullptr && effect->enabled->getValue(); + bool isSelected = effect->selected == nullptr ? true : effect->selected->getBoolValue(); + if (isEnabled && isSelected) { if (effect->getId() == custom->getId()) { effect->setExternalInput(osci::Point{ left, right }); } diff --git a/Source/components/EffectTypeGridComponent.cpp b/Source/components/EffectTypeGridComponent.cpp index 03b99298..8f1ccbbf 100644 --- a/Source/components/EffectTypeGridComponent.cpp +++ b/Source/components/EffectTypeGridComponent.cpp @@ -6,6 +6,10 @@ EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& proce { setupEffectItems(); setSize(400, 200); + addAndMakeVisible(cancelButton); + cancelButton.onClick = [this]() { + if (onCanceled) onCanceled(); + }; } EffectTypeGridComponent::~EffectTypeGridComponent() = default; @@ -44,6 +48,8 @@ void EffectTypeGridComponent::paint(juce::Graphics& g) void EffectTypeGridComponent::resized() { auto bounds = getLocalBounds(); + auto topBar = bounds.removeFromTop(30); + cancelButton.setBounds(topBar.removeFromRight(80).reduced(4)); // Create FlexBox for responsive grid layout flexBox = juce::FlexBox(); diff --git a/Source/components/EffectTypeGridComponent.h b/Source/components/EffectTypeGridComponent.h index e98b3f31..79fc7095 100644 --- a/Source/components/EffectTypeGridComponent.h +++ b/Source/components/EffectTypeGridComponent.h @@ -14,11 +14,13 @@ public: int calculateRequiredHeight(int availableWidth) const; std::function onEffectSelected; + std::function onCanceled; // optional cancel handler private: OscirenderAudioProcessor& audioProcessor; juce::OwnedArray effectItems; juce::FlexBox flexBox; + juce::TextButton cancelButton { "Cancel" }; static constexpr int ITEM_HEIGHT = 80; static constexpr int MIN_ITEM_WIDTH = 180; diff --git a/Source/components/EffectTypeItemComponent.cpp b/Source/components/EffectTypeItemComponent.cpp index 2d503e51..e52e9714 100644 --- a/Source/components/EffectTypeItemComponent.cpp +++ b/Source/components/EffectTypeItemComponent.cpp @@ -1,8 +1,7 @@ #include "EffectTypeItemComponent.h" EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id) - : effectName(name), effectId(id), - hoverAnimation(std::make_unique(this)) + : effectName(name), effectId(id) { juce::String iconSvg = icon; if (icon.isEmpty()) { @@ -26,8 +25,8 @@ void EffectTypeItemComponent::paint(juce::Graphics& g) { auto bounds = getLocalBounds().toFloat().reduced(10); - // Get animation progress from the hover animation mixin - auto animationProgress = hoverAnimation->getAnimationProgress(); + // Get animation progress from inherited HoverAnimationMixin + auto animationProgress = getAnimationProgress(); // Apply upward shift based on animation progress auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; @@ -40,10 +39,11 @@ void EffectTypeItemComponent::paint(juce::Graphics& g) shadow.radius = 15 * animationProgress; shadow.offset = juce::Point(0, 4); - juce::Path shadowPath; - shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); - shadow.drawForPath(g, shadowPath); - + if (shadow.radius > 0) { + juce::Path shadowPath; + shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); + shadow.drawForPath(g, shadowPath); + } } // Draw background with rounded corners - interpolate between normal and hover colors @@ -79,32 +79,19 @@ void EffectTypeItemComponent::resized() iconButton->setBounds(iconArea); // Get animation progress and calculate Y offset - auto animationProgress = hoverAnimation->getAnimationProgress(); + auto animationProgress = getAnimationProgress(); auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; iconButton->setTransform(juce::AffineTransform::translation(0, yOffset)); } -void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event) -{ - hoverAnimation->handleMouseEnter(); -} - -void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event) -{ - hoverAnimation->handleMouseExit(); -} - void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event) { - if (onEffectSelected) + // Extend base behavior to keep hover press animation + HoverAnimationMixin::mouseDown(event); + if (onEffectSelected) { onEffectSelected(effectId); - hoverAnimation->handleMouseDown(); -} - -void EffectTypeItemComponent::mouseUp(const juce::MouseEvent& event) -{ - hoverAnimation->handleMouseUp(event.getPosition(), getLocalBounds()); + } } void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) { diff --git a/Source/components/EffectTypeItemComponent.h b/Source/components/EffectTypeItemComponent.h index f8f5fc7e..184c5d36 100644 --- a/Source/components/EffectTypeItemComponent.h +++ b/Source/components/EffectTypeItemComponent.h @@ -4,7 +4,7 @@ #include "HoverAnimationMixin.h" #include "SvgButton.h" -class EffectTypeItemComponent : public juce::Component +class EffectTypeItemComponent : public HoverAnimationMixin { public: EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id); @@ -12,10 +12,7 @@ public: void paint(juce::Graphics& g) override; void resized() override; - void mouseEnter(const juce::MouseEvent& event) override; - void mouseExit(const juce::MouseEvent& event) override; void mouseDown(const juce::MouseEvent& event) override; - void mouseUp(const juce::MouseEvent& event) override; void mouseMove(const juce::MouseEvent& event) override; const juce::String& getEffectId() const { return effectId; } @@ -27,9 +24,6 @@ private: juce::String effectName; juce::String effectId; - // Hover animation functionality - std::unique_ptr hoverAnimation; - // Icon for the effect std::unique_ptr iconButton; diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp index 30ee7ed6..1abcc0ae 100644 --- a/Source/components/EffectsListComponent.cpp +++ b/Source/components/EffectsListComponent.cpp @@ -114,6 +114,10 @@ std::shared_ptr EffectsListComponent::createComponent(osci::Eff int EffectsListBoxModel::getRowHeight(int row) { auto data = (AudioEffectListBoxItemData&)modelData; + if (row == data.getNumItems() - 1) { + // Last row is the "Add new effect" button + return 44; // a tidy button height + } return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING; } @@ -124,11 +128,24 @@ bool EffectsListBoxModel::hasVariableHeightRows() const { juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) { auto data = (AudioEffectListBoxItemData&)modelData; - if (juce::isPositiveAndBelow(rowNumber, data.getNumItems())) { + if (juce::isPositiveAndBelow(rowNumber, data.getNumItems() - 1)) { // Regular effect component std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); - item = std::make_unique(listBox, data, rowNumber, *data.getEffect(rowNumber)); + item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber)); return item.release(); + } else if (rowNumber == data.getNumItems() - 1) { + // Last row becomes an "Add new effect" button + auto* btn = dynamic_cast(existingComponentToUpdate); + if (btn == nullptr) + btn = new juce::TextButton("+ Add new effect"); + + btn->setButtonText("+ Add new effect"); + auto onAdd = data.onAddNewEffectRequested; // copy to avoid dangling reference + btn->onClick = [onAdd]() mutable { + if (onAdd) + onAdd(); + }; + return btn; } return nullptr; diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h index 0ae88d90..62b5b4fe 100644 --- a/Source/components/EffectsListComponent.h +++ b/Source/components/EffectsListComponent.h @@ -5,6 +5,7 @@ #include "EffectComponent.h" #include "ComponentList.h" #include "SwitchButton.h" +#include "EffectTypeGridComponent.h" #include // Application-specific data container @@ -14,6 +15,7 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData std::vector> data; OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessorEditor& editor; + std::function onAddNewEffectRequested; // callback hooked by parent to open the grid AudioEffectListBoxItemData(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), editor(editor) { resetData(); @@ -21,11 +23,31 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData void randomise() { juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); - - for (int i = 0; i < data.size(); i++) { - auto effect = data[i]; + // Decide how many effects to select (1..5 or up to available) + int total = (int) audioProcessor.toggleableEffects.size(); + int maxPick = juce::jmin(5, total); + int numPick = juce::jmax(1, juce::Random::getSystemRandom().nextInt({1, maxPick + 1})); + + // Build indices [0..total) + std::vector indices(total); + std::iota(indices.begin(), indices.end(), 0); + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(indices.begin(), indices.end(), g); + + // First, deselect and disable all + for (auto& effect : audioProcessor.toggleableEffects) { + effect->markSelectable(false); + effect->markEnableable(false); + } + + // Pick numPick to select & enable, and randomise params + for (int k = 0; k < numPick && k < indices.size(); ++k) { + auto& effect = audioProcessor.toggleableEffects[indices[k]]; + effect->markSelectable(true); + effect->markEnableable(true); + auto id = effect->getId().toLowerCase(); - if (id.contains("scale") || id.contains("translate") || id.contains("trace")) { continue; } @@ -35,25 +57,22 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData if (parameter->lfo != nullptr) { parameter->lfo->setUnnormalisedValueNotifyingHost((int) osci::LfoType::Static); parameter->lfoRate->setUnnormalisedValueNotifyingHost(1); - if (juce::Random::getSystemRandom().nextFloat() > 0.8) { parameter->lfo->setUnnormalisedValueNotifyingHost((int)(juce::Random::getSystemRandom().nextFloat() * (int) osci::LfoType::Noise)); parameter->lfoRate->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() * 0.1); } } } - effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7); } - // shuffle precedence - std::random_device rd; - std::mt19937 g(rd()); + // Refresh local data with only selected effects + resetData(); + + // shuffle precedence of the selected subset std::shuffle(data.begin(), data.end(), g); - for (int i = 0; i < data.size(); i++) { data[i]->setPrecedence(i); } - audioProcessor.updateEffectPrecedence(); } @@ -63,12 +82,16 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData for (int i = 0; i < audioProcessor.toggleableEffects.size(); i++) { auto effect = audioProcessor.toggleableEffects[i]; effect->setValue(effect->getValue()); - data.push_back(effect); + // Ensure 'selected' exists and defaults to true for older projects + effect->markSelectable(effect->selected == nullptr ? true : effect->selected->getBoolValue()); + if (effect->selected == nullptr || effect->selected->getBoolValue()) { + data.push_back(effect); + } } } int getNumItems() override { - return data.size(); + return data.size() + 1; } // CURRENTLY NOT USED diff --git a/Source/components/HoverAnimationMixin.cpp b/Source/components/HoverAnimationMixin.cpp index c9920b10..12d02e7d 100644 --- a/Source/components/HoverAnimationMixin.cpp +++ b/Source/components/HoverAnimationMixin.cpp @@ -1,17 +1,14 @@ #include "HoverAnimationMixin.h" -HoverAnimationMixin::HoverAnimationMixin(juce::Component* targetComponent) - : component(targetComponent), - animatorUpdater(targetComponent), +HoverAnimationMixin::HoverAnimationMixin() + : animatorUpdater(this), hoverAnimator(juce::ValueAnimatorBuilder{} .withEasing(getEasingFunction()) .withDurationMs(getHoverAnimationDurationMs()) .withValueChangedCallback([this](auto value) { animationProgress = static_cast(value); - if (component != nullptr) { - component->repaint(); - component->resized(); - } + repaint(); + resized(); }) .build()), unhoverAnimator(juce::ValueAnimatorBuilder{} @@ -19,10 +16,8 @@ HoverAnimationMixin::HoverAnimationMixin(juce::Component* targetComponent) .withDurationMs(getHoverAnimationDurationMs()) .withValueChangedCallback([this](auto value) { animationProgress = 1.0f - static_cast(value); - if (component != nullptr) { - component->repaint(); - component->resized(); - } + repaint(); + resized(); }) .build()) { @@ -49,27 +44,27 @@ void HoverAnimationMixin::animateHover(bool isHovering) } } -void HoverAnimationMixin::handleMouseEnter() +void HoverAnimationMixin::mouseEnter(const juce::MouseEvent&) { isHovered = true; animateHover(true); } -void HoverAnimationMixin::handleMouseExit() +void HoverAnimationMixin::mouseExit(const juce::MouseEvent&) { isHovered = false; // Fixed logic to prevent getting stuck in hovered state animateHover(false); } -void HoverAnimationMixin::handleMouseDown() +void HoverAnimationMixin::mouseDown(const juce::MouseEvent&) { animateHover(false); } -void HoverAnimationMixin::handleMouseUp(const juce::Point& mousePosition, const juce::Rectangle& componentBounds) +void HoverAnimationMixin::mouseUp(const juce::MouseEvent& event) { // Only animate hover if the mouse is still within the component bounds - if (componentBounds.contains(mousePosition)) + if (getLocalBounds().contains(event.getEventRelativeTo(this).getPosition())) animateHover(true); } diff --git a/Source/components/HoverAnimationMixin.h b/Source/components/HoverAnimationMixin.h index 5154c410..65c0b075 100644 --- a/Source/components/HoverAnimationMixin.h +++ b/Source/components/HoverAnimationMixin.h @@ -1,32 +1,33 @@ #pragma once #include -class HoverAnimationMixin +// Base Component providing animated hover behavior via JUCE mouse overrides. +class HoverAnimationMixin : public juce::Component { public: - HoverAnimationMixin(juce::Component* targetComponent); - virtual ~HoverAnimationMixin() = default; + HoverAnimationMixin(); + ~HoverAnimationMixin() override = default; - // Animation control + // Animation control (available for programmatic triggers if needed) void animateHover(bool isHovering); // Getters float getAnimationProgress() const { return animationProgress; } bool getIsHovered() const { return isHovered; } - - // Mouse event handlers to be called from the component - void handleMouseEnter(); - void handleMouseExit(); - void handleMouseDown(); - void handleMouseUp(const juce::Point& mousePosition, const juce::Rectangle& componentBounds); protected: - // Override this to customize animation parameters + // Customization hooks virtual int getHoverAnimationDurationMs() const { return 200; } virtual std::function getEasingFunction() const { return juce::Easings::createEaseOut(); } +public: + // juce::Component overrides for mouse events - keep public so children can call base explicitly + void mouseEnter(const juce::MouseEvent& event) override; + void mouseExit(const juce::MouseEvent& event) override; + void mouseDown(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + private: - juce::Component* component; float animationProgress = 0.0f; bool isHovered = false; diff --git a/modules/osci_render_core b/modules/osci_render_core index 475f3017..e21dd509 160000 --- a/modules/osci_render_core +++ b/modules/osci_render_core @@ -1 +1 @@ -Subproject commit 475f3017cda5377062611d287605b4f26d4e3551 +Subproject commit e21dd509d93aba1ee8ef31eecb54f5f9fc6f0644