Feature Request: Live Value Tree Viewer and Editor


#1

No more to say really.  It'd be very handy for configuring, experimenting and so on.  And it'd save me writing one... :)

I suppose i could hack one out of a CodeEditor and the to/from string Xml stuff...though I'm not sure what that'd do to my existing listeners.


#2

Yes, agreed it'd be very handy!


#3

Just made myself one.  I didn't bother with creating/removing or re-arranging the child nodes as it's not something I need to do.  However it's very useful!  I can see everything that's going on inside my software ... and edit the GUI parameters in real-time ... which is nice!

Here's an exciting action shot:

Issues

But - a few issues. All suggestions welcome

1. It leaks Tree View Items ... must be some idiotic mistake but I can't see it at the moment. [Solved]

2. It'd be great to be able to get information on ValueTree listeners: how many are subscribed, what the derived type of the listener is.

3. I'm in some kind of synchronous vs. asynchronous callback hell. When I get a property changed callback I want to consider rebuilding the PropertyComponent's in the property panel (in case a property has been added or removed).  

However if the change comes from me editing it in the PropertyPanel it crashes.  I'm inferring that something on the message loop makes a callback to the old, now deleted, PropertyComponent.  I've ditched refreshing the PropertyPanel  - but I'm not sure what a sensible way around this problem is?  Do I delay the refresh of PropertyPanel? Or keep the old version around for a bit...?  

The best fix would be to identify whether a property was added, removed or changed on the property tree, then based on that make the specific changes to the PropertyComponents in the property panel. But I don't think PropertyPanel lends itself to that, so I'll need to do a bit more typing...

 

/** Display a separate desktop window for viewed and editing a value tree's

 property fields.

 */

class ValueTreeEditor :

public DocumentWindow

