Hi all,
I was wondering if someone could give me information on best practices to follow when creating/destroying components based on updates on the valuee tree.
For example, when a midiClip is created a node on the parent tree ( the track in this case ) is added and the valueTreeChildAdded()
method is called while when it is removed the valureTreeChildRemoved()
method is called.
Should I build/destroy my graphic objects in such methods ?
So I would like some advice on how to manage their lifecycle.
Thank you very much
This is a tricky problem, Iâm not sure there is a one size fits all answer. You donât want to delete when valureTreeChildRemoved()
called, or if you are dragging a clip between tracks you component may get deleted mid drag. Pretty much everything in the engine is Selectable
which means you can listen for selectableAboutToBeDeleted()
, just be careful your UI doesnât increase the reference count of any clips or then youâll create a loop and then theyâll never get deleted. You may want to asynchronously update your UI after items are added or removed, but then be careful you have no dangling pointers.
I agree with you⊠but what about the undoManager ?
Il you donât create/delete objs in valueTreeChildAdded()
/valueTreeChildRemoved()
how to deal with the undo/redo operations ?
For example, suppose that you have an EditComponent
that has a listener on the editâs state.
The âEditComponentâ has also an OwnedArray<Track> tracks;
When a new track is created a new node TRACK
on the edit state is created, so :
class EditComponent : public Component , private ValueTreeAllEventListener{
public:
EditComponent(){}
protected :
void valueTreeChildAdded(ValueTree& parent,ValueTree& childAdded) override{
Track* t = getTrackFromValueTree(childAdded);
tracks.add(t);
resized();
}
void valueTreeChildRemoved(ValueTree& parent,ValueTree& childRemoved,int order) override{
String id = childRemoved.getProperty(IDs::id);
for(auto * t : tracks){
if(t->itemId.toString() == id){
tracks.remove(t);
}
}
resized();
}
private:
OwnedArray<Track> tracks;
};
In this way if you undo or redo the tracks array is consistent with the edit state ?
But as you said when a clip ( for example ) pass from one track to another there could be problems.
Maybe i can create the obj in the valueTreeChildAdded and remove them in the selectableAboutToBeDeleted() ?
In this case i need to add the listener to the object⊠and then remove it⊠for example if you have
class EditComponent : public Component , private ValueTreeAllEventListener, private SelectableListener{
public:
EditComponent(){}
protected :
void valueTreeChildAdded(ValueTree& parent,ValueTree& childAdded) override{
Track* t = getTrackFromValueTree(childAdded);
t->addSelectableListener(this);
tracks.add(t);
resized();
}
void valueTreeChildRemoved(ValueTree& parent,ValueTree& childRemoved,int order) override{
}
void selectableAboutToBeDeleted(Selectable* s) override{
Track* t = dynamic_cast<Track*>(s);
if(t){
for(auto* temp : tracks){
if(temp == t)
temp->removeSelectableListener(this);
tracks.remove(temp);
}
}
}
private:
OwnedArray<Track> tracks;
};
Maybe it could work in this way ?
The way we do this in Waveform is to have a pool of clips and purge them periodically if they no longer belong on any tracks.
Thereâs two reasons for doing this, firstly, it solves the problem you mention here as we update asynchronously after clips have been added/removed, so if youâre dragging between tracks, the clip will belong to the new track by the time the update happens.
The second reason is that we can purge UI clips that are offscreen. This is a bit of an optimisation that probably isnât required this day and age but itâs useful to know its possible.
This kind of question is coming up a lot recently and whether youâre synchronising UI or passing passing object representations between threads, the idea is the same. When something changes, you want to coalesce those changes in to a single update message. In the update message you can then say âdoes what Iâm showing look like the model? If not, Iâll update so I doâ.
This sounds long and complicated but itâs really not. There are all sorts of optimisations available doing it this way, pooling or ref-counting internal expensive state are some examples.
Thank you for your response,
I understand what you mean but I have implementation doubts :
- How are the graphic objects created ? through the
valueTreeChildAdded()
? - the âperiodicallyâ is meant through the use of a timer ? If yes, at what frequency ?
Ultimately, when the addClip()
is called on a track, which operation must I perform to create the graphic object ? And when I delete it from the track how should I properly delete the existing object ?
Thanks again.
A very short example would be really appreciated
What I do is to have an array of component objects I can resize. These objects have a pointer to the data, which can be nullptr. When I add / remove childs to the value tree, I do create data objects. However, I do not create component objects, instead I call to a ChangeBroadcaster.
Then the parent of the components listen the ChangeListener. And it assign all its objects to the objects it already has. These component objects which are assigned to a new data, changes their content. If it needs more or it needs to delete, it does it here too (keeping a buffer). Unused components objects (data == nullptr) arenât visible.
I do not use a Timer, I donât like the idea of being constantly checking. Also, notice Iâm not using Tracktion Engine, but maybe you could like this approach.
ok but in this way you will have an array with a very large size where so many elements in there has a nullptr⊠does it wort ?
Well, no, you may have an array with a very large size where zero elements (components) are nullptr, but the lastest ones may have a member variable pointing to a âdataâ which is nullptr.
It works in a similar way basic juce::Array
does: when it needs to add an element and it doesnât have remaining space, it will attach 8 spaces, but it will only use one of them, giving you the ilussion it only added the last element. It does this to avoid resize the data all the time.
In this way, you will have the easier of the two worlds: async heavy component object generation, and sync ValueTree generated/deleted small data objects for easier operation.
However, this async call may be extended to the âdata generationâ too, using the same mechanism, but usually for me itâs quite light.
Ok perfect,
i understood that you stored a reference to business object ( e.g a MidiClip ) in a component ( e.g MidiClipComponent ) and make null that reference when it no longer exists keeping the component alive.
If you make directly the component null it has more sense.
Thx.
I do not do the component null to avoid create&destroy them constantly. But yes, you got the idea. I attach another listener to the data inside each component, and so onâŠ
Because juce::Component
canât be copied, containers of those are usually stored on the heap, using something like:
std::vector<std::unique_ptr<Component>>
or OwnedArray<Component>
.
So creating a new Component should be a very light operation, as all the container has to do is to move the pointers for the existing elements, without destroying them or copying their contents.
I believe that operation should be very fast even with thousands of existing Components.
Ok but how you decide to put the reference inside the component to null ?
I mean, you has an OwnedArray comps;
When you create the component ? It lives inside a class that has a listenere to the valueTree and so when a new node is added you can create and add a new Component to the array passing the reference to the create business object (e.g midiClip) ?.
Then supposing that you have created the comp⊠what type of listener do you attacch on it ? a valueTree listener ? a SelectableListener ?
Can you give me a short snipped of code⊠?
Let me ilustrate with some pseudo-code:
You have a class which owns the ValueTree and generate / delete its childs. Lets call it DataContainer. This class have a âDataâ class, which is related to the ValueTreeChild. Both, are ChangeBroadcaster
class DataContainer: public ValueTree::Listener, public ChangeBroadcaster
{
void valueTreeChildAdded()
{
myChilds.add(new Data());
sendChangeMessage();
}
void void valueTreeChildRemoved()
{
myChilds.remove(a target Data);
sendChangeMessage();
}
class Data: public ChangeBroadcaster
{
setProperties(p1, p2)
{
property1 = p1;
property2 = p2;
sendChangeMessage();
}
int property1;
int property2;
}
OwnedArray<Data> myChilds;
}
Now, you have a gui container for gui child, both classes should be Components / ChangeListeners. This is the GuiContainer:
class GuiContainer: public Component, public ChangeListener
{
~GuiContainer() {setData(nullptr);}
void setData(DataContainer* container)
{
if (myContainer != container)
{
if (myContainer) myContainer.removeChangeListener(this);
myContainer = container;
if (myContainer) myContainer.addChangeListener(this);
dataUpdated();
}
}
void changeListenerCallback() {dataUpdated();}
void dataUpdated()
{
// Here you read DataContainer and you create at least as many GuiChilds as you need
if (myContainer.myChilds.size() > childs.size())
{
// add GuiChild to the OwnedArray<GuiChild> at least until it has childs.size() + 8 elements (up to you), to avoid constant resizing / generate / delete
}
else if (myContainer.myChilds.size() < childs.size() - 8)
{
// Remove childs here, but leave at least childs.size() + 8
}
// Now you have them created, you assing them a child
for (int i=0; i!=myContainer->myChilds.size(); ++i)
{
childs.getUnchecked(i)->setData(myContainer->myChilds.getUnchecked(i));
childs.setVisible(true);
}
// And you hide the unused components
for (int i=myContainer->myChilds.size(); i!=childs.size(); ++i)
{
childs.getUnchecked(i)->setVisible(false);
}
// You would place the components bounds, or you would repaint here
}
DataContainer* myContainer = nullptr;
OwnedArray<GuiChild> childs;
}
The Child gui should be something like ChildContainer. It may also have other childs, and since itâs quite similar to GuiContainer (in fact is the same idea), you may template them, Iâm not going to repeat it all, but in this way:
class GuiChild: public Component, public ChangeListener
{
void setData(DataContainer::Data* data)
{
// same technique as GuiComponent::setData
}
void changeListenerCallback() {dataUpdated();}
void dataUpdated() {repaint();}
DataContainer::Data* myData = nullptr;
}
Ok perfect⊠thxs so much