Help with TreeView and the UndoManager Tutorial

Hello,

I don’t understand the UndoManager Tutorial completly. I understand how to add an UndoManager but I don’t understand the structure of the code. I actually don’t need an undoManager but I need the TreeList. For my app I need a list of Tracknames which can be dragged around and deleted. That is actually all implemented in the TreeView in the undomanager tutorial. I’m stuck to transfer the code into my project. So far, I have the component visible in my app but the functionality is wrong. If I just drag an item from top to bottom, all the items underneath are deleted.
If I don’t use the ValueTree created in my project and create a new ValueTree, the functionality works fine.

FileViewComponent::FileViewComponent(juce::ValueTree& vT)
{
    addAndMakeVisible(tree);
    tree.setDefaultOpenness(true);
    tree.setMultiSelectEnabled(true);
    vT.setProperty("name", "ValueTree", nullptr);

    //rootItem.reset(new FileViewItem(vT)); <---- Using my Project Value Tree(Buggy)

    rootItem.reset(new FileViewItem(createTree("TEST"))); // <----- Test Value Tree(works fine)
    tree.setRootItem(rootItem.get());
    setSize(600, 400);
}

Here is my FileViewItem:

FileViewItem::FileViewItem(const juce::ValueTree& v): tree(v)
    {
        tree.addListener(this);
    }

    juce::String FileViewItem::getUniqueName() const
    {
        return tree["name"].toString();
    }

    bool FileViewItem::mightContainSubItems()
    {
        return tree.getNumChildren() > 0;
    }

    void FileViewItem::paintItem(juce::Graphics& g, int width, int height)
    {
        g.setColour(juce::Colours::white);
        g.setFont(15.0f);

        g.drawText(tree["name"].toString(),
            4, 0, width - 4, height,
            juce::Justification::centredLeft, true);
    }

    void FileViewItem::itemOpennessChanged(bool isNowOpen)
    {
        if (isNowOpen && getNumSubItems() == 0)
            refreshSubItems();
        else
            clearSubItems();
    }

    juce::var FileViewItem::getDragSourceDescription()
    {
        return "Drag Source";
    }

    bool FileViewItem::isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails)
    {
        return dragSourceDetails.description == "Drag Source";
    }

    void FileViewItem::itemDropped(const juce::DragAndDropTarget::SourceDetails&, int insertIndex)
    {
        juce::OwnedArray<juce::ValueTree> selectedTrees;
        getSelectedTreeViewItems(*getOwnerView(), selectedTrees);
        moveItems(*getOwnerView(), selectedTrees, tree, insertIndex);
    }

    void FileViewItem::moveItems(juce::TreeView& treeView, const juce::OwnedArray<juce::ValueTree>& items,
        juce::ValueTree newParent, int insertIndex)
    {
        DBG("Das ist der Tree vor dem Move: " << items[0]->getParent().toXmlString());
        if (items.size() > 0)
        {
            std::unique_ptr<juce::XmlElement> oldOpenness(treeView.getOpennessState(false));
            DBG("This is old Openness: " << oldOpenness->toString());
            for (auto i = items.size(); --i >= 0;)
            {
                auto& v = *items.getUnchecked(i);
                DBG("This is unchecked: " << v.toXmlString());
                if (v.getParent().isValid() && newParent != v && !newParent.isAChildOf(v))
                {
                    if (v.getParent() == newParent && newParent.indexOf(v) < insertIndex)
                        --insertIndex;

                    v.getParent().removeChild(v, nullptr);
                    newParent.addChild(v, insertIndex, nullptr);
                }
            }

            if (oldOpenness != nullptr)
                treeView.restoreOpennessState(*oldOpenness, false);
        }
        DBG("Tree after movin: " << items[0]->getParent().toXmlString());
    }

    void FileViewItem::getSelectedTreeViewItems(juce::TreeView& treeView, juce::OwnedArray<juce::ValueTree>& items)
    {
        auto numSelected = treeView.getNumSelectedItems();

        for (auto i = 0; i < numSelected; ++i)
            if (auto* vti = dynamic_cast<FileViewItem*> (treeView.getSelectedItem(i)))
                items.add(new juce::ValueTree(vti->tree));
    }

    void FileViewItem::refreshSubItems()
    {
        clearSubItems();

        for (auto i = 0; i < tree.getNumChildren(); ++i)
            addSubItem(new FileViewItem(tree.getChild(i)));
    }

    void FileViewItem::valueTreePropertyChanged(juce::ValueTree&, const juce::Identifier&)
    {
        repaintItem();
    }

    void FileViewItem::treeChildrenChanged(const juce::ValueTree& parentTree)
    {
        if (parentTree == tree)
        {
            refreshSubItems();
            treeHasChanged();
            setOpen(true);
        }
    }

    void FileViewItem::addItem(juce::ValueTree vT)
	{
        DBG("Adding Item: " << vT.getType().toString() << " to: " << tree.getType().toString());
		tree.addChild(vT, -1, nullptr);
	}

