ValueTreeDemo questions

I’m messing with the ValueTreeDemo, adding some functionality to it. there are a couple things that it does (or doesn’t do) that I can’t figure out why. Anyone have any ideas why?

  1. if a node has children, you can’t select it.
  2. if you drag a node with children, it’s not visually indicated that you’re also dragging the children.

here’s my update in PIP format with a custom component that allows you to edit the node name, and includes a checkbox.

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             ValueTreesDemo
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Showcases value tree features.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics
 exporters:        xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             Component
 mainClass:        ValueTreesDemo

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "DemoUtilities.h"
struct ValueTreeItem;
struct ValueTreeItemComponent : public Component
{
    ValueTreeItemComponent(ValueTree vt, ValueTreeItem* o) : tree(vt), owner(o)
    {
        label.getTextValue().referTo( tree.getPropertyAsValue("name", nullptr) );
        addAndMakeVisible(label);
        label.setInterceptsMouseClicks(false, false);
        
        label.onEditorHide = [this]()
        {
            if( label.getText().isEmpty() )  //no text?  delete this node
            {
                tree.getParent().removeChild(tree, nullptr);
            }
        };
        
        toggleButton.getToggleStateValue().referTo( tree.getPropertyAsValue("completed", nullptr ) );
        addAndMakeVisible(toggleButton);
    }
    void paint(Graphics& g) override;
    
    void resized() override
    {
        auto bounds = getLocalBounds().reduced(2);
        auto toggleButtonBounds = bounds.removeFromLeft( bounds.getHeight() );
        toggleButton.setBounds(toggleButtonBounds);
        bounds.removeFromLeft(5);
        label.setBounds(bounds);
    }
    ValueTree tree;
    ValueTreeItem* owner;
    Label label;
    ToggleButton toggleButton;
    
    void mouseDown(const MouseEvent& e) override;
    void mouseDrag(const MouseEvent& e) override;
    void mouseDoubleClick(const MouseEvent& e) override;
//    void mouseUp(const MouseEvent& e) override;
};
//==============================================================================
class ValueTreeItem  : public TreeViewItem,
                       private ValueTree::Listener
{
public:
    ValueTreeItem (const ValueTree& v, UndoManager& um)
        : tree (v), undoManager (um)
    {
        tree.addListener (this);
    }
    
    Component* createItemComponent() override
    {
        return new ValueTreeItemComponent(tree, this);
    }

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

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

    int getItemHeight() const override { return 30; }

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

    var getDragSourceDescription() override
    {
        return "Drag Demo";
    }

    bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails) override
    {
        return dragSourceDetails.description == "Drag Demo";
    }

    void itemDropped (const DragAndDropTarget::SourceDetails&, int insertIndex) override
    {
        OwnedArray<ValueTree> selectedTrees;
        getSelectedTreeViewItems (*getOwnerView(), selectedTrees);

        moveItems (*getOwnerView(), selectedTrees, tree, insertIndex, undoManager);
    }

    static void moveItems (TreeView& treeView, const OwnedArray<ValueTree>& items,
                           ValueTree newParent, int insertIndex, UndoManager& undoManager)
    {
        if (items.size() > 0)
        {
            std::unique_ptr<XmlElement> oldOpenness (treeView.getOpennessState (false));

            for (auto* v : items)
            {
                if (v->getParent().isValid() && newParent != *v && ! newParent.isAChildOf (*v))
                {
                    if (v->getParent() == newParent && newParent.indexOf (*v) < insertIndex)
                        --insertIndex;

                    v->getParent().removeChild (*v, &undoManager);
                    newParent.addChild (*v, insertIndex, &undoManager);
                }
            }

            if (oldOpenness.get() != nullptr)
                treeView.restoreOpennessState (*oldOpenness, false);
        }
    }

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

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

private:
    ValueTree tree;
    UndoManager& undoManager;

    void refreshSubItems()
    {
        clearSubItems();

        for (int i = 0; i < tree.getNumChildren(); ++i)
            addSubItem (new ValueTreeItem (tree.getChild (i), undoManager));
    }

    void valueTreePropertyChanged (ValueTree&, const Identifier&) override
    {
        repaintItem();
    }

    void valueTreeChildAdded (ValueTree& parentTree, ValueTree&) override         { treeChildrenChanged (parentTree); }
    void valueTreeChildRemoved (ValueTree& parentTree, ValueTree&, int) override  { treeChildrenChanged (parentTree); }
    void valueTreeChildOrderChanged (ValueTree& parentTree, int, int) override    { treeChildrenChanged (parentTree); }
    void valueTreeParentChanged (ValueTree&) override {}

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

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueTreeItem)
};

