Add new 'add effect' button and toggle between the list of effects and the effects grid

pull/307/head
James H Ball 2025-08-09 15:32:40 +01:00
rodzic cfd2773f52
commit 3fc9fa208b
12 zmienionych plików z 164 dodań i 79 usunięć

Wyświetl plik

@ -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<EffectTypeGridComponent>(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) {

Wyświetl plik

@ -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<EffectTypeGridComponent> grid;
bool showingGrid = true; // show grid by default
EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false);

Wyświetl plik

@ -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<float>& 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 });
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -14,11 +14,13 @@ public:
int calculateRequiredHeight(int availableWidth) const;
std::function<void(const juce::String& effectId)> onEffectSelected;
std::function<void()> onCanceled; // optional cancel handler
private:
OscirenderAudioProcessor& audioProcessor;
juce::OwnedArray<EffectTypeItemComponent> effectItems;
juce::FlexBox flexBox;
juce::TextButton cancelButton { "Cancel" };
static constexpr int ITEM_HEIGHT = 80;
static constexpr int MIN_ITEM_WIDTH = 180;

Wyświetl plik

@ -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<HoverAnimationMixin>(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<int>(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) {

Wyświetl plik

@ -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<HoverAnimationMixin> hoverAnimation;
// Icon for the effect
std::unique_ptr<SvgButton> iconButton;

Wyświetl plik

@ -114,6 +114,10 @@ std::shared_ptr<juce::Component> 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<EffectsListComponent> item(dynamic_cast<EffectsListComponent*>(existingComponentToUpdate));
item = std::make_unique<EffectsListComponent>(listBox, data, rowNumber, *data.getEffect(rowNumber));
item = std::make_unique<EffectsListComponent>(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<juce::TextButton*>(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;

Wyświetl plik

@ -5,6 +5,7 @@
#include "EffectComponent.h"
#include "ComponentList.h"
#include "SwitchButton.h"
#include "EffectTypeGridComponent.h"
#include <random>
// Application-specific data container
@ -14,6 +15,7 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData
std::vector<std::shared_ptr<osci::Effect>> data;
OscirenderAudioProcessor& audioProcessor;
OscirenderAudioProcessorEditor& editor;
std::function<void()> 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<int> 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

Wyświetl plik

@ -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<float>(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<float>(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<int>& mousePosition, const juce::Rectangle<int>& 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);
}

Wyświetl plik

@ -1,32 +1,33 @@
#pragma once
#include <JuceHeader.h>
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<int>& mousePosition, const juce::Rectangle<int>& componentBounds);
protected:
// Override this to customize animation parameters
// Customization hooks
virtual int getHoverAnimationDurationMs() const { return 200; }
virtual std::function<float(float)> 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;

@ -1 +1 @@
Subproject commit 475f3017cda5377062611d287605b4f26d4e3551
Subproject commit e21dd509d93aba1ee8ef31eecb54f5f9fc6f0644