ListBox Drag-To-Reorder [Solved]

I’m trying to figure out how to get Drag-To-Reorder working with the ListBox class.
Here’s what I’m doing:
MainComponent.h

#ifndef MAINCOMPONENT_H_INCLUDED
#define MAINCOMPONENT_H_INCLUDED

#include "../JuceLibraryCode/JuceHeader.h"

struct ListBoxItem : public Component, public DragAndDropTarget
{
    ListBoxItem(int id) : idNum(id)
    {
        Random r;
        Colour _c((uint8)r.nextInt({50,255}),
                 (uint8)r.nextInt({50,255}),
                 (uint8)r.nextInt({50,255}));
        c = _c;
        setInterceptsMouseClicks(false, false);
    }

    ListBoxItem(const ListBoxItem& other)
    {
        c = other.c;
        idNum = other.idNum;
        setInterceptsMouseClicks(false, false);
    }

    void paint(Graphics& g) override
    {
        g.fillAll(c);
        g.setColour(Colours::black);
        g.drawText(String(idNum), getLocalBounds(), Justification::centred);
    }
    bool isInterestedInDragSource (const SourceDetails &dragSourceDetails) override
    {
        return true;
    }

    void itemDragEnter (const SourceDetails &dragSourceDetails) override
    {
        DBG( "itemDragEnter" );
    }
    void itemDragMove (const SourceDetails &dragSourceDetails) override
    {
        DBG( "itemDragMove" );
    }
    void itemDragExit (const SourceDetails &dragSourceDetails) override
    {
        DBG( "itemDragExit" );
    }
    void itemDropped (const SourceDetails &dragSourceDetails) override
    {
        DBG( "item dropped: " << dragSourceDetails.description.toString() );
    }
    bool shouldDrawDragImageWhenOver () override
    {
        return true;
    }
    int idNum;
    Colour c;
};

struct Rules : public ListBoxModel
{
    int getNumRows() override
    {
        return items.size();
    }
    void paintListBoxItem(int rowNumber,
                          Graphics &g,
                          int width,
                          int height,
                          bool rowIsSelected) override
    {
    }
    var getDragSourceDescription (const SparseSet< int > &rowsToDescribe) override
    {
        return "Rules";
    }
    Component* refreshComponentForRow (int rowNumber,
                                       bool isRowSelected,
                                       Component *existingComponentToUpdate) override
    {
        //ScopedPointer<Component> toBeDeleted(existingComponentToUpdate);
        if( rowNumber < items.size() )
        {
            return &items[rowNumber];
        }
        return nullptr;
    }
    std::vector< ListBoxItem > items
    {
        {0},{1},{2},{3}
    };
};

struct ListBoxContainer : public Component, public DragAndDropContainer
{
    ListBoxContainer()
    {
        listBox.setModel(&rules);
        addAndMakeVisible(listBox);
    }
    void paint(Graphics& g) override
    {
        g.fillAll(Colours::white);
        g.setColour(Colours::red);
        g.drawRect(getLocalBounds());
    }
    void resized() override
    {
        listBox.setBounds(getLocalBounds().reduced(2) );
    }
    void dragOperationStarted (const DragAndDropTarget::SourceDetails &details) override
    {
        DBG("dragOperationStarted " << details.description.toString() );
    }
    void dragOperationEnded (const DragAndDropTarget::SourceDetails &details) override
    {
        DBG("dragOperationEnded " << details.description.toString() );
    }
    Rules rules;
    ListBox listBox;
};

//==============================================================================
/*
    This component lives inside our window, and this is where you should put all
    your controls and content.
*/
class MainContentComponent   : public Component
{
public:
    //==============================================================================
    MainContentComponent();
    ~MainContentComponent();

    void paint (Graphics&) override;
    void resized() override;

private:
    ListBoxContainer listBoxContainer;
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};


#endif  // MAINCOMPONENT_H_INCLUDED

and the MainComponent.cpp:

#include "MainComponent.h"


//==============================================================================
MainContentComponent::MainContentComponent()
{
    addAndMakeVisible(listBoxContainer);
    setSize (600, 400);
}

MainContentComponent::~MainContentComponent()
{
}

void MainContentComponent::paint (Graphics& g)
{
    g.fillAll (Colour (0xff001F36));

    g.setFont (Font (16.0f));
    g.setColour (Colours::white);
    g.drawText ("Hello World!", getLocalBounds(), Justification::centred, true);
}

void MainContentComponent::resized()
{
    // This is called when the MainContentComponent is resized.
    // If you add any child components, this is where you should
    // update their positions.
    listBoxContainer.setBounds(20, 20, 100, 100);
}

