diff --git a/Resources/svg/bit-crush.svg b/Resources/svg/bit-crush.svg new file mode 100644 index 0000000..ca26ce1 --- /dev/null +++ b/Resources/svg/bit-crush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/bounce.svg b/Resources/svg/bounce.svg new file mode 100644 index 0000000..c68238b --- /dev/null +++ b/Resources/svg/bounce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/bulge.svg b/Resources/svg/bulge.svg new file mode 100644 index 0000000..b1104a2 --- /dev/null +++ b/Resources/svg/bulge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/close.svg b/Resources/svg/close.svg new file mode 100644 index 0000000..096198c --- /dev/null +++ b/Resources/svg/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/dash.svg b/Resources/svg/dash.svg new file mode 100644 index 0000000..dd789d2 --- /dev/null +++ b/Resources/svg/dash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/delay.svg b/Resources/svg/delay.svg new file mode 100644 index 0000000..c45a2d8 --- /dev/null +++ b/Resources/svg/delay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/distort.svg b/Resources/svg/distort.svg new file mode 100644 index 0000000..e50f267 --- /dev/null +++ b/Resources/svg/distort.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/kaleidoscope.svg b/Resources/svg/kaleidoscope.svg new file mode 100644 index 0000000..fb970f0 --- /dev/null +++ b/Resources/svg/kaleidoscope.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Resources/svg/lua.svg b/Resources/svg/lua.svg new file mode 100644 index 0000000..895ffbf --- /dev/null +++ b/Resources/svg/lua.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/multiplex.svg b/Resources/svg/multiplex.svg new file mode 100644 index 0000000..3874d78 --- /dev/null +++ b/Resources/svg/multiplex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/ripple.svg b/Resources/svg/ripple.svg new file mode 100644 index 0000000..2a9c193 --- /dev/null +++ b/Resources/svg/ripple.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/rotate.svg b/Resources/svg/rotate.svg new file mode 100644 index 0000000..ef55c71 --- /dev/null +++ b/Resources/svg/rotate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/scale.svg b/Resources/svg/scale.svg new file mode 100644 index 0000000..1256369 --- /dev/null +++ b/Resources/svg/scale.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/smoothing.svg b/Resources/svg/smoothing.svg new file mode 100644 index 0000000..c3278a7 --- /dev/null +++ b/Resources/svg/smoothing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/swirl.svg b/Resources/svg/swirl.svg new file mode 100644 index 0000000..31d8cda --- /dev/null +++ b/Resources/svg/swirl.svg @@ -0,0 +1,7 @@ + + + Laravelnova Streamline Icon: https://streamlinehq.com + + Laravel Nova + + \ No newline at end of file diff --git a/Resources/svg/trace.svg b/Resources/svg/trace.svg new file mode 100644 index 0000000..e7c85c8 --- /dev/null +++ b/Resources/svg/trace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/translate.svg b/Resources/svg/translate.svg new file mode 100644 index 0000000..b59c7a0 --- /dev/null +++ b/Resources/svg/translate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/twist.svg b/Resources/svg/twist.svg new file mode 100644 index 0000000..36d697b --- /dev/null +++ b/Resources/svg/twist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/vector-cancelling.svg b/Resources/svg/vector-cancelling.svg new file mode 100644 index 0000000..3b48420 --- /dev/null +++ b/Resources/svg/vector-cancelling.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/svg/wobble.svg b/Resources/svg/wobble.svg new file mode 100644 index 0000000..aa7f4f6 --- /dev/null +++ b/Resources/svg/wobble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Source/CommonPluginEditor.cpp b/Source/CommonPluginEditor.cpp index f7ecdf1..af2e605 100644 --- a/Source/CommonPluginEditor.cpp +++ b/Source/CommonPluginEditor.cpp @@ -60,7 +60,7 @@ CommonPluginEditor::CommonPluginEditor(CommonAudioProcessor& p, juce::String app setResizeLimits(250, 250, 999999, 999999); tooltipDropShadow.setOwner(&tooltipWindow.get()); - tooltipWindow->setMillisecondsBeforeTipAppears(0); + tooltipWindow->setMillisecondsBeforeTipAppears(100); updateTitle(); diff --git a/Source/EffectPluginEditor.cpp b/Source/EffectPluginEditor.cpp index d5aada0..71a4b76 100644 --- a/Source/EffectPluginEditor.cpp +++ b/Source/EffectPluginEditor.cpp @@ -32,7 +32,7 @@ EffectPluginEditor::EffectPluginEditor(EffectAudioProcessor& p) setResizable(false, false); tooltipDropShadow.setOwner(&tooltipWindow.get()); - tooltipWindow->setMillisecondsBeforeTipAppears(0); + tooltipWindow->setMillisecondsBeforeTipAppears(100); audioProcessor.bitCrush->addListener(0, this); } diff --git a/Source/EffectPluginProcessor.h b/Source/EffectPluginProcessor.h index c1f6e6d..73bbfeb 100644 --- a/Source/EffectPluginProcessor.h +++ b/Source/EffectPluginProcessor.h @@ -40,19 +40,9 @@ public: int getCurrentProgram() override; void setCurrentProgram(int index) override; const juce::String getProgramName(int index) override; - void changeProgramName(int index, const juce::String& newName) override; std::shared_ptr bitCrush = std::make_shared( - std::make_shared(), - new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.0, 0.0, 1.0) - ); + void changeProgramName(int index, const juce::String& newName) override; std::shared_ptr bitCrush = BitCrushEffect().build(); - std::shared_ptr autoGain = std::make_shared( - std::make_shared(), - std::vector{ - new osci::EffectParameter("Intensity", "Controls how aggressively the gain adjustment is applied", "agcIntensity", VERSION_HINT, 1.0, 0.0, 1.0), - new osci::EffectParameter("Target Level", "Target output level for the automatic gain control", "agcTarget", VERSION_HINT, 0.6, 0.0, 1.0), - new osci::EffectParameter("Response", "How quickly the effect responds to level changes (lower is slower)", "agcResponse", VERSION_HINT, 0.0001, 0.0, 1.0) - } - ); + std::shared_ptr autoGain = AutoGainControlEffect().build(); VisualiserParameters visualiserParameters; diff --git a/Source/EffectsComponent.cpp b/Source/EffectsComponent.cpp index 69b9148..0ae8fe4 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -2,7 +2,8 @@ #include "audio/BitCrushEffect.h" #include "PluginEditor.h" -EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), itemData(p, editor), listBoxModel(listBox, itemData) { +EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) + : audioProcessor(p), itemData(p, editor), listBoxModel(listBox, itemData), grid(p) { setText("Audio Effects"); addAndMakeVisible(frequency); @@ -11,14 +12,6 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP frequency.slider.setTextValueSuffix("Hz"); frequency.slider.setValue(audioProcessor.frequencyEffect->getValue(), juce::dontSendNotification); - /*addBtn.setButtonText("Add Item..."); - addBtn.onClick = [this]() - { - itemData.data.push_back(juce::String("Item " + juce::String(1 + itemData.getNumItems()))); - listBox.updateContent(); - }; - addAndMakeVisible(addBtn);*/ - addAndMakeVisible(randomiseButton); randomiseButton.setTooltip("Randomise all effect parameter values, randomise which effects are enabled, and randomise their order."); @@ -33,8 +26,93 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP audioProcessor.broadcaster.addChangeListener(this); } + // Wire list model to notify when user wants to add + itemData.onAddNewEffectRequested = [this]() { + showingGrid = true; + grid.setVisible(true); + grid.refreshDisabledStates(); + listBox.setVisible(false); + resized(); + repaint(); + }; + + // Decide initial view: show grid only if there are no selected effects + bool anySelected = false; + { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + for (const auto& eff : audioProcessor.toggleableEffects) { + const bool isSelected = (eff->selected == nullptr) ? true : eff->selected->getBoolValue(); + if (isSelected) { anySelected = true; break; } + } + } + showingGrid = !anySelected; + grid.onEffectSelected = [this](const juce::String& effectId) { + { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + // Mark the chosen effect as selected and enabled, and move it to the end + std::shared_ptr chosen; + for (auto& eff : audioProcessor.toggleableEffects) { + if (eff->getId() == effectId) { + eff->selected->setBoolValueNotifyingHost(true); + eff->enabled->setBoolValueNotifyingHost(true); + chosen = eff; + break; + } + } + // Place chosen effect at the end of the visible (selected) list and update precedence + if (chosen != nullptr) { + int idx = 0; + for (auto& e : itemData.data) { + if (e != chosen) e->setPrecedence(idx++); + } + chosen->setPrecedence(idx++); + audioProcessor.updateEffectPrecedence(); + } + } + // Refresh list content to include newly selected + itemData.resetData(); + listBox.updateContent(); + showingGrid = false; + listBox.setVisible(true); + grid.setVisible(false); + resized(); + repaint(); + }; + grid.onCanceled = [this]() { + // If canceled while default grid, just show list + showingGrid = false; + listBox.setVisible(true); + grid.setVisible(false); + resized(); + repaint(); + }; + listBox.setModel(&listBoxModel); addAndMakeVisible(listBox); + // Add a small top spacer so the drop indicator can be visible above the first row + { + auto spacer = std::make_unique(); + spacer->setSize(1, LIST_SPACER); // top padding + listBox.setHeaderComponent(std::move(spacer)); + } + // Setup scroll fade mixin + initScrollFade(*this); + attachToListBox(listBox); + // Wire "+ Add new effect" button below the list + addEffectButton.onClick = [this]() { + if (itemData.onAddNewEffectRequested) itemData.onAddNewEffectRequested(); + }; + addAndMakeVisible(addEffectButton); + addAndMakeVisible(grid); + // Keep disabled states in sync whenever grid is shown + if (showingGrid) { + grid.setVisible(true); + grid.refreshDisabledStates(); + listBox.setVisible(false); + } else { + grid.setVisible(false); + listBox.setVisible(true); + } } EffectsComponent::~EffectsComponent() { @@ -52,10 +130,28 @@ void EffectsComponent::resized() { frequency.setBounds(area.removeFromTop(30)); area.removeFromTop(6); - listBox.setBounds(area); + if (showingGrid) { + grid.setBounds(area); + addEffectButton.setVisible(false); + // Hide fade when grid is shown + setScrollFadeVisible(false); + } else { + // Reserve space at bottom for the add button + auto addBtnHeight = 44; + auto listArea = area; + auto buttonArea = listArea.removeFromBottom(addBtnHeight); + listBox.setBounds(listArea); + // Layout bottom fade overlay; visible if list is scrollable + layoutScrollFade(listArea.withTrimmedTop(LIST_SPACER), true, 48); + addEffectButton.setVisible(true); + addEffectButton.setBounds(buttonArea.reduced(0, 4)); + } } void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { itemData.resetData(); listBox.updateContent(); + // Re-layout scroll fades after content changes + if (! showingGrid) + layoutScrollFade(listBox.getBounds().withTrimmedTop(LIST_SPACER), true, 48); } diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index 16c917f..18d7cd1 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -6,9 +6,11 @@ #include "PluginProcessor.h" #include "components/DraggableListBox.h" #include "components/EffectsListComponent.h" +#include "components/ScrollFadeMixin.h" +#include "components/EffectTypeGridComponent.h" class OscirenderAudioProcessorEditor; -class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener { +class EffectsComponent : public juce::GroupComponent, public juce::ChangeListener, private ScrollFadeMixin { public: EffectsComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); ~EffectsComponent() override; @@ -26,6 +28,11 @@ private: AudioEffectListBoxItemData itemData; EffectsListBoxModel listBoxModel; DraggableListBox listBox; + juce::TextButton addEffectButton { "+ Add new effect" }; // Separate button under the list + EffectTypeGridComponent grid { audioProcessor }; + bool showingGrid = true; // show grid by default + + const int LIST_SPACER = 4; // Space above the list to show drop indicator EffectComponent frequency = EffectComponent(*audioProcessor.frequencyEffect, false); diff --git a/Source/LookAndFeel.cpp b/Source/LookAndFeel.cpp index 3e1c1a6..b16e212 100644 --- a/Source/LookAndFeel.cpp +++ b/Source/LookAndFeel.cpp @@ -24,7 +24,7 @@ OscirenderLookAndFeel::OscirenderLookAndFeel() { setColour(juce::PopupMenu::backgroundColourId, Colours::darker); setColour(juce::PopupMenu::highlightedBackgroundColourId, Colours::grey); setColour(juce::TooltipWindow::backgroundColourId, Colours::darker); - setColour(juce::TooltipWindow::outlineColourId, juce::Colours::white); + setColour(juce::TooltipWindow::outlineColourId, Colours::darker); setColour(juce::TextButton::buttonOnColourId, Colours::darker); setColour(juce::AlertWindow::outlineColourId, Colours::darker); setColour(juce::AlertWindow::backgroundColourId, Colours::darker); diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index c369d51..50b144b 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -17,7 +17,7 @@ void OscirenderAudioProcessorEditor::registerFileRemovedCallback() { }); } -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, 770), audioProcessor(p), collapseButton("Collapse", juce::Colours::white, juce::Colours::white, juce::Colours::white) { // Register the file removal callback registerFileRemovedCallback(); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index ad26577..e1730c5 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -13,11 +13,18 @@ #include "audio/BulgeEffect.h" #include "audio/TwistEffect.h" #include "audio/DistortEffect.h" +#include "audio/KaleidoscopeEffect.h" #include "audio/MultiplexEffect.h" #include "audio/SmoothEffect.h" #include "audio/WobbleEffect.h" #include "audio/DashedLineEffect.h" #include "audio/VectorCancellingEffect.h" +#include "audio/ScaleEffect.h" +#include "audio/RotateEffect.h" +#include "audio/TranslateEffect.h" +#include "audio/RippleEffect.h" +#include "audio/SwirlEffect.h" +#include "audio/BounceEffect.h" #include "parser/FileParser.h" #include "parser/FrameProducer.h" @@ -29,138 +36,39 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(BusesProperties().withInput("Input", juce::AudioChannelSet::namedChannelSet(2), true).withOutput("Output", juce::AudioChannelSet::stereo(), true)) { // locking isn't necessary here because we are in the constructor - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.6, 0.0, 1.0))); - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - new osci::EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.5, 0.0, 1.0))); - auto multiplexEffect = std::make_shared( - std::make_shared(), - std::vector{ - new osci::EffectParameter("Multiplex X", "Controls the horizontal grid size for the multiplex effect.", "multiplexGridX", VERSION_HINT, 1.0, 1.0, 8.0), - new osci::EffectParameter("Multiplex Y", "Controls the vertical grid size for the multiplex effect.", "multiplexGridY", VERSION_HINT, 1.0, 1.0, 8.0), - new osci::EffectParameter("Multiplex Z", "Controls the depth grid size for the multiplex effect.", "multiplexGridZ", VERSION_HINT, 1.0, 1.0, 8.0), - new osci::EffectParameter("Multiplex Smooth", "Controls the smoothness of transitions between grid sizes.", "multiplexSmooth", VERSION_HINT, 0.0, 0.0, 1.0), - 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), - }); - // 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); - toggleableEffects.push_back(multiplexEffect); - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - new osci::EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.1111111, 0.0, 1.0))); - auto scaleEffect = 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]); - }, - std::vector{ - new osci::EffectParameter("Scale X", "Scales the object in the horizontal direction.", "scaleX", VERSION_HINT, 1.0, -3.0, 3.0), - 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->markLockable(true); + toggleableEffects.push_back(BitCrushEffect().build()); + toggleableEffects.push_back(BulgeEffect().build()); + toggleableEffects.push_back(MultiplexEffect().build()); + toggleableEffects.push_back(KaleidoscopeEffect().build()); + toggleableEffects.push_back(BounceEffect().build()); + toggleableEffects.push_back(VectorCancellingEffect().build()); + toggleableEffects.push_back(RippleEffectApp().build()); + toggleableEffects.push_back(RotateEffectApp().build()); + toggleableEffects.push_back(TranslateEffectApp().build()); + toggleableEffects.push_back(SwirlEffectApp().build()); + toggleableEffects.push_back(SmoothEffect().build()); + toggleableEffects.push_back(TwistEffect().build()); + toggleableEffects.push_back(DelayEffect().build()); + toggleableEffects.push_back(DashedLineEffect(*this).build()); + toggleableEffects.push_back(TraceEffect(*this).build()); + toggleableEffects.push_back(WobbleEffect(*this).build()); + + auto scaleEffect = ScaleEffectApp().build(); booleanParameters.push_back(scaleEffect->linked); toggleableEffects.push_back(scaleEffect); - auto distortEffect = std::make_shared( - [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { - int flip = index % 2 == 0 ? 1 : -1; - osci::Point jitter = osci::Point(flip * values[0], flip * values[1], flip * values[2]); - return input + jitter; - }, - std::vector{ - new osci::EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0), - 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->markLockable(false); + + auto distortEffect = DistortEffect().build(); booleanParameters.push_back(distortEffect->linked); toggleableEffects.push_back(distortEffect); - auto rippleEffect = std::make_shared( - [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { - double phase = values[1] * std::numbers::pi; - double distance = 100 * values[2] * (input.x * input.x + input.y * input.y); - input.z += values[0] * std::sin(phase + distance); - return input; - }, - std::vector{ - new osci::EffectParameter("Ripple Depth", "Controls how large the ripples applied to the image are.", "rippleDepth", VERSION_HINT, 0.2, 0.0, 1.0), - 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->getParameter("ripplePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); - toggleableEffects.push_back(rippleEffect); - auto rotateEffect = std::make_shared( - [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { - input.rotate(values[0] * std::numbers::pi, values[1] * std::numbers::pi, values[2] * std::numbers::pi); - return input; - }, - std::vector{ - new osci::EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "rotateX", VERSION_HINT, 0.0, -1.0, 1.0), - 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->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( - [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { - return input + osci::Point(values[0], values[1], values[2]); - }, - std::vector{ - 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), - })); - 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(); - double newX = input.x * std::cos(length) - input.y * std::sin(length); - double newY = input.x * std::sin(length) + input.y * std::cos(length); - return osci::Point(newX, newY, input.z); - }, - std::vector{ - new osci::EffectParameter("Swirl", "Swirls the image in a spiral pattern.", "swirl", VERSION_HINT, 0.3, -1.0, 1.0), - })); - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - std::vector{ - new osci::EffectParameter("Twist", "Twists the image in a corkscrew pattern.", "twist", VERSION_HINT, 0.5, -1.0, 1.0), - })); - toggleableEffects.push_back(std::make_shared( - std::make_shared(), - new osci::EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", "smoothing", VERSION_HINT, 0.75, 0.0, 1.0))); - std::shared_ptr wobble = std::make_shared( - std::make_shared(*this), - std::vector{ - 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->getParameter("wobblePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); - toggleableEffects.push_back(wobble); - toggleableEffects.push_back(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)})); - auto dashedLineEffect = std::make_shared( - std::make_shared(*this), - std::vector{ - new osci::EffectParameter("Dash Count", "Controls the number of dashed lines in the drawing.", "dashCount", VERSION_HINT, 16.0, 1.0, 32.0), - new osci::EffectParameter("Dash Coverage", "Controls the fraction of each dash unit that is drawn.", "dashCoverage", VERSION_HINT, 0.5, 0.0, 1.0), - new osci::EffectParameter("Dash Offset", "Offsets the location of the dashed lines.", "dashOffset", VERSION_HINT, 0.0, 0.0, 1.0), - }); - dashedLineEffect->getParameter("dashOffset")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); - dashedLineEffect->getParameter("dashOffset")->lfoRate->setUnnormalisedValueNotifyingHost(1.0); - toggleableEffects.push_back(dashedLineEffect); + + custom->setIcon(BinaryData::lua_svg); toggleableEffects.push_back(custom); - toggleableEffects.push_back(trace); - trace->getParameter("traceLength")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); 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); @@ -175,6 +83,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse for (int i = 0; i < 26; i++) { addLuaSlider(); + if (i < luaEffects.size()) { + luaEffects[i]->setIcon(BinaryData::lua_svg); + } } effects.insert(effects.end(), toggleableEffects.begin(), toggleableEffects.end()); @@ -467,6 +378,17 @@ void OscirenderAudioProcessor::setObjectServerPort(int port) { objectServer.reload(); } +void OscirenderAudioProcessor::setPreviewEffectId(const juce::String& effectId) { + previewEffect.reset(); + for (auto& eff : toggleableEffects) { + if (eff->getId() == effectId) { previewEffect = eff; break; } + } +} + +void OscirenderAudioProcessor::clearPreviewEffect() { + previewEffect.reset(); +} + void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; // Audio info variables @@ -629,13 +551,25 @@ 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 }); } channels = effect->apply(sample, channels, currentVolume); } } + // Apply preview effect if present and not already active in the main chain + if (previewEffect) { + const bool prevEnabled = (previewEffect->enabled != nullptr) && previewEffect->enabled->getValue(); + const bool prevSelected = (previewEffect->selected == nullptr) ? true : previewEffect->selected->getBoolValue(); + if (!(prevEnabled && prevSelected)) { + if (previewEffect->getId() == custom->getId()) + previewEffect->setExternalInput(osci::Point{ left, right }); + channels = previewEffect->apply(sample, channels, currentVolume); + } + } } for (auto& effect : permanentEffects) { channels = effect->apply(sample, channels, currentVolume); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index eb65ab5..548ad64 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -8,8 +8,6 @@ #pragma once -#define VERSION_HINT 2 - #include #include @@ -59,6 +57,8 @@ public: std::vector> toggleableEffects; std::vector> luaEffects; + // Temporary preview effect applied while hovering effects in the grid (guarded by effectsLock) + std::shared_ptr previewEffect; std::atomic luaValues[26] = {0.0}; std::shared_ptr frequencyEffect = std::make_shared( @@ -72,20 +72,6 @@ public: "frequency", VERSION_HINT, 220.0, 0.0, 4200.0)); - std::shared_ptr trace = std::make_shared( - std::vector{ - new osci::EffectParameter( - "Trace Start", - "Defines how far into the frame the drawing is started at. This has the effect of 'tracing' out the image from a single dot when animated. By default, we start drawing from the beginning of the frame, so this value is 0.0.", - "traceStart", - VERSION_HINT, 0.0, 0.0, 1.0, 0.001), - new osci::EffectParameter( - "Trace Length", - "Defines how much of the frame is drawn per cycle. This has the effect of 'tracing' out the image from a single dot when animated. By default, we draw the whole frame, corresponding to a value of 1.0.", - "traceLength", - VERSION_HINT, 1.0, 0.0, 1.0, 0.001), - }); - std::shared_ptr delayEffect = std::make_shared(); std::function errorCallback = [this](int lineNum, juce::String fileName, juce::String error) { notifyErrorListeners(lineNum, fileName, error); }; @@ -94,13 +80,7 @@ public: customEffect, new osci::EffectParameter("Lua Effect", "Controls the strength of the custom Lua effect applied. You can write your own custom effect using Lua by pressing the edit button on the right.", "customEffectStrength", VERSION_HINT, 1.0, 0.0, 1.0)); - std::shared_ptr perspectiveEffect = std::make_shared(); - std::shared_ptr perspective = std::make_shared( - perspectiveEffect, - std::vector{ - new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0), - new osci::EffectParameter("FOV", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0), - }); + std::shared_ptr perspective = PerspectiveEffect().build(); osci::BooleanParameter* midiEnabled = new osci::BooleanParameter("MIDI Enabled", "midiEnabled", VERSION_HINT, false, "Enable MIDI input for the synth. If disabled, the synth will play a constant tone, as controlled by the frequency slider."); osci::BooleanParameter* inputEnabled = new osci::BooleanParameter("Audio Input Enabled", "inputEnabled", VERSION_HINT, false, "Enable to use input audio, instead of the generated audio."); @@ -200,6 +180,10 @@ public: void removeErrorListener(ErrorListener* listener); void notifyErrorListeners(int lineNumber, juce::String id, juce::String error); + // Preview API: set/clear a temporary effect by ID for hover auditioning + void setPreviewEffectId(const juce::String& effectId); + void clearPreviewEffect(); + // Setter for the callback void setFileRemovedCallback(std::function callback); diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index a5c3bcf..87cf493 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -13,7 +13,7 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi addChildComponent(frame); double midiLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("midiLayoutPreferredSize", pluginEditor.CLOSED_PREF_SIZE)); - double mainLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("mainLayoutPreferredSize", -0.4)); + double mainLayoutPreferredSize = std::any_cast(audioProcessor.getProperty("mainLayoutPreferredSize", -0.5)); midiLayout.setItemLayout(0, -0.1, -1.0, -(1.0 + midiLayoutPreferredSize)); midiLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); @@ -46,8 +46,6 @@ void SettingsComponent::resized() { mainLayout.layOutComponents(columns, 3, dummy.getX(), dummy.getY(), dummy.getWidth(), dummy.getHeight(), false, true); auto bounds = dummy2.getBounds(); - perspective.setBounds(bounds.removeFromBottom(120)); - bounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); main.setBounds(bounds); juce::Component* effectSettings = nullptr; @@ -65,6 +63,9 @@ void SettingsComponent::resized() { dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); } + perspective.setBounds(dummyBounds.removeFromBottom(120)); + dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); + effects.setBounds(dummyBounds); if (isVisible() && getWidth() > 0 && getHeight() > 0) { diff --git a/Source/TestMain.cpp b/Source/TestMain.cpp index eed3c3c..e5d3a7f 100644 --- a/Source/TestMain.cpp +++ b/Source/TestMain.cpp @@ -1,118 +1,4 @@ #include -#include "obj/Camera.h" -#include "mathter/Common/Approx.hpp" - -class FrustumTest : public juce::UnitTest { -public: - FrustumTest() : juce::UnitTest("Frustum Culling") {} - - void runTest() override { - double focalLength = 1; - - Camera camera; - camera.setFocalLength(focalLength); - Vec3 position = Vec3(0, 0, -focalLength); - camera.setPosition(position); - Frustum frustum = camera.getFrustum(); - - beginTest("Focal Plane Frustum In-Bounds"); - - // Focal plane is at z = 0 - Vec3 vecs[] = { - Vec3(0, 0, 0), Vec3(0, 0, 0), - Vec3(1, 1, 0), Vec3(1, 1, 0), - Vec3(-1, -1, 0), Vec3(-1, -1, 0), - Vec3(1, -1, 0), Vec3(1, -1, 0), - Vec3(-1, 1, 0), Vec3(-1, 1, 0), - Vec3(0.5, 0.5, 0), Vec3(0.5, 0.5, 0), - }; - - testFrustumClippedEqualsExpected(vecs, camera, 6); - - beginTest("Focal Plane Frustum Out-Of-Bounds"); - - // Focal plane is at z = 0 - Vec3 vecs2[] = { - Vec3(1.1, 1.1, 0), Vec3(1, 1, 0), - Vec3(-1.1, -1.1, 0), Vec3(-1, -1, 0), - Vec3(1.1, -1.1, 0), Vec3(1, -1, 0), - Vec3(-1.1, 1.1, 0), Vec3(-1, 1, 0), - Vec3(1.1, 0.5, 0), Vec3(1, 0.5, 0), - Vec3(-1.1, 0.5, 0), Vec3(-1, 0.5, 0), - Vec3(0.5, -1.1, 0), Vec3(0.5, -1, 0), - Vec3(0.5, 1.1, 0), Vec3(0.5, 1, 0), - Vec3(10, 10, 0), Vec3(1, 1, 0), - Vec3(-10, -10, 0), Vec3(-1, -1, 0), - Vec3(10, -10, 0), Vec3(1, -1, 0), - Vec3(-10, 10, 0), Vec3(-1, 1, 0), - }; - - testFrustumClippedEqualsExpected(vecs2, camera, 12); - - beginTest("Behind Camera Out-Of-Bounds"); - - double minZWorldCoords = -focalLength + camera.getFrustum().nearDistance; - - Vec3 vecs3[] = { - Vec3(0, 0, -focalLength), Vec3(0, 0, minZWorldCoords), - Vec3(0, 0, -100), Vec3(0, 0, minZWorldCoords), - Vec3(0.5, 0.5, -focalLength), Vec3(0.1, 0.1, minZWorldCoords), - Vec3(10, -10, -focalLength), Vec3(0.1, -0.1, minZWorldCoords), - Vec3(-0.5, 0.5, -100), Vec3(-0.1, 0.1, minZWorldCoords), - Vec3(-10, 10, -100), Vec3(-0.1, 0.1, minZWorldCoords), - }; - - testFrustumClippedEqualsExpected(vecs3, camera, 6); - - beginTest("3D Point Out-Of-Bounds"); - - Vec3 vecs4[] = { - Vec3(1, 1, -0.1), - Vec3(-1, -1, -0.1), - Vec3(1, -1, -0.1), - Vec3(-1, 1, -0.1), - Vec3(0.5, 0.5, minZWorldCoords), - }; - - testFrustumClipOccurs(vecs4, camera, 5); - } - - Vec3 project(Vec3& p, double focalLength) { - return Vec3( - p.x * focalLength / p.z, - p.y * focalLength / p.z, - 0 - ); - } - - juce::String vec3ToString(Vec3& p) { - return "(" + juce::String(p.x) + ", " + juce::String(p.y) + ", " + juce::String(p.z) + ")"; - } - - juce::String errorMessage(Vec3& actual, Vec3& expected) { - return "Expected: " + vec3ToString(expected) + ", Actual: " + vec3ToString(actual); - } - - void testFrustumClippedEqualsExpected(Vec3 vecs[], Camera& camera, int length) { - for (int i = 0; i < length; i++) { - Vec3 p = vecs[2 * i]; - p = camera.toCameraSpace(p); - camera.getFrustum().clipToFrustum(p); - p = camera.toWorldSpace(p); - expect(mathter::AlmostEqual(p, vecs[2 * i + 1]), errorMessage(p, vecs[2 * i + 1])); - } - } - - void testFrustumClipOccurs(Vec3 vecs[], Camera& camera, int length) { - for (int i = 0; i < length; i++) { - Vec3 p = vecs[i]; - p = camera.toCameraSpace(p); - camera.getFrustum().clipToFrustum(p); - p = camera.toWorldSpace(p); - expect(!mathter::AlmostEqual(p, vecs[i]), errorMessage(p, vecs[i])); - } - } -}; class ProducerThread : public juce::Thread { public: @@ -183,7 +69,6 @@ public: } }; -static FrustumTest frustumTest; static BufferConsumerTest bufferConsumerTest; int main(int argc, char* argv[]) { diff --git a/Source/audio/AutoGainControlEffect.h b/Source/audio/AutoGainControlEffect.h index ab5ca47..a75b10b 100644 --- a/Source/audio/AutoGainControlEffect.h +++ b/Source/audio/AutoGainControlEffect.h @@ -40,6 +40,18 @@ public: return input * gainAdjust; } + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Intensity", "Controls how aggressively the gain adjustment is applied", "agcIntensity", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("Target Level", "Target output level for the automatic gain control", "agcTarget", VERSION_HINT, 0.6, 0.0, 1.0), + new osci::EffectParameter("Response", "How quickly the effect responds to level changes (lower is slower)", "agcResponse", VERSION_HINT, 0.0001, 0.0, 1.0) + } + ); + return eff; + } + private: const double EPSILON = 0.00001; double smoothedLevel = 0.01; // Start with a small non-zero value to avoid division by zero diff --git a/Source/audio/BitCrushEffect.h b/Source/audio/BitCrushEffect.h index af84a66..fe14fc8 100644 --- a/Source/audio/BitCrushEffect.h +++ b/Source/audio/BitCrushEffect.h @@ -15,4 +15,12 @@ public: double dequant = 1.0f / quant; return osci::Point(dequant * (int)(input.x * quant), dequant * (int)(input.y * quant), dequant * (int)(input.z * quant)); } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + new osci::EffectParameter("Bit Crush", "Limits the resolution of points drawn to the screen, making the object look pixelated, and making the audio sound more 'digital' and distorted.", "bitCrush", VERSION_HINT, 0.7, 0.0, 1.0)); + eff->setIcon(BinaryData::bitcrush_svg); + return eff; + } }; diff --git a/Source/audio/BounceEffect.h b/Source/audio/BounceEffect.h new file mode 100644 index 0000000..ce6c126 --- /dev/null +++ b/Source/audio/BounceEffect.h @@ -0,0 +1,59 @@ +// BounceEffect.h (Simplified DVD-style 2D bounce) +// Scales the original shape, then translates it within [-1,1] x [-1,1] using +// constant-velocity motion that bounces off the edges. Z coordinate is unchanged. +#pragma once + +#include + +class BounceEffect : public osci::EffectApplication { +public: + osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { + // values[0] = size (0.05..1.0) + // values[1] = speed (0..2) + // values[2] = angle (0..1 -> 0..2π) + double size = juce::jlimit(0.05, 1.0, values[0].load()); + double speed = juce::jlimit(0.0, 2.0, values[1].load()); + double angle = values[2].load() * juce::MathConstants::twoPi; + + // Base direction from user + double dirX = std::cos(angle); + double dirY = std::sin(angle); + if (flipX) dirX = -dirX; + if (flipY) dirY = -dirY; + + double dt = 1.0 / sampleRate; + position.x += dirX * speed * dt; + position.y += dirY * speed * dt; + + double maxP = 1.0 - size; + double minP = -1.0 + size; + if (position.x > maxP) { position.x = maxP; flipX = !flipX; } + else if (position.x < minP) { position.x = minP; flipX = !flipX; } + if (position.y > maxP) { position.y = maxP; flipY = !flipY; } + else if (position.y < minP) { position.y = minP; flipY = !flipY; } + + osci::Point scaled = input * size; + osci::Point out = scaled + osci::Point(position.x, position.y, 0.0); + out.z = input.z; // preserve original Z + return out; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Bounce Size", "Size (scale) of the bouncing object.", "bounceSize", VERSION_HINT, 0.3, 0.05, 1.0), + new osci::EffectParameter("Bounce Speed", "Speed of motion.", "bounceSpeed", VERSION_HINT, 5.0, 0.0, 10.0), + new osci::EffectParameter("Bounce Angle", juce::String(juce::CharPointer_UTF8("Direction of travel (0..1 -> 0..360°).")), "bounceAngle", VERSION_HINT, 0.16, 0.0, 1.0), + } + ); + eff->setName("Bounce"); + eff->setIcon(BinaryData::bounce_svg); + return eff; + } + +private: + osci::Point position { 0.0, 0.0, 0.0 }; + bool flipX = false; + bool flipY = false; +}; diff --git a/Source/audio/BulgeEffect.h b/Source/audio/BulgeEffect.h index 4cca78f..c15c9c1 100644 --- a/Source/audio/BulgeEffect.h +++ b/Source/audio/BulgeEffect.h @@ -13,4 +13,12 @@ public: return osci::Point(rn * cos(theta), rn * sin(theta), input.z); } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + new osci::EffectParameter("Bulge", "Applies a bulge that makes the centre of the image larger, and squishes the edges of the image. This applies a distortion to the audio.", "bulge", VERSION_HINT, 0.5, 0.0, 1.0)); + eff->setIcon(BinaryData::bulge_svg); + return eff; + } }; diff --git a/Source/audio/CustomEffect.h b/Source/audio/CustomEffect.h index 6c66e03..b057e33 100644 --- a/Source/audio/CustomEffect.h +++ b/Source/audio/CustomEffect.h @@ -18,6 +18,15 @@ public: double frequency = 0; + std::shared_ptr build() const override { + // Note: callers needing CustomEffect with callback/vars should construct directly. + auto eff = std::make_shared( + std::make_shared([](int, juce::String, juce::String) {}, nullptr), + new osci::EffectParameter("Lua Effect", "Controls the strength of the custom Lua effect applied. You can write your own custom effect using Lua by pressing the edit button on the right.", "customEffectStrength", VERSION_HINT, 1.0, 0.0, 1.0)); + eff->setIcon(BinaryData::lua_svg); + return eff; + } + private: const juce::String DEFAULT_SCRIPT = "return { x, y, z }"; juce::String code = DEFAULT_SCRIPT; diff --git a/Source/audio/DashedLineEffect.h b/Source/audio/DashedLineEffect.h index e48c3f3..aaaa084 100644 --- a/Source/audio/DashedLineEffect.h +++ b/Source/audio/DashedLineEffect.h @@ -7,9 +7,18 @@ public: DashedLineEffect(OscirenderAudioProcessor& p) : audioProcessor(p) {} osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { - double dashCount = juce::jmax(1.0, values[0].load()); // Dashes per cycle - double dashCoverage = juce::jlimit(0.0, 1.0, values[1].load()); - double dashOffset = values[2]; + // if only 2 parameters are provided, this is being used as a 'trace effect' + // where the dash count is 1. + double dashCount = 1.0; + int i = 0; + + if (values.size() > 2) { + dashCount = juce::jmax(1.0, values[i++].load()); // Dashes per cycle + } + + double dashOffset = values[i++]; + double dashCoverage = juce::jlimit(0.0, 1.0, values[i++].load()); + double dashLengthSamples = (sampleRate / audioProcessor.frequency) / dashCount; double dashPhase = framePhase * dashCount - dashOffset; dashPhase = dashPhase - std::floor(dashPhase); // Wrap @@ -33,10 +42,53 @@ public: return output; } -private: + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(audioProcessor), + std::vector{ + new osci::EffectParameter("Dash Count", "Controls the number of dashed lines in the drawing.", "dashCount", VERSION_HINT, 16.0, 1.0, 32.0), + new osci::EffectParameter("Dash Offset", "Offsets the location of the dashed lines.", "dashOffset", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 1.0f), + new osci::EffectParameter("Dash Width", "Controls how much each dash unit is drawn.", "dashWidth", VERSION_HINT, 0.5, 0.0, 1.0), + } + ); + eff->setName("Dash"); + eff->setIcon(BinaryData::dash_svg); + return eff; + } + +protected: OscirenderAudioProcessor &audioProcessor; +private: const static int MAX_BUFFER = 192000; std::vector buffer = std::vector(MAX_BUFFER); int bufferIndex = 0; double framePhase = 0.0; // [0, 1] -}; \ No newline at end of file +}; + +class TraceEffect : public DashedLineEffect { +public: + TraceEffect(OscirenderAudioProcessor& p) : DashedLineEffect(p) {} + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(audioProcessor), + std::vector{ + new osci::EffectParameter( + "Trace Start", + "Defines how far into the frame the drawing is started at. This has the effect of 'tracing' out the image from a single dot when animated. By default, we start drawing from the beginning of the frame, so this value is 0.0.", + "traceStart", + VERSION_HINT, 0.0, 0.0, 1.0, 0.001 + ), + new osci::EffectParameter( + "Trace Length", + "Defines how much of the frame is drawn per cycle. This has the effect of 'tracing' out the image from a single dot when animated. By default, we draw the whole frame, corresponding to a value of 1.0.", + "traceLength", + VERSION_HINT, 1.0, 0.0, 1.0, 0.001, osci::LfoType::Sawtooth + ) + } + ); + eff->setName("Trace"); + eff->setIcon(BinaryData::trace_svg); + return eff; + } +}; diff --git a/Source/audio/DelayEffect.h b/Source/audio/DelayEffect.h index 74056ff..26c7591 100644 --- a/Source/audio/DelayEffect.h +++ b/Source/audio/DelayEffect.h @@ -37,6 +37,19 @@ public: return vector; } + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + 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) + } + ); + eff->setName("Delay"); + eff->setIcon(BinaryData::delay_svg); + return eff; + } + private: const static int MAX_DELAY = 192000 * 10; std::vector delayBuffer = std::vector(MAX_DELAY); diff --git a/Source/audio/DistortEffect.h b/Source/audio/DistortEffect.h index 6b19c20..90bda53 100644 --- a/Source/audio/DistortEffect.h +++ b/Source/audio/DistortEffect.h @@ -1,20 +1,27 @@ #pragma once #include + class DistortEffect : public osci::EffectApplication { public: - DistortEffect(bool vertical) : vertical(vertical) {} - - osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { - double value = values[0]; - int vertical = (int)this->vertical; - if (index % 2 == 0) { - input.translate((1 - vertical) * value, vertical * value, 0); - } else { - input.translate((1 - vertical) * -value, vertical * -value, 0); - } - return input; + osci::Point apply(int index, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + int flip = index % 2 == 0 ? 1 : -1; + osci::Point jitter = osci::Point(flip * values[0], flip * values[1], flip * values[2]); + return input + jitter; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Distort X", "Distorts the image in the horizontal direction by jittering the audio sample being drawn.", "distortX", VERSION_HINT, 0.0, 0.0, 1.0), + 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.4, 0.0, 1.0), + } + ); + eff->setName("Distort"); + eff->setIcon(BinaryData::distort_svg); + eff->markLockable(false); + return eff; } -private: - bool vertical; }; diff --git a/Source/audio/KaleidoscopeEffect.h b/Source/audio/KaleidoscopeEffect.h new file mode 100644 index 0000000..4c56d1e --- /dev/null +++ b/Source/audio/KaleidoscopeEffect.h @@ -0,0 +1,88 @@ +// KaleidoscopeEffect.h +// Repeats and mirrors the input around the origin to create a kaleidoscope pattern. +// The effect supports a floating point number of segments, allowing smooth morphing +// between different symmetry counts. +#pragma once + +#include + +class KaleidoscopeEffect : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + // values[0] = segments (can be fractional) + // values[1] = phase (0-1) selecting which segment is currently being drawn + double segments = juce::jmax(values[0].load(), 1.0); // ensure at least 1 segment + double phase = values.size() > 1 ? values[1].load() : 0.0; + + // Polar conversion + double r = std::sqrt(input.x * input.x + input.y * input.y); + if (r < 1e-12) return input; + double theta = std::atan2(input.y, input.x); + + int fullSegments = (int)std::floor(segments); + double fractionalPart = segments - fullSegments; // in [0,1) + + // Use 'segments' for timing so partial segment gets proportionally shorter time. + double currentSegmentFloat = phase * segments; // [0, segments) + int currentSegmentIndex = (int)std::floor(currentSegmentFloat); + int maxIndex = fractionalPart > 1e-9 ? fullSegments : fullSegments - 1; // include partial index if exists + if (currentSegmentIndex > maxIndex) currentSegmentIndex = maxIndex; // safety + + // Base full wedge angle (all full wedges) and size of partial wedge + double baseWedgeAngle = juce::MathConstants::twoPi / segments; // size of a "unit" wedge + double partialScale = (currentSegmentIndex == fullSegments && fractionalPart > 1e-9) ? fractionalPart : 1.0; + double wedgeAngle = baseWedgeAngle * partialScale; + + // Normalize theta to [0,1) for compression + double thetaNorm = (theta + juce::MathConstants::pi) / juce::MathConstants::twoPi; // 0..1 + + // Offset for this segment: each preceding full segment occupies baseWedgeAngle + double segmentOffset = 0.0; + if (currentSegmentIndex < fullSegments) { + segmentOffset = currentSegmentIndex * baseWedgeAngle; + } else { // partial segment + segmentOffset = fullSegments * baseWedgeAngle; + } + // Map entire original angle range into [segmentOffset, segmentOffset + wedgeAngle) so edges line up exactly. + double finalTheta = segmentOffset + thetaNorm * wedgeAngle - juce::MathConstants::pi; // constant 180° rotation + + double newX = r * std::cos(finalTheta); + double newY = r * std::sin(finalTheta); + return osci::Point(newX, newY, input.z); + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter( + "Kaleidoscope Segments", + "Controls how many times the image is rotationally repeated around the centre. Fractional values smoothly morph the repetition.", + "kaleidoscopeSegments", + VERSION_HINT, + 3.0, // default + 1.0, // min + 10.0, // max + 0.0001f, // step + osci::LfoType::Sine, + 0.25f // LFO frequency (Hz) – slow, visible rotation + ), + new osci::EffectParameter( + "Kaleidoscope Phase", + "Selects which kaleidoscope segment is currently being drawn (time-multiplexed). Animate to sweep around the circle.", + "kaleidoscopePhase", + VERSION_HINT, + 0.0, // default + 0.0, // min + 1.0, // max + 0.0001f, // step + osci::LfoType::Sawtooth, + 55.0f // LFO frequency (Hz) – slow, visible rotation + ), + } + ); + eff->setName("Kaleidoscope"); + eff->setIcon(BinaryData::kaleidoscope_svg); + return eff; + } +}; diff --git a/Source/audio/MultiplexEffect.h b/Source/audio/MultiplexEffect.h index 3074981..e4891dc 100644 --- a/Source/audio/MultiplexEffect.h +++ b/Source/audio/MultiplexEffect.h @@ -7,9 +7,9 @@ public: osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { jassert(values.size() >= 6); - double gridX = values[0].load(); - double gridY = values[1].load(); - double gridZ = values[2].load(); + double gridX = values[0].load() + 0.0001; + double gridY = values[1].load() + 0.0001; + double gridZ = values[2].load() + 0.0001; double interpolation = values[3].load(); double phase = values[4].load(); double gridDelay = values[5].load(); @@ -51,6 +51,23 @@ public: return (1.0 - interpolationFactor) * current + interpolationFactor * next; } + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Multiplex X", "Controls the horizontal grid size for the multiplex effect.", "multiplexGridX", VERSION_HINT, 2.0, 1.0, 8.0), + new osci::EffectParameter("Multiplex Y", "Controls the vertical grid size for the multiplex effect.", "multiplexGridY", VERSION_HINT, 2.0, 1.0, 8.0), + new osci::EffectParameter("Multiplex Z", "Controls the depth grid size for the multiplex effect.", "multiplexGridZ", VERSION_HINT, 1.0, 1.0, 8.0), + new osci::EffectParameter("Multiplex Smooth", "Controls the smoothness of transitions between grid sizes.", "multiplexSmooth", VERSION_HINT, 0.0, 0.0, 1.0), + new osci::EffectParameter("Multiplex Phase", "Controls the current phase of the multiplex grid animation.", "gridPhase", VERSION_HINT, 0.0, 0.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 55.0f), + 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), + } + ); + eff->setName("Multiplex"); + eff->setIcon(BinaryData::multiplex_svg); + return eff; + } + private: osci::Point multiplex(osci::Point point, double position, osci::Point grid) { osci::Point unit = 1.0 / grid; diff --git a/Source/audio/PerspectiveEffect.h b/Source/audio/PerspectiveEffect.h index d237c20..3e047d0 100644 --- a/Source/audio/PerspectiveEffect.h +++ b/Source/audio/PerspectiveEffect.h @@ -25,6 +25,17 @@ public: ); } + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Perspective", "Controls the strength of the 3D perspective projection.", "perspectiveStrength", VERSION_HINT, 1.0, 0.0, 1.0), + new osci::EffectParameter("FOV", "Controls the camera's field of view in degrees. A lower field of view makes the image look more flat, and a higher field of view makes the image look more 3D.", "perspectiveFov", VERSION_HINT, 50.0, 5.0, 130.0), + } + ); + return eff; + } + private: Camera camera; diff --git a/Source/audio/RippleEffect.h b/Source/audio/RippleEffect.h new file mode 100644 index 0000000..36f8bc5 --- /dev/null +++ b/Source/audio/RippleEffect.h @@ -0,0 +1,26 @@ +#pragma once +#include + +class RippleEffectApp : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + double phase = values[1] * std::numbers::pi; + double distance = 100 * values[2] * (input.x * input.x + input.y * input.y); + input.z += values[0] * std::sin(phase + distance); + return input; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Ripple Depth", "Controls how large the ripples applied to the image are.", "rippleDepth", VERSION_HINT, 0.2, 0.0, 1.0), + 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, 0.0001f, osci::LfoType::Sawtooth, 1.0f), + new osci::EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0), + } + ); + eff->setName("Ripple"); + eff->setIcon(BinaryData::ripple_svg); + return eff; + } +}; diff --git a/Source/audio/RotateEffect.h b/Source/audio/RotateEffect.h new file mode 100644 index 0000000..dc3e04d --- /dev/null +++ b/Source/audio/RotateEffect.h @@ -0,0 +1,24 @@ +#pragma once +#include + +class RotateEffectApp : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + input.rotate(values[0] * std::numbers::pi, values[1] * std::numbers::pi, values[2] * std::numbers::pi); + return input; + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Rotate X", "Controls the rotation of the object in the X axis.", "rotateX", VERSION_HINT, 0.0, -1.0, 1.0), + new osci::EffectParameter("Rotate Y", "Controls the rotation of the object in the Y axis.", "rotateY", VERSION_HINT, 0.0, -1.0, 1.0, 0.0001f, osci::LfoType::Sawtooth, 0.2f), + new osci::EffectParameter("Rotate Z", "Controls the rotation of the object in the Z axis.", "rotateZ", VERSION_HINT, 0.0, -1.0, 1.0), + } + ); + eff->setName("Rotate"); + eff->setIcon(BinaryData::rotate_svg); + return eff; + } +}; diff --git a/Source/audio/ScaleEffect.h b/Source/audio/ScaleEffect.h new file mode 100644 index 0000000..5368a9a --- /dev/null +++ b/Source/audio/ScaleEffect.h @@ -0,0 +1,24 @@ +#pragma once +#include + +class ScaleEffectApp : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + return input * osci::Point(values[0], values[1], values[2]); + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Scale X", "Scales the object in the horizontal direction.", "scaleX", VERSION_HINT, 1.2, -3.0, 3.0), + new osci::EffectParameter("Scale Y", "Scales the object in the vertical direction.", "scaleY", VERSION_HINT, 1.2, -3.0, 3.0), + new osci::EffectParameter("Scale Z", "Scales the depth of the object.", "scaleZ", VERSION_HINT, 1.2, -3.0, 3.0), + } + ); + eff->setName("Scale"); + eff->setIcon(BinaryData::scale_svg); + eff->markLockable(true); + return eff; + } +}; diff --git a/Source/audio/ShapeVoice.cpp b/Source/audio/ShapeVoice.cpp index a2ffd8b..e980f7d 100644 --- a/Source/audio/ShapeVoice.cpp +++ b/Source/audio/ShapeVoice.cpp @@ -1,10 +1,7 @@ #include "ShapeVoice.h" #include "../PluginProcessor.h" -ShapeVoice::ShapeVoice(OscirenderAudioProcessor& p, juce::AudioSampleBuffer& externalAudio) : audioProcessor(p), externalAudio(externalAudio) { - actualTraceStart = audioProcessor.trace->getValue(0); - actualTraceLength = audioProcessor.trace->getValue(1); -} +ShapeVoice::ShapeVoice(OscirenderAudioProcessor& p, juce::AudioSampleBuffer& externalAudio) : audioProcessor(p), externalAudio(externalAudio) {} bool ShapeVoice::canPlaySound(juce::SynthesiserSound* sound) { return dynamic_cast (sound) != nullptr; @@ -93,13 +90,7 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star } for (auto sample = startSample; sample < startSample + numSamples; ++sample) { - bool traceEnabled = audioProcessor.trace->enabled->getBoolValue(); - - // update length increment - double traceLen = traceEnabled ? actualTraceLength : 1.0; - double traceMin = traceEnabled ? actualTraceStart : 0.0; - double proportionalLength = std::max(0.001, traceLen) * frameLength; - lengthIncrement = juce::jmax(proportionalLength / (audioProcessor.currentSampleRate / actualFrequency), MIN_LENGTH_INCREMENT); + lengthIncrement = juce::jmax(frameLength / (audioProcessor.currentSampleRate / actualFrequency), MIN_LENGTH_INCREMENT); osci::Point channels; double x = 0.0; @@ -163,27 +154,11 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star outputBuffer.addSample(0, sample, x * gain); } - double traceStartValue = audioProcessor.trace->getActualValue(0); - double traceLengthValue = audioProcessor.trace->getActualValue(1); - traceLengthValue = traceEnabled ? traceLengthValue : 1.0; - traceStartValue = traceEnabled ? traceStartValue : 0.0; - actualTraceLength = std::max(0.01, traceLengthValue); - actualTraceStart = traceStartValue; - if (actualTraceStart < 0) { - actualTraceStart = 0; - } - if (!renderingSample) { incrementShapeDrawing(); } - double drawnFrameLength = frameLength; - bool willLoopOver = false; - if (traceEnabled) { - drawnFrameLength *= actualTraceLength + actualTraceStart; - } - - if (!renderingSample && frameDrawn >= drawnFrameLength) { + if (!renderingSample && frameDrawn >= frameLength) { double currentShapeLength = 0; if (currentShape < frame.size()) { currentShapeLength = frame[currentShape]->len; @@ -191,22 +166,9 @@ void ShapeVoice::renderNextBlock(juce::AudioSampleBuffer& outputBuffer, int star if (sound.load() != nullptr && currentlyPlaying) { frameLength = sound.load()->updateFrame(frame); } - frameDrawn -= drawnFrameLength; - if (traceEnabled) { - shapeDrawn = juce::jlimit(0.0, currentShapeLength, frameDrawn); - } + double prevFrameLength = frameLength; + frameDrawn -= prevFrameLength; currentShape = 0; - - // TODO: updateFrame already iterates over all the shapes, - // so we can improve performance by calculating frameDrawn - // and shapeDrawn directly. frameDrawn is simply actualTraceStart * frameLength - // but shapeDrawn is the amount of the current shape that has been drawn so - // we need to iterate over all the shapes to calculate it. - if (traceEnabled) { - while (frameDrawn < actualTraceStart * frameLength) { - incrementShapeDrawing(); - } - } } } } diff --git a/Source/audio/ShapeVoice.h b/Source/audio/ShapeVoice.h index 3e9f110..b99c4ad 100644 --- a/Source/audio/ShapeVoice.h +++ b/Source/audio/ShapeVoice.h @@ -21,14 +21,11 @@ public: bool renderingSample = false; private: - const double MIN_TRACE = 0.005; const double MIN_LENGTH_INCREMENT = 0.000001; OscirenderAudioProcessor& audioProcessor; std::vector> frame; std::atomic sound = nullptr; - double actualTraceStart; - double actualTraceLength; double frameLength = 0.0; int currentShape = 0; diff --git a/Source/audio/SmoothEffect.h b/Source/audio/SmoothEffect.h index 5698eb3..0c88e9a 100644 --- a/Source/audio/SmoothEffect.h +++ b/Source/audio/SmoothEffect.h @@ -3,6 +3,9 @@ class SmoothEffect : public osci::EffectApplication { public: + SmoothEffect() = default; + explicit SmoothEffect(juce::String prefix, float defaultValue = 0.75f) : idPrefix(prefix), smoothingDefault(defaultValue) {} + osci::Point apply(int index, osci::Point input, const std::vector>& values, double sampleRate) override { double weight = juce::jmax(values[0].load(), 0.00001); weight *= 0.95; @@ -13,6 +16,18 @@ public: return avg; } + + std::shared_ptr build() const override { + auto id = idPrefix.isEmpty() ? juce::String("smoothing") : (idPrefix + "Smoothing"); + auto eff = std::make_shared( + std::make_shared(id), + new osci::EffectParameter("Smoothing", "This works as a low-pass frequency filter that removes high frequencies, making the image look smoother, and audio sound less harsh.", id, VERSION_HINT, smoothingDefault, 0.0, 1.0) + ); + eff->setIcon(BinaryData::smoothing_svg); + return eff; + } private: osci::Point avg; + juce::String idPrefix; + float smoothingDefault = 0.5f; }; diff --git a/Source/audio/StereoEffect.h b/Source/audio/StereoEffect.h index 6d85637..44a86af 100644 --- a/Source/audio/StereoEffect.h +++ b/Source/audio/StereoEffect.h @@ -27,6 +27,18 @@ public: return osci::Point(input.x, buffer[readHead].y, input.z); } + std::shared_ptr build() const override { + return std::make_shared( + std::make_shared(), + new osci::EffectParameter( + "Stereo", + "Turns mono audio that is uninteresting to visualise into stereo audio that is interesting to visualise.", + "stereo", + VERSION_HINT, 0.0, 0.0, 1.0 + ) + ); + } + private: void initialiseBuffer(double sampleRate) { diff --git a/Source/audio/SwirlEffect.h b/Source/audio/SwirlEffect.h new file mode 100644 index 0000000..d570d16 --- /dev/null +++ b/Source/audio/SwirlEffect.h @@ -0,0 +1,23 @@ +#pragma once +#include + +class SwirlEffectApp : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + double length = 10 * values[0] * input.magnitude(); + double newX = input.x * std::cos(length) - input.y * std::sin(length); + double newY = input.x * std::sin(length) + input.y * std::cos(length); + return osci::Point(newX, newY, input.z); + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Swirl", "Swirls the image in a spiral pattern.", "swirl", VERSION_HINT, 0.4, -1.0, 1.0), + } + ); + eff->setIcon(BinaryData::swirl_svg); + return eff; + } +}; diff --git a/Source/audio/TranslateEffect.h b/Source/audio/TranslateEffect.h new file mode 100644 index 0000000..3a4c5b1 --- /dev/null +++ b/Source/audio/TranslateEffect.h @@ -0,0 +1,23 @@ +#pragma once +#include + +class TranslateEffectApp : public osci::EffectApplication { +public: + osci::Point apply(int /*index*/, osci::Point input, const std::vector>& values, double /*sampleRate*/) override { + return input + osci::Point(values[0], values[1], values[2]); + } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + std::vector{ + new osci::EffectParameter("Translate X", "Moves the object horizontally.", "translateX", VERSION_HINT, 0.3, -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), + } + ); + eff->setName("Translate"); + eff->setIcon(BinaryData::translate_svg); + return eff; + } +}; diff --git a/Source/audio/TwistEffect.h b/Source/audio/TwistEffect.h index b4754c1..59472cc 100644 --- a/Source/audio/TwistEffect.h +++ b/Source/audio/TwistEffect.h @@ -10,4 +10,13 @@ public: input.rotate(0.0, twistTheta, 0.0); return input; } -}; \ No newline at end of file + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + new osci::EffectParameter("Twist", "Twists the image in a corkscrew pattern.", "twist", VERSION_HINT, 0.5, 0.0, 1.0, 0.0001, osci::LfoType::Sine, 0.5) + ); + eff->setIcon(BinaryData::twist_svg); + return eff; + } +}; diff --git a/Source/audio/VectorCancellingEffect.h b/Source/audio/VectorCancellingEffect.h index 5052623..7239834 100644 --- a/Source/audio/VectorCancellingEffect.h +++ b/Source/audio/VectorCancellingEffect.h @@ -20,6 +20,14 @@ public: } return input; } + + std::shared_ptr build() const override { + auto eff = std::make_shared( + std::make_shared(), + new osci::EffectParameter("Vector Cancelling", "Inverts the audio and image every few samples to 'cancel out' the audio, making the audio quiet, and distorting the image.", "vectorCancelling", VERSION_HINT, 0.5, 0.0, 1.0)); + eff->setIcon(BinaryData::vectorcancelling_svg); + return eff; + } private: int lastIndex = 0; double nextInvert = 0; diff --git a/Source/audio/WobbleEffect.h b/Source/audio/WobbleEffect.h index c82e853..3cb2563 100644 --- a/Source/audio/WobbleEffect.h +++ b/Source/audio/WobbleEffect.h @@ -14,6 +14,18 @@ public: return input + delta; } + std::shared_ptr build() const override { + auto wobble = std::make_shared( + std::make_shared(audioProcessor), + std::vector{ + 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, 0.0001f, osci::LfoType::Sawtooth, 1.0f), + }); + wobble->setName("Wobble"); + wobble->setIcon(BinaryData::wobble_svg); + return wobble; + } + private: OscirenderAudioProcessor& audioProcessor; double smoothedFrequency = 0; diff --git a/Source/components/DraggableListBox.cpp b/Source/components/DraggableListBox.cpp index d4f0da0..b54177a 100644 --- a/Source/components/DraggableListBox.cpp +++ b/Source/components/DraggableListBox.cpp @@ -1,19 +1,12 @@ #include "DraggableListBox.h" +#include DraggableListBoxItemData::~DraggableListBoxItemData() {}; void DraggableListBoxItem::paint(juce::Graphics& g) { - if (insertAfter) - { - g.setColour(juce::Colour(0xff00ff00)); - g.fillRect(0, getHeight() - 4, getWidth(), 4); - } - else if (insertBefore) - { - g.setColour(juce::Colour(0xff00ff00)); - g.fillRect(0, 0, getWidth(), 4); - } + // Per-item insertion lines are suppressed in favour of a single overlay drawn by DraggableListBox. + juce::ignoreUnused(g); } void DraggableListBoxItem::mouseEnter(const juce::MouseEvent&) @@ -31,7 +24,10 @@ void DraggableListBoxItem::mouseDrag(const juce::MouseEvent&) { if (juce::DragAndDropContainer* container = juce::DragAndDropContainer::findParentDragContainerFor(this)) { - container->startDragging("DraggableListBoxItem", this); + auto* obj = new juce::DynamicObject(); + obj->setProperty("type", juce::var("DraggableListBoxItem")); + obj->setProperty("row", juce::var(rowNum)); + container->startDragging(juce::var(obj), this); } } @@ -60,31 +56,247 @@ void DraggableListBoxItem::hideInsertLines() void DraggableListBoxItem::itemDragEnter(const SourceDetails& dragSourceDetails) { updateInsertLines(dragSourceDetails); + updateAutoScroll(dragSourceDetails); + // Update the global overlay on the parent list box + auto ptGlobal = localPointToGlobal(dragSourceDetails.localPosition); + auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal); + listBox.updateDropIndicatorAt(ptInLB); } void DraggableListBoxItem::itemDragMove(const SourceDetails& dragSourceDetails) { updateInsertLines(dragSourceDetails); + updateAutoScroll(dragSourceDetails); + auto ptGlobal = localPointToGlobal(dragSourceDetails.localPosition); + auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal); + listBox.updateDropIndicatorAt(ptInLB); } void DraggableListBoxItem::itemDragExit(const SourceDetails& /*dragSourceDetails*/) { hideInsertLines(); + stopAutoScroll(); + listBox.clearDropIndicator(); } void DraggableListBoxItem::itemDropped(const juce::DragAndDropTarget::SourceDetails &dragSourceDetails) { hideInsertLines(); + stopAutoScroll(); + listBox.clearDropIndicator(); if (DraggableListBoxItem* item = dynamic_cast(dragSourceDetails.sourceComponent.get())) { - if (dragSourceDetails.localPosition.y < getHeight() / 2) - modelData.moveBefore(item->rowNum, rowNum); - else - modelData.moveAfter(item->rowNum, rowNum); + if (auto* vp = listBox.getViewport()) + { + // Compute the global insertion index using the list box, not the item local midpoint + auto ptInThis = dragSourceDetails.localPosition; + auto ptGlobal = localPointToGlobal(ptInThis); + auto ptInLB = listBox.getLocalPoint(nullptr, ptGlobal); + int insertIndex = listBox.getInsertionIndexForPosition(ptInLB.x, ptInLB.y); + insertIndex = juce::jlimit(0, modelData.getNumItems(), insertIndex); + + // If dragging an item that appears before the insertion point and we're moving it down, + // account for the removal shifting indices. + const int fromIndex = item->rowNum; + int toIndex = insertIndex; + if (toIndex > fromIndex) toIndex -= 1; + + if (toIndex < 0) toIndex = 0; + if (toIndex >= modelData.getNumItems()) + modelData.moveAfter(fromIndex, modelData.getNumItems() - 1); + else if (toIndex <= 0) + modelData.moveBefore(fromIndex, 0); + else + modelData.moveBefore(fromIndex, toIndex); + } listBox.updateContent(); } } +void DraggableListBoxItem::updateAutoScroll(const SourceDetails& dragSourceDetails) +{ + // Determine pointer position within the list's viewport + if (auto* vp = listBox.getViewport()) + { + // Convert the drag position to the viewport's local coordinates + auto ptInThis = dragSourceDetails.localPosition; + auto ptInLB = localPointToGlobal(ptInThis); + ptInLB = listBox.getLocalPoint(nullptr, ptInLB); // convert from screen to listbox + + auto viewBounds = vp->getLocalBounds(); + auto viewTop = viewBounds.getY(); + auto viewBottom = viewBounds.getBottom(); + + // Position of pointer within the viewport component + auto ptInVP = vp->getLocalPoint(&listBox, ptInLB); + const int yInVP = ptInVP.y; + + const int edgeZone = juce::jmax(12, viewBounds.getHeight() / 6); // scrolling zone near edges + double velocity = 0.0; + + if (yInVP < viewTop + edgeZone) + { + // Near top: scroll up. Speed increases closer to edge. + const double t = juce::jlimit(0.0, 1.0, (double)(edgeZone - (yInVP - viewTop)) / (double)edgeZone); + velocity = - (1.0 + 14.0 * t * t); // pixels per tick (quadratic ramp), negative = up + } + else if (yInVP > viewBottom - edgeZone) + { + // Near bottom: scroll down. + const double d = (double)(yInVP - (viewBottom - edgeZone)); + const double t = juce::jlimit(0.0, 1.0, d / (double)edgeZone); + velocity = (1.0 + 14.0 * t * t); // pixels per tick, positive = down + } + + scrollPixelsPerTick = velocity; + + if (std::abs(scrollPixelsPerTick) > 0.0) + { + if (!autoScrollActive) + { + autoScrollActive = true; + startTimerHz(60); // smooth-ish scrolling tied to UI thread + } + } + else + { + stopAutoScroll(); + } + } +} + +void DraggableListBoxItem::stopAutoScroll() +{ + autoScrollActive = false; + stopTimer(); + scrollPixelsPerTick = 0.0; +} + +void DraggableListBoxItem::timerCallback() +{ + if (!autoScrollActive || scrollPixelsPerTick == 0.0) + return; + + if (auto* vp = listBox.getViewport()) + { + auto current = vp->getViewPosition(); + const int contentH = vp->getViewedComponent() != nullptr ? vp->getViewedComponent()->getHeight() : listBox.getHeight(); + const int maxY = juce::jmax(0, contentH - vp->getHeight()); + int newY = juce::jlimit(0, maxY, current.y + (int)std::lround(scrollPixelsPerTick)); + if (newY != current.y) + { + vp->setViewPosition(current.x, newY); + } + + // Update the global drop indicator position based on current mouse position (even if the mouse isn't moving) + auto screenPos = juce::Desktop::getInstance().getMainMouseSource().getScreenPosition(); + auto posInLB = listBox.getLocalPoint(nullptr, screenPos.toInt()); + listBox.updateDropIndicatorAt(posInLB); + } +} + +// ===================== DraggableListBox overlay indicator ===================== + +void DraggableListBox::updateDropIndicator(const SourceDetails& details) +{ + // localPosition is already in this component's coordinate space + const auto pt = details.localPosition; + int index = getInsertionIndexForPosition(pt.x, pt.y); + if (index < 0) index = 0; // allow showing at very top (over header spacer) + dropInsertIndex = index; + showDropIndicator = true; + repaint(); +} + +void DraggableListBox::clearDropIndicator() +{ + showDropIndicator = false; + dropInsertIndex = -1; + repaint(); +} + +void DraggableListBox::updateDropIndicatorAt(const juce::Point& listLocalPos) +{ + int index = getInsertionIndexForPosition(listLocalPos.x, listLocalPos.y); + if (index < 0) index = 0; // allow showing at very top (over header spacer) + dropInsertIndex = index; + showDropIndicator = true; + repaint(); +} + +void DraggableListBox::paintOverChildren(juce::Graphics& g) +{ + VListBox::paintOverChildren(g); + if (!showDropIndicator) return; + + const int numRows = getModel() != nullptr ? getModel()->getNumRows() : 0; + if (dropInsertIndex < 0 || dropInsertIndex > numRows) return; + + auto* vp = getViewport(); + if (vp == nullptr) return; + + // Determine the y position between rows to draw the indicator line + int y = 0; + if (dropInsertIndex == 0) + { + // Top of first row (below header) + if (numRows > 0) + y = getRowPosition(0, true).getY(); + else + y = 0; + } + else if (dropInsertIndex >= numRows) + { + auto lastRowBounds = getRowPosition(numRows - 1, true); + y = lastRowBounds.getBottom(); + } + else + { + auto prevBounds = getRowPosition(dropInsertIndex - 1, true); + y = prevBounds.getBottom(); + } + + // Draw a prominent indicator spanning the visible row width + const int x = 0; + const int w = getVisibleRowWidth(); + const int thickness = 3; + const juce::Colour colour = juce::Colours::lime.withAlpha(0.9f); + + const float yOffset = -2.5f; // Offset to center the line visually + + g.setColour(colour); + g.fillRoundedRectangle(x, y - thickness / 2 + yOffset, w, thickness, 2.0f); +} + +void DraggableListBox::itemDropped(const SourceDetails& details) +{ + // Background drop: compute insertion index and use model to move + int insertIndex = -1; + // localPosition is already relative to this list + insertIndex = getInsertionIndexForPosition(details.localPosition.x, details.localPosition.y); + if (insertIndex < 0) insertIndex = 0; // clamp to top when over header spacer + + if (auto* m = dynamic_cast(getModel())) + { + int fromIndex = -1; + const juce::var& desc = details.description; + if (desc.isObject()) + { + auto* obj = desc.getDynamicObject(); + if (obj != nullptr) + { + auto v = obj->getProperty("row"); + if (v.isInt()) fromIndex = (int)v; + } + } + + if (fromIndex >= 0 && insertIndex >= 0) + m->moveByInsertIndex(fromIndex, insertIndex); + } + + clearDropIndicator(); +} + juce::Component* DraggableListBoxModel::refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component *existingComponentToUpdate) { std::unique_ptr item(dynamic_cast(existingComponentToUpdate)); diff --git a/Source/components/DraggableListBox.h b/Source/components/DraggableListBox.h index f21dff9..f7c88e9 100644 --- a/Source/components/DraggableListBox.h +++ b/Source/components/DraggableListBox.h @@ -18,14 +18,39 @@ struct DraggableListBoxItemData virtual void addItemAtEnd() {}; }; -// DraggableListBox is basically just a ListBox, that inherits from DragAndDropContainer. -// Declare your list box using this type. -class DraggableListBox : public juce::jc::ListBox, public juce::DragAndDropContainer +// DraggableListBox extends VListBox to both initiate drags and act as a target, so +// it can paint a clean, consistent drop indicator between row components. +class DraggableListBox : public VListBox, + public juce::DragAndDropContainer, + public juce::DragAndDropTarget { +public: + using VListBox::VListBox; + + // DragAndDropTarget + bool isInterestedInDragSource(const SourceDetails&) override { return true; } + void itemDragEnter(const SourceDetails& details) override { updateDropIndicator(details); } + void itemDragMove(const SourceDetails& details) override { updateDropIndicator(details); } + void itemDragExit(const SourceDetails&) override { clearDropIndicator(); } + void itemDropped(const SourceDetails& details) override; + bool shouldDrawDragImageWhenOver() override { return true; } + + // Paint a global drop indicator between rows + void paintOverChildren(juce::Graphics& g) override; + + // Allow children to drive indicator positioning + void updateDropIndicatorAt(const juce::Point& listLocalPos); + void clearDropIndicator(); + +private: + void updateDropIndicator(const SourceDetails& details); + + bool showDropIndicator = false; + int dropInsertIndex = -1; // index to insert before; may be getNumRows() for end }; // Everything below this point should be generic. -class DraggableListBoxItem : public juce::Component, public juce::DragAndDropTarget +class DraggableListBoxItem : public juce::Component, public juce::DragAndDropTarget, public juce::Timer { public: DraggableListBoxItem(DraggableListBox& lb, DraggableListBoxItemData& data, int rn) @@ -49,6 +74,9 @@ public: protected: void updateInsertLines(const SourceDetails &dragSourceDetails); void hideInsertLines(); + void updateAutoScroll(const SourceDetails& dragSourceDetails); + void stopAutoScroll(); + void timerCallback() override; int rowNum; DraggableListBoxItemData& modelData; @@ -57,9 +85,13 @@ protected: juce::MouseCursor savedCursor; bool insertAfter = false; bool insertBefore = false; + + // Auto-scroll state while dragging near viewport edges + double scrollPixelsPerTick = 0.0; // positive = scroll down, negative = up + bool autoScrollActive = false; }; -class DraggableListBoxModel : public juce::jc::ListBoxModel +class DraggableListBoxModel : public VListBoxModel { public: DraggableListBoxModel(DraggableListBox& lb, DraggableListBoxItemData& md) @@ -70,6 +102,27 @@ public: juce::Component* refreshComponentForRow(int, bool, juce::Component*) override; + // Convenience: move an item using an insertion index (before position). Handles index shifting. + void moveByInsertIndex(int fromIndex, int insertIndex) + { + const int count = modelData.getNumItems(); + if (count <= 0) return; + insertIndex = juce::jlimit(0, count, insertIndex); + int toIndex = insertIndex; + if (toIndex > fromIndex) toIndex -= 1; + + if (count == 1 || fromIndex == toIndex) return; + + if (toIndex <= 0) + modelData.moveBefore(fromIndex, 0); + else if (toIndex >= count) + modelData.moveAfter(fromIndex, count - 1); + else + modelData.moveBefore(fromIndex, toIndex); + + listBox.updateContent(); + } + protected: // Draggable model has a reference to its owner ListBox, so it can tell it to update after DnD. DraggableListBox &listBox; diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index ba5bd37..1c0454f 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)); @@ -213,14 +215,8 @@ void EffectComponent::resized() { } void EffectComponent::paint(juce::Graphics& g) { - auto bounds = getLocalBounds(); - auto length = effect.parameters.size(); - auto isEnd = index == length - 1; - auto isStart = index == 0; g.setColour(findColour(effectComponentBackgroundColourId, true)); - juce::Path path; - path.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, isStart, false, isEnd); - g.fillPath(path); + g.fillRect(getLocalBounds()); } void EffectComponent::parameterValueChanged(int parameterIndex, float newValue) { diff --git a/Source/components/EffectTypeGridComponent.cpp b/Source/components/EffectTypeGridComponent.cpp new file mode 100644 index 0000000..54e5dec --- /dev/null +++ b/Source/components/EffectTypeGridComponent.cpp @@ -0,0 +1,205 @@ +#include "EffectTypeGridComponent.h" +#include "../LookAndFeel.h" +#include +#include +#include + +EffectTypeGridComponent::EffectTypeGridComponent(OscirenderAudioProcessor& processor) + : audioProcessor(processor) +{ + // Setup scrollable viewport and content + addAndMakeVisible(viewport); + viewport.setViewedComponent(&content, false); + viewport.setScrollBarsShown(true, false); // vertical only + // Setup reusable bottom fade + initScrollFade(*this); + attachToViewport(viewport); + setupEffectItems(); + setSize(400, 200); + addAndMakeVisible(cancelButton); + cancelButton.onClick = [this]() { + if (onCanceled) onCanceled(); + }; + refreshDisabledStates(); +} + +EffectTypeGridComponent::~EffectTypeGridComponent() = default; + +void EffectTypeGridComponent::setupEffectItems() +{ + // Clear existing items + effectItems.clear(); + content.removeAllChildren(); + + // Get effect types directly from the audio processor's toggleableEffects + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + const int n = (int) audioProcessor.toggleableEffects.size(); + std::vector order(n); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [this](int a, int b) { + auto ea = audioProcessor.toggleableEffects[a]; + auto eb = audioProcessor.toggleableEffects[b]; + const int cmp = ea->getName().compareIgnoreCase(eb->getName()); + if (cmp != 0) + return cmp < 0; // ascending alphabetical, case-insensitive + // Stable tie-breaker to ensure deterministic layout + return ea->getId().compare(eb->getId()) < 0; + }); + + for (int idx : order) + { + auto effect = audioProcessor.toggleableEffects[idx]; + // Extract effect name from the effect + juce::String effectName = effect->getName(); + + // Create new item component + auto* item = new EffectTypeItemComponent(effectName, effect->getIcon(), effect->getId()); + + // Set up callback to forward effect selection + item->onEffectSelected = [this](const juce::String& effectId) { + if (onEffectSelected) + onEffectSelected(effectId); + }; + // Hover preview: request temporary preview of this effect while hovered + item->onHoverStart = [this](const juce::String& effectId) { + if (audioProcessor.getGlobalBoolValue("previewEffectOnHover", true)) { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + audioProcessor.setPreviewEffectId(effectId); + } + }; + item->onHoverEnd = [this]() { + if (audioProcessor.getGlobalBoolValue("previewEffectOnHover", true)) { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + audioProcessor.clearPreviewEffect(); + } + }; + + effectItems.add(item); + content.addAndMakeVisible(item); + } +} + +void EffectTypeGridComponent::refreshDisabledStates() +{ + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + // Build a quick lookup of selected ids + std::unordered_set selectedIds; + selectedIds.reserve((size_t) audioProcessor.toggleableEffects.size()); + bool anySelected = false; + for (const auto& eff : audioProcessor.toggleableEffects) { + const bool isSelected = (eff->selected == nullptr) ? true : eff->selected->getBoolValue(); + if (isSelected) { + anySelected = true; + selectedIds.insert(eff->getId().toStdString()); + } + } + for (auto* item : effectItems) { + const bool disable = selectedIds.find(item->getEffectId().toStdString()) != selectedIds.end(); + item->setEnabled(! disable); + } + cancelButton.setVisible(anySelected); + // Update fade visibility/layout in case scrollability changed + layoutScrollFade(viewport.getBounds(), true, 48); +} + +void EffectTypeGridComponent::paint(juce::Graphics& g) +{ + // No background - make component transparent +} + +void EffectTypeGridComponent::resized() +{ + auto bounds = getLocalBounds(); + auto topBar = bounds.removeFromTop(30); + cancelButton.setBounds(topBar.removeFromRight(80).reduced(4)); + viewport.setBounds(bounds); + auto contentArea = viewport.getLocalBounds(); + // Lock content width to viewport width to avoid horizontal scrolling + content.setSize(contentArea.getWidth(), content.getHeight()); + + // Create FlexBox for responsive grid layout within content + 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; + + // Determine fixed per-item width for this viewport width + const int viewW = contentArea.getWidth(); + const int viewH = contentArea.getHeight(); + const int itemsPerRow = juce::jmax(1, viewW / MIN_ITEM_WIDTH); + const int fixedItemWidth = (itemsPerRow > 0 ? viewW / itemsPerRow : viewW); + + // Add each effect item with a fixed width, and pad the final row with placeholders so it's centered + const int total = effectItems.size(); + const int fullRows = (itemsPerRow > 0 ? total / itemsPerRow : 0); + const int remainder = (itemsPerRow > 0 ? total % itemsPerRow : 0); + + auto addItemFlex = [&](juce::Component* c) + { + flexBox.items.add(juce::FlexItem(*c) + .withMinWidth((float) fixedItemWidth) + .withMaxWidth((float) fixedItemWidth) + .withHeight((float) ITEM_HEIGHT) + .withFlex(1.0f) // keep existing flex behaviour; fixed max width holds size + .withMargin(juce::FlexItem::Margin(0))); + }; + + auto addPlaceholder = [&]() + { + // Placeholder occupies a slot visually but has no component; ensures last row is centered + juce::FlexItem placeholder((float) fixedItemWidth, (float) ITEM_HEIGHT); + placeholder.flexGrow = 1.0f; // match item flex for consistent spacing + placeholder.margin = juce::FlexItem::Margin(0); + flexBox.items.add(std::move(placeholder)); + }; + + int index = 0; + // Add complete rows + for (int r = 0; r < fullRows; ++r) + for (int c = 0; c < itemsPerRow; ++c) + addItemFlex(effectItems.getUnchecked(index++)); + + // Add last row centered with balanced placeholders + if (remainder > 0) + { + const int missing = itemsPerRow - remainder; + const int leftPad = missing / 2; + const int rightPad = missing - leftPad; + + for (int i = 0; i < leftPad; ++i) addPlaceholder(); + for (int i = 0; i < remainder; ++i) addItemFlex(effectItems.getUnchecked(index++)); + for (int i = 0; i < rightPad; ++i) addPlaceholder(); + } + + // Compute required content height + const int requiredHeight = calculateRequiredHeight(viewW); + + // If content is shorter than viewport, make content at least as tall as viewport + int yOffset = 0; + if (requiredHeight < viewH) { + content.setSize(viewW, viewH); + yOffset = (viewH - requiredHeight) / 2; + } else { + content.setSize(viewW, requiredHeight); + } + // Layout items within content at the computed offset + flexBox.performLayout(juce::Rectangle(0.0f, (float) yOffset, (float) viewW, (float) requiredHeight)); + + // Layout bottom scroll fade over the viewport area + layoutScrollFade(viewport.getBounds(), true, 48); +} + +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..cafdcf6 --- /dev/null +++ b/Source/components/EffectTypeGridComponent.h @@ -0,0 +1,35 @@ +#pragma once +#include +#include "../PluginProcessor.h" +#include "EffectTypeItemComponent.h" +#include "ScrollFadeMixin.h" + +class EffectTypeGridComponent : public juce::Component, private ScrollFadeMixin +{ +public: + EffectTypeGridComponent(OscirenderAudioProcessor& processor); + ~EffectTypeGridComponent() override; + + void paint(juce::Graphics& g) override; + void resized() override; + + int calculateRequiredHeight(int availableWidth) const; + std::function onEffectSelected; + std::function onCanceled; // optional cancel handler + void refreshDisabledStates(); // grey-out items that are already selected + +private: + OscirenderAudioProcessor& audioProcessor; + juce::Viewport viewport; // scroll container + juce::Component content; // holds the grid items + juce::OwnedArray effectItems; + juce::FlexBox flexBox; + juce::TextButton cancelButton { "Cancel" }; + + 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..e15c970 --- /dev/null +++ b/Source/components/EffectTypeItemComponent.cpp @@ -0,0 +1,123 @@ +#include "EffectTypeItemComponent.h" + +EffectTypeItemComponent::EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id) + : effectName(name), effectId(id) +{ + juce::String iconSvg = icon; + if (icon.isEmpty()) { + // Default icon if none is provided + iconSvg = juce::String::createStringFromData(BinaryData::rotate_svg, BinaryData::rotate_svgSize); + } + iconButton = std::make_unique( + "effectIcon", + iconSvg, + juce::Colours::white.withAlpha(0.7f) + ); + + // Make the icon non-interactive since this is just a visual element + iconButton->setInterceptsMouseClicks(false, false); + addAndMakeVisible(*iconButton); +} + +EffectTypeItemComponent::~EffectTypeItemComponent() = default; + +void EffectTypeItemComponent::paint(juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat().reduced(10); + + // Get animation progress from inherited HoverAnimationMixin (disabled => no hover) + auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f; + + // 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); + + 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 + juce::Colour normalBgColour = Colours::veryDark; + juce::Colour hoverBgColour = normalBgColour.brighter(0.05f); + 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 text area - now accounting for icon space on the left + auto textArea = bounds.reduced(8, 4); + textArea.removeFromLeft(28); // Remove space for icon (24px + 4px gap) + + g.setColour(juce::Colours::white); + g.setFont(juce::FontOptions(16.0f, juce::Font::plain)); + g.drawText(effectName, textArea, juce::Justification::centred, true); + + // If disabled, draw a dark transparent overlay over the rounded rect to simplify visuals + if (! isEnabled()) { + g.setColour(juce::Colours::black.withAlpha(0.35f)); + g.fillRoundedRectangle(bounds.toFloat(), CORNER_RADIUS); + } +} + +void EffectTypeItemComponent::resized() +{ + auto bounds = getLocalBounds().reduced(10); + + // Reserve space for the icon on the left + auto iconArea = bounds.removeFromLeft(60); // 24px for icon + iconArea = iconArea.withSizeKeepingCentre(40, 40); // Make icon 20x20px + + iconButton->setBounds(iconArea); + + // Get animation progress and calculate Y offset + auto animationProgress = isEnabled() ? getAnimationProgress() : 0.0f; + auto yOffset = -animationProgress * HOVER_LIFT_AMOUNT; + + iconButton->setTransform(juce::AffineTransform::translation(0, yOffset)); +} + +void EffectTypeItemComponent::mouseDown(const juce::MouseEvent& event) +{ + if (! isEnabled()) return; + // Extend base behavior to keep hover press animation + HoverAnimationMixin::mouseDown(event); + // Ensure any hover preview is cleared before permanently selecting/enabling the effect + if (onHoverEnd) onHoverEnd(); + if (onEffectSelected) { + onEffectSelected(effectId); + } +} + +void EffectTypeItemComponent::mouseMove(const juce::MouseEvent& event) { + setMouseCursor(isEnabled() ? juce::MouseCursor::PointingHandCursor : juce::MouseCursor::NormalCursor); + juce::Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate(); +} + +void EffectTypeItemComponent::mouseEnter(const juce::MouseEvent& event) +{ + HoverAnimationMixin::mouseEnter(event); + if (isEnabled() && onHoverStart) + onHoverStart(effectId); +} + +void EffectTypeItemComponent::mouseExit(const juce::MouseEvent& event) +{ + HoverAnimationMixin::mouseExit(event); + if (onHoverEnd) + onHoverEnd(); +} diff --git a/Source/components/EffectTypeItemComponent.h b/Source/components/EffectTypeItemComponent.h new file mode 100644 index 0000000..4bfab69 --- /dev/null +++ b/Source/components/EffectTypeItemComponent.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include "../LookAndFeel.h" +#include "HoverAnimationMixin.h" +#include "SvgButton.h" + +class EffectTypeItemComponent : public HoverAnimationMixin +{ +public: + EffectTypeItemComponent(const juce::String& name, const juce::String& icon, const juce::String& id); + ~EffectTypeItemComponent() override; + + void paint(juce::Graphics& g) override; + void resized() override; + void mouseDown(const juce::MouseEvent& event) override; + void mouseMove(const juce::MouseEvent& event) override; + void mouseEnter(const juce::MouseEvent& event) override; + void mouseExit(const juce::MouseEvent& event) override; + + const juce::String& getEffectId() const { return effectId; } + const juce::String& getEffectName() const { return effectName; } + + std::function onEffectSelected; + std::function onHoverStart; + std::function onHoverEnd; + +private: + juce::String effectName; + juce::String effectId; + + // Icon for the effect + std::unique_ptr iconButton; + + static constexpr int CORNER_RADIUS = 8; + static constexpr float HOVER_LIFT_AMOUNT = 2.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EffectTypeItemComponent) +}; diff --git a/Source/components/EffectsListComponent.cpp b/Source/components/EffectsListComponent.cpp index 7c48aaa..c225fb2 100644 --- a/Source/components/EffectsListComponent.cpp +++ b/Source/components/EffectsListComponent.cpp @@ -6,45 +6,70 @@ 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()); - list.setEnabled(selected.getToggleState()); - } + for (int i = 0; i < parameters.size(); i++) { + std::shared_ptr effectComponent = std::make_shared(effect, i); + enabled.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(enabled.getToggleState()); + enabled.onClick = [this, weakEffectComponent] { + if (auto effectComponent = weakEffectComponent.lock()) { + auto data = (AudioEffectListBoxItemData&)modelData; + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + data.setSelected(rowNum, enabled.getToggleState()); + list.setEnabled(enabled.getToggleState()); + } repaint(); - }; + }; effectComponent->updateToggleState = [this, i, weakEffectComponent] { if (auto effectComponent = weakEffectComponent.lock()) { - selected.setToggleState(effectComponent->effect.enabled == nullptr || effectComponent->effect.enabled->getValue(), juce::dontSendNotification); - list.setEnabled(selected.getToggleState()); + enabled.setToggleState(effectComponent->effect.enabled == nullptr || effectComponent->effect.enabled->getValue(), juce::dontSendNotification); + list.setEnabled(enabled.getToggleState()); } 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(enabled); + + closeButton.setEdgeIndent(2); + closeButton.onClick = [this]() { + // Flip flags under lock + { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + if (this->effect.enabled) this->effect.enabled->setValueNotifyingHost(false); + if (this->effect.selected) this->effect.selected->setValueNotifyingHost(false); + // Reset all parameters/flags for this effect back to defaults on removal + this->effect.resetToDefault(); + } + // Defer model reset and outer list refresh to avoid re-entrancy on current row + juce::MessageManager::callAsync([this]() { + auto& data = static_cast(modelData); + data.resetData(); + // Update the outer DraggableListBox, not the inner parameter list + this->listBox.updateContent(); + // If there are no effects visible, open the grid + if (data.getNumItems() == 0) { + if (data.onAddNewEffectRequested) + data.onAddNewEffectRequested(); + } + }); + }; + addAndMakeVisible(closeButton); } EffectsListComponent::~EffectsListComponent() { @@ -52,26 +77,34 @@ 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); + + auto rightBar = getLocalBounds().removeFromRight(RIGHT_BAR_WIDTH); + rightBar.removeFromBottom(PADDING); + g.setColour(findColour(effectComponentHandleColourId)); + juce::Path rightPath; + rightPath.addRoundedRectangle(rightBar.getX(), rightBar.getY(), rightBar.getWidth(), rightBar.getHeight(), OscirenderLookAndFeel::RECT_RADIUS, OscirenderLookAndFeel::RECT_RADIUS, false, true, false, true); + g.fillPath(rightPath); + + DraggableListBoxItem::paint(g); } void EffectsListComponent::paintOverChildren(juce::Graphics& g) { @@ -81,51 +114,53 @@ 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 (!enabled.getToggleState()) { g.fillPath(path); } } void EffectsListComponent::resized() { - auto area = getLocalBounds(); + auto area = getLocalBounds(); auto leftBar = area.removeFromLeft(LEFT_BAR_WIDTH); + auto buttonBounds = area.removeFromRight(RIGHT_BAR_WIDTH); + closeButton.setBounds(buttonBounds); + closeButton.setImageTransform(juce::AffineTransform::translation(0, -2)); leftBar.removeFromLeft(20); - area.removeFromRight(PADDING); - selected.setBounds(leftBar.withSizeKeepingCentre(30, 20)); - list.setBounds(area); + enabled.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; + 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) { + auto data = (AudioEffectListBoxItemData&)modelData; + if (! juce::isPositiveAndBelow(rowNumber, data.getNumItems())) + return nullptr; 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)); - } + item = std::make_unique(listBox, (AudioEffectListBoxItemData&)modelData, rowNumber, *data.getEffect(rowNumber)); return item.release(); } diff --git a/Source/components/EffectsListComponent.h b/Source/components/EffectsListComponent.h index d813224..9fc9b93 100644 --- a/Source/components/EffectsListComponent.h +++ b/Source/components/EffectsListComponent.h @@ -5,6 +5,8 @@ #include "EffectComponent.h" #include "ComponentList.h" #include "SwitchButton.h" +#include "EffectTypeGridComponent.h" +#include "SvgButton.h" #include // Application-specific data container @@ -14,6 +16,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 +24,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]; - auto id = effect->getId().toLowerCase(); - + // 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,26 +58,23 @@ 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()); - std::shuffle(data.begin(), data.end(), g); - - for (int i = 0; i < data.size(); i++) { - data[i]->setPrecedence(i); - } - - audioProcessor.updateEffectPrecedence(); + // 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(); } void resetData() { @@ -63,17 +83,22 @@ 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(); + // Only the effects themselves are rows; the "+ Add new effect" button is a separate control below the list + return (int) data.size(); } // CURRENTLY NOT USED void deleteItem(int indexOfItemToDelete) override { - // data.erase(data.begin() + indexOfItemToDelete); + // data.erase(data.begin() + indexOfItemToDelete); } // CURRENTLY NOT USED @@ -81,10 +106,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 +117,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); @@ -150,6 +175,7 @@ public: void resized() override; static const int LEFT_BAR_WIDTH = 50; + static const int RIGHT_BAR_WIDTH = 15; // space for close button static const int ROW_HEIGHT = 30; static const int PADDING = 4; @@ -157,7 +183,8 @@ protected: osci::Effect& effect; ComponentListModel listModel; juce::ListBox list; - jux::SwitchButton selected = { effect.enabled }; + jux::SwitchButton enabled = { effect.enabled }; + SvgButton closeButton = SvgButton("closeEffect", juce::String::createStringFromData(BinaryData::close_svg, BinaryData::close_svgSize), juce::Colours::white, juce::Colours::white); private: OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessorEditor& editor; diff --git a/Source/components/HoverAnimationMixin.cpp b/Source/components/HoverAnimationMixin.cpp new file mode 100644 index 0000000..12d02e7 --- /dev/null +++ b/Source/components/HoverAnimationMixin.cpp @@ -0,0 +1,70 @@ +#include "HoverAnimationMixin.h" + +HoverAnimationMixin::HoverAnimationMixin() + : animatorUpdater(this), + hoverAnimator(juce::ValueAnimatorBuilder{} + .withEasing(getEasingFunction()) + .withDurationMs(getHoverAnimationDurationMs()) + .withValueChangedCallback([this](auto value) { + animationProgress = static_cast(value); + repaint(); + resized(); + }) + .build()), + unhoverAnimator(juce::ValueAnimatorBuilder{} + .withEasing(getEasingFunction()) + .withDurationMs(getHoverAnimationDurationMs()) + .withValueChangedCallback([this](auto value) { + animationProgress = 1.0f - static_cast(value); + repaint(); + resized(); + }) + .build()) +{ + setupAnimators(); +} + +void HoverAnimationMixin::setupAnimators() +{ + animatorUpdater.addAnimator(hoverAnimator); + animatorUpdater.addAnimator(unhoverAnimator); +} + +void HoverAnimationMixin::animateHover(bool isHovering) +{ + if (isHovering) + { + unhoverAnimator.complete(); + hoverAnimator.start(); + } + else + { + hoverAnimator.complete(); + unhoverAnimator.start(); + } +} + +void HoverAnimationMixin::mouseEnter(const juce::MouseEvent&) +{ + isHovered = true; + animateHover(true); +} + +void HoverAnimationMixin::mouseExit(const juce::MouseEvent&) +{ + isHovered = false; + // Fixed logic to prevent getting stuck in hovered state + animateHover(false); +} + +void HoverAnimationMixin::mouseDown(const juce::MouseEvent&) +{ + animateHover(false); +} + +void HoverAnimationMixin::mouseUp(const juce::MouseEvent& event) +{ + // Only animate hover if the mouse is still within the component bounds + if (getLocalBounds().contains(event.getEventRelativeTo(this).getPosition())) + animateHover(true); +} diff --git a/Source/components/HoverAnimationMixin.h b/Source/components/HoverAnimationMixin.h new file mode 100644 index 0000000..65c0b07 --- /dev/null +++ b/Source/components/HoverAnimationMixin.h @@ -0,0 +1,42 @@ +#pragma once +#include + +// Base Component providing animated hover behavior via JUCE mouse overrides. +class HoverAnimationMixin : public juce::Component +{ +public: + HoverAnimationMixin(); + ~HoverAnimationMixin() override = default; + + // Animation control (available for programmatic triggers if needed) + void animateHover(bool isHovering); + + // Getters + float getAnimationProgress() const { return animationProgress; } + bool getIsHovered() const { return isHovered; } + +protected: + // 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: + float animationProgress = 0.0f; + bool isHovered = false; + + // Animation components + juce::VBlankAnimatorUpdater animatorUpdater; + juce::Animator hoverAnimator; + juce::Animator unhoverAnimator; + + void setupAnimators(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(HoverAnimationMixin) +}; diff --git a/Source/components/MainMenuBarModel.cpp b/Source/components/MainMenuBarModel.cpp index 2dff617..e07787b 100644 --- a/Source/components/MainMenuBarModel.cpp +++ b/Source/components/MainMenuBarModel.cpp @@ -6,12 +6,17 @@ MainMenuBarModel::~MainMenuBarModel() {} void MainMenuBarModel::addTopLevelMenu(const juce::String& name) { topLevelMenuNames.add(name); - menuItems.push_back(std::vector>>()); + menuItems.push_back({}); menuItemsChanged(); } void MainMenuBarModel::addMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action) { - menuItems[topLevelMenuIndex].push_back(std::make_pair(name, action)); + menuItems[topLevelMenuIndex].push_back({ name, std::move(action), {}, false }); + menuItemsChanged(); +} + +void MainMenuBarModel::addToggleMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action, std::function isTicked) { + menuItems[topLevelMenuIndex].push_back({ name, std::move(action), std::move(isTicked), true }); menuItemsChanged(); } @@ -26,8 +31,13 @@ juce::PopupMenu MainMenuBarModel::getMenuForIndex(int topLevelMenuIndex, const j customMenuLogic(menu, topLevelMenuIndex); } - for (int i = 0; i < menuItems[topLevelMenuIndex].size(); i++) { - menu.addItem(i + 1, menuItems[topLevelMenuIndex][i].first); + for (int i = 0; i < (int) menuItems[topLevelMenuIndex].size(); i++) { + auto& mi = menuItems[topLevelMenuIndex][i]; + juce::PopupMenu::Item item(mi.name); + item.itemID = i + 1; + if (mi.hasTick && mi.isTicked) + item.setTicked(mi.isTicked()); + menu.addItem(item); } return menu; @@ -37,7 +47,9 @@ void MainMenuBarModel::menuItemSelected(int menuItemID, int topLevelMenuIndex) { if (customMenuSelectedLogic && customMenuSelectedLogic(menuItemID, topLevelMenuIndex)) { return; } - menuItems[topLevelMenuIndex][menuItemID - 1].second(); + auto& mi = menuItems[topLevelMenuIndex][menuItemID - 1]; + if (mi.action) + mi.action(); } void MainMenuBarModel::menuBarActivated(bool isActive) {} diff --git a/Source/components/MainMenuBarModel.h b/Source/components/MainMenuBarModel.h index 5e80fd8..ea9f993 100644 --- a/Source/components/MainMenuBarModel.h +++ b/Source/components/MainMenuBarModel.h @@ -8,6 +8,8 @@ public: void addTopLevelMenu(const juce::String& name); void addMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action); + // Adds a toggle (ticked) menu item whose tick state is provided dynamically via isTicked() + void addToggleMenuItem(int topLevelMenuIndex, const juce::String& name, std::function action, std::function isTicked); void resetMenuItems(); std::function customMenuLogic; @@ -19,6 +21,14 @@ private: void menuItemSelected(int menuItemID, int topLevelMenuIndex) override; void menuBarActivated(bool isActive); + struct MenuItem + { + juce::String name; + std::function action; + std::function isTicked; // optional tick state + bool hasTick = false; + }; + juce::StringArray topLevelMenuNames; - std::vector>>> menuItems; + std::vector> menuItems; }; diff --git a/Source/components/OsciMainMenuBarModel.cpp b/Source/components/OsciMainMenuBarModel.cpp index c81bf7f..16e2974 100644 --- a/Source/components/OsciMainMenuBarModel.cpp +++ b/Source/components/OsciMainMenuBarModel.cpp @@ -10,12 +10,13 @@ OsciMainMenuBarModel::OsciMainMenuBarModel(OscirenderAudioProcessor& p, Oscirend void OsciMainMenuBarModel::resetMenuItems() { MainMenuBarModel::resetMenuItems(); - addTopLevelMenu("File"); - addTopLevelMenu("About"); - addTopLevelMenu("Video"); + addTopLevelMenu("File"); // index 0 + addTopLevelMenu("About"); // index 1 + addTopLevelMenu("Video"); // index 2 if (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) { - addTopLevelMenu("Audio"); + addTopLevelMenu("Audio"); // index 3 (only if standalone) } + addTopLevelMenu("Interface"); // index 3 (if not standalone) or 4 (if standalone) addMenuItem(0, "Open Project", [this] { editor.openProject(); }); addMenuItem(0, "Save Project", [this] { editor.saveProject(); }); @@ -60,11 +61,7 @@ void OsciMainMenuBarModel::resetMenuItems() { }); addMenuItem(1, "Randomize Blender Port", [this] { audioProcessor.setObjectServerPort(juce::Random::getSystemRandom().nextInt(juce::Range(51600, 51700))); - }); - addMenuItem(1, audioProcessor.getAcceptsKeys() ? "Disable Special Keys" : "Enable Special Keys", [this] { - audioProcessor.setAcceptsKeys(!audioProcessor.getAcceptsKeys()); - resetMenuItems(); - }); + }); #if !OSCI_PREMIUM addMenuItem(1, "Purchase osci-render premium!", [this] { @@ -92,6 +89,26 @@ void OsciMainMenuBarModel::resetMenuItems() { editor.openAudioSettings(); }); } + + // Interface menu index depends on whether Audio menu exists + int interfaceMenuIndex = (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) ? 4 : 3; + addToggleMenuItem(interfaceMenuIndex, "Preview effect on hover", [this] { + bool current = audioProcessor.getGlobalBoolValue("previewEffectOnHover", true); + bool newValue = ! current; + audioProcessor.setGlobalValue("previewEffectOnHover", newValue); + audioProcessor.saveGlobalSettings(); + if (! newValue) { + juce::SpinLock::ScopedLockType lock(audioProcessor.effectsLock); + audioProcessor.clearPreviewEffect(); + } + resetMenuItems(); // update tick state + }, [this] { return audioProcessor.getGlobalBoolValue("previewEffectOnHover", true); + }); + + addToggleMenuItem(interfaceMenuIndex, "Listen for Special Keys", [this] { + audioProcessor.setAcceptsKeys(! audioProcessor.getAcceptsKeys()); + resetMenuItems(); + }, [this] { return audioProcessor.getAcceptsKeys(); }); } #if (JUCE_MAC || JUCE_WINDOWS) && OSCI_PREMIUM diff --git a/Source/components/ScrollFadeMixin.h b/Source/components/ScrollFadeMixin.h new file mode 100644 index 0000000..4204c83 --- /dev/null +++ b/Source/components/ScrollFadeMixin.h @@ -0,0 +1,196 @@ +#pragma once + +#include +#include "VListBox.h" +#include "../LookAndFeel.h" + +// Overlay component that can render top and/or bottom scroll fades with adaptive strength. +class ScrollFadeOverlay : public juce::Component { +public: + ScrollFadeOverlay() { + setInterceptsMouseClicks(false, false); + setOpaque(false); + } + + void setFadeHeight(int hTopAndBottom) { + fadeHeightTop = fadeHeightBottom = juce::jmax(4, hTopAndBottom); + } + + void setFadeHeights(int topH, int bottomH) { + fadeHeightTop = juce::jmax(4, topH); + fadeHeightBottom = juce::jmax(4, bottomH); + } + + void setSidesEnabled(bool top, bool bottom) { + enableTop = top; + enableBottom = bottom; + } + + // Position the overlay to fully cover the scrollable viewport area (owner coordinates) + void layoutOver(const juce::Rectangle& listBounds) { + setBounds(listBounds); + } + + // Toggle per-side visibility/strength based on viewport scroll and enable flag. + void updateVisibilityFromViewport(juce::Viewport* vp, bool enabled) { + showTop = showBottom = false; + strengthTop = strengthBottom = 0.0f; + + if (enabled && vp != nullptr && vp->getVerticalScrollBar().isVisible()) { + auto& sb = vp->getVerticalScrollBar(); + const double start = sb.getCurrentRangeStart(); + const double size = sb.getCurrentRangeSize(); + const double max = sb.getMaximumRangeLimit(); + + // Top fade strength scales with how far from top we are + const bool atTop = start <= 0.5; + const double topDist = start; // pixels scrolled down + strengthTop = (float) juce::jlimit(0.0, 1.0, topDist / (double) juce::jmax(1, fadeHeightTop)); + showTop = enableTop && !atTop && strengthTop > 0.01f; + + // Bottom fade strength scales with how far from bottom we are + const double remaining = (max - (start + size)); + const bool atBottom = remaining <= 0.5; + strengthBottom = (float) juce::jlimit(0.0, 1.0, remaining / (double) juce::jmax(1, fadeHeightBottom)); + showBottom = enableBottom && !atBottom && strengthBottom > 0.01f; + } + + const bool anyVisible = (showTop || showBottom); + setVisible(anyVisible); + if (anyVisible) + repaint(); + } + + void paint(juce::Graphics& g) override { + auto area = getLocalBounds(); + const auto bg = findColour(groupComponentBackgroundColourId); + + if (showTop && fadeHeightTop > 0) { + const int h = juce::jmin(fadeHeightTop, area.getHeight()); + auto topRect = area.removeFromTop(h); + juce::ColourGradient gradTop(bg.withAlpha(strengthTop), + (float) topRect.getX(), (float) topRect.getY(), + bg.withAlpha(0.0f), + (float) topRect.getX(), (float) topRect.getBottom(), + false); + g.setGradientFill(gradTop); + g.fillRect(topRect); + } + + // Reset area for bottom drawing + area = getLocalBounds(); + if (showBottom && fadeHeightBottom > 0) { + const int h = juce::jmin(fadeHeightBottom, area.getHeight()); + auto bottomRect = area.removeFromBottom(h); + juce::ColourGradient gradBottom(bg.withAlpha(strengthBottom), + (float) bottomRect.getX(), (float) bottomRect.getBottom(), + bg.withAlpha(0.0f), + (float) bottomRect.getX(), (float) bottomRect.getY(), + false); + g.setGradientFill(gradBottom); + g.fillRect(bottomRect); + } + } + +private: + int fadeHeightTop { 48 }; + int fadeHeightBottom { 48 }; + bool enableTop { true }; + bool enableBottom { true }; + bool showTop { false }; + bool showBottom { false }; + float strengthTop { 0.0f }; + float strengthBottom { 0.0f }; +}; + +// Mixin to attach a bottom fade overlay for any scrollable area (ListBox or Viewport). +class ScrollFadeMixin { +public: + virtual ~ScrollFadeMixin() { + detachScrollListeners(); + } + +protected: + void initScrollFade(juce::Component& owner) { + if (! scrollFade) + scrollFade = std::make_unique(); + if (scrollFade->getParentComponent() != &owner) + owner.addAndMakeVisible(*scrollFade); + + scrollListener.owner = this; + } + + void attachToListBox(VListBox& list) { + detachScrollListeners(); + scrollViewport = list.getViewport(); + attachScrollListeners(); + } + + void attachToViewport(juce::Viewport& vp) { + detachScrollListeners(); + scrollViewport = &vp; + attachScrollListeners(); + } + + // Call from owner's resized(). listBounds must be in the owner's coordinate space. + void layoutScrollFade(const juce::Rectangle& listBounds, bool enabled = true, int fadeHeight = 48) { + if (! scrollFade) + return; + lastListBounds = listBounds; + lastEnabled = enabled; + lastFadeHeight = fadeHeight; + scrollFade->setFadeHeight(fadeHeight); + scrollFade->layoutOver(listBounds); + scrollFade->toFront(false); + scrollFade->updateVisibilityFromViewport(getViewport(), enabled); + } + + // Explicitly hide/show (e.g., when switching views) + void setScrollFadeVisible(bool shouldBeVisible) { + lastEnabled = shouldBeVisible; + if (scrollFade) + scrollFade->setVisible(shouldBeVisible); + } + + // Allow configuring which sides to render (default is both true) + void setScrollFadeSides(bool enableTop, bool enableBottom) { + if (scrollFade) { + scrollFade->setSidesEnabled(enableTop, enableBottom); + // Recompute since sides changed + scrollFade->updateVisibilityFromViewport(getViewport(), lastEnabled); + } + } + +protected: + std::unique_ptr scrollFade; + juce::Component::SafePointer scrollViewport; + juce::Viewport* getViewport() const noexcept { return static_cast(scrollViewport.getComponent()); } + +private: + // Listen to vertical scrollbar to update fade visibility while scrolling + struct VScrollListener : juce::ScrollBar::Listener { + ScrollFadeMixin* owner { nullptr }; + void scrollBarMoved(juce::ScrollBar*, double) override { + if (owner && owner->scrollFade) { + // Recompute visibility using last-known enabled state + owner->scrollFade->updateVisibilityFromViewport(owner->getViewport(), owner->lastEnabled); + } + } + } scrollListener; + + void attachScrollListeners() { + if (auto* vp = getViewport()) { + vp->getVerticalScrollBar().addListener(&scrollListener); + } + } + + void detachScrollListeners() { + if (auto* vp = getViewport()) { + vp->getVerticalScrollBar().removeListener(&scrollListener); + } + } + + juce::Rectangle lastListBounds; + bool lastEnabled { true }; + int lastFadeHeight { 48 }; +}; diff --git a/Source/components/SosciMainMenuBarModel.cpp b/Source/components/SosciMainMenuBarModel.cpp index 7e7d81f..324f8dd 100644 --- a/Source/components/SosciMainMenuBarModel.cpp +++ b/Source/components/SosciMainMenuBarModel.cpp @@ -96,10 +96,6 @@ void SosciMainMenuBarModel::resetMenuItems() { juce::DialogWindow* dw = options.launchAsync(); }); - addMenuItem(1, processor.getAcceptsKeys() ? "Disable Special Keys" : "Enable Special Keys", [this] { - processor.setAcceptsKeys(!processor.getAcceptsKeys()); - resetMenuItems(); - }); addMenuItem(2, "Settings...", [this] { editor.openRecordingSettings(); @@ -113,4 +109,11 @@ void SosciMainMenuBarModel::resetMenuItems() { if (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) { addMenuItem(3, "Settings...", [&]() { editor.openAudioSettings(); }); } + + // Interface menu index depends on whether Audio menu exists + int interfaceMenuIndex = (editor.processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone) ? 4 : 3; + addToggleMenuItem(interfaceMenuIndex, "Listen for Special Keys", [this] { + processor.setAcceptsKeys(! processor.getAcceptsKeys()); + resetMenuItems(); + }, [this] { return processor.getAcceptsKeys(); }); } diff --git a/Source/components/SosciMainMenuBarModel.h b/Source/components/SosciMainMenuBarModel.h index 4c9c90b..88b1f47 100644 --- a/Source/components/SosciMainMenuBarModel.h +++ b/Source/components/SosciMainMenuBarModel.h @@ -9,6 +9,7 @@ class SosciAudioProcessor; class SosciMainMenuBarModel : public MainMenuBarModel { public: SosciMainMenuBarModel(SosciPluginEditor& editor, SosciAudioProcessor& processor); + void resetMenuItems(); SosciPluginEditor& editor; diff --git a/Source/components/SvgButton.h b/Source/components/SvgButton.h index b08e605..a6fd293 100644 --- a/Source/components/SvgButton.h +++ b/Source/components/SvgButton.h @@ -8,12 +8,12 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame changeSvgColour(doc.get(), colour); normalImage = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colour.withBrightness(0.7f)); - overImage = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colour.withBrightness(0.5f)); - downImage = juce::Drawable::createFromSVG(*doc); - changeSvgColour(doc.get(), colour.withBrightness(0.3f)); - disabledImage = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colour.withBrightness(0.7f)); + overImage = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colour.withBrightness(0.5f)); + downImage = juce::Drawable::createFromSVG(*doc); + changeSvgColour(doc.get(), colour.withBrightness(0.3f)); + disabledImage = juce::Drawable::createFromSVG(*doc); // If a toggled SVG is provided, use it for the "on" state images if (toggledSvg.isNotEmpty()) { @@ -44,7 +44,8 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame if (colour != colourOn) { setClickingTogglesState(true); } - setImages(normalImage.get(), overImage.get(), downImage.get(), disabledImage.get(), normalImageOn.get(), overImageOn.get(), downImageOn.get(), disabledImageOn.get()); + + setImages(normalImage.get(), overImage.get(), downImage.get(), disabledImage.get(), normalImageOn.get(), overImageOn.get(), downImageOn.get(), disabledImageOn.get()); if (toggle != nullptr) { toggle->addListener(this); @@ -94,8 +95,7 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame juce::DrawableButton::resized(); if (pulseUsed) { resizedPath = basePath; - // scale path to fit image - resizedPath.applyTransform(resizedPath.getTransformToScaleToFit(getImageBounds(), true)); + resizedPath.applyTransform(getImageTransform()); repaint(); } } @@ -122,13 +122,13 @@ class SvgButton : public juce::DrawableButton, public juce::AudioProcessorParame private: std::unique_ptr normalImage; - std::unique_ptr overImage; - std::unique_ptr downImage; - std::unique_ptr disabledImage; + std::unique_ptr overImage; + std::unique_ptr downImage; + std::unique_ptr disabledImage; std::unique_ptr normalImageOn; - std::unique_ptr overImageOn; - std::unique_ptr downImageOn; + std::unique_ptr overImageOn; + std::unique_ptr downImageOn; std::unique_ptr disabledImageOn; osci::BooleanParameter* toggle; @@ -139,6 +139,7 @@ private: bool prevToggleState = false; juce::Path basePath; juce::Path resizedPath; + juce::AffineTransform imageTransform; // Transform applied to all state images juce::Animator pulse = juce::ValueAnimatorBuilder {} .withEasing([] (float t) { return std::sin(3.14159 * t) / 2 + 0.5; }) .withDurationMs(500) @@ -154,4 +155,38 @@ private: xmlnode->setAttribute("fill", '#' + colour.toDisplayString(false)); } } + +public: + // Allows callers to adjust the placement/scale/rotation of the SVG within the button. + void setImageTransform(const juce::AffineTransform& t) { + imageTransform = juce::RectanglePlacement(juce::RectanglePlacement::centred).getTransformToFit(normalImage->getDrawableBounds(), getImageBounds()).followedBy(t); + if (getLocalBounds().isEmpty()) { + return; + } + setButtonStyle(juce::DrawableButton::ButtonStyle::ImageRaw); + applyImageTransform(); + // Keep the pulse overlay in sync + resized(); + } + + juce::AffineTransform getImageTransform() const { return imageTransform; } + +private: + void applyImageTransform() { + auto apply = [this](std::unique_ptr& d) { + if (d != nullptr) { + d->setTransform(imageTransform); + } + }; + apply(normalImage); + apply(overImage); + apply(downImage); + apply(disabledImage); + apply(normalImageOn); + apply(overImageOn); + apply(downImageOn); + apply(disabledImageOn); + + setImages(normalImage.get(), overImage.get(), downImage.get(), disabledImage.get(), normalImageOn.get(), overImageOn.get(), downImageOn.get(), disabledImageOn.get()); + } }; diff --git a/Source/components/VListBox.cpp b/Source/components/VListBox.cpp index b783d13..2c5294b 100644 --- a/Source/components/VListBox.cpp +++ b/Source/components/VListBox.cpp @@ -25,16 +25,12 @@ #include "VListBox.h" -namespace juce -{ -namespace jc -{ -class ListBox::RowComponent : public Component, public TooltipClient +class VListBox::RowComponent : public juce::Component, public TooltipClient { public: - RowComponent (ListBox& lb) : owner (lb) {} + RowComponent (VListBox& lb) : owner (lb) {} - void paint (Graphics& g) override + void paint (juce::Graphics& g) override { if (auto* m = owner.getModel()) m->paintListBoxItem (row, g, getWidth(), getHeight(), selected); @@ -63,7 +59,7 @@ public: } } - void performSelection (const MouseEvent& e, bool isMouseUp) + void performSelection (const juce::MouseEvent& e, bool isMouseUp) { owner.selectRowsBasedOnModifierKeys (row, e.mods, isMouseUp); @@ -79,7 +75,7 @@ public: return false; } - void mouseDown (const MouseEvent& e) override + void mouseDown (const juce::MouseEvent& e) override { isDragging = false; isDraggingToScroll = false; @@ -94,31 +90,31 @@ public: } } - void mouseUp (const MouseEvent& e) override + void mouseUp (const juce::MouseEvent& e) override { if (isEnabled() && selectRowOnMouseUp && ! (isDragging || isDraggingToScroll)) performSelection (e, true); } - void mouseDoubleClick (const MouseEvent& e) override + void mouseDoubleClick (const juce::MouseEvent& e) override { if (isEnabled()) if (auto* m = owner.getModel()) m->listBoxItemDoubleClicked (row, e); } - void mouseDrag (const MouseEvent& e) override + void mouseDrag (const juce::MouseEvent& e) override { if (auto* m = owner.getModel()) { if (isEnabled() && e.mouseWasDraggedSinceMouseDown() && ! isDragging) { - SparseSet rowsToDrag; + juce::SparseSet rowsToDrag; if (owner.selectOnMouseDown || owner.isRowSelected (row)) rowsToDrag = owner.getSelectedRows(); else - rowsToDrag.addRange (Range::withStartAndLength (row, 1)); + rowsToDrag.addRange (juce::Range::withStartAndLength (row, 1)); if (rowsToDrag.size() > 0) { @@ -144,7 +140,7 @@ public: customComponent->setBounds (getLocalBounds()); } - String getTooltip() override + juce::String getTooltip() override { if (auto* m = owner.getModel()) return m->getTooltipForRow (row); @@ -152,8 +148,8 @@ public: return {}; } - ListBox& owner; - std::unique_ptr customComponent; + VListBox& owner; + std::unique_ptr customComponent; int row = -1; bool selected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; @@ -161,21 +157,21 @@ public: }; //============================================================================== -class ListBox::ListViewport : public Viewport +class VListBox::ListViewport : public juce::Viewport { public: - ListViewport (ListBox& lb) : owner (lb) + ListViewport (VListBox& lb) : owner (lb) { setWantsKeyboardFocus (false); - auto content = new Component(); + auto content = new juce::Component(); setViewedComponent (content); content->setWantsKeyboardFocus (false); updateAllRows(); } - RowComponent* getComponentForRow (const int row) const noexcept { return rows[row % jmax (1, rows.size())]; } + RowComponent* getComponentForRow (const int row) const noexcept { return rows[row % juce::jmax (1, rows.size())]; } RowComponent* getComponentForRowIfOnscreen (const int row) const noexcept { @@ -204,19 +200,19 @@ public: } } - int getRowNumberOfComponent (Component* const rowComponent) const noexcept + int getRowNumberOfComponent (juce::Component* const rowComponent) const noexcept { const int index = getViewedComponent()->getIndexOfChildComponent (rowComponent); const int num = rows.size(); for (int i = num; --i >= 0;) - if (((firstIndex + i) % jmax (1, num)) == index) + if (((firstIndex + i) % juce::jmax (1, num)) == index) return firstIndex + i; return -1; } - void visibleAreaChanged (const Rectangle&) override + void visibleAreaChanged (const juce::Rectangle&) override { updateVisibleArea (true); @@ -231,7 +227,7 @@ public: auto& content = *getViewedComponent(); auto newX = content.getX(); auto newY = content.getY(); - auto newW = jmax (owner.minimumRowWidth, getMaximumVisibleWidth()); + auto newW = juce::jmax (owner.minimumRowWidth, getMaximumVisibleWidth()); auto newH = owner.getContentHeight(); if (newY + newH < getMaximumVisibleHeight() && newH > getMaximumVisibleHeight()) @@ -262,7 +258,7 @@ public: ++firstWholeIndex; } - auto lastRow = jmin (owner.getRowForPosition(y + getMaximumVisibleHeight()), owner.totalItems - 1); + auto lastRow = juce::jmin (owner.getRowForPosition(y + getMaximumVisibleHeight()), owner.totalItems - 1); lastWholeIndex = lastRow - 1; @@ -280,7 +276,7 @@ public: } if (owner.headerComponent != nullptr) { - auto width = jmax(owner.getWidth() - owner.outlineThickness * 2, content.getWidth()); + auto width = juce::jmax(owner.getWidth() - owner.outlineThickness * 2, content.getWidth()); owner.headerComponent->setBounds(owner.outlineThickness + content.getX(), owner.outlineThickness, width, owner.headerComponent->getHeight()); } } @@ -301,14 +297,14 @@ public: { // put row at the top of the screen, or as close as we can make it. but this position is already constrained by // setViewPosition's internals. - // auto y = jlimit (0, jmax (0, totalRows - rowsOnScreen), row) * rowH; + // auto y = juce::jlimit (0, juce::jmax (0, totalRows - rowsOnScreen), row) * rowH; setViewPosition (getViewPositionX(), owner.getPositionForRow (row)); } else { auto p = owner.getPositionForRow (row); auto rh = owner.getRowHeight (row); - setViewPosition (getViewPositionX(), jmax (0, p + rh - getMaximumVisibleHeight())); + setViewPosition (getViewPositionX(), juce::jmax (0, p + rh - getMaximumVisibleHeight())); } } @@ -326,21 +322,21 @@ public: { auto p = owner.getPositionForRow (row); auto rh = owner.getRowHeight (row); - setViewPosition (getViewPositionX(), jmax (0, p + rh - getMaximumVisibleHeight())); + setViewPosition (getViewPositionX(), juce::jmax (0, p + rh - getMaximumVisibleHeight())); } } - void paint (Graphics& g) override + void paint (juce::Graphics& g) override { if (isOpaque()) - g.fillAll (owner.findColour (ListBox::backgroundColourId)); + g.fillAll (owner.findColour (VListBox::backgroundColourId)); } - bool keyPressed (const KeyPress& key) override + bool keyPressed (const juce::KeyPress& key) override { - if (Viewport::respondsToKey (key)) + if (juce::Viewport::respondsToKey (key)) { - const int allowableMods = owner.multipleSelection ? ModifierKeys::shiftModifier : 0; + const int allowableMods = owner.multipleSelection ? juce::ModifierKeys::shiftModifier : 0; if ((key.getModifiers().getRawFlags() & ~allowableMods) == 0) { @@ -350,12 +346,12 @@ public: } } - return Viewport::keyPressed (key); + return juce::Viewport::keyPressed (key); } private: - ListBox& owner; - OwnedArray rows; + VListBox& owner; + juce::OwnedArray rows; int firstIndex = 0, firstWholeIndex = 0, lastWholeIndex = 0; bool hasUpdated = false; @@ -363,25 +359,25 @@ private: }; //============================================================================== -struct ListBoxMouseMoveSelector : public MouseListener +struct ListBoxMouseMoveSelector : public juce::MouseListener { - ListBoxMouseMoveSelector (ListBox& lb) : owner (lb) { owner.addMouseListener (this, true); } + ListBoxMouseMoveSelector (VListBox& lb) : owner (lb) { owner.addMouseListener (this, true); } ~ListBoxMouseMoveSelector() override { owner.removeMouseListener (this); } - void mouseMove (const MouseEvent& e) override + void mouseMove (const juce::MouseEvent& e) override { auto pos = e.getEventRelativeTo (&owner).position.toInt(); owner.selectRow (owner.getRowContainingPosition (pos.x, pos.y), true); } - void mouseExit (const MouseEvent& e) override { mouseMove (e); } + void mouseExit (const juce::MouseEvent& e) override { mouseMove (e); } - ListBox& owner; + VListBox& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListBoxMouseMoveSelector) }; -int ListBoxModel::getRowForPosition (int yPos) +int VListBoxModel::getRowForPosition (int yPos) { if (! hasVariableHeightRows()) return yPos / getRowHeight (0); @@ -397,10 +393,10 @@ int ListBoxModel::getRowForPosition (int yPos) return r; } - return numRows; + return numRows - 1; } -int ListBoxModel::getPositionForRow (int rowNumber) +int VListBoxModel::getPositionForRow (int rowNumber) { if (! hasVariableHeightRows()) return rowNumber * getRowHeight (0); @@ -412,22 +408,22 @@ int ListBoxModel::getPositionForRow (int rowNumber) return y; } //============================================================================== -ListBox::ListBox (const String& name, ListBoxModel* const m) : Component (name), model (m) +VListBox::VListBox (const juce::String& name, VListBoxModel* const m) : juce::Component (name), model (m) { viewport.reset (new ListViewport (*this)); addAndMakeVisible (viewport.get()); - ListBox::setWantsKeyboardFocus (true); - ListBox::colourChanged(); + VListBox::setWantsKeyboardFocus (true); + VListBox::colourChanged(); } -ListBox::~ListBox() +VListBox::~VListBox() { headerComponent.reset(); viewport.reset(); } -void ListBox::setModel (ListBoxModel* const newModel) +void VListBox::setModel (VListBoxModel* const newModel) { if (model != newModel) { @@ -437,20 +433,20 @@ void ListBox::setModel (ListBoxModel* const newModel) } } -void ListBox::setMultipleSelectionEnabled (bool b) noexcept +void VListBox::setMultipleSelectionEnabled (bool b) noexcept { multipleSelection = b; } -void ListBox::setClickingTogglesRowSelection (bool b) noexcept +void VListBox::setClickingTogglesRowSelection (bool b) noexcept { alwaysFlipSelection = b; } -void ListBox::setRowSelectedOnMouseDown (bool b) noexcept +void VListBox::setRowSelectedOnMouseDown (bool b) noexcept { selectOnMouseDown = b; } -void ListBox::setMouseMoveSelectsRows (bool b) +void VListBox::setMouseMoveSelectsRows (bool b) { if (b) { @@ -464,7 +460,7 @@ void ListBox::setMouseMoveSelectsRows (bool b) } //============================================================================== -void ListBox::paint (Graphics& g) +void VListBox::paint (juce::Graphics& g) { if (! hasDoneInitialUpdate) updateContent(); @@ -472,7 +468,7 @@ void ListBox::paint (Graphics& g) g.fillAll (findColour (backgroundColourId)); } -void ListBox::paintOverChildren (Graphics& g) +void VListBox::paintOverChildren (juce::Graphics& g) { if (outlineThickness > 0) { @@ -481,9 +477,9 @@ void ListBox::paintOverChildren (Graphics& g) } } -void ListBox::resized() +void VListBox::resized() { - viewport->setBoundsInset (BorderSize (outlineThickness + (headerComponent != nullptr ? headerComponent->getHeight() : 0), + viewport->setBoundsInset (juce::BorderSize (outlineThickness + (headerComponent != nullptr ? headerComponent->getHeight() : 0), outlineThickness, outlineThickness, outlineThickness)); @@ -493,18 +489,18 @@ void ListBox::resized() viewport->updateVisibleArea (false); } -void ListBox::visibilityChanged() +void VListBox::visibilityChanged() { viewport->updateVisibleArea (true); } -Viewport* ListBox::getViewport() const noexcept +juce::Viewport* VListBox::getViewport() const noexcept { return viewport.get(); } //============================================================================== -void ListBox::updateContent() { +void VListBox::updateContent() { hasDoneInitialUpdate = true; totalItems = (model != nullptr) ? model->getNumRows() : 0; @@ -526,19 +522,19 @@ void ListBox::updateContent() { } //============================================================================== -void ListBox::selectRow (int row, bool dontScroll, bool deselectOthersFirst) +void VListBox::selectRow (int row, bool dontScroll, bool deselectOthersFirst) { selectRowInternal (row, dontScroll, deselectOthersFirst, false); } -void ListBox::selectRowInternal (const int row, bool dontScroll, bool deselectOthersFirst, bool isMouseClick) +void VListBox::selectRowInternal (const int row, bool dontScroll, bool deselectOthersFirst, bool isMouseClick) { if (! multipleSelection) deselectOthersFirst = true; if ((! isRowSelected (row)) || (deselectOthersFirst && getNumSelectedRows() > 1)) { - if (isPositiveAndBelow (row, totalItems)) + if (juce::isPositiveAndBelow (row, totalItems)) { if (deselectOthersFirst) selected.clear(); @@ -561,7 +557,7 @@ void ListBox::selectRowInternal (const int row, bool dontScroll, bool deselectOt } } -void ListBox::deselectRow (const int row) +void VListBox::deselectRow (const int row) { if (selected.contains (row)) { @@ -575,7 +571,7 @@ void ListBox::deselectRow (const int row) } } -void ListBox::setSelectedRows (const SparseSet& setOfRowsToBeSelected, const NotificationType sendNotificationEventToModel) +void VListBox::setSelectedRows (const juce::SparseSet& setOfRowsToBeSelected, const juce::NotificationType sendNotificationEventToModel) { selected = setOfRowsToBeSelected; selected.removeRange ({ totalItems, std::numeric_limits::max() }); @@ -585,24 +581,24 @@ void ListBox::setSelectedRows (const SparseSet& setOfRowsToBeSelected, cons viewport->updateContents(); - if (model != nullptr && sendNotificationEventToModel == sendNotification) + if (model != nullptr && sendNotificationEventToModel == juce::sendNotification) model->selectedRowsChanged (lastRowSelected); } -SparseSet ListBox::getSelectedRows() const +juce::SparseSet VListBox::getSelectedRows() const { return selected; } -void ListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToShowThisRange) +void VListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToShowThisRange) { if (multipleSelection && (firstRow != lastRow)) { const int numRows = totalItems - 1; - firstRow = jlimit (0, jmax (0, numRows), firstRow); - lastRow = jlimit (0, jmax (0, numRows), lastRow); + firstRow = juce::jlimit (0, juce::jmax (0, numRows), firstRow); + lastRow = juce::jlimit (0, juce::jmax (0, numRows), lastRow); - selected.addRange ({ jmin (firstRow, lastRow), jmax (firstRow, lastRow) + 1 }); + selected.addRange ({ juce::jmin (firstRow, lastRow), juce::jmax (firstRow, lastRow) + 1 }); selected.removeRange ({ lastRow, lastRow + 1 }); } @@ -610,7 +606,7 @@ void ListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToSho selectRowInternal (lastRow, dontScrollToShowThisRange, false, true); } -void ListBox::flipRowSelection (const int row) +void VListBox::flipRowSelection (const int row) { if (isRowSelected (row)) deselectRow (row); @@ -618,7 +614,7 @@ void ListBox::flipRowSelection (const int row) selectRowInternal (row, false, false, true); } -void ListBox::deselectAllRows() +void VListBox::deselectAllRows() { if (! selected.isEmpty()) { @@ -632,7 +628,7 @@ void ListBox::deselectAllRows() } } -void ListBox::selectRowsBasedOnModifierKeys (const int row, ModifierKeys mods, const bool isMouseUpEvent) +void VListBox::selectRowsBasedOnModifierKeys (const int row, juce::ModifierKeys mods, const bool isMouseUpEvent) { if (multipleSelection && (mods.isCommandDown() || alwaysFlipSelection)) { @@ -648,41 +644,41 @@ void ListBox::selectRowsBasedOnModifierKeys (const int row, ModifierKeys mods, c } } -int ListBox::getNumSelectedRows() const +int VListBox::getNumSelectedRows() const { return selected.size(); } -int ListBox::getSelectedRow (const int index) const +int VListBox::getSelectedRow (const int index) const { - return (isPositiveAndBelow (index, selected.size())) ? selected[index] : -1; + return (juce::isPositiveAndBelow (index, selected.size())) ? selected[index] : -1; } -bool ListBox::isRowSelected (const int row) const +bool VListBox::isRowSelected (const int row) const { return selected.contains (row); } -int ListBox::getLastRowSelected() const +int VListBox::getLastRowSelected() const { return isRowSelected (lastRowSelected) ? lastRowSelected : -1; } //============================================================================== -int ListBox::getRowContainingPosition (const int x, const int y) const noexcept +int VListBox::getRowContainingPosition (const int x, const int y) const noexcept { - if (isPositiveAndBelow (x, getWidth())) + if (juce::isPositiveAndBelow (x, getWidth())) { const int row = getRowForPosition (viewport->getViewPositionY() + y - viewport->getY()); - if (isPositiveAndBelow (row, totalItems)) + if (juce::isPositiveAndBelow (row, totalItems)) return row; } return -1; } -int ListBox::getRowHeight (int row) const +int VListBox::getRowHeight (int row) const { if (model == nullptr) return 0; @@ -690,9 +686,9 @@ int ListBox::getRowHeight (int row) const return model->getRowHeight (row); } -int ListBox::getInsertionIndexForPosition (const int x, const int y) const noexcept +int VListBox::getInsertionIndexForPosition (const int x, const int y) const noexcept { - if (isPositiveAndBelow (x, getWidth())) + if (juce::isPositiveAndBelow (x, getWidth())) { auto cursorY = y + viewport->getViewPositionY() - viewport->getY(); auto row = getRowForPosition (cursorY); @@ -702,13 +698,13 @@ int ListBox::getInsertionIndexForPosition (const int x, const int y) const noexc if (rowCentre < cursorY) ++row; - return jlimit (0, totalItems, row); + return juce::jlimit (0, totalItems, row); } return -1; } -Component* ListBox::getComponentForRowNumber (const int row) const noexcept +juce::Component* VListBox::getComponentForRowNumber (const int row) const noexcept { if (auto* listRowComp = viewport->getComponentForRowIfOnscreen (row)) return listRowComp->customComponent.get(); @@ -716,12 +712,12 @@ Component* ListBox::getComponentForRowNumber (const int row) const noexcept return nullptr; } -int ListBox::getRowNumberOfComponent (Component* const rowComponent) const noexcept +int VListBox::getRowNumberOfComponent (juce::Component* const rowComponent) const noexcept { return viewport->getRowNumberOfComponent (rowComponent); } -Rectangle ListBox::getRowPosition (int rowNumber, bool relativeToComponentTopLeft) const noexcept +juce::Rectangle VListBox::getRowPosition (int rowNumber, bool relativeToComponentTopLeft) const noexcept { auto y = viewport->getY() + getPositionForRow (rowNumber); @@ -731,52 +727,52 @@ Rectangle ListBox::getRowPosition (int rowNumber, bool relativeToComponentT return { viewport->getX(), y, viewport->getViewedComponent()->getWidth(), getRowHeight (rowNumber) }; } -void ListBox::setVerticalPosition (const double proportion) +void VListBox::setVerticalPosition (const double proportion) { auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); - viewport->setViewPosition (viewport->getViewPositionX(), jmax (0, roundToInt (proportion * offscreen))); + viewport->setViewPosition (viewport->getViewPositionX(), juce::jmax (0, juce::roundToInt (proportion * offscreen))); } -double ListBox::getVerticalPosition() const +double VListBox::getVerticalPosition() const { auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); return offscreen > 0 ? viewport->getViewPositionY() / (double) offscreen : 0; } -int ListBox::getVisibleRowWidth() const noexcept +int VListBox::getVisibleRowWidth() const noexcept { return viewport->getViewWidth(); } -void ListBox::scrollToEnsureRowIsOnscreen (const int row) +void VListBox::scrollToEnsureRowIsOnscreen (const int row) { viewport->scrollToEnsureRowIsOnscreen (row); } //============================================================================== -bool ListBox::keyPressed (const KeyPress& key) +bool VListBox::keyPressed (const juce::KeyPress& key) { const bool multiple = multipleSelection && lastRowSelected >= 0 && key.getModifiers().isShiftDown(); - if (key.isKeyCode (KeyPress::upKey)) + if (key.isKeyCode (juce::KeyPress::upKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected - 1); else - selectRow (jmax (0, lastRowSelected - 1)); + selectRow (juce::jmax (0, lastRowSelected - 1)); } - else if (key.isKeyCode (KeyPress::downKey)) + else if (key.isKeyCode (juce::KeyPress::downKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected + 1); else - selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected + 1))); + selectRow (juce::jmin (totalItems - 1, juce::jmax (0, lastRowSelected + 1))); } - else if (key.isKeyCode (KeyPress::pageUpKey)) + else if (key.isKeyCode (juce::KeyPress::pageUpKey)) { - auto rowToSelect = jmax(0, lastRowSelected); + auto rowToSelect = juce::jmax(0, lastRowSelected); auto pageHeight = viewport->getMaximumVisibleHeight(); while (pageHeight > 0 && rowToSelect > 0) @@ -792,9 +788,9 @@ bool ListBox::keyPressed (const KeyPress& key) else selectRow (rowToSelect); } - else if (key.isKeyCode (KeyPress::pageDownKey)) + else if (key.isKeyCode (juce::KeyPress::pageDownKey)) { - auto rowToSelect = jmax(0, lastRowSelected); + auto rowToSelect = juce::jmax(0, lastRowSelected); auto pageHeight= viewport->getMaximumVisibleHeight(); while (pageHeight > 0 && rowToSelect < totalItems - 1) @@ -810,31 +806,31 @@ bool ListBox::keyPressed (const KeyPress& key) else selectRow (rowToSelect); } - else if (key.isKeyCode (KeyPress::homeKey)) + else if (key.isKeyCode (juce::KeyPress::homeKey)) { if (multiple) selectRangeOfRows (lastRowSelected, 0); else selectRow (0); } - else if (key.isKeyCode (KeyPress::endKey)) + else if (key.isKeyCode (juce::KeyPress::endKey)) { if (multiple) selectRangeOfRows (lastRowSelected, totalItems - 1); else selectRow (totalItems - 1); } - else if (key.isKeyCode (KeyPress::returnKey) && isRowSelected (lastRowSelected)) + else if (key.isKeyCode (juce::KeyPress::returnKey) && isRowSelected (lastRowSelected)) { if (model != nullptr) model->returnKeyPressed (lastRowSelected); } - else if ((key.isKeyCode (KeyPress::deleteKey) || key.isKeyCode (KeyPress::backspaceKey)) && isRowSelected (lastRowSelected)) + else if ((key.isKeyCode (juce::KeyPress::deleteKey) || key.isKeyCode (juce::KeyPress::backspaceKey)) && isRowSelected (lastRowSelected)) { if (model != nullptr) model->deleteKeyPressed (lastRowSelected); } - else if (multipleSelection && key == KeyPress ('a', ModifierKeys::commandModifier, 0)) + else if (multipleSelection && key == juce::KeyPress ('a', juce::ModifierKeys::commandModifier, 0)) { selectRangeOfRows (0, std::numeric_limits::max()); } @@ -846,16 +842,16 @@ bool ListBox::keyPressed (const KeyPress& key) return true; } -bool ListBox::keyStateChanged (const bool isKeyDown) +bool VListBox::keyStateChanged (const bool isKeyDown) { return isKeyDown - && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageUpKey) - || KeyPress::isKeyCurrentlyDown (KeyPress::downKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageDownKey) - || KeyPress::isKeyCurrentlyDown (KeyPress::homeKey) || KeyPress::isKeyCurrentlyDown (KeyPress::endKey) - || KeyPress::isKeyCurrentlyDown (KeyPress::returnKey)); + && (juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::upKey) || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::pageUpKey) + || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::downKey) || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::pageDownKey) + || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::homeKey) || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::endKey) + || juce::KeyPress::isKeyCurrentlyDown (juce::KeyPress::returnKey)); } -void ListBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) +void VListBox::mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) { bool eventWasUsed = false; @@ -872,10 +868,10 @@ void ListBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& whee } if (! eventWasUsed) - Component::mouseWheelMove (e, wheel); + juce::Component::mouseWheelMove (e, wheel); } -void ListBox::mouseUp (const MouseEvent& e) +void VListBox::mouseUp (const juce::MouseEvent& e) { if (e.mouseWasClicked() && model != nullptr) model->backgroundClicked (e); @@ -883,7 +879,7 @@ void ListBox::mouseUp (const MouseEvent& e) //============================================================================== -int ListBox::getNumRowsOnScreen() const noexcept +int VListBox::getNumRowsOnScreen() const noexcept { // todo: not clear this function can work as previously intended @@ -897,59 +893,59 @@ int ListBox::getNumRowsOnScreen() const noexcept return rowNumber - start; } -void ListBox::setMinimumContentWidth (const int newMinimumWidth) +void VListBox::setMinimumContentWidth (const int newMinimumWidth) { minimumRowWidth = newMinimumWidth; updateContent(); } -int ListBox::getVisibleContentWidth() const noexcept +int VListBox::getVisibleContentWidth() const noexcept { return viewport->getMaximumVisibleWidth(); } -ScrollBar& ListBox::getVerticalScrollBar() const noexcept +juce::ScrollBar& VListBox::getVerticalScrollBar() const noexcept { return viewport->getVerticalScrollBar(); } -ScrollBar& ListBox::getHorizontalScrollBar() const noexcept +juce::ScrollBar& VListBox::getHorizontalScrollBar() const noexcept { return viewport->getHorizontalScrollBar(); } -void ListBox::colourChanged() +void VListBox::colourChanged() { setOpaque (findColour (backgroundColourId).isOpaque()); viewport->setOpaque (isOpaque()); repaint(); } -void ListBox::parentHierarchyChanged() +void VListBox::parentHierarchyChanged() { colourChanged(); } -void ListBox::setOutlineThickness (int newThickness) +void VListBox::setOutlineThickness (int newThickness) { outlineThickness = newThickness; resized(); } -void ListBox::setHeaderComponent (std::unique_ptr newHeaderComponent) +void VListBox::setHeaderComponent (std::unique_ptr newHeaderComponent) { headerComponent = std::move (newHeaderComponent); addAndMakeVisible (headerComponent.get()); - ListBox::resized(); + VListBox::resized(); } -void ListBox::repaintRow (const int rowNumber) noexcept +void VListBox::repaintRow (const int rowNumber) noexcept { repaint (getRowPosition (rowNumber, true)); } -Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, int& imageY) +juce::Image VListBox::createSnapshotOfRows (const juce::SparseSet& rows, int& imageX, int& imageY) { - Rectangle imageArea; + juce::Rectangle imageArea; auto firstRow = getRowContainingPosition (0, viewport->getY()); for (int i = getNumRowsOnScreen() + 2; --i >= 0;) @@ -958,7 +954,7 @@ Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, in { if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) { - auto pos = getLocalPoint (rowComp, Point()); + auto pos = getLocalPoint (rowComp, juce::Point()); imageArea = imageArea.getUnion ({ pos.x, pos.y, rowComp->getWidth(), rowComp->getHeight() }); } @@ -969,9 +965,9 @@ Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, in imageX = imageArea.getX(); imageY = imageArea.getY(); - auto listScale = Component::getApproximateScaleFactorForComponent (this); - Image snapshot ( - Image::ARGB, roundToInt ((float) imageArea.getWidth() * listScale), roundToInt ((float) imageArea.getHeight() * listScale), true); + auto listScale = juce::Component::getApproximateScaleFactorForComponent (this); + juce::Image snapshot ( + juce::Image::ARGB, juce::roundToInt ((float) imageArea.getWidth() * listScale), juce::roundToInt ((float) imageArea.getHeight() * listScale), true); for (int i = getNumRowsOnScreen() + 2; --i >= 0;) { @@ -979,15 +975,15 @@ Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, in { if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) { - Graphics g (snapshot); - g.setOrigin (getLocalPoint (rowComp, Point()) - imageArea.getPosition()); + juce::Graphics g (snapshot); + g.setOrigin (getLocalPoint (rowComp, juce::Point()) - imageArea.getPosition()); - auto rowScale = Component::getApproximateScaleFactorForComponent (rowComp); + auto rowScale = juce::Component::getApproximateScaleFactorForComponent (rowComp); if (g.reduceClipRegion (rowComp->getLocalBounds() * rowScale)) { g.beginTransparencyLayer (0.6f); - g.addTransform (AffineTransform::scale (rowScale)); + g.addTransform (juce::AffineTransform::scale (rowScale)); rowComp->paintEntireComponent (g, false); g.endTransparencyLayer(); } @@ -998,28 +994,28 @@ Image ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, in return snapshot; } -void ListBox::startDragAndDrop (const MouseEvent& e, - const SparseSet& rowsToDrag, - const var& dragDescription, +void VListBox::startDragAndDrop (const juce::MouseEvent& e, + const juce::SparseSet& rowsToDrag, + const juce::var& dragDescription, bool allowDraggingToOtherWindows) { - if (auto* dragContainer = DragAndDropContainer::findParentDragContainerFor (this)) + if (auto* dragContainer = juce::DragAndDropContainer::findParentDragContainerFor (this)) { int x, y; auto dragImage = createSnapshotOfRows (rowsToDrag, x, y); - auto p = Point (x, y) - e.getEventRelativeTo (this).position.toInt(); + auto p = juce::Point (x, y) - e.getEventRelativeTo (this).position.toInt(); dragContainer->startDragging (dragDescription, this, dragImage, allowDraggingToOtherWindows, &p, &e.source); } else { // to be able to do a drag-and-drop operation, the listbox needs to - // be inside a component which is also a DragAndDropContainer. + // be inside a component which is also a juce::DragAndDropContainer. jassertfalse; } } -int ListBox::getContentHeight() const +int VListBox::getContentHeight() const { if (model == nullptr || totalItems == 0) return 0; @@ -1027,7 +1023,7 @@ int ListBox::getContentHeight() const return getPositionForRow (totalItems - 1) + getRowHeight (totalItems - 1); } -int ListBox::getRowForPosition (int y) const +int VListBox::getRowForPosition (int y) const { if (model == nullptr || y < 0) return 0; @@ -1035,7 +1031,7 @@ int ListBox::getRowForPosition (int y) const return model->getRowForPosition (y); } -int ListBox::getPositionForRow (int row) const +int VListBox::getPositionForRow (int row) const { if (model == nullptr) return 0; @@ -1046,32 +1042,29 @@ int ListBox::getPositionForRow (int row) const } //============================================================================== -Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) +juce::Component* VListBoxModel::refreshComponentForRow (int, bool, juce::Component* existingComponentToUpdate) { ignoreUnused (existingComponentToUpdate); jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components return nullptr; } -void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} -void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} -void ListBoxModel::backgroundClicked (const MouseEvent&) {} -void ListBoxModel::selectedRowsChanged (int) {} -void ListBoxModel::deleteKeyPressed (int) {} -void ListBoxModel::returnKeyPressed (int) {} -void ListBoxModel::listWasScrolled() {} -var ListBoxModel::getDragSourceDescription (const SparseSet&) +void VListBoxModel::listBoxItemClicked (int, const juce::MouseEvent&) {} +void VListBoxModel::listBoxItemDoubleClicked (int, const juce::MouseEvent&) {} +void VListBoxModel::backgroundClicked (const juce::MouseEvent&) {} +void VListBoxModel::selectedRowsChanged (int) {} +void VListBoxModel::deleteKeyPressed (int) {} +void VListBoxModel::returnKeyPressed (int) {} +void VListBoxModel::listWasScrolled() {} +juce::var VListBoxModel::getDragSourceDescription (const juce::SparseSet&) { return {}; } -String ListBoxModel::getTooltipForRow (int) +juce::String VListBoxModel::getTooltipForRow (int) { return {}; } -MouseCursor ListBoxModel::getMouseCursorForRow (int) +juce::MouseCursor VListBoxModel::getMouseCursorForRow (int) { - return MouseCursor::NormalCursor; + return juce::MouseCursor::NormalCursor; } - -} // namespace jc -} // namespace juce \ No newline at end of file diff --git a/Source/components/VListBox.h b/Source/components/VListBox.h index 437e864..466ab1f 100644 --- a/Source/components/VListBox.h +++ b/Source/components/VListBox.h @@ -23,600 +23,598 @@ ============================================================================== */ +#pragma once + #include "JuceHeader.h" -namespace juce { - namespace jc { - //============================================================================== - /** - A subclass of this is used to drive a ListBox. - @see ListBox - - @tags{GUI} - */ - class ListBoxModel { - public: - //============================================================================== - /** Destructor. */ - virtual ~ListBoxModel() = default; - - //============================================================================== - /** This has to return the number of items in the list. - @see ListBox::getNumRows() - */ - virtual int getNumRows() = 0; +//============================================================================== +/** + A subclass of this is used to drive a VListBox. - /** This method must be implemented to draw a row of the list. - Note that the rowNumber value may be greater than the number of rows in your - list, so be careful that you don't assume it's less than getNumRows(). - */ - virtual void paintListBoxItem(int rowNumber, Graphics& g, int width, int height, bool rowIsSelected) = 0; - - /** This is used to create or update a custom component to go in a row of the list. + @see VListBox - Any row may contain a custom component, or can just be drawn with the paintListBoxItem() method - and handle mouse clicks with listBoxItemClicked(). - - This method will be called whenever a custom component might need to be updated - e.g. - when the list is changed, or ListBox::updateContent() is called. - - If you don't need a custom component for the specified row, then return nullptr. - (Bear in mind that even if you're not creating a new component, you may still need to - delete existingComponentToUpdate if it's non-null). - - If you do want a custom component, and the existingComponentToUpdate is null, then - this method must create a suitable new component and return it. - - If the existingComponentToUpdate is non-null, it will be a pointer to a component previously created - by this method. In this case, the method must either update it to make sure it's correctly representing - the given row (which may be different from the one that the component was created for), or it can - delete this component and return a new one. - - The component that your method returns will be deleted by the ListBox when it is no longer needed. - - Bear in mind that if you put a custom component inside the row but still want the - listbox to automatically handle clicking, selection, etc, then you'll need to make sure - your custom component doesn't intercept all the mouse events that land on it, e.g by - using Component::setInterceptsMouseClicks(). - */ - virtual Component* refreshComponentForRow(int rowNumber, bool isRowSelected, Component* existingComponentToUpdate); - - /** This can be overridden to react to the user clicking on a row. - @see listBoxItemDoubleClicked - */ - virtual void listBoxItemClicked(int row, const MouseEvent&); - - /** This can be overridden to react to the user double-clicking on a row. - @see listBoxItemClicked - */ - virtual void listBoxItemDoubleClicked(int row, const MouseEvent&); - - /** This can be overridden to react to the user clicking on a part of the list where - there are no rows. - @see listBoxItemClicked - */ - virtual void backgroundClicked(const MouseEvent&); - - /** Override this to be informed when rows are selected or deselected. - - This will be called whenever a row is selected or deselected. If a range of - rows is selected all at once, this will just be called once for that event. - - @param lastRowSelected the last row that the user selected. If no - rows are currently selected, this may be -1. - */ - virtual void selectedRowsChanged(int lastRowSelected); - - /** Override this to be informed when the delete key is pressed. - - If no rows are selected when they press the key, this won't be called. - - @param lastRowSelected the last row that had been selected when they pressed the - key - if there are multiple selections, this might not be - very useful - */ - virtual void deleteKeyPressed(int lastRowSelected); - - /** Override this to be informed when the return key is pressed. - - If no rows are selected when they press the key, this won't be called. - - @param lastRowSelected the last row that had been selected when they pressed the - key - if there are multiple selections, this might not be - very useful - */ - virtual void returnKeyPressed(int lastRowSelected); - - /** Override this to be informed when the list is scrolled. - - This might be caused by the user moving the scrollbar, or by programmatic changes - to the list position. - */ - virtual void listWasScrolled(); - - /** To allow rows from your list to be dragged-and-dropped, implement this method. - - If this returns a non-null variant then when the user drags a row, the listbox will - try to find a DragAndDropContainer in its parent hierarchy, and will use it to trigger - a drag-and-drop operation, using this string as the source description, with the listbox - itself as the source component. - - @see DragAndDropContainer::startDragging - */ - virtual var getDragSourceDescription(const SparseSet& rowsToDescribe); - - /** You can override this to provide tool tips for specific rows. - @see TooltipClient - */ - virtual String getTooltipForRow(int row); - - /** You can override this to return a custom mouse cursor for each row. */ - virtual MouseCursor getMouseCursorForRow(int row); - - /** - * Override this to return the row height for a given row. - */ - virtual int getRowHeight(int row) { return 22; } - - /** - * Override this if your list may have variable row heights. - * - * Performance is slightly improved if this returns false, but getRowHeight - * must then return the same number for all rows. - */ - virtual bool hasVariableHeightRows() const { return false; } - - /** - * You can override this to improve performance with very long lists. - * - * If you have many variable height rows you may be able to improve - * performance by directly calculating the row for a given y position. - * - * If the y position is greater than the number of rows return the number - * of rows. yPos will never be less than zero. - */ - virtual int getRowForPosition(int yPos); - - /** - * You can override this to improve performance with very long lists. - * - * If you have a large number (e.g. thousands of) variable height rows you - * may be able to improve performance by overriding this function and - * directly calculating the y position of a given row. - */ - virtual int getPositionForRow(int rowNumber); - }; - - //============================================================================== - /** - A list of items that can be scrolled vertically. - - To create a list, you'll need to create a subclass of ListBoxModel. This can - either paint each row of the list and respond to events via callbacks, or for - more specialised tasks, it can supply a custom component to fill each row. - - @see ComboBox, TableListBox - - @tags{GUI} - */ - class JUCE_API ListBox : public Component, public SettableTooltipClient { - public: - //============================================================================== - /** Creates a ListBox. - - The model pointer passed-in can be null, in which case you can set it later - with setModel(). - */ - ListBox(const String& componentName = String(), ListBoxModel* model = nullptr); - - /** Destructor. */ - ~ListBox() override; - - //============================================================================== - /** Changes the current data model to display. */ - void setModel(ListBoxModel* newModel); - - /** Returns the current list model. */ - ListBoxModel* getModel() const noexcept { return model; } - - //============================================================================== - /** Causes the list to refresh its content. - - Call this when the number of rows in the list changes, or if you want it - to call refreshComponentForRow() on all the row components. - - This must only be called from the main message thread. - */ - void updateContent(); - - //============================================================================== - /** Turns on multiple-selection of rows. - - By default this is disabled. - - When your row component gets clicked you'll need to call the - selectRowsBasedOnModifierKeys() method to tell the list that it's been - clicked and to get it to do the appropriate selection based on whether - the ctrl/shift keys are held down. - */ - void setMultipleSelectionEnabled(bool shouldBeEnabled) noexcept; - - /** If enabled, this makes the listbox flip the selection status of - each row that the user clicks, without affecting other selected rows. - - (This only has an effect if multiple selection is also enabled). - If not enabled, you can still get the same row-flipping behaviour by holding - down CMD or CTRL when clicking. - */ - void setClickingTogglesRowSelection(bool flipRowSelection) noexcept; - - /** Sets whether a row should be selected when the mouse is pressed or released. - By default this is true, but you may want to turn it off. - */ - void setRowSelectedOnMouseDown(bool isSelectedOnMouseDown) noexcept; - - /** Makes the list react to mouse moves by selecting the row that the mouse if over. - - This function is here primarily for the ComboBox class to use, but might be - useful for some other purpose too. - */ - void setMouseMoveSelectsRows(bool shouldSelect); - - //============================================================================== - /** Selects a row. - - If the row is already selected, this won't do anything. - - @param rowNumber the row to select - @param dontScrollToShowThisRow if true, the list's position won't change; if false and - the selected row is off-screen, it'll scroll to make - sure that row is on-screen - @param deselectOthersFirst if true and there are multiple selections, these will - first be deselected before this item is selected - @see isRowSelected, selectRowsBasedOnModifierKeys, flipRowSelection, deselectRow, - deselectAllRows, selectRangeOfRows - */ - void selectRow(int rowNumber, bool dontScrollToShowThisRow = false, bool deselectOthersFirst = true); - - /** Selects a set of rows. + @tags{GUI} +*/ +class VListBoxModel { +public: + //============================================================================== + /** Destructor. */ + virtual ~VListBoxModel() = default; - This will add these rows to the current selection, so you might need to - clear the current selection first with deselectAllRows() + //============================================================================== + /** This has to return the number of items in the list. + @see VListBox::getNumRows() + */ + virtual int getNumRows() = 0; - @param firstRow the first row to select (inclusive) - @param lastRow the last row to select (inclusive) - @param dontScrollToShowThisRange if true, the list's position won't change; if false and - the selected range is off-screen, it'll scroll to make - sure that the range of rows is on-screen - */ - void selectRangeOfRows(int firstRow, int lastRow, bool dontScrollToShowThisRange = false); + /** This method must be implemented to draw a row of the list. + Note that the rowNumber value may be greater than the number of rows in your + list, so be careful that you don't assume it's less than getNumRows(). + */ + virtual void paintListBoxItem(int rowNumber, juce::Graphics& g, int width, int height, bool rowIsSelected) = 0; - /** Deselects a row. - If it's not currently selected, this will do nothing. - @see selectRow, deselectAllRows - */ - void deselectRow(int rowNumber); + /** This is used to create or update a custom component to go in a row of the list. - /** Deselects any currently selected rows. - @see deselectRow - */ - void deselectAllRows(); + Any row may contain a custom component, or can just be drawn with the paintListBoxItem() method + and handle mouse clicks with listBoxItemClicked(). - /** Selects or deselects a row. - If the row's currently selected, this deselects it, and vice-versa. - */ - void flipRowSelection(int rowNumber); + This method will be called whenever a custom component might need to be updated - e.g. + when the list is changed, or VListBox::updateContent() is called. - /** Returns a sparse set indicating the rows that are currently selected. - @see setSelectedRows - */ - SparseSet getSelectedRows() const; + If you don't need a custom component for the specified row, then return nullptr. + (Bear in mind that even if you're not creating a new component, you may still need to + delete existingComponentToUpdate if it's non-null). - /** Sets the rows that should be selected, based on an explicit set of ranges. + If you do want a custom component, and the existingComponentToUpdate is null, then + this method must create a suitable new component and return it. - If sendNotificationEventToModel is true, the ListBoxModel::selectedRowsChanged() - method will be called. If it's false, no notification will be sent to the model. + If the existingComponentToUpdate is non-null, it will be a pointer to a component previously created + by this method. In this case, the method must either update it to make sure it's correctly representing + the given row (which may be different from the one that the component was created for), or it can + delete this component and return a new one. + + The component that your method returns will be deleted by the VListBox when it is no longer needed. + + Bear in mind that if you put a custom component inside the row but still want the + listbox to automatically handle clicking, selection, etc, then you'll need to make sure + your custom component doesn't intercept all the mouse events that land on it, e.g by + using Component::setInterceptsMouseClicks(). + */ + virtual juce::Component* refreshComponentForRow(int rowNumber, bool isRowSelected, juce::Component* existingComponentToUpdate); + + /** This can be overridden to react to the user clicking on a row. + @see listBoxItemDoubleClicked + */ + virtual void listBoxItemClicked(int row, const juce::MouseEvent&); + + /** This can be overridden to react to the user double-clicking on a row. + @see listBoxItemClicked + */ + virtual void listBoxItemDoubleClicked(int row, const juce::MouseEvent&); + + /** This can be overridden to react to the user clicking on a part of the list where + there are no rows. + @see listBoxItemClicked + */ + virtual void backgroundClicked(const juce::MouseEvent&); + + /** Override this to be informed when rows are selected or deselected. + + This will be called whenever a row is selected or deselected. If a range of + rows is selected all at once, this will just be called once for that event. + + @param lastRowSelected the last row that the user selected. If no + rows are currently selected, this may be -1. + */ + virtual void selectedRowsChanged(int lastRowSelected); + + /** Override this to be informed when the delete key is pressed. + + If no rows are selected when they press the key, this won't be called. + + @param lastRowSelected the last row that had been selected when they pressed the + key - if there are multiple selections, this might not be + very useful + */ + virtual void deleteKeyPressed(int lastRowSelected); + + /** Override this to be informed when the return key is pressed. + + If no rows are selected when they press the key, this won't be called. + + @param lastRowSelected the last row that had been selected when they pressed the + key - if there are multiple selections, this might not be + very useful + */ + virtual void returnKeyPressed(int lastRowSelected); + + /** Override this to be informed when the list is scrolled. + + This might be caused by the user moving the scrollbar, or by programmatic changes + to the list position. + */ + virtual void listWasScrolled(); + + /** To allow rows from your list to be dragged-and-dropped, implement this method. + + If this returns a non-null variant then when the user drags a row, the listbox will + try to find a DragAndDropContainer in its parent hierarchy, and will use it to trigger + a drag-and-drop operation, using this string as the source description, with the listbox + itself as the source component. + + @see DragAndDropContainer::startDragging + */ + virtual juce::var getDragSourceDescription(const juce::SparseSet& rowsToDescribe); + + /** You can override this to provide tool tips for specific rows. + @see TooltipClient + */ + virtual juce::String getTooltipForRow(int row); + + /** You can override this to return a custom mouse cursor for each row. */ + virtual juce::MouseCursor getMouseCursorForRow(int row); + + /** + * Override this to return the row height for a given row. + */ + virtual int getRowHeight(int row) { return 22; } + + /** + * Override this if your list may have variable row heights. + * + * Performance is slightly improved if this returns false, but getRowHeight + * must then return the same number for all rows. + */ + virtual bool hasVariableHeightRows() const { return false; } + + /** + * You can override this to improve performance with very long lists. + * + * If you have many variable height rows you may be able to improve + * performance by directly calculating the row for a given y position. + * + * If the y position is greater than the number of rows return the number + * of rows. yPos will never be less than zero. + */ + virtual int getRowForPosition(int yPos); + + /** + * You can override this to improve performance with very long lists. + * + * If you have a large number (e.g. thousands of) variable height rows you + * may be able to improve performance by overriding this function and + * directly calculating the y position of a given row. + */ + virtual int getPositionForRow(int rowNumber); +}; + +//============================================================================== +/** + A list of items that can be scrolled vertically. + + To create a list, you'll need to create a subclass of VListBoxModel. This can + either paint each row of the list and respond to events via callbacks, or for + more specialised tasks, it can supply a custom component to fill each row. + + @see ComboBox, TableListBox + + @tags{GUI} +*/ +class JUCE_API VListBox : public juce::Component, public juce::SettableTooltipClient { +public: + //============================================================================== + /** Creates a VListBox. + + The model pointer passed-in can be null, in which case you can set it later + with setModel(). + */ + VListBox(const juce::String& componentName = juce::String(), VListBoxModel* model = nullptr); + + /** Destructor. */ + ~VListBox() override; + + //============================================================================== + /** Changes the current data model to display. */ + void setModel(VListBoxModel* newModel); + + /** Returns the current list model. */ + VListBoxModel* getModel() const noexcept { return model; } + + //============================================================================== + /** Causes the list to refresh its content. + + Call this when the number of rows in the list changes, or if you want it + to call refreshComponentForRow() on all the row components. + + This must only be called from the main message thread. + */ + void updateContent(); + + //============================================================================== + /** Turns on multiple-selection of rows. + + By default this is disabled. + + When your row component gets clicked you'll need to call the + selectRowsBasedOnModifierKeys() method to tell the list that it's been + clicked and to get it to do the appropriate selection based on whether + the ctrl/shift keys are held down. + */ + void setMultipleSelectionEnabled(bool shouldBeEnabled) noexcept; + + /** If enabled, this makes the listbox flip the selection status of + each row that the user clicks, without affecting other selected rows. + + (This only has an effect if multiple selection is also enabled). + If not enabled, you can still get the same row-flipping behaviour by holding + down CMD or CTRL when clicking. + */ + void setClickingTogglesRowSelection(bool flipRowSelection) noexcept; + + /** Sets whether a row should be selected when the mouse is pressed or released. + By default this is true, but you may want to turn it off. + */ + void setRowSelectedOnMouseDown(bool isSelectedOnMouseDown) noexcept; + + /** Makes the list react to mouse moves by selecting the row that the mouse if over. + + This function is here primarily for the ComboBox class to use, but might be + useful for some other purpose too. + */ + void setMouseMoveSelectsRows(bool shouldSelect); + + //============================================================================== + /** Selects a row. + + If the row is already selected, this won't do anything. + + @param rowNumber the row to select + @param dontScrollToShowThisRow if true, the list's position won't change; if false and + the selected row is off-screen, it'll scroll to make + sure that row is on-screen + @param deselectOthersFirst if true and there are multiple selections, these will + first be deselected before this item is selected + @see isRowSelected, selectRowsBasedOnModifierKeys, flipRowSelection, deselectRow, + deselectAllRows, selectRangeOfRows + */ + void selectRow(int rowNumber, bool dontScrollToShowThisRow = false, bool deselectOthersFirst = true); + + /** Selects a set of rows. - @see getSelectedRows - */ - void setSelectedRows(const SparseSet& setOfRowsToBeSelected, NotificationType sendNotificationEventToModel = sendNotification); + This will add these rows to the current selection, so you might need to + clear the current selection first with deselectAllRows() - /** Checks whether a row is selected. - */ - bool isRowSelected(int rowNumber) const; + @param firstRow the first row to select (inclusive) + @param lastRow the last row to select (inclusive) + @param dontScrollToShowThisRange if true, the list's position won't change; if false and + the selected range is off-screen, it'll scroll to make + sure that the range of rows is on-screen + */ + void selectRangeOfRows(int firstRow, int lastRow, bool dontScrollToShowThisRange = false); - /** Returns the number of rows that are currently selected. - @see getSelectedRow, isRowSelected, getLastRowSelected - */ - int getNumSelectedRows() const; + /** Deselects a row. + If it's not currently selected, this will do nothing. + @see selectRow, deselectAllRows + */ + void deselectRow(int rowNumber); - /** Returns the row number of a selected row. + /** Deselects any currently selected rows. + @see deselectRow + */ + void deselectAllRows(); - This will return the row number of the Nth selected row. The row numbers returned will - be sorted in order from low to high. + /** Selects or deselects a row. + If the row's currently selected, this deselects it, and vice-versa. + */ + void flipRowSelection(int rowNumber); - @param index the index of the selected row to return, (from 0 to getNumSelectedRows() - 1) - @returns the row number, or -1 if the index was out of range or if there aren't any rows - selected - @see getNumSelectedRows, isRowSelected, getLastRowSelected - */ - int getSelectedRow(int index = 0) const; + /** Returns a sparse set indicating the rows that are currently selected. + @see setSelectedRows + */ + juce::SparseSet getSelectedRows() const; - /** Returns the last row that the user selected. + /** Sets the rows that should be selected, based on an explicit set of ranges. - This isn't the same as the highest row number that is currently selected - if the user - had multiply-selected rows 10, 5 and then 6 in that order, this would return 6. + If sendNotificationEventToModel is true, the VListBoxModel::selectedRowsChanged() + method will be called. If it's false, no notification will be sent to the model. - If nothing is selected, it will return -1. - */ - int getLastRowSelected() const; + @see getSelectedRows + */ + void setSelectedRows(const juce::SparseSet& setOfRowsToBeSelected, juce::NotificationType sendNotificationEventToModel = juce::sendNotification); - /** Multiply-selects rows based on the modifier keys. + /** Checks whether a row is selected. + */ + bool isRowSelected(int rowNumber) const; - If no modifier keys are down, this will select the given row and - deselect any others. + /** Returns the number of rows that are currently selected. + @see getSelectedRow, isRowSelected, getLastRowSelected + */ + int getNumSelectedRows() const; - If the ctrl (or command on the Mac) key is down, it'll flip the - state of the selected row. + /** Returns the row number of a selected row. - If the shift key is down, it'll select up to the given row from the - last row selected. + This will return the row number of the Nth selected row. The row numbers returned will + be sorted in order from low to high. - @see selectRow - */ - void selectRowsBasedOnModifierKeys(int rowThatWasClickedOn, ModifierKeys modifiers, bool isMouseUpEvent); + @param index the index of the selected row to return, (from 0 to getNumSelectedRows() - 1) + @returns the row number, or -1 if the index was out of range or if there aren't any rows + selected + @see getNumSelectedRows, isRowSelected, getLastRowSelected + */ + int getSelectedRow(int index = 0) const; - //============================================================================== - /** Scrolls the list to a particular position. + /** Returns the last row that the user selected. - The proportion is between 0 and 1.0, so 0 scrolls to the top of the list, - 1.0 scrolls to the bottom. + This isn't the same as the highest row number that is currently selected - if the user + had multiply-selected rows 10, 5 and then 6 in that order, this would return 6. - If the total number of rows all fit onto the screen at once, then this - method won't do anything. + If nothing is selected, it will return -1. + */ + int getLastRowSelected() const; - @see getVerticalPosition - */ - void setVerticalPosition(double newProportion); + /** Multiply-selects rows based on the modifier keys. - /** Returns the current vertical position as a proportion of the total. + If no modifier keys are down, this will select the given row and + deselect any others. - This can be used in conjunction with setVerticalPosition() to save and restore - the list's position. It returns a value in the range 0 to 1. + If the ctrl (or command on the Mac) key is down, it'll flip the + state of the selected row. - @see setVerticalPosition - */ - double getVerticalPosition() const; + If the shift key is down, it'll select up to the given row from the + last row selected. - /** Scrolls if necessary to make sure that a particular row is visible. */ - void scrollToEnsureRowIsOnscreen(int row); + @see selectRow + */ + void selectRowsBasedOnModifierKeys(int rowThatWasClickedOn, juce::ModifierKeys modifiers, bool isMouseUpEvent); - /** Returns a reference to the vertical scrollbar. */ - ScrollBar& getVerticalScrollBar() const noexcept; + //============================================================================== + /** Scrolls the list to a particular position. - /** Returns a reference to the horizontal scrollbar. */ - ScrollBar& getHorizontalScrollBar() const noexcept; + The proportion is between 0 and 1.0, so 0 scrolls to the top of the list, + 1.0 scrolls to the bottom. - /** Finds the row index that contains a given x,y position. - The position is relative to the ListBox's top-left. - If no row exists at this position, the method will return -1. - @see getComponentForRowNumber - */ - int getRowContainingPosition(int x, int y) const noexcept; + If the total number of rows all fit onto the screen at once, then this + method won't do anything. - /** - * Returns the height of the specified row. Returns zero if no model has - * been set. - */ - int getRowHeight(int row) const; + @see getVerticalPosition + */ + void setVerticalPosition(double newProportion); - /** Finds a row index that would be the most suitable place to insert a new - item for a given position. + /** Returns the current vertical position as a proportion of the total. - This is useful when the user is e.g. dragging and dropping onto the listbox, - because it lets you easily choose the best position to insert the item that - they drop, based on where they drop it. + This can be used in conjunction with setVerticalPosition() to save and restore + the list's position. It returns a value in the range 0 to 1. - If the position is out of range, this will return -1. If the position is - beyond the end of the list, it will return getNumRows() to indicate the end - of the list. + @see setVerticalPosition + */ + double getVerticalPosition() const; - @see getComponentForRowNumber - */ - int getInsertionIndexForPosition(int x, int y) const noexcept; + /** Scrolls if necessary to make sure that a particular row is visible. */ + void scrollToEnsureRowIsOnscreen(int row); - /** Returns the position of one of the rows, relative to the top-left of - the listbox. + /** Returns a reference to the vertical scrollbar. */ + juce::ScrollBar& getVerticalScrollBar() const noexcept; - This may be off-screen, and the range of the row number that is passed-in is - not checked to see if it's a valid row. - */ - Rectangle getRowPosition(int rowNumber, bool relativeToComponentTopLeft) const noexcept; + /** Returns a reference to the horizontal scrollbar. */ + juce::ScrollBar& getHorizontalScrollBar() const noexcept; - /** Finds the row component for a given row in the list. + /** Finds the row index that contains a given x,y position. + The position is relative to the VListBox's top-left. + If no row exists at this position, the method will return -1. + @see getComponentForRowNumber + */ + int getRowContainingPosition(int x, int y) const noexcept; - The component returned will have been created using ListBoxModel::refreshComponentForRow(). + /** + * Returns the height of the specified row. Returns zero if no model has + * been set. + */ + int getRowHeight(int row) const; - If the component for this row is off-screen or if the row is out-of-range, - this will return nullptr. + /** Finds a row index that would be the most suitable place to insert a new + item for a given position. - @see getRowContainingPosition - */ - Component* getComponentForRowNumber(int rowNumber) const noexcept; + This is useful when the user is e.g. dragging and dropping onto the listbox, + because it lets you easily choose the best position to insert the item that + they drop, based on where they drop it. - /** Returns the row number that the given component represents. - If the component isn't one of the list's rows, this will return -1. - */ - int getRowNumberOfComponent(Component* rowComponent) const noexcept; + If the position is out of range, this will return -1. If the position is + beyond the end of the list, it will return getNumRows() to indicate the end + of the list. - /** Returns the width of a row (which may be less than the width of this component - if there's a scrollbar). - */ - int getVisibleRowWidth() const noexcept; + @see getComponentForRowNumber + */ + int getInsertionIndexForPosition(int x, int y) const noexcept; - //============================================================================== + /** Returns the position of one of the rows, relative to the top-left of + the listbox. - /** Returns the number of rows actually visible. + This may be off-screen, and the range of the row number that is passed-in is + not checked to see if it's a valid row. + */ + juce::Rectangle getRowPosition(int rowNumber, bool relativeToComponentTopLeft) const noexcept; - This is the number of whole rows which will fit on-screen, so the value might - be more than the actual number of rows in the list. - */ - int getNumRowsOnScreen() const noexcept; + /** Finds the row component for a given row in the list. - //============================================================================== - /** A set of colour IDs to use to change the colour of various aspects of the label. + The component returned will have been created using VListBoxModel::refreshComponentForRow(). - These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() - methods. - - @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour - */ - enum ColourIds { - backgroundColourId = 0x1002800, /**< The background colour to fill the list with. - Make this transparent if you don't want the background to be filled. */ - outlineColourId = 0x1002810, /**< An optional colour to use to draw a border around the list. - Make this transparent to not have an outline. */ - textColourId = 0x1002820 /**< The preferred colour to use for drawing text in the listbox. */ - }; - - /** Sets the thickness of a border that will be drawn around the box. - - To set the colour of the outline, use @code setColour (ListBox::outlineColourId, colourXYZ); @endcode - @see outlineColourId - */ - void setOutlineThickness(int outlineThickness); - - /** Returns the thickness of outline that will be drawn around the listbox. - @see setOutlineColour - */ - int getOutlineThickness() const noexcept { return outlineThickness; } - - /** Sets a component that the list should use as a header. - - This will position the given component at the top of the list, maintaining the - height of the component passed-in, but rescaling it horizontally to match the - width of the items in the listbox. - - The component will be deleted when setHeaderComponent() is called with a - different component, or when the listbox is deleted. - */ - void setHeaderComponent(std::unique_ptr newHeaderComponent); - - /** Returns whatever header component was set with setHeaderComponent(). */ - Component* getHeaderComponent() const noexcept { return headerComponent.get(); } - - /** Changes the width of the rows in the list. - - This can be used to make the list's row components wider than the list itself - the - width of the rows will be either the width of the list or this value, whichever is - greater, and if the rows become wider than the list, a horizontal scrollbar will - appear. - - The default value for this is 0, which means that the rows will always - be the same width as the list. - */ - void setMinimumContentWidth(int newMinimumWidth); - - /** Returns the space currently available for the row items, taking into account - borders, scrollbars, etc. - */ - int getVisibleContentWidth() const noexcept; - - /** Repaints one of the rows. - - This does not invoke updateContent(), it just invokes a straightforward repaint - for the area covered by this row. - */ - void repaintRow(int rowNumber) noexcept; - - /** This fairly obscure method creates an image that shows the row components specified - in rows (for example, these could be the currently selected row components). - - It's a handy method for doing drag-and-drop, as it can be passed to the - DragAndDropContainer for use as the drag image. - - Note that it will make the row components temporarily invisible, so if you're - using custom components this could affect them if they're sensitive to that - sort of thing. - - @see Component::createComponentSnapshot - */ - virtual Image createSnapshotOfRows(const SparseSet& rows, int& x, int& y); - - /** Returns the viewport that this ListBox uses. - - You may need to use this to change parameters such as whether scrollbars - are shown, etc. - */ - Viewport* getViewport() const noexcept; - - //============================================================================== - /** @internal */ - bool keyPressed(const KeyPress&) override; - /** @internal */ - bool keyStateChanged(bool isKeyDown) override; - /** @internal */ - void paint(Graphics&) override; - /** @internal */ - void paintOverChildren(Graphics&) override; - /** @internal */ - void resized() override; - /** @internal */ - void visibilityChanged() override; - /** @internal */ - void mouseWheelMove(const MouseEvent&, const MouseWheelDetails&) override; - /** @internal */ - void mouseUp(const MouseEvent&) override; - /** @internal */ - void colourChanged() override; - /** @internal */ - void parentHierarchyChanged() override; - /** @internal */ - void startDragAndDrop(const MouseEvent&, - const SparseSet& rowsToDrag, - const var& dragDescription, - bool allowDraggingToOtherWindows); - - private: - //============================================================================== - JUCE_PUBLIC_IN_DLL_BUILD(class ListViewport) - JUCE_PUBLIC_IN_DLL_BUILD(class RowComponent) - friend class ListViewport; - friend class TableListBox; - ListBoxModel* model; - std::unique_ptr viewport; - std::unique_ptr headerComponent; - std::unique_ptr mouseMoveSelector; - SparseSet selected; - int totalItems = 0, minimumRowWidth = 0; - int outlineThickness = 0; - int lastRowSelected = -1; - bool multipleSelection = false, alwaysFlipSelection = false, hasDoneInitialUpdate = false, selectOnMouseDown = true; - - void selectRowInternal(int rowNumber, bool dontScrollToShowThisRow, bool deselectOthersFirst, bool isMouseClick); - int getContentHeight() const; - int getRowForPosition(int y) const; - int getPositionForRow(int row) const; + If the component for this row is off-screen or if the row is out-of-range, + this will return nullptr. + + @see getRowContainingPosition + */ + juce::Component* getComponentForRowNumber(int rowNumber) const noexcept; + + /** Returns the row number that the given component represents. + If the component isn't one of the list's rows, this will return -1. + */ + int getRowNumberOfComponent(juce::Component* rowComponent) const noexcept; + + /** Returns the width of a row (which may be less than the width of this component + if there's a scrollbar). + */ + int getVisibleRowWidth() const noexcept; + + //============================================================================== + + /** Returns the number of rows actually visible. + + This is the number of whole rows which will fit on-screen, so the value might + be more than the actual number of rows in the list. + */ + int getNumRowsOnScreen() const noexcept; + + //============================================================================== + /** A set of colour IDs to use to change the colour of various aspects of the label. + + These constants can be used either via the Component::setColour(), or LookAndFeel::setColour() + methods. + + @see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour + */ + enum ColourIds { + backgroundColourId = 0x1002800, /**< The background colour to fill the list with. + Make this transparent if you don't want the background to be filled. */ + outlineColourId = 0x1002810, /**< An optional colour to use to draw a border around the list. + Make this transparent to not have an outline. */ + textColourId = 0x1002820 /**< The preferred colour to use for drawing text in the listbox. */ + }; + + /** Sets the thickness of a border that will be drawn around the box. + + To set the colour of the outline, use @code setColour (VListBox::outlineColourId, colourXYZ); @endcode + @see outlineColourId + */ + void setOutlineThickness(int outlineThickness); + + /** Returns the thickness of outline that will be drawn around the listbox. + @see setOutlineColour + */ + int getOutlineThickness() const noexcept { return outlineThickness; } + + /** Sets a component that the list should use as a header. + + This will position the given component at the top of the list, maintaining the + height of the component passed-in, but rescaling it horizontally to match the + width of the items in the listbox. + + The component will be deleted when setHeaderComponent() is called with a + different component, or when the listbox is deleted. + */ + void setHeaderComponent(std::unique_ptr newHeaderComponent); + + /** Returns whatever header component was set with setHeaderComponent(). */ + juce::Component* getHeaderComponent() const noexcept { return headerComponent.get(); } + + /** Changes the width of the rows in the list. + + This can be used to make the list's row components wider than the list itself - the + width of the rows will be either the width of the list or this value, whichever is + greater, and if the rows become wider than the list, a horizontal scrollbar will + appear. + + The default value for this is 0, which means that the rows will always + be the same width as the list. + */ + void setMinimumContentWidth(int newMinimumWidth); + + /** Returns the space currently available for the row items, taking into account + borders, scrollbars, etc. + */ + int getVisibleContentWidth() const noexcept; + + /** Repaints one of the rows. + + This does not invoke updateContent(), it just invokes a straightforward repaint + for the area covered by this row. + */ + void repaintRow(int rowNumber) noexcept; + + /** This fairly obscure method creates an image that shows the row components specified + in rows (for example, these could be the currently selected row components). + + It's a handy method for doing drag-and-drop, as it can be passed to the + DragAndDropContainer for use as the drag image. + + Note that it will make the row components temporarily invisible, so if you're + using custom components this could affect them if they're sensitive to that + sort of thing. + + @see Component::createComponentSnapshot + */ + virtual juce::Image createSnapshotOfRows(const juce::SparseSet& rows, int& x, int& y); + + /** Returns the viewport that this VListBox uses. + + You may need to use this to change parameters such as whether scrollbars + are shown, etc. + */ + juce::Viewport* getViewport() const noexcept; + + //============================================================================== + /** @internal */ + bool keyPressed(const juce::KeyPress&) override; + /** @internal */ + bool keyStateChanged(bool isKeyDown) override; + /** @internal */ + void paint(juce::Graphics&) override; + /** @internal */ + void paintOverChildren(juce::Graphics&) override; + /** @internal */ + void resized() override; + /** @internal */ + void visibilityChanged() override; + /** @internal */ + void mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails&) override; + /** @internal */ + void mouseUp(const juce::MouseEvent&) override; + /** @internal */ + void colourChanged() override; + /** @internal */ + void parentHierarchyChanged() override; + /** @internal */ + void startDragAndDrop(const juce::MouseEvent&, + const juce::SparseSet& rowsToDrag, + const juce::var& dragDescription, + bool allowDraggingToOtherWindows); + +private: + //============================================================================== + JUCE_PUBLIC_IN_DLL_BUILD(class ListViewport) + JUCE_PUBLIC_IN_DLL_BUILD(class RowComponent) + friend class ListViewport; + friend class juce::TableListBox; + VListBoxModel* model; + std::unique_ptr viewport; + std::unique_ptr headerComponent; + std::unique_ptr mouseMoveSelector; + juce::SparseSet selected; + int totalItems = 0, minimumRowWidth = 0; + int outlineThickness = 0; + int lastRowSelected = -1; + bool multipleSelection = false, alwaysFlipSelection = false, hasDoneInitialUpdate = false, selectOnMouseDown = true; + + void selectRowInternal(int rowNumber, bool dontScrollToShowThisRow, bool deselectOthersFirst, bool isMouseClick); + int getContentHeight() const; + int getRowForPosition(int y) const; + int getPositionForRow(int row) const; #if JUCE_CATCH_DEPRECATED_CODE_MISUSE - // This method's bool parameter has changed: see the new method signature. - JUCE_DEPRECATED(void setSelectedRows(const SparseSet&, bool)); + // This method's bool parameter has changed: see the new method signature. + JUCE_DEPRECATED(void setSelectedRows(const juce::SparseSet&, bool)); - // Rows can now have different heights. See getRowHeight(int rowNumber) instead. - JUCE_DEPRECATED(void getRowHeight()); + // Rows can now have different heights. See getRowHeight(int rowNumber) instead. + JUCE_DEPRECATED(void getRowHeight()); - // Row height is now set by the ListBoxModel as rows can have different heights. - // Implement ListBoxModel::getRowHeight(int rowNumber). - JUCE_DEPRECATED(void setRowHeight(int)); + // Row height is now set by the VListBoxModel as rows can have different heights. + // Implement VListBoxModel::getRowHeight(int rowNumber). + JUCE_DEPRECATED(void setRowHeight(int)); - // This method has been replaced by the more flexible method createSnapshotOfRows. - // Please call createSnapshotOfRows (getSelectedRows(), x, y) to get the same behaviour. - JUCE_DEPRECATED(virtual void createSnapshotOfSelectedRows(int&, int&)) {} + // This method has been replaced by the more flexible method createSnapshotOfRows. + // Please call createSnapshotOfRows (getSelectedRows(), x, y) to get the same behaviour. + JUCE_DEPRECATED(virtual void createSnapshotOfSelectedRows(int&, int&)) {} #endif - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ListBox) - }; - - } // namespace jc -} // namespace juce \ No newline at end of file + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VListBox) +}; \ No newline at end of file diff --git a/Source/visualiser/VisualiserParameters.h b/Source/visualiser/VisualiserParameters.h index 8da10c5..5a515cc 100644 --- a/Source/visualiser/VisualiserParameters.h +++ b/Source/visualiser/VisualiserParameters.h @@ -222,16 +222,7 @@ public: VERSION_HINT, 0.5, 0.0, 1.0 ) ); - std::shared_ptr stereoEffectApplication = std::make_shared(); - std::shared_ptr stereoEffect = std::make_shared( - stereoEffectApplication, - new osci::EffectParameter( - "Stereo", - "Turns mono audio that is uninteresting to visualise into stereo audio that is interesting to visualise.", - "stereo", - VERSION_HINT, 0.0, 0.0, 1.0 - ) - ); + std::shared_ptr stereoEffect = StereoEffect().build(); std::shared_ptr scaleEffect = std::make_shared( [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { input.scale(values[0].load(), values[1].load(), 1.0); @@ -243,12 +234,12 @@ public: "xScale", VERSION_HINT, 1.0, -3.0, 3.0 ), - new osci::EffectParameter( - "Y Scale", - "Controls the vertical scale of the oscilloscope display.", - "yScale", - VERSION_HINT, 1.0, -3.0, 3.0 - ), + new osci::EffectParameter( + "Y Scale", + "Controls the vertical scale of the oscilloscope display.", + "yScale", + VERSION_HINT, 1.0, -3.0, 3.0 + ), }); std::shared_ptr offsetEffect = std::make_shared( [this](int index, osci::Point input, const std::vector>& values, double sampleRate) { @@ -331,18 +322,10 @@ public: "Ambient Light", "Controls how much ambient light is added to the oscilloscope display.", "ambient", - VERSION_HINT, 0.7, 0.0, 5.0 - ) - ); - std::shared_ptr smoothEffect = std::make_shared( - std::make_shared(), - new osci::EffectParameter( - "Smoothing", - "This works as a low-pass frequency filter, effectively reducing the sample rate of the audio being visualised.", - "visualiserSmoothing", - VERSION_HINT, 0, 0.0, 1.0 + VERSION_HINT, 0.0, 0.0, 5.0 ) ); + std::shared_ptr smoothEffect = SmoothEffect("visualiser", 0.0f).build(); std::shared_ptr sweepMsEffect = std::make_shared( new osci::EffectParameter( "Sweep (ms)", diff --git a/modules/osci_render_core b/modules/osci_render_core index 913c3b0..fedc246 160000 --- a/modules/osci_render_core +++ b/modules/osci_render_core @@ -1 +1 @@ -Subproject commit 913c3b052404818c45ad93c5e6022244d57df5e9 +Subproject commit fedc2463e64e13f1348d49ae6504d31c9db41cc0 diff --git a/osci-bitcrush.jucer b/osci-bitcrush.jucer index 4cb31bc..22f901c 100644 --- a/osci-bitcrush.jucer +++ b/osci-bitcrush.jucer @@ -51,14 +51,23 @@ file="Resources/sosci/vector_display.sosci"/> + + + + + + + + + @@ -70,11 +79,21 @@ + + + + + + + + + diff --git a/osci-render.jucer b/osci-render.jucer index 020d707..37cbaba 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -46,15 +46,26 @@ resource="1" file="Resources/oscilloscope/vector_display_reflection.png"/> + + + + + + + + + + @@ -66,11 +77,22 @@ + + + + + + + + + + @@ -92,18 +114,27 @@ + + + + + + + @@ -156,8 +187,20 @@ file="Source/components/EffectsListComponent.cpp"/> + + + + + + @@ -660,6 +703,7 @@ + + + @@ -758,6 +804,7 @@ useGlobalPath="0"/> + diff --git a/sosci.jucer b/sosci.jucer index f46a379..87bab75 100644 --- a/sosci.jucer +++ b/sosci.jucer @@ -50,15 +50,23 @@ file="Resources/sosci/vector_display.sosci"/> + + + + + + + + @@ -70,11 +78,21 @@ + + + + + + + + +