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/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/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/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/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/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 2486d5a..48109d7 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/EffectsComponent.cpp b/Source/EffectsComponent.cpp index c94f7d5..6f7b89f 100644 --- a/Source/EffectsComponent.cpp +++ b/Source/EffectsComponent.cpp @@ -93,6 +93,12 @@ EffectsComponent::EffectsComponent(OscirenderAudioProcessor& p, OscirenderAudioP 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); @@ -140,10 +146,9 @@ void EffectsComponent::resized() { auto addBtnHeight = 44; auto listArea = area; auto buttonArea = listArea.removeFromBottom(addBtnHeight); - listArea.removeFromTop(6); listBox.setBounds(listArea); // Layout bottom fade overlay; visible if list is scrollable - layoutScrollFade(listArea, true, 48); + layoutScrollFade(listArea.withTrimmedTop(LIST_SPACER), true, 48); if (addEffectButton) { addEffectButton->setVisible(true); addEffectButton->setBounds(buttonArea.reduced(0, 4)); @@ -156,5 +161,5 @@ void EffectsComponent::changeListenerCallback(juce::ChangeBroadcaster* source) { listBox.updateContent(); // Re-layout scroll fades after content changes if (! showingGrid) - layoutScrollFade(listBox.getBounds(), true, 48); + layoutScrollFade(listBox.getBounds().withTrimmedTop(LIST_SPACER), true, 48); } diff --git a/Source/EffectsComponent.h b/Source/EffectsComponent.h index be47cb4..8ae6589 100644 --- a/Source/EffectsComponent.h +++ b/Source/EffectsComponent.h @@ -31,6 +31,8 @@ private: std::unique_ptr addEffectButton; // Separate button under the list std::unique_ptr grid; 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/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 88fa132..f94fff6 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -26,12 +26,17 @@ 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( + auto bitCrushEffect = 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( + 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)); + bitCrushEffect->setIcon(BinaryData::bitcrush_svg); + toggleableEffects.push_back(bitCrushEffect); + + auto bulgeEffect = 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))); + 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)); + bulgeEffect->setIcon(BinaryData::bulge_svg); + toggleableEffects.push_back(bulgeEffect); auto multiplexEffect = std::make_shared( std::make_shared(), std::vector{ @@ -43,23 +48,26 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Multiplex Delay", "Controls the delay of the audio samples used in the multiplex effect.", "gridDelay", VERSION_HINT, 0.0, 0.0, 1.0), }); multiplexEffect->setName("Multiplex"); - // Set up the Grid Phase parameter with sawtooth LFO at 100Hz + multiplexEffect->setIcon(BinaryData::multiplex_svg); 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( + auto vectorCancellingEffect = 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))); + 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)); + vectorCancellingEffect->setIcon(BinaryData::vectorcancelling_svg); + toggleableEffects.push_back(vectorCancellingEffect); 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 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->setName("Scale"); + scaleEffect->setIcon(BinaryData::scale_svg); scaleEffect->markLockable(true); booleanParameters.push_back(scaleEffect->linked); toggleableEffects.push_back(scaleEffect); @@ -75,6 +83,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Distort Z", "Distorts the depth of the image by jittering the audio sample being drawn.", "distortZ", VERSION_HINT, 0.1, 0.0, 1.0), }); distortEffect->setName("Distort"); + distortEffect->setIcon(BinaryData::distort_svg); distortEffect->markLockable(false); booleanParameters.push_back(distortEffect->linked); toggleableEffects.push_back(distortEffect); @@ -91,6 +100,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Ripple Amount", "Controls how many ripples are applied to the image.", "rippleAmount", VERSION_HINT, 0.1, 0.0, 1.0), }); rippleEffect->setName("Ripple"); + rippleEffect->setIcon(BinaryData::ripple_svg); rippleEffect->getParameter("ripplePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); toggleableEffects.push_back(rippleEffect); auto rotateEffect = std::make_shared( @@ -118,8 +128,9 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Translate Z", "Moves the object away from the camera.", "translateZ", VERSION_HINT, 0.0, -1.0, 1.0), }); translateEffect->setName("Translate"); + translateEffect->setIcon(BinaryData::translate_svg); toggleableEffects.push_back(translateEffect); - toggleableEffects.push_back(std::make_shared( + auto swirlEffect = 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); @@ -128,10 +139,15 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse }, 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( + }); + swirlEffect->setIcon(BinaryData::swirl_svg); + toggleableEffects.push_back(swirlEffect); + + auto smoothingEffect = 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))); + 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)); + smoothingEffect->setIcon(BinaryData::smoothing_svg); + toggleableEffects.push_back(smoothingEffect); std::shared_ptr wobble = std::make_shared( wobbleEffect, std::vector{ @@ -139,6 +155,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Wobble Phase", "Controls the phase of the wobble.", "wobblePhase", VERSION_HINT, 0.0, -1.0, 1.0), }); wobble->setName("Wobble"); + wobble->setIcon(BinaryData::wobble_svg); wobble->getParameter("wobblePhase")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); toggleableEffects.push_back(wobble); auto delay = std::make_shared( @@ -148,6 +165,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Delay Length", "Controls the time in seconds between echos.", "delayLength", VERSION_HINT, 0.5, 0.0, 1.0) }); delay->setName("Delay"); + delay->setIcon(BinaryData::delay_svg); toggleableEffects.push_back(delay); auto dashEffect = std::make_shared( dashedLineEffect, @@ -155,8 +173,14 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse new osci::EffectParameter("Dash Length", "Controls the length of the dashed line.", "dashLength", VERSION_HINT, 0.2, 0.0, 1.0), }); dashEffect->setName("Dash"); + dashEffect->setIcon(BinaryData::dash_svg); toggleableEffects.push_back(dashEffect); + + custom->setIcon(BinaryData::lua_svg); toggleableEffects.push_back(custom); + + trace->setName("Trace"); + trace->setIcon(BinaryData::trace_svg); toggleableEffects.push_back(trace); trace->getParameter("traceLength")->lfo->setUnnormalisedValueNotifyingHost((int)osci::LfoType::Sawtooth); @@ -179,6 +203,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()); diff --git a/Source/components/DraggableListBox.cpp b/Source/components/DraggableListBox.cpp index 3234ba2..b54177a 100644 --- a/Source/components/DraggableListBox.cpp +++ b/Source/components/DraggableListBox.cpp @@ -5,16 +5,8 @@ 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&) @@ -32,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); } } @@ -62,30 +57,58 @@ 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(); } } @@ -164,9 +187,116 @@ void DraggableListBoxItem::timerCallback() { 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 fafaef4..f7c88e9 100644 --- a/Source/components/DraggableListBox.h +++ b/Source/components/DraggableListBox.h @@ -18,10 +18,35 @@ struct DraggableListBoxItemData virtual void addItemAtEnd() {}; }; -// DraggableListBox is basically just a VListBox, that inherits from DragAndDropContainer. -// Declare your list box using this type. -class DraggableListBox : public VListBox, 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. @@ -77,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/VListBox.cpp b/Source/components/VListBox.cpp index a1d38b3..2c5294b 100644 --- a/Source/components/VListBox.cpp +++ b/Source/components/VListBox.cpp @@ -393,7 +393,7 @@ int VListBoxModel::getRowForPosition (int yPos) return r; } - return numRows; + return numRows - 1; } int VListBoxModel::getPositionForRow (int rowNumber) @@ -1067,4 +1067,4 @@ juce::String VListBoxModel::getTooltipForRow (int) juce::MouseCursor VListBoxModel::getMouseCursorForRow (int) { return juce::MouseCursor::NormalCursor; -} \ No newline at end of file +} diff --git a/osci-render.jucer b/osci-render.jucer index 5e8e718..2dc14f7 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -46,16 +46,23 @@ resource="1" file="Resources/oscilloscope/vector_display_reflection.png"/> + + + + + + + @@ -67,12 +74,21 @@ + + + + + + + +