diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 535f7e9..705cfec 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -42,6 +42,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Multiplex Phase", "Controls the current phase of the multiplex grid animation.", "gridPhase", VERSION_HINT, 0.0, 0.0, 1.0), new osci::EffectParameter("Multiplex Delay", "Controls the delay of the audio samples used in the multiplex effect.", "gridDelay", VERSION_HINT, 0.0, 0.0, 1.0), }); + multiplexEffect->setName("Multiplex"); // Set up the Grid Phase parameter with sawtooth LFO at 100Hz multiplexEffect->getParameter("gridPhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); multiplexEffect->getParameter("gridPhase")->lfoRate->setUnnormalisedValueNotifyingHost(100.0); @@ -58,6 +59,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Scale Y", "Scales the object in the vertical direction.", "scaleY", VERSION_HINT, 1.0, -3.0, 3.0), new osci::EffectParameter("Scale Z", "Scales the depth of the object.", "scaleZ", VERSION_HINT, 1.0, -3.0, 3.0), }); + scaleEffect->setName("Scale"); scaleEffect->markLockable(true); booleanParameters.push_back(scaleEffect->linked); toggleableEffects.push_back(scaleEffect); @@ -72,6 +74,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Distort Y", "Distorts the image in the vertical direction by jittering the audio sample being drawn.", "distortY", VERSION_HINT, 0.0, 0.0, 1.0), new osci::EffectParameter("Distort Z", "Distorts the depth of the image by jittering the audio sample being drawn.", "distortZ", VERSION_HINT, 0.1, 0.0, 1.0), }); + distortEffect->setName("Distort"); distortEffect->markLockable(false); booleanParameters.push_back(distortEffect->linked); toggleableEffects.push_back(distortEffect); @@ -87,6 +90,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Ripple Phase", "Controls the position of the ripple. Animate this to see a moving ripple effect.", "ripplePhase", VERSION_HINT, 0.0, -1.0, 1.0), new osci::EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0), }); + rippleEffect->setName("Ripple"); rippleEffect->getParameter("ripplePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); toggleableEffects.push_back(rippleEffect); auto rotateEffect = std::make_shared( @@ -99,10 +103,11 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "rotateY", VERSION_HINT, 0.0, -1.0, 1.0), new osci::EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "rotateZ", VERSION_HINT, 0.0, -1.0, 1.0), }); + rotateEffect->setName("Rotate"); rotateEffect->getParameter("rotateY")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); rotateEffect->getParameter("rotateY")->lfoRate->setUnnormalisedValueNotifyingHost(0.2); toggleableEffects.push_back(rotateEffect); - toggleableEffects.push_back(std::make_shared( + auto translateEffect = std::make_shared( [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { return input + osci::Point(values[0], values[1], values[2]); }, @@ -110,7 +115,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Translate X", "Moves the object horizontally.", "translateX", VERSION_HINT, 0.0, -1.0, 1.0), new osci::EffectParameter("Translate Y", "Moves the object vertically.", "translateY", VERSION_HINT, 0.0, -1.0, 1.0), new osci::EffectParameter("Translate Z", "Moves the object away from the camera.", "translateZ", VERSION_HINT, 0.0, -1.0, 1.0), - })); + }); + translateEffect->setName("Translate"); + toggleableEffects.push_back(translateEffect); toggleableEffects.push_back(std::make_shared( [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { double length = 10 * values[0] * input.magnitude(); @@ -130,18 +137,24 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Wobble Amount", "Adds a sine wave of the prominent frequency in the audio currently playing. The sine wave's frequency is slightly offset to create a subtle 'wobble' in the image. Increasing the slider increases the strength of the wobble.", "wobble", VERSION_HINT, 0.3, 0.0, 1.0), new osci::EffectParameter("Wobble Phase", "Controls the phase of the wobble.", "wobblePhase", VERSION_HINT, 0.0, -1.0, 1.0), }); + wobble->setName("Wobble"); wobble->getParameter("wobblePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); toggleableEffects.push_back(wobble); - toggleableEffects.push_back(std::make_shared( + auto delay = std::make_shared( delayEffect, std::vector{ new osci::EffectParameter("Delay Decay", "Adds repetitions, delays, or echos to the audio. This slider controls the volume of the echo.", "delayDecay", VERSION_HINT, 0.4, 0.0, 1.0), - new osci::EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0)})); - toggleableEffects.push_back(std::make_shared( + new osci::EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0) + }); + delay->setName("Delay"); + toggleableEffects.push_back(delay); + auto dashEffect = std::make_shared( dashedLineEffect, std::vector{ new osci::EffectParameter("Dash Length", "Controls the length of the dashed line.", "dashLength", VERSION_HINT, 0.2, 0.0, 1.0), - })); + }); + dashEffect->setName("Dash"); + toggleableEffects.push_back(dashEffect); toggleableEffects.push_back(custom); toggleableEffects.push_back(trace); trace->getParameter("traceLength")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index ba5bd37..fa21d1f 100644 --- a/Source/components/EffectComponent.cpp +++ b/Source/components/EffectComponent.cpp @@ -28,6 +28,7 @@ EffectComponent::EffectComponent(osci::Effect& effect, int index) : effect(effec slider.setSliderStyle(juce::Slider::LinearHorizontal); slider.setTextBoxStyle(juce::Slider::TextBoxRight, false, TEXT_BOX_WIDTH, slider.getTextBoxHeight()); + slider.setScrollWheelEnabled(false); if (effect.parameters[index]->step == 1.0) { slider.setNumDecimalPlacesToDisplay(0); } else { @@ -39,6 +40,7 @@ EffectComponent::EffectComponent(osci::Effect& effect, int index) : effect(effec lfoSlider.setTextValueSuffix("Hz"); lfoSlider.setColour(sliderThumbOutlineColourId, juce::Colour(0xff00ff00)); lfoSlider.setNumDecimalPlacesToDisplay(3); + lfoSlider.setScrollWheelEnabled(false); label.setFont(juce::Font(14.0f)); diff --git a/Source/components/EffectTypeGridComponent.cpp b/Source/components/EffectTypeGridComponent.cpp new file mode 100644 index 0000000..e04c792 --- /dev/null +++ b/Source/components/EffectTypeGridComponent.cpp @@ -0,0 +1,81 @@ +#include "EffectTypeGridComponent.h" +#include "../LookAndFeel.h" + +EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor) + : audioProcessor(processor) +{ + setupEffectItems(); + setSize(400, 200); // Default size, will be resized by parent + setMouseCursor(juce::MouseCursor::PointingHandCursor); +} + +EffectTypeGridComponent::~EffectTypeGridComponent() = default; + +void EffectTypeGridComponent::setupEffectItems() +{ + // Clear existing items + effectItems.clear(); + + // Get effect types directly from the audio processor's toggleableEffects + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + for (const auto& effect : audioProcessor.toggleableEffects) + { + // Extract effect name from the effect ID or first parameter name + juce::String effectName = effect->getName(); + + // Create new item component + auto* item = new EffectTypeItemComponent(effectName, effect->getId()); + + // Set up callback to forward effect selection + item->onEffectSelected = [this](const juce::String& effectId) { + if (onEffectSelected) + onEffectSelected(effectId); + }; + + effectItems.add(item); + addAndMakeVisible(item); + } +} + +void EffectTypeGridComponent::paint(juce::Graphics& g) +{ + // No background - make component transparent +} + +void EffectTypeGridComponent::resized() +{ + auto bounds = getLocalBounds(); + + // Create FlexBox for responsive grid layout + flexBox = juce::FlexBox(); + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceBetween; + flexBox.alignContent = juce::FlexBox::AlignContent::flexStart; + flexBox.flexDirection = juce::FlexBox::Direction::row; + + // Add each effect item as a FlexItem with flex-grow to fill available space + for (auto* item : effectItems) + { + flexBox.items.add(juce::FlexItem(*item) + .withMinWidth(MIN_ITEM_WIDTH) + .withHeight(ITEM_HEIGHT) + .withFlex(1.0f) // Allow items to grow to fill available space + .withMargin(juce::FlexItem::Margin(0))); + } + + flexBox.performLayout(bounds.toFloat()); +} + +int EffectTypeGridComponent::calculateRequiredHeight(int availableWidth) const +{ + if (effectItems.isEmpty()) + return ITEM_HEIGHT; + + // Calculate how many items can fit per row + int itemsPerRow = juce::jmax(1, availableWidth / MIN_ITEM_WIDTH); + + // Calculate number of rows needed + int numRows = (effectItems.size() + itemsPerRow - 1) / itemsPerRow; // Ceiling division + + return numRows * ITEM_HEIGHT; +} diff --git a/Source/components/EffectTypeGridComponent.h b/Source/components/EffectTypeGridComponent.h new file mode 100644 index 0000000..e98b3f3 --- /dev/null +++ b/Source/components/EffectTypeGridComponent.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include "../PluginProcessor.h" +#include "EffectTypeItemComponent.h" + +class EffectTypeGridComponent : public juce::Component +{ +public: + EffectTypeGridComponent(OscirenderAudioProcessor& processor); + ~EffectTypeGridComponent() override; + + void paint(juce::Graphics& g) override; + void resized() override; + + int calculateRequiredHeight(int availableWidth) const; + std::function onEffectSelected; + +private: + OscirenderAudioProcessor& audioProcessor; + juce::OwnedArray effectItems; + juce::FlexBox flexBox; + + static constexpr int ITEM_HEIGHT = 80; + static constexpr int MIN_ITEM_WIDTH = 180; + + void setupEffectItems(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeGridComponent) +}; diff --git a/Source/components/EffectTypeItemComponent.cpp b/Source/components/EffectTypeItemComponent.cpp new file mode 100644 index 0000000..377c83e --- /dev/null +++ b/Source/components/EffectTypeItemComponent.cpp @@ -0,0 +1,120 @@ +#include "EffectTypeItemComponent.h" + +EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& id) + : effectName(name), effectId(id), + hoverAnimator(juce::ValueAnimatorBuilder{} + .withEasing(juce::Easings::createEaseOut()) + .withDurationMs(200) + .withValueChangedCallback([this](auto value) { + animationProgress = static_cast(value); + repaint(); + }) + .build()), + unhoverAnimator(juce::ValueAnimatorBuilder{} + .withEasing(juce::Easings::createEaseOut()) + .withDurationMs(200) + .withValueChangedCallback([this](auto value) { + animationProgress = 1.0f - static_cast(value); + repaint(); + }) + .build()) +{ + setupAnimators(); +} + +EffectTypeItemComponent::~EffectTypeItemComponent() = default; + +void EffectTypeItemComponent::setupAnimators() +{ + animatorUpdater.addAnimator(hoverAnimator); + animatorUpdater.addAnimator(unhoverAnimator); +} + +void EffectTypeItemComponent::animateHover(bool isHovering) +{ + if (isHovering) + { + unhoverAnimator.complete(); + hoverAnimator.start(); + } + else + { + hoverAnimator.complete(); + unhoverAnimator.start(); + } +} + +void EffectTypeItemComponent::paint(juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat().reduced(10); + + // Apply upward shift based on animation progress + auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; + bounds = bounds.translated(0, yOffset); + + // Draw drop shadow + if (animationProgress > 0.01f) { + juce::DropShadow shadow; + shadow.colour = juce::Colours::lime.withAlpha(animationProgress * 0.2f); + shadow.radius = 15 * animationProgress; + shadow.offset = juce::Point(0, 4); + + juce::Path shadowPath; + shadowPath.addRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); + shadow.drawForPath(g, shadowPath); + + } + + // Draw background with rounded corners - interpolate between normal and hover colors + juce::Colour normalBgColour = juce::Colour::fromRGB(25, 25, 25); + juce::Colour hoverBgColour = juce::Colour::fromRGB(40, 40, 40); + juce::Colour bgColour = normalBgColour.interpolatedWith(hoverBgColour, animationProgress); + + g.setColour(bgColour); + g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); + + // Draw colored outline + juce::Colour outlineColour = juce::Colour::fromRGB(160, 160, 160); + g.setColour(outlineColour.withAlpha(0.9f)); + g.drawRoundedRectangle(bounds.toFloat(), CORNER_RADIUS, 1.0f); + + // Create areas for text (left) and icon (right) + auto textArea = bounds.reduced(8, 4); + auto iconArea = textArea.removeFromRight(20); // Reserve 20px for icon on right + textArea = textArea.withTrimmedRight(4); // Add small gap between text and icon + + g.setColour(juce::Colours::white); + g.setFont(juce::FontOptions(16.0f, juce::Font::plain)); + g.drawText(effectName, textArea, juce::Justification::centred, true); + + // Draw placeholder icon (simple circle with "+" symbol) + g.setColour(juce::Colours::white.withAlpha(0.7f)); + auto iconSize = juce::jmin(iconArea.getWidth(), iconArea.getHeight()) - 4; + auto iconBounds = iconArea.withSizeKeepingCentre(iconSize, iconSize); + g.drawEllipse(iconBounds.toFloat(), 1.5f); + + // Draw "+" symbol in the circle + auto centerX = iconBounds.getCentreX(); + auto centerY = iconBounds.getCentreY(); + auto halfSize = iconSize * 0.25f; + g.drawLine(centerX - halfSize, centerY, centerX + halfSize, centerY, 2.0f); + g.drawLine(centerX, centerY - halfSize, centerX, centerY + halfSize, 2.0f); +} + +void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event) +{ + isHovered = true; + animateHover(true); +} + +void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event) +{ + isHovered = false; + animateHover(false); +} + +void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event) +{ + if (onEffectSelected) + onEffectSelected(effectId); +} diff --git a/Source/components/EffectTypeItemComponent.h b/Source/components/EffectTypeItemComponent.h new file mode 100644 index 0000000..9cb8bee --- /dev/null +++ b/Source/components/EffectTypeItemComponent.h @@ -0,0 +1,38 @@ +#pragma once +#include + +class EffectTypeItemComponent : public juce::Component +{ +public: + EffectTypeItemComponent(const juce::String& name, const juce::String& id); + ~EffectTypeItemComponent() override; + + void paint(juce::Graphics& g) override; + void mouseEnter(const juce::MouseEvent& event) override; + void mouseExit(const juce::MouseEvent& event) override; + void mouseDown(const juce::MouseEvent& event) override; + + const juce::String& getEffectId() const { return effectId; } + const juce::String& getEffectName() const { return effectName; } + + std::function onEffectSelected; + +private: + juce::String effectName; + juce::String effectId; + float animationProgress = 0.0f; + bool isHovered = false; + + // Animation components + juce::VBlankAnimatorUpdater animatorUpdater { this }; + juce::Animator hoverAnimator; + juce::Animator unhoverAnimator; + + static constexpr int CORNER_RADIUS = 8; + static constexpr float HOVER_LIFT_AMOUNT = 2.0f; + + void setupAnimators(); + void animateHover(bool isHovering); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeItemComponent) +}; diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp index 7c48aaa..64726b0 100644 --- a/Source/components/EffectsListComponent.cpp +++ b/Source/components/EffectsListComponent.cpp @@ -2,27 +2,28 @@ #include "SvgButton.h" #include "../PluginEditor.h" #include "../LookAndFeel.h" +#include "EffectTypeGridComponent.h" EffectsListComponent::EffectsListComponent(DraggableListBox& lb, AudioEffectListBoxItemData& data, int rn, osci::Effect& effect) : DraggableListBoxItem(lb, data, rn), effect(effect), audioProcessor(data.audioProcessor), editor(data.editor) { auto parameters = effect.parameters; - for (int i = 0; i < parameters.size(); i++) { - std::shared_ptr effectComponent = std::make_shared(effect, i); - selected.setToggleState(effect.enabled == nullptr || effect.enabled->getValue(), juce::dontSendNotification); - // using weak_ptr to avoid circular reference and memory leak - std::weak_ptr weakEffectComponent = effectComponent; - effectComponent->slider.setValue(parameters[i]->getValueUnnormalised(), juce::dontSendNotification); - - list.setEnabled(selected.getToggleState()); - selected.onClick = [this, weakEffectComponent] { - if (auto effectComponent = weakEffectComponent.lock()) { - auto data = (AudioEffectListBoxItemData&)modelData; - juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); - data.setSelected(rowNum, selected.getToggleState()); + for (int i = 0; i < parameters.size(); i++) { + std::shared_ptr effectComponent = std::make_shared(effect, i); + selected.setToggleState(effect.enabled == nullptr || effect.enabled->getValue(), juce::dontSendNotification); + // using weak_ptr to avoid circular reference and memory leak + std::weak_ptr weakEffectComponent = effectComponent; + effectComponent->slider.setValue(parameters[i]->getValueUnnormalised(), juce::dontSendNotification); + + list.setEnabled(selected.getToggleState()); + selected.onClick = [this, weakEffectComponent] { + if (auto effectComponent = weakEffectComponent.lock()) { + auto data = (AudioEffectListBoxItemData&)modelData; + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + data.setSelected(rowNum, selected.getToggleState()); list.setEnabled(selected.getToggleState()); - } + } repaint(); - }; + }; effectComponent->updateToggleState = [this, i, weakEffectComponent] { if (auto effectComponent = weakEffectComponent.lock()) { selected.setToggleState(effectComponent->effect.enabled == nullptr || effectComponent->effect.enabled->getValue(), juce::dontSendNotification); @@ -31,20 +32,20 @@ effect(effect), audioProcessor(data.audioProcessor), editor(data.editor) { repaint(); }; - auto component = createComponent(parameters[i]); - if (component != nullptr) { + auto component = createComponent(parameters[i]); + if (component != nullptr) { effectComponent->setComponent(component); } - listModel.addComponent(effectComponent); - } + listModel.addComponent(effectComponent); + } list.setColour(effectComponentBackgroundColourId, juce::Colours::transparentBlack.withAlpha(0.2f)); - list.setModel(&listModel); - list.setRowHeight(ROW_HEIGHT); - list.updateContent(); - addAndMakeVisible(list); - addAndMakeVisible(selected); + list.setModel(&listModel); + list.setRowHeight(ROW_HEIGHT); + list.updateContent(); + addAndMakeVisible(list); + addAndMakeVisible(selected); } EffectsListComponent::~EffectsListComponent() { @@ -52,26 +53,26 @@ EffectsListComponent::~EffectsListComponent() { } void EffectsListComponent::paint(juce::Graphics& g) { - auto bounds = getLocalBounds().removeFromLeft(LEFT_BAR_WIDTH); + auto bounds = getLocalBounds().removeFromLeft(LEFT_BAR_WIDTH); g.setColour(findColour(effectComponentHandleColourId)); bounds.removeFromBottom(PADDING); juce::Path path; path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, true, false, true, false); g.fillPath(path); - g.setColour(juce::Colours::white); - // draw drag and drop handle using circles - double size = 4; - double leftPad = 4; - double spacing = 7; - double topPad = 6; - double y = bounds.getHeight() / 2 - 15; - g.fillEllipse(leftPad, y + topPad, size, size); - g.fillEllipse(leftPad, y + topPad + spacing, size, size); - g.fillEllipse(leftPad, y + topPad + 2 * spacing, size, size); - g.fillEllipse(leftPad + spacing, y + topPad, size, size); - g.fillEllipse(leftPad + spacing, y + topPad + spacing, size, size); - g.fillEllipse(leftPad + spacing, y + topPad + 2 * spacing, size, size); - DraggableListBoxItem::paint(g); + g.setColour(juce::Colours::white); + // draw drag and drop handle using circles + double size = 4; + double leftPad = 4; + double spacing = 7; + double topPad = 6; + double y = bounds.getHeight() / 2 - 15; + g.fillEllipse(leftPad, y + topPad, size, size); + g.fillEllipse(leftPad, y + topPad + spacing, size, size); + g.fillEllipse(leftPad, y + topPad + 2 * spacing, size, size); + g.fillEllipse(leftPad + spacing, y + topPad, size, size); + g.fillEllipse(leftPad + spacing, y + topPad + spacing, size, size); + g.fillEllipse(leftPad + spacing, y + topPad + 2 * spacing, size, size); + DraggableListBoxItem::paint(g); } void EffectsListComponent::paintOverChildren(juce::Graphics& g) { @@ -81,51 +82,78 @@ void EffectsListComponent::paintOverChildren(juce::Graphics& g) { juce::Path path; path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, true, false, true); - if (!selected.getToggleState()) { + if (!selected.getToggleState()) { g.fillPath(path); } } void EffectsListComponent::resized() { - auto area = getLocalBounds(); + auto area = getLocalBounds(); auto leftBar = area.removeFromLeft(LEFT_BAR_WIDTH); leftBar.removeFromLeft(20); area.removeFromRight(PADDING); - selected.setBounds(leftBar.withSizeKeepingCentre(30, 20)); - list.setBounds(area); + selected.setBounds(leftBar.withSizeKeepingCentre(30, 20)); + list.setBounds(area); } std::shared_ptr EffectsListComponent::createComponent(osci::EffectParameter* parameter) { - if (parameter->paramID == "customEffectStrength") { - std::shared_ptr button = std::make_shared(parameter->name, BinaryData::pencil_svg, juce::Colours::white, juce::Colours::red); - std::weak_ptr weakButton = button; - button->setEdgeIndent(5); - button->setToggleState(editor.editingCustomFunction, juce::dontSendNotification); - button->setTooltip("Toggles whether the text editor is editing the currently open file, or the custom Lua effect."); - button->onClick = [this, weakButton] { - if (auto button = weakButton.lock()) { + if (parameter->paramID == "customEffectStrength") { + std::shared_ptr button = std::make_shared(parameter->name, BinaryData::pencil_svg, juce::Colours::white, juce::Colours::red); + std::weak_ptr weakButton = button; + button->setEdgeIndent(5); + button->setToggleState(editor.editingCustomFunction, juce::dontSendNotification); + button->setTooltip("Toggles whether the text editor is editing the currently open file, or the custom Lua effect."); + button->onClick = [this, weakButton] { + if (auto button = weakButton.lock()) { editor.editCustomFunction(button->getToggleState()); } - }; - return button; - } - return nullptr; + }; + return button; + } + return nullptr; } int EffectsListBoxModel::getRowHeight(int row) { - auto data = (AudioEffectListBoxItemData&)modelData; - return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING; + auto data = (AudioEffectListBoxItemData&)modelData; + if (row == data.getNumItems() - 1) { + // Effect type grid row - calculate dynamic height based on layout + // Get the available width from the listbox + int availableWidth = listBox.getWidth() - 20; // Account for scrollbar and margins + + // Create a temporary grid component to calculate required height + EffectTypeGridComponent tempGrid(data.audioProcessor); + return tempGrid.calculateRequiredHeight(availableWidth); + } + return data.getEffect(row)->parameters.size() * EffectsListComponent::ROW_HEIGHT + EffectsListComponent::PADDING; } bool EffectsListBoxModel::hasVariableHeightRows() const { - return true; + return true; } juce::Component* EffectsListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) { - std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); - if (juce::isPositiveAndBelow(rowNumber, modelData.getNumItems())) { - auto data = (AudioEffectListBoxItemData&)modelData; - item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber)); + auto data = (AudioEffectListBoxItemData&)modelData; + + 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)); + return item.release(); + } else if (rowNumber == data.getNumItems() - 1) { + // Create the effect type grid component + std::unique_ptr gridComponent(dynamic_cast(existingComponentToUpdate)); + if (gridComponent == nullptr) { + gridComponent = std::make_unique(data.audioProcessor); + + // Set up callback for when an effect is selected + gridComponent->onEffectSelected = [&data](const juce::String& effectId) { + // TODO: Implement adding new effect instance based on effectId + // This would need to be implemented based on how effects are created in the system + DBG("Effect selected: " + effectId); + }; + } + return gridComponent.release(); } - return item.release(); + + return nullptr; } diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h index d813224..7f71a5b 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 @@ -22,9 +23,9 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData void randomise() { juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); - for (int i = 0; i < data.size(); i++) { - auto effect = data[i]; - auto id = effect->getId().toLowerCase(); + for (int i = 0; i < data.size(); i++) { + auto effect = data[i]; + auto id = effect->getId().toLowerCase(); if (id.contains("scale") || id.contains("translate") || id.contains("trace")) { continue; @@ -42,19 +43,19 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData } } } - effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7); - } + effect->enabled->setValueNotifyingHost(juce::Random::getSystemRandom().nextFloat() > 0.7); + } // shuffle precedence std::random_device rd; std::mt19937 g(rd()); - std::shuffle(data.begin(), data.end(), g); + std::shuffle(data.begin(), data.end(), g); - for (int i = 0; i < data.size(); i++) { - data[i]->setPrecedence(i); - } + for (int i = 0; i < data.size(); i++) { + data[i]->setPrecedence(i); + } - audioProcessor.updateEffectPrecedence(); + audioProcessor.updateEffectPrecedence(); } void resetData() { @@ -68,12 +69,12 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData } int getNumItems() override { - return data.size(); + return data.size() + 1; } // CURRENTLY NOT USED void deleteItem(int indexOfItemToDelete) override { - // data.erase(data.begin() + indexOfItemToDelete); + // data.erase(data.begin() + indexOfItemToDelete); } // CURRENTLY NOT USED @@ -81,10 +82,10 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData // data.push_back(juce::String("Yahoo")); } - void moveBefore(int indexOfItemToMove, int indexOfItemToPlaceBefore) override { + void moveBefore(int indexOfItemToMove, int indexOfItemToPlaceBefore) override { juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); - auto effect = data[indexOfItemToMove]; + auto effect = data[indexOfItemToMove]; if (indexOfItemToMove < indexOfItemToPlaceBefore) { move(data, indexOfItemToMove, indexOfItemToPlaceBefore - 1); @@ -92,12 +93,12 @@ struct AudioEffectListBoxItemData : public DraggableListBoxItemData move(data, indexOfItemToMove, indexOfItemToPlaceBefore); } - for (int i = 0; i < data.size(); i++) { - data[i]->setPrecedence(i); - } + for (int i = 0; i < data.size(); i++) { + data[i]->setPrecedence(i); + } audioProcessor.updateEffectPrecedence(); - } + } void moveAfter(int indexOfItemToMove, int indexOfItemToPlaceAfter) override { juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); diff --git a/modules/osci_render_core b/modules/osci_render_core index 913c3b0..56a625f 160000 --- a/modules/osci_render_core +++ b/modules/osci_render_core @@ -1 +1 @@ -Subproject commit 913c3b052404818c45ad93c5e6022244d57df5e9 +Subproject commit 56a625fcead4d7fd428a75afddf541363a0adf63 diff --git a/osci-render.jucer b/osci-render.jucer index be38c77..fd5a710 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -157,6 +157,14 @@ file="Source/components/EffectsListComponent.cpp"/> + + + +