{

    /** Display properties for a tree. */

    class PropertyEditor :

    public PropertyPanel {

    public:

        PropertyEditor()

        {

            noEditValue = "not editable";

        }

        void setSource(ValueTree & tree)

        {

            clear();


            t = tree;


            const int maxChars = 200;


            Array<PropertyComponent *> pc;

            for (int i = 0; i < t.getNumProperties(); ++i)

            {

                const Identifier name = t.getPropertyName(i).toString();

                Value v = t.getPropertyAsValue(name, nullptr);

                TextPropertyComponent * tpc;


                if (v.getValue().isObject())

                {

                    tpc = new TextPropertyComponent(noEditValue, name.toString(), maxChars, false);

                    tpc->setEnabled(false);

                }

                else

                {

                    tpc = new TextPropertyComponent(v, name.toString(), maxChars, false);

                }


                pc.add(tpc);

            }

            addProperties(pc);

        }


    private:

        Value noEditValue;

        ValueTree t;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PropertyEditor);

    };


    class Item :

    public TreeViewItem,

    public ValueTree::Listener

    {

    public:

        Item (PropertyEditor * propertiesEditor, ValueTree tree)

        :

        propertiesEditor(propertiesEditor),

        t(tree)

        {

            t.addListener(this);

        }


        ~Item()

        {

            clearSubItems();

        }


        bool mightContainSubItems()

        {

            return t.getNumChildren() > 0;

        }


        void itemOpennessChanged(bool isNowOpen)

        {

            clearSubItems();

            

            if (! isNowOpen) return;


            int children = t.getNumChildren();

            for (int i = 0; i<children; ++i)

                addSubItem(new Item(propertiesEditor, t.getChild(i)));

        }


        void paintItem(Graphics & g, int w, int h) {


            Font font;

            Font smallFont(11.0);



            if (isSelected())

                g.fillAll(Colours::white);



            const float padding = 20.0f;


            String typeName = t.getType().toString();


            const float nameWidth = font.getStringWidthFloat(typeName);

            const float propertyX = padding + nameWidth;


            g.setColour(Colours::black);


            g.setFont(font);


            g.drawText(t.getType().toString(), 0, 0, w, h, Justification::left, false);

            g.setColour(Colours::blue);


            g.setFont(smallFont);


            String propertySummary;


            for (int i = 0; i < t.getNumProperties(); ++i)

            {

                const Identifier name = t.getPropertyName(i).toString();

                propertySummary += " [" + name.toString() + "] " + t.getProperty(name).toString();

            }


            g.drawText(propertySummary, propertyX, 0, w-propertyX, h, Justification::left, true);

        }


        void itemSelectionChanged(bool isNowSelected)

        {

            if (isNowSelected)

            {

                t.removeListener(this);

                propertiesEditor->setSource(t);

                t.addListener(this);

            }

        }



        /* Enormous list of ValueTree::Listener options... */

        void valueTreePropertyChanged (ValueTree &treeWhosePropertyHasChanged, const Identifier &property)

        {

            if (t != treeWhosePropertyHasChanged) return;

            t.removeListener(this);

//            if (isSelected())

//                propertiesEditor->setSource(t);

            repaintItem();

            t.addListener(this);

        }

        void valueTreeChildAdded (ValueTree &parentTree, ValueTree &childWhichHasBeenAdded) {

            treeHasChanged();

        }

        void valueTreeChildRemoved (ValueTree &parentTree, ValueTree &childWhichHasBeenRemoved) {

            treeHasChanged();

        }

        void valueTreeChildOrderChanged (ValueTree &parentTreeWhoseChildrenHaveMoved) {

            treeHasChanged();

        }

        void valueTreeParentChanged (ValueTree &treeWhoseParentHasChanged) {

            treeHasChanged();

        }

        void valueTreeRedirected (ValueTree &treeWhichHasBeenChanged)  {

            treeHasChanged();

        }


    private:

        PropertyEditor * propertiesEditor;

        ValueTree t;

        Array<Identifier> currentProperties;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Item);

    };



    /** Display a tree. */

    class Editor :

    public Component {

    public:

        Editor() :

        layoutResizer(&layout, 1, false)

        {

            layout.setItemLayout(0, -0.1, -0.9, -0.6);

            layout.setItemLayout(1, 5, 5, 5);

            layout.setItemLayout(2, -0.1, -0.9, -0.4);


            setSize (1000, 700);

            addAndMakeVisible(treeView);

            addAndMakeVisible(propertyEditor);

            addAndMakeVisible(layoutResizer);

        }

        ~Editor() {

            treeView.setRootItem (nullptr);

        }


        void resized()

        {

            Component * comps[] = { &treeView, &layoutResizer, &propertyEditor };

            layout.layOutComponents(comps, 3, 0, 0, getWidth(), getHeight(), true, true);

        }


        void setTree(ValueTree newTree)

        {

            if (newTree == ValueTree::invalid)

            {

                treeView.setRootItem(nullptr);

            }

            else if (tree != newTree)

            {

                tree = newTree;

                treeView.setRootItem(new Item(&propertyEditor, tree));

            }

        }


    public:

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Editor);


        ValueTree tree;

        TreeView treeView;

        PropertyEditor propertyEditor;

        StretchableLayoutManager layout;

        StretchableLayoutResizerBar layoutResizer;

    };


public:

    ValueTreeEditor() :

    DocumentWindow("Value Tree Editor",

                   Colours::lightgrey,

                   DocumentWindow::allButtons)

    {

        editor = new Editor();

        setContentNonOwned(editor, true);

        setResizable(true, false);

        setUsingNativeTitleBar(true);

        centreWithSize(getWidth(), getHeight());

        setVisible(true);

    }

    ~ValueTreeEditor()

    {

        editor->setTree(ValueTree::invalid);

    }


    void closeButtonPressed()

    {

        setVisible(false);

    }

    

    void setSource(ValueTree & v)

    {

        editor->setTree(v);

    }


private:

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ValueTreeEditor);

    ScopedPointer<Editor> editor;

};