cello
JUCE ValueTrees for Humans
Loading...
Searching...
No Matches
cello_object.cpp
1/*
2 Copyright (c) 2023 Brett g Porter
3 Permission is hereby granted, free of charge, to any person obtaining a copy
4 of this software and associated documentation files (the "Software"), to deal
5 in the Software without restriction, including without limitation the rights
6 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 copies of the Software, and to permit persons to whom the Software is
8 furnished to do so, subject to the following conditions:
9 The above copyright notice and this permission notice shall be included in all
10 copies or substantial portions of the Software.
11 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17 SOFTWARE.
18*/
19
20#include "JuceHeader.h"
21
22#include "cello_object.h"
23
24namespace cello
25{
26
27Object::Object (const juce::String& type, const Object* state)
28: Object { type, (state != nullptr ? static_cast<juce::ValueTree> (*state) : juce::ValueTree ()) }
29{
30 if (state != nullptr)
31 undoManager = state->getUndoManager ();
32}
33
34Object::Object (const juce::String& type, const Object& state)
35: Object (type, &state)
36{
37}
38
39#define PATH_IMPL 1
40Object::Object (const juce::String& type, juce::ValueTree tree)
41{
42 wrap (type, static_cast<juce::ValueTree> (tree));
43}
44
45Object::Object (const juce::String& type, juce::File file, Object::FileFormat format)
46: Object { type, Object::load (file, format) }
47{
48}
49
51: data { rhs.data }
53{
54 // register to receive callbacks when the tree changes.
55 data.addListener (this);
56}
57
58Object::CreationType Object::wrap (const Object& other)
59{
60 data.removeListener (this);
61 const auto result { wrap (getType ().toString (), other) };
62 undoManager = other.getUndoManager ();
63 return result;
64}
65
67{
68 // can't change this object's type by doing this.
69 jassert (getType () == rhs.getType ());
70 data.copyPropertiesAndChildrenFrom (rhs.data, getUndoManager ());
71 return *this;
72}
73
75{
76 data.removeListener (this);
77}
78
79juce::ValueTree Object::clone (bool deep) const
80{
81 auto cloneTree { juce::ValueTree { getType () } };
82 if (deep)
83 cloneTree.copyPropertiesAndChildrenFrom (data, nullptr);
84 else
85 cloneTree.copyPropertiesFrom (data, nullptr);
86 return cloneTree;
87}
88
89void Object::update (const juce::MemoryBlock& updateBlock)
90{
91 juce::ValueTreeSynchroniser::applyChange (data, updateBlock.getData (), updateBlock.getSize (), getUndoManager ());
92}
93
94juce::ValueTree Object::find (const cello::Query& query, bool deep)
95{
96 return query.search (data, deep, false);
97}
98
99juce::ValueTree Object::findOne (const cello::Query& query, bool deep)
100{
101 return query.search (data, deep, true);
102}
103
104int Object::remove (const cello::Query& query)
105{
106 return query.remove (data);
107}
108
109bool Object::upsert (const Object* object, const juce::Identifier& key, bool deep)
110{
111 if (!object->hasattr (key))
112 return false;
113
114 const auto val { object->data[key] };
115
116 auto existingItem { data.getChildWithProperty (key, val) };
117 if (existingItem.isValid ())
118 {
119 // we found the match -- update in place.
120 if (deep)
121 existingItem.copyPropertiesAndChildrenFrom (*object, getUndoManager ());
122 else
123 existingItem.copyPropertiesFrom (*object, getUndoManager ());
124 return true;
125 }
126 // else, we need to add a copy to the end of our children.
127 data.appendChild (object->clone (deep), getUndoManager ());
128 return true;
129}
130
131void Object::upsertAll (const Object* parent, const juce::Identifier& key, bool deep)
132{
133 juce::ValueTree parentTree { *parent };
134 for (const auto& child : parentTree)
135 {
136 const auto type { child.getType () };
137 Object item { type.toString (), child };
138
139 if (!upsert (&item, key, deep))
140 jassertfalse;
141 }
142}
143
144void Object::setUndoManager (juce::UndoManager* undo)
145{
147}
148
149bool Object::canUndo () const
150{
151 if (auto* undoMgr = getUndoManager ())
152 return undoMgr->canUndo ();
153 return false;
154}
155
157{
158 if (auto* undoMgr = getUndoManager ())
159 return undoMgr->undo ();
160 return false;
161}
162
163bool Object::canRedo () const
164{
165 if (auto* undoMgr = getUndoManager ())
166 return undoMgr->canRedo ();
167 return false;
168}
169
171{
172 if (auto* undoMgr = getUndoManager ())
173 return undoMgr->redo ();
174 return false;
175}
176
178{
179 if (auto* undoMgr = getUndoManager ())
180 undoMgr->clearUndoHistory ();
181}
182
184{
185 return data.getNumChildren ();
186}
187
188juce::ValueTree Object::operator[] (int index) const
189{
190 if (index < 0 || index >= data.getNumChildren ())
191 return {};
192
193 return data.getChild (index);
194}
195
196void Object::append (Object* object)
197{
198 insert (object, -1);
199}
200
201void Object::insert (Object* object, int index)
202{
203 if (object == this)
204 {
205 // can't add an object to itself!
206 jassertfalse;
207 return;
208 }
209 // a value tree can only have 1 parent -- if the new object has a parent,
210 // remove it there first.
211 juce::ValueTree newChild { *object };
212 juce::ValueTree parent { newChild.getParent () };
213
214 if (parent.isValid ())
215 {
216 // we can get into a weird state if we try to mix operations on
217 // different undo managers.
218 jassert (getUndoManager () == object->getUndoManager ());
219 parent.removeChild (newChild, getUndoManager ());
220 }
221 data.addChild (*object, index, getUndoManager ());
222 // make sure that the new child is using this object's undo manager.
223 object->setUndoManager (getUndoManager ());
224}
225
227{
228 if (object == this)
229 {
230 jassertfalse;
231 return nullptr;
232 }
233 auto removedTree { remove (data.indexOf (*object)) };
234 return removedTree.isValid () ? object : nullptr;
235}
236
237juce::ValueTree Object::remove (int index)
238{
239 auto treeToRemove { data.getChild (index) };
240 if (treeToRemove.isValid ())
241 data.removeChild (treeToRemove, getUndoManager ());
242 return treeToRemove;
243}
244
245void Object::move (int fromIndex, int toIndex)
246{
247 data.moveChild (fromIndex, toIndex, getUndoManager ());
248}
249
250template <typename Comparator> void Object::sort (Comparator& comp, bool stableSort)
251{
252 data.sort (comp, getUndoManager (), stableSort);
253}
254
255juce::UndoManager* Object::getUndoManager () const
256{
257 return undoManager;
258}
259
260void Object::onPropertyChange (const juce::Identifier& id, PropertyUpdateFn callback)
261{
262 // replace an existing callback?
263 for (auto& updater : propertyUpdaters)
264 {
265 if (updater.id == id)
266 {
267 updater.fn = callback;
268 return;
269 }
270 }
271 // nope, append to the list.
272 propertyUpdaters.emplace_back (id, callback);
273}
274
275void Object::onPropertyChange (const ValueBase& val, PropertyUpdateFn callback)
276{
277 onPropertyChange (val.getId (), callback);
278}
279
280bool Object::hasattr (const juce::Identifier& attr) const
281{
282 return data.hasProperty (attr);
283}
284
285void Object::delattr (const juce::Identifier& attr)
286{
287 data.removeProperty (attr, getUndoManager ());
288}
289
290juce::ValueTree Object::load (juce::File file, FileFormat format)
291{
292 if (format == Object::FileFormat::xml)
293 {
294 const auto xmlText { file.loadFileAsString () };
295 return juce::ValueTree::fromXml (xmlText);
296 }
297
298 // one of the binary formats
299 juce::MemoryBlock mb;
300 if (!file.loadFileAsData (mb))
301 {
302 jassertfalse;
303 return {};
304 }
305 if (format == Object::FileFormat::binary)
306 return juce::ValueTree::readFromData (mb.getData (), mb.getSize ());
307 else if (format == Object::FileFormat::zipped)
308 return juce::ValueTree::readFromGZIPData (mb.getData (), mb.getSize ());
309
310 // unknown format
311 jassertfalse;
312 return {};
313}
314
315juce::Result Object::save (juce::File file, FileFormat format) const
316{
317 if (format == FileFormat::xml)
318 {
319 auto res { file.create () };
320 if (res.wasOk ())
321 {
322 if (file.replaceWithText (data.toXmlString ()))
323 return juce::Result::ok ();
324 res = juce::Result::fail ("Error writing to " + file.getFullPathName ());
325 }
326 return res;
327 }
328
329 juce::FileOutputStream fos { file };
330 if (!fos.openedOk ())
331 {
332 jassertfalse;
333 return juce::Result::fail ("Unable to open " + file.getFullPathName () + " for writing");
334 }
335
336 if (format == FileFormat::binary)
337 {
338 data.writeToStream (fos);
339 return juce::Result::ok ();
340 }
341
342 else if (format == FileFormat::zipped)
343 {
344 juce::GZIPCompressorOutputStream zipper { fos };
345 data.writeToStream (zipper);
346 return juce::Result::ok ();
347 }
348
349 // unknown format
350 jassertfalse;
351 return juce::Result::fail ("Unknown file format");
352}
353
354Object::CreationType Object::wrap (const juce::String& type, juce::ValueTree tree)
355{
356 creationType = CreationType::wrapped;
357 Path path { type };
358 // DBG(tree.toXmlString());
359 data = path.findValueTree (tree, Path::SearchType::createAll, nullptr);
360 // DBG(data.toXmlString());
361 if (path.getSearchResult () == Path::SearchResult::created)
362 creationType = CreationType::initialized;
363
364 // register to receive callbacks when the tree changes.
365 data.addListener (this);
366 return creationType;
367}
368
369void Object::valueTreePropertyChanged (juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property)
370{
371 if (treeWhosePropertyHasChanged != data)
372 return;
373 // look for an update callback for this property. Returns true if a callback
374 // was registered and called.
375 auto callUpdaterForProperty = [this] (const juce::Identifier& key, const juce::Identifier& prop) -> bool
376 {
377 for (const auto& updater : propertyUpdaters)
378 {
379 if (updater.id == key)
380 {
381 if (updater.fn != nullptr)
382 updater.fn (prop);
383 return true;
384 }
385 }
386 return false;
387 };
388
389 // first, try to find a callback for that exact property.
390 if (callUpdaterForProperty (property, property))
391 return;
392 // ...then see if a generic callback is registered for the type of the tree.
393 callUpdaterForProperty (getType (), property);
394}
395
396void Object::valueTreeChildAdded (juce::ValueTree& parentTree, juce::ValueTree& childTree)
397{
398 if (parentTree == data && onChildAdded != nullptr)
399 onChildAdded (childTree, -1, data.indexOf (childTree));
400}
401
402void Object::valueTreeChildRemoved (juce::ValueTree& parentTree, juce::ValueTree& childTree, int index)
403{
404 if (parentTree == data && onChildRemoved != nullptr)
405 onChildRemoved (childTree, index, -1);
406}
407
408void Object::valueTreeChildOrderChanged (juce::ValueTree& parentTree, int oldIndex, int newIndex)
409{
410 if (parentTree == data && onChildMoved != nullptr)
411 {
412 auto childTree { data.getChild (newIndex) };
413 onChildMoved (childTree, oldIndex, newIndex);
414 }
415}
416
417void Object::valueTreeParentChanged (juce::ValueTree& tree)
418{
419 if (tree == data && onParentChanged != nullptr)
420 onParentChanged ();
421}
422
423void Object::valueTreeRedirected (juce::ValueTree& tree)
424{
425 if (tree == data && onTreeRedirected != nullptr)
426 onTreeRedirected ();
427}
428
429} // namespace cello
430
431#if RUN_UNIT_TESTS
432#include "test/test_cello_object.inl"
433#endif
void move(int fromIndex, int toIndex)
Change the position of one of this object's children.
Definition cello_object.cpp:245
void setUndoManager(juce::UndoManager *undo)
Set the undo manager to use in this object (and others created from it).
Definition cello_object.cpp:144
void sort(Comparator &comp, bool stableSort)
Sort this object's children using the provided comparison object.
Definition cello_object.cpp:250
bool upsert(const Object *object, const juce::Identifier &key, bool deep=false)
Update or insert a child object (concept borrowed from MongoDB) Looks for a child with a 'key' value ...
Definition cello_object.cpp:109
~Object() override
Destroy the Object object The important thing done here is to remove ourselves as a listener to the v...
Definition cello_object.cpp:74
juce::ValueTree data
The tree where our data lives.
Definition cello_object.h:646
void onPropertyChange(const juce::Identifier &id, PropertyUpdateFn callback)
Install (or clear) a function to be called when one of this Object's properties changes....
Definition cello_object.cpp:260
CreationType wrap(const Object &other)
Wrap another Object's tree after this object is created.
Definition cello_object.cpp:58
int remove(const cello::Query &query)
Remove all children from the tree that match the query.
Definition cello_object.cpp:104
Object(const juce::String &type, const Object *state)
Construct a new cello::Object object, which will attempt to initialize from the 'state' parameter....
Definition cello_object.cpp:27
juce::Result save(juce::File file, FileFormat format=FileFormat::xml) const
Save the object tree to disk.
Definition cello_object.cpp:315
bool redo()
Attempt to redo the last transaction.
Definition cello_object.cpp:170
bool hasattr(const juce::Identifier &attr) const
test the object to see if it has an attribute with this id.
Definition cello_object.cpp:280
juce::ValueTree clone(bool deep) const
Make and return a copy of our underlying value tree.
Definition cello_object.cpp:79
bool canUndo() const
Test whether this object/tree has anything that can be undone.
Definition cello_object.cpp:149
void delattr(const juce::Identifier &attr)
Remove the specified property from this object.
Definition cello_object.cpp:285
int getNumChildren() const
Check how many children this object has.
Definition cello_object.cpp:183
bool canRedo() const
Test whether this object/tree has anything that can be redone.
Definition cello_object.cpp:163
juce::UndoManager * undoManager
The undo manager to use for set() operations.
Definition cello_object.h:649
bool undo()
Attempt to undo the last transaction.
Definition cello_object.cpp:156
juce::Identifier getType() const
Get the type of this object as a juce::Identifier.
Definition cello_object.h:154
CreationType creationType
Remember how this Object was created.
Definition cello_object.h:652
juce::UndoManager * getUndoManager() const
Get the current undo manager; only useful to this object's Value objects and when creating other Obje...
Definition cello_object.cpp:255
juce::ValueTree find(const cello::Query &query, bool deep=false)
Perform a query against the children of this Object, returning a new ValueTree containing zero or mor...
Definition cello_object.cpp:94
juce::ValueTree findOne(const cello::Query &query, bool deep=false)
Perform a query against the children of this object, returning a copy of the first child found that m...
Definition cello_object.cpp:99
operator juce::ValueTree() const
Get the ValueTree we're using as our data store.
Definition cello_object.h:206
void clearUndoHistory()
reset the undo manager
Definition cello_object.cpp:177
void update(const juce::MemoryBlock &updateBlock)
Apply delta/update generated by the juce::ValueTreeSynchroniser class; this is used in the sync and i...
Definition cello_object.cpp:89
void append(Object *object)
Add a new child object to the end of our child object list,.
Definition cello_object.cpp:196
static juce::ValueTree load(juce::File file, FileFormat format=FileFormat::xml)
Reload data from disk. Used in the ctor that accepts file name and format.
Definition cello_object.cpp:290
Object & operator=(const Object &rhs)
set this object to use a different Object's value tree, which we will begin listening to....
Definition cello_object.cpp:66
juce::ValueTree operator[](int index) const
return a child tree of this object by its index. NOTE that it does not return an object; to work with...
Definition cello_object.cpp:188
void insert(Object *object, int index)
add a new child object at a specific index in the list.
Definition cello_object.cpp:201
void upsertAll(const Object *parent, const juce::Identifier &key, bool deep=false)
Perform an upsert using each of the children of the parent being passed. Common workflow here:
Definition cello_object.cpp:131
Class to navigate between subtrees that are all connected together.
Definition cello_path.h:50
@ createAll
create final tree and all intermediate trees needed to reach it.
Definition cello_path.h:67
juce::ValueTree findValueTree(juce::ValueTree &origin, SearchType searchType, juce::UndoManager *undo=nullptr)
Navigate the path from origin to a tree that is expected at the end of the current path specification...
Definition cello_path.cpp:59
@ created
performing a search created a new tree
Definition cello_path.h:74
Definition cello_query.h:28
int remove(juce::ValueTree tree) const
Remove all children from the tree that match the query.
Definition cello_query.cpp:66
juce::ValueTree search(juce::ValueTree tree, bool deep, bool returnFirstFound=false) const
Execute the query we're programmed for – iterate through the children of tree, returning a new tree o...
Definition cello_query.cpp:41
Definition cello_value.h:32
juce::Identifier getId() const
Definition cello_value.h:37