//==============================================================================
void ValueTreeItemComponent::paint(Graphics& g)
{
    if( owner->isSelected())
    {
        g.fillAll(Colours::red);
    }
}
void ValueTreeItemComponent::mouseDown(const MouseEvent& e)
{
    DBG( "mouseDown: " << tree["name"].toString() );
    owner->setSelected(true, true);
}
void ValueTreeItemComponent::mouseDrag(const MouseEvent& e)
{
    DragAndDropContainer* dragC = DragAndDropContainer::findParentDragContainerFor(this);
    if( !dragC->isDragAndDropActive() )
    {
        dragC->startDragging(owner->getDragSourceDescription(), this);
    }
}
void ValueTreeItemComponent::mouseDoubleClick(const MouseEvent& e)
{
    label.showEditor();
}
//==============================================================================
class ValueTreesDemo   : public Component,
                         public DragAndDropContainer,
                         private Timer
{
public:
    ValueTreesDemo()
    {
        addAndMakeVisible (tree);

        tree.setDefaultOpenness (true);
        tree.setMultiSelectEnabled (true);
        rootItem.reset (new ValueTreeItem (createRootValueTree(), undoManager));
        tree.setRootItem (rootItem.get());
        tree.setRootItemVisible(false);

        addAndMakeVisible (undoButton);
        addAndMakeVisible (redoButton);
        undoButton.onClick = [this] { undoManager.undo(); };
        redoButton.onClick = [this] { undoManager.redo(); };

        startTimer (500);
        setSize (500, 500);
    }

    ~ValueTreesDemo()
    {
        tree.setRootItem (nullptr);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
    }

    void resized() override
    {
        auto r = getLocalBounds().reduced (8);

        auto buttons = r.removeFromBottom (22);
        undoButton.setBounds (buttons.removeFromLeft (100));
        buttons.removeFromLeft (6);
        redoButton.setBounds (buttons.removeFromLeft (100));

        r.removeFromBottom (4);
        tree.setBounds (r);
    }

    static ValueTree createTree (const String& desc)
    {
        ValueTree t ("Item");
        t.setProperty ("name", desc, nullptr);
        t.setProperty("completed", false, nullptr );
        return t;
    }

    static ValueTree createRootValueTree()
    {
        auto vt = createTree ("This demo displays a ValueTree as a treeview.");
        vt.appendChild (createTree ("You can drag around the nodes to rearrange them"),               nullptr);
        vt.appendChild (createTree ("..and press 'delete' to delete them"),                           nullptr);
        vt.appendChild (createTree ("Then, you can use the undo/redo buttons to undo these changes"), nullptr);

        int n = 1;
        vt.appendChild (createRandomTree (n, 0), nullptr);

        return vt;
    }

    static ValueTree createRandomTree (int& counter, int depth)
    {
        auto t = createTree ("Item " + String (counter++));

        if (depth < 3)
            for (int i = 1 + Random::getSystemRandom().nextInt (7); --i >= 0;)
                t.appendChild (createRandomTree (counter, depth + 1), nullptr);

        return t;
    }

    void deleteSelectedItems()
    {
        OwnedArray<ValueTree> selectedItems;
        ValueTreeItem::getSelectedTreeViewItems (tree, selectedItems);

        for (auto* v : selectedItems)
        {
            if (v->getParent().isValid())
                v->getParent().removeChild (*v, &undoManager);
        }
    }

    bool keyPressed (const KeyPress& key) override
    {
        if (key == KeyPress::deleteKey || key == KeyPress::backspaceKey)
        {
            deleteSelectedItems();
            return true;
        }

        if (key == KeyPress ('z', ModifierKeys::commandModifier, 0))
        {
            undoManager.undo();
            return true;
        }

        if (key == KeyPress ('z', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0))
        {
            undoManager.redo();
            return true;
        }

        return Component::keyPressed (key);
    }
private:
    TreeView tree;
    TextButton undoButton  { "Undo" },
               redoButton  { "Redo" };

    std::unique_ptr<ValueTreeItem> rootItem;
    UndoManager undoManager;

    void timerCallback() override
    {
        undoManager.beginNewTransaction();
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueTreesDemo)
};
1 Like

It’s likely that that example has simply not been looked at for a while. Your changes look sensible.

I was more asking how to make those things happen. the problem with #1 seems to be an issue with the TreeView design itself when you have Components being used instead of paintItem

I can select nodes with children (and see that they’re highlighted, move them, delete them) in the demo…

thanks for pushing those fixes to the demo. it doesn’t address the issue of node selection when you’re using a component instead of paintItem though.

run my PIP or add this. you’ll see what i mean.

struct ValueTreeItem;
struct ValueTreeItemComponent : public Component
{
    ValueTreeItemComponent(ValueTree vt, ValueTreeItem* o) : tree(vt), owner(o)
    {
        label.getTextValue().referTo( tree.getPropertyAsValue("name", nullptr) );
        addAndMakeVisible(label);
        label.setInterceptsMouseClicks(false, false);
        
        label.onEditorHide = [this]()
        {
            if( label.getText().isEmpty() )  //no text?  delete this node
            {
                tree.getParent().removeChild(tree, nullptr);
            }
        };
        
        toggleButton.getToggleStateValue().referTo( tree.getPropertyAsValue("completed", nullptr ) );
        addAndMakeVisible(toggleButton);
    }
    void paint(Graphics& g) override;
    