When I drag one of the list box items, none of the DBG messages appear. What’s the problem? Shown here dragging item #1 and the DBG output that appears…
39%20AM

Ok, this is solved. Here’s a git repo for anyone that wants to see the solution for ‘Drag-To-Reorder’ your list box. Helpful people were @'d in the code

4 Likes

Thanks a lot for sharing this!

you’re welcome!

Many thanks to @matkatmusic for providing detailed example code. I’ve taken this a few steps further at https://github.com/getdunne/juce-draggableListBox.

6 Likes

Thanks for your amazing work on this! Seems to work brilliantly!
I’m trying to adapt this for my own purposes, where I’d like to have different custom components in each row. Or at least be able to change the background colour etc. or text within a custom class. I’m not working out how to access the subcomponents. Is there anything you could point me to? Thanks!

Hi Jamie. You’re on your own here, I’m afraid. I don’t have any useful pointers, and this is a pretty advanced JUCE technique, so you likely won’t find any relevant sample code around.

Use your model data to decide what type of component to return, and construct the appropriate class type accordingly in the function that returns the row.

1 Like

I am trying to edit this code to allow for rowSelection so that I can add Duplicate and Copy/Paste Functionality for Multiple rows at a time. I am trying to add an indication of selection by editing mouseDown() on the DraggableListBoxItem

void DraggableListBoxItem::mouseDown(const MouseEvent &e)
{
    listBox.selectRow(rowNum, false, true);
}

which is painted in the pain function trivially

void DraggableListBoxItem::paint(Graphics& g)
{
    modelData.paintContents(rowNum, g, getLocalBounds());
  
    ...
    if(listBox.isRowSelected(rowNum))
    {
        g.setColour(Colours::yellow);
        g.drawRect(getLocalBounds(),5);
    }
 
}

This accomplishes the task of display selection but breaks the drag. You now must select then drag but what I want is to be able to select and drag at once.

Current User Interaction:
MouseDown() - selects and adds yellow border to indicate
MouseDown()
MouseDrag() - begins moving object

Desired User Interaction
MouseDown () - selects and adds yellow border to indicate
MouseDrag () - begins moving object

If I remove my code from the mouseDown() function it works fine but doesn’t have my selection logic obviously. I imagine that this is due to functions that happen in ListBox::selectRowInternal() but I am just curious if anyone has any thoughts

Below is my rendition of the DraggableListBox, and a small app to try it out.

Thanks to all who contributed here for their efforts! I refactored until it was something I can understand.

Enjoy!


#pragma once

#include <JuceHeader.h>
class DraggableListBox : public Component, public DragAndDropContainer, public AsyncUpdater
{
public:
    DraggableListBox(std::vector<std::string>& data) : draggableListBoxModel(*this, data)
    {
        setAlwaysOnTop(true);
        listBox.setModel(&draggableListBoxModel);
        listBox.setRowHeight(25);
        addAndMakeVisible(listBox);
        addAndMakeVisible(closeButton);
        closeButton.onClick = [this]() { setVisible(false);  };
        setSize(200, 400);
    }

    void handleAsyncUpdate() override
    {
        listBox.updateContent();
    }

private:
    // *********************************************************************************************************************************************
    class DraggableListBoxModel : public ListBoxModel
    {
    public:
        DraggableListBoxModel(DraggableListBox& dragBox, std::vector<std::string >& data ) : draggableListBox(dragBox), dataVector(data) { }

        int getNumRows() override { return int(dataVector.size()); }

    private:
        Component* refreshComponentForRow(int rowNumber, bool /*isRowSelected*/, Component* existingComponentToUpdate) override
        {
            std::unique_ptr<ListBoxItem> item(dynamic_cast<ListBoxItem*>(existingComponentToUpdate));

            if (isPositiveAndBelow(rowNumber, dataVector.size()))
                item = std::make_unique<ListBoxItem>(rowNumber, Colours::green, *this, rowNumber);
            return item.release();
        }

        void paintListBoxItem(int /*rowNumber*/, Graphics& /*g*/, int /*width*/, int /*height*/, bool /*rowIsSelected*/) override { }

        // *********************************************************************************************************************************************
        class ListBoxItem : public Component, public DragAndDropTarget
        {
        public:
            ListBoxItem(int id, Colour colr, DraggableListBoxModel& trkLstModl, int rwNm) : idNum(id), colour(colr), draggableListBoxModel(trkLstModl), rowNum(rwNm)
            {
            }
        private:
            void paint(Graphics& g) override
            {
                g.fillAll(colour);
                g.setColour(colour.contrasting());
                g.drawFittedText(draggableListBoxModel.dataVector[idNum], getLocalBounds(), Justification::centred, 1);
                g.setColour(juce::Colours::black);
                g.drawRect(getLocalBounds());

                if (displayInsertLine)
                {
                    g.setColour(Colours::white);
                    g.fillRect(0, (insertBefore ? 0 : (getHeight() - 3)), getWidth(), 3);
                }
            }

