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;
};