    void resized() override
    {
        auto bounds = getLocalBounds().reduced(2);
        auto toggleButtonBounds = bounds.removeFromLeft( bounds.getHeight() );
        toggleButton.setBounds(toggleButtonBounds);
        bounds.removeFromLeft(5);
        label.setBounds(bounds);
    }
    ValueTree tree;
    ValueTreeItem* owner;
    Label label;
    ToggleButton toggleButton;
    
    void mouseDown(const MouseEvent& e) override;
    void mouseDrag(const MouseEvent& e) override;
    void mouseDoubleClick(const MouseEvent& e) override;
//    void mouseUp(const MouseEvent& e) override;
};

and this

    Component* createItemComponent() override
    {
        return new ValueTreeItemComponent(tree, this);
    }

and this

void ValueTreeItemComponent::paint(Graphics& g)
{
    if( owner->isSelected())
    {
        g.fillAll(Colours::red);
    }
}
void ValueTreeItemComponent::mouseDown(const MouseEvent& e)
{
    DBG( "mouseDown: " << tree["name"].toString() );
    owner->setSelected(true, true);
}
void ValueTreeItemComponent::mouseDrag(const MouseEvent& e)
{
    DragAndDropContainer* dragC = DragAndDropContainer::findParentDragContainerFor(this);
    if( !dragC->isDragAndDropActive() )
    {
        dragC->startDragging(owner->getDragSourceDescription(), this);
    }
}
void ValueTreeItemComponent::mouseDoubleClick(const MouseEvent& e)
{
    label.showEditor();
}

when i click a node with children, i see this. check out the position of the insert line:

image

but clicking on a node without children positions the insert line below it, like it’s trying to insert it as a child of itself:

image

i’m not sure it’s related to the problem, but it’s weird behavior.

I’m still a bit confused as to what you’re saying. With your PIP you have just shown that you can select nodes with children. So when do you get the selection discrepancy between nodes with and without children?

a 30second vid will explain

Ah, I see!

That looks like it’s something amiss with mouseDrag handling - macOS sends a mouseDrag immediately after a click, which is cancelling the selection.

1 Like

So what would you do to solve it? just set a flag in mouseDown to block that first mouse drag?

Checking MouseEvent::mouseWasDraggedSinceMouseDown() helps ignore the initial event on macOS

SOVLED!!

void ValueTreeItemComponent::mouseDown(const MouseEvent& e)
{
    DBG( "mouseDown: " << tree["name"].toString() );
    owner->setSelected(true, true);
    blockDrag = true;
}
void ValueTreeItemComponent::mouseDrag(const MouseEvent& e)
{
    if( blockDrag == true )
    {
        blockDrag = false;
    }
    else
    {
        DragAndDropContainer* dragC = DragAndDropContainer::findParentDragContainerFor(this);
        if( !dragC->isDragAndDropActive() )
        {
            dragC->startDragging(owner->getDragSourceDescription(), this);
        }
    }
}

where would that get checked?

In mouseDrag(), so you could simply do:

void ValueTreeItemComponent::mouseDrag(const MouseEvent& e)
{
    if (e.mouseWasDraggedSinceMouseDown())
    {
        DragAndDropContainer* dragC = DragAndDropContainer::findParentDragContainerFor(this);

        if (!dragC->isDragAndDropActive())
            dragC->startDragging(owner->getDragSourceDescription(), this);
    }
}
1 Like

Thank you @t0m @TonyAtHarrison for the assist!!! I don’t think I would have solved this otherwise and would have blamed TreeView as faulty if you use components instead of paintItem.

1 Like

You guys should update the valueTrees demo to show it using a Component.

Alright, so I am struggling with this. I’m trying to make it so that when I click one of these checkboxes, the parents/children updates their (completed / total) counter, and also their checkboxes

here’s what my XML looks like:

<?xml version="1.0" encoding="UTF-8"?>

<HiddenRoot name="HiddenRoot" completed="0">
  <TaskQueue name="Test2" completed="0">
    <Task name="A" completed="1">
      <Task name="_A" completed="1"/>
    </Task>
    <Task name="B" completed="1">
      <Task name="_B" completed="1"/>
    </Task>
    <Task name="C" completed="0">
      <Task name="_C" completed="0">
        <Task name="__C" completed="0">
          <Task name="____C" completed="0"/>
        </Task>
        <Task name="__D" completed="0">
          <Task name="___D" completed="0">
            <Task name="____D" completed="0">
              <Task name="_____D" completed="0"/>
              <Task name="new task" completed="0" showEditor="1"/>
            </Task>
          </Task>
        </Task>
      </Task>
    </Task>
  </TaskQueue>
</HiddenRoot>

the toggleButtons are all referring to the completed property in their respective ValueTree node.

so, if I were to click on the ____D checkbox, toggling it on, these two checkboxes would also turn on (because the parent was marked as completed)

              <Task name="_____D" completed="0"/>
              <Task name="new task" completed="0" showEditor="1"/>

and because ____D is now completed, ___D can also be marked as completed
and because ___D is now completed, __D can also be marked complete
and because __D is now complete, _C (the parent of __D) can be updated to show that 1 of its two children is completed.

If I’m not supposed to call ‘setProperty’ in the setProperty callbacks, how am I supposed to modify the parent/child nodes?