So as you can see, I actually didn’t change anything compared to the tutorial. The issue of deleting the bottom childs arises somewhere in the moveItem function, I guess. As you can see my DBG marcos, I was checking how the Tree looks before and after. The following shows the console outs for the project ValueTree. This is not as intended:

<Channel_0 name="ValueTree" ChannelNumber="1" State="Stopped" Output="4"
           Input="2">
  <Item name="path\to\file.wav"/>
  <Item name="path\to\file.wav"/>
  <Item name="path\to\file.wav"/>
</Channel_0>

Das ist die alte Openness: <?xml version="1.0" encoding="UTF-8"?>

<OPEN id="ValueTree">
  <SELECTED id="/ValueTree/path\to\file.wav"/>
</OPEN>

Das sind die unchecked: <?xml version="1.0" encoding="UTF-8"?>

<Item name=""path\to\file.wav""/>

Das ist der Tree nach dem Move: <?xml version="1.0" encoding="UTF-8"?>

<Channel_0 name="ValueTree" ChannelNumber="1" State="Stopped" Output="4"
           Input="2">
  <Item name=""path\to\file.wav""/>
</Channel_0>

But when I use the Test Tree, it’s all as expected:

Tree before Move: <?xml version="1.0" encoding="UTF-8"?>

<Item name="TEST">
  <Item name="C:\Users\Lazlo\Desktop\5 Drumgroove 1.wav"/>
  <Item name="C:\Users\Lazlo\Desktop\6 Drumgroove 2.wav"/>
  <Item name="C:\Users\Lazlo\Desktop\7 cello.wav"/>
  <Item name="C:\Users\Lazlo\Desktop\cello.wav"/>
</Item>

Das ist die alte Openness: <?xml version="1.0" encoding="UTF-8"?>

<OPEN id="TESTEN">
  <SELECTED id="/TESTEN/Path/TO/FILE"/>
</OPEN>

Das sind die unchecked: <?xml version="1.0" encoding="UTF-8"?>

<Item name="path\to\file.wav"/>

Das ist der Tree nach dem Move: <?xml version="1.0" encoding="UTF-8"?>

<Item name="TEST">
  <Item name="path\to\file.wav"/>
  <Item name="path\to\file.wav"/>
  <Item name="path\to\file.wav"/>
  <Item name="path\to\file.wav"/>
</Item>

I can’t find any differences apart from that my Project ValueTree doesn’t have the Name “Item” but also can not verify that it’s really the issue. Does anybody see the issue or has a hint for me? I’m stuck here since 3 days now :frowning:
Cheers,

LaZzle

So I hope there is somebody who could take a look and help me. I provided some code now, hope that helps understanding the issue.

I’m still searching for a solution. I just need a List, where I can drag the items with a mouse, do a right click for more options and delete a list item with backspace or right click → delete. The list needs to be connected to my Value Tree, to update if a file is deleted from the list or the order of files is changed. The list is like a cue list for files which are going to be played on a transport source. Does somebody has a recommendation what to use best for this usecase? Or is a usual list enough? I thought a treeview is the best option but it’s really hard to implement it correctly.

What kind of object is “tree”. And what does createTree() return?

In

FileViewItem::FileViewItem(const juce::ValueTree& v): tree(v)

the var v is a reference. You need to make sure it’s alive (in the calling function) throughout the lifetime of FileViewItem. Perhaps it’s better to use a copy of v instead like

FileViewItem::FileViewItem(const juce::ValueTree v): tree(v)

It all depends on the context in which FileViewItem lives.
That’s all that comes in mind right now…

Hey oxxyyd,

thanks for the reply. Yesterday I found the issue. I forgot to think about that not only my TreeView gets triggered when I remove a TreeItem. In my case the issue was that my logic of reordering the TreeView triggered in another class valueTreeChildRemoved() and in this method I was acidentally deleting the ValueTree Child I actually only wanted to move. So that’s why it always happened that the childs where deleted when I dragged a TreeViewItem to the bottom of the TreeView.

I think what I learned from that is, even if I think the issue arises only in the class I’m working on right now, I should at least take a look in the other classes after searching for a while without a result. Value Trees can really make a programm act completely weird by only changing a little bit of code.

I think the thread can be closed.

Once again, thank you for having a look @oxxyyd :slight_smile: