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 @@
+
\ 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 @@
+
+
+
+
+
+
+
+