            void updateInsertLine(const SourceDetails& dragSourceDetails)
            {
                displayInsertLine = true;
                insertBefore = (dragSourceDetails.localPosition.y < (getHeight() / 2));
                repaint();
            }

            void hideInsertLine()
            {
                displayInsertLine = false;
                repaint();
            }

            void itemDragEnter(const SourceDetails& dragSourceDetails) override { updateInsertLine(dragSourceDetails); }

            void itemDragMove(const SourceDetails& dragSourceDetails) override { updateInsertLine(dragSourceDetails); }

            void itemDragExit(const SourceDetails& /*dragSourceDetails*/) override { hideInsertLine(); }

            void itemDropped(const SourceDetails& dragSourceDetails) override
            {
                if (ListBoxItem* item = dynamic_cast<ListBoxItem*>(dragSourceDetails.sourceComponent.get()))
                {
                    auto& data{ draggableListBoxModel.dataVector };
                    auto value{ data[item->rowNum] };// save before erasing
                    data.erase(data.begin() + item->rowNum);
                    data.insert((data.begin() + rowNum), value);
                    draggableListBoxModel.draggableListBox.triggerAsyncUpdate();
                }
                hideInsertLine();
            }

            void mouseDrag(const MouseEvent& /*event*/) override
            {
                if (DragAndDropContainer* container = DragAndDropContainer::findParentDragContainerFor(this))
                    container->startDragging(var(), this);
            }

            bool isInterestedInDragSource(const SourceDetails& /*dragSourceDetails*/) override { return true; }

            int idNum;
            Colour colour;
            bool displayInsertLine{ false };
            bool insertBefore{ false };
            DraggableListBoxModel& draggableListBoxModel;
            int rowNum;
        };// ListBoxItem

        std::vector<std::string >& dataVector;
        DraggableListBox& draggableListBox;

    };// DraggableListBoxModel

    // *********************************************************************************************************************************************
    void paint(Graphics& g) override { g.fillAll(); } // needed to be opaque

    void resized() override
    {
        auto localBounds{ getLocalBounds() };
        listBox.setBounds(localBounds.removeFromTop(int(getHeight() * 0.95)));
        closeButton.setBounds(localBounds);
    }

    DraggableListBoxModel draggableListBoxModel;
    ListBox listBox;
    TextButton closeButton{ "Close" };
};

And then the code for the small app to try it out. It is simply a juce GUI app.

#include <JuceHeader.h>
#include "DraggableListBox.h"

class MainComponent : public juce::Component
{
public:
    MainComponent()
    {
        addAndMakeVisible(draggableListBox);
        setSize(200, 400);
    }

private:
    void paint(juce::Graphics& g) override
    {
        g.fillAll();
        g.setFont(juce::Font(16.0f));
        g.setColour(juce::Colours::white);
        g.drawText("DraggableListBox Demo", getLocalBounds(), juce::Justification::centred, true);
    }

    void resized() override
    {
        draggableListBox.setBounds(getLocalBounds());
    }

    std::vector<std::string> vectorData = { "one", "two", "three", "four", "five", "six" };
    DraggableListBox draggableListBox{ vectorData };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
};

class DraggableListBoxApplication  : public juce::JUCEApplication
{
public:
    DraggableListBoxApplication() {}

    const juce::String getApplicationName() override       { return ProjectInfo::projectName; }
    const juce::String getApplicationVersion() override    { return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override             { return false; }

    void initialise (const juce::String& /*commandLine*/) override
    {
         mainWindow.reset (new MainWindow (getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr; // (deletes our window)
    }

    void systemRequestedQuit() override
    {
        quit();
    }

    void anotherInstanceStarted (const juce::String& /*commandLine*/) override
    {
    }

    class MainWindow    : public juce::DocumentWindow
    {
    public:
        MainWindow (juce::String name)
            : DocumentWindow (name, Colours::grey, DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar (true);
            setContentOwned (new MainComponent(), true);
            setResizable (true, true);
            centreWithSize (getWidth(), getHeight());
            setVisible (true);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }
    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };

private:
    std::unique_ptr<MainWindow> mainWindow;
};
START_JUCE_APPLICATION (DraggableListBoxApplication)
2 Likes