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
104bool Object::upsert (const Object* object, const juce::Identifier& key, bool deep)
105{
106 if (!object->hasattr (key))
107 return false;
108
109 const auto val { object->data[key] };
110
111 auto existingItem { data.getChildWithProperty (key, val) };
112 if (existingItem.isValid ())
113 {
114 // we found the match -- update in place.
115 if (deep)
116 existingItem.copyPropertiesAndChildrenFrom (*object, getUndoManager ());
117 else
118 existingItem.copyPropertiesFrom (*object, getUndoManager ());
119 return true;
120 }
121 // else, we need to add a copy to the end of our children.
122 data.appendChild (object->clone (deep), getUndoManager ());
123 return true;
124}
125
126void Object::upsertAll (const Object* parent, const juce::Identifier& key, bool deep)
127{
128 juce::ValueTree parentTree { *parent };
129 for (const auto& child : parentTree)
130 {
131 const auto type { child.getType () };
132 Object item { type.toString (), child };
133
134 if (!upsert (&item, key, deep))
135 jassertfalse;
136 }
137}
138
139void Object::setUndoManager (juce::UndoManager* undo)
140{
142}
143
144bool Object::canUndo () const
145{
146 if (auto* undoMgr = getUndoManager ())
147 return undoMgr->canUndo ();
148 return false;
149}
150
152{
153 if (auto* undoMgr = getUndoManager ())
154 return undoMgr->undo ();
155 return false;
156}
157
158bool Object::canRedo () const
159{
160 if (auto* undoMgr = getUndoManager ())
161 return undoMgr->canRedo ();
162 return false;
163}
164
166{
167 if (auto* undoMgr = getUndoManager ())
168 return undoMgr->redo ();
169 return false;
170}
171
173{
174 if (auto* undoMgr = getUndoManager ())
175 undoMgr->clearUndoHistory ();
176}
177
179{
180 return data.getNumChildren ();
181}
182
183juce::ValueTree Object::operator[] (int index) const
184{
185 if (index < 0 || index >= data.getNumChildren ())
186 return {};
187
188 return data.getChild (index);
189}
190
191void Object::append (Object* object)
192{
193 insert (object, -1);
194}
195
196void Object::insert (Object* object, int index)
197{
198 if (object == this)
199 {
200 // can't add an object to itself!
201 jassertfalse;
202 return;
203 }
204 // a value tree can only have 1 parent -- if the new object has a parent,
205 // remove it there first.
206 juce::ValueTree newChild { *object };
207 juce::ValueTree parent { newChild.getParent () };
208
209 if (parent.isValid ())
210 {
211 // we can get into a weird state if we try to mix operations on
212 // different undo managers.
213 jassert (getUndoManager () == object->getUndoManager ());
214 parent.removeChild (newChild, getUndoManager ());
215 }
216 data.addChild (*object, index, getUndoManager ());
217 // make sure that the new child is using this object's undo manager.
218 object->setUndoManager (getUndoManager ());
219}
220
222{
223 if (object == this)
224 {
225 jassertfalse;
226 return nullptr;
227 }
228 auto removedTree { remove (data.indexOf (*object)) };
229 return removedTree.isValid () ? object : nullptr;
230}
231
232juce::ValueTree Object::remove (int index)
233{
234 auto treeToRemove { data.getChild (index) };
235 if (treeToRemove.isValid ())
236 data.removeChild (treeToRemove, getUndoManager ());
237 return treeToRemove;
238}
239
240void Object::move (int fromIndex, int toIndex)
241{
242 data.moveChild (fromIndex, toIndex, getUndoManager ());
243}
244
245template <typename Comparator> void Object::sort (Comparator& comp, bool stableSort)
246{
247 data.sort (comp, getUndoManager (), stableSort);
248}
249
250juce::UndoManager* Object::getUndoManager () const
251{
252 return undoManager;
253}
254
255void Object::onPropertyChange (juce::Identifier id, PropertyUpdateFn callback)
256{
257 // replace an existing callback?
258 for (auto& updater : propertyUpdaters)
259 {
260 if (updater.id == id)
261 {
262 updater.fn = callback;
263 return;
264 }
265 }
266 // nope, append to the list.
267 propertyUpdaters.emplace_back (id, callback);
268}
269
270void Object::onPropertyChange (const ValueBase& val, PropertyUpdateFn callback)
271{
272 onPropertyChange (val.getId (), callback);
273}
274
275bool Object::hasattr (const juce::Identifier& attr) const
276{
277 return data.hasProperty (attr);
278}
279
280void Object::delattr (const juce::Identifier& attr)
281{
282 data.removeProperty (attr, getUndoManager ());
283}
284
285juce::ValueTree Object::load (juce::File file, FileFormat format)
286{
287 if (format == Object::FileFormat::xml)
288 {
289 const auto xmlText { file.loadFileAsString () };
290 return juce::ValueTree::fromXml (xmlText);
291 }
292
293 // one of the binary formats
294 juce::MemoryBlock mb;
295 if (!file.loadFileAsData (mb))
296 {
297 jassertfalse;
298 return {};
299 }
300 if (format == Object::FileFormat::binary)
301 return juce::ValueTree::readFromData (mb.getData (), mb.getSize ());
302 else if (format == Object::FileFormat::zipped)
303 return juce::ValueTree::readFromGZIPData (mb.getData (), mb.getSize ());
304
305 // unknown format
306 jassertfalse;
307 return {};
308}
309
310juce::Result Object::save (juce::File file, FileFormat format) const
311{
312 if (format == FileFormat::xml)
313 {
314 auto res { file.create () };
315 if (res.wasOk ())
316 {
317 if (file.replaceWithText (data.toXmlString ()))
318 return juce::Result::ok ();
319 res = juce::Result::fail ("Error writing to " + file.getFullPathName ());
320 }
321 return res;
322 }
323
324 juce::FileOutputStream fos { file };
325 if (!fos.openedOk ())
326 {
327 jassertfalse;
328 return juce::Result::fail ("Unable to open " + file.getFullPathName () + " for writing");
329 }
330
331 if (format == FileFormat::binary)
332 {
333 data.writeToStream (fos);
334 return juce::Result::ok ();
335 }
336
337 else if (format == FileFormat::zipped)
338 {
339 juce::GZIPCompressorOutputStream zipper { fos };
340 data.writeToStream (zipper);
341 return juce::Result::ok ();
342 }
343
344 // unknown format
345 jassertfalse;
346 return juce::Result::fail ("Unknown file format");
347}
348
349Object::CreationType Object::wrap (const juce::String& type, juce::ValueTree tree)
350{
351 creationType = CreationType::wrapped;
352 Path path { type };
353 // DBG(tree.toXmlString());
354 data = path.findValueTree (tree, Path::SearchType::createAll, nullptr);
355 // DBG(data.toXmlString());
356 if (path.getSearchResult () == Path::SearchResult::created)
357 creationType = CreationType::initialized;
358
359 // register to receive callbacks when the tree changes.
360 data.addListener (this);
361 return creationType;
362}
363
364void Object::valueTreePropertyChanged (juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property)
365{
366 if (treeWhosePropertyHasChanged != data)
367 return;
368 // look for an update callback for this property. Returns true if a callback
369 // was registered and called.
370 auto callUpdaterForProperty = [this] (const juce::Identifier& key, const juce::Identifier& prop) -> bool
371 {
372 for (const auto& updater : propertyUpdaters)
373 {
374 if (updater.id == key)
375 {
376 if (updater.fn != nullptr)
377 updater.fn (prop);
378 return true;
379 }
380 }
381 return false;
382 };
383
384 // first, try to find a callback for that exact property.
385 if (callUpdaterForProperty (property, property))
386 return;
387 // ...then see if a generic callback is registered for the type of the tree.
388 callUpdaterForProperty (getType (), property);
389}
390
391void Object::valueTreeChildAdded (juce::ValueTree& parentTree, juce::ValueTree& childTree)
392{
393 if (parentTree == data && onChildAdded != nullptr)
394 onChildAdded (childTree, -1, data.indexOf (childTree));
395}
396
397void Object::valueTreeChildRemoved (juce::ValueTree& parentTree, juce::ValueTree& childTree, int index)
398{
399 if (parentTree == data && onChildRemoved != nullptr)
400 onChildRemoved (childTree, index, -1);
401}
402
403void Object::valueTreeChildOrderChanged (juce::ValueTree& parentTree, int oldIndex, int newIndex)
404{
405 if (parentTree == data && onChildMoved != nullptr)
406 {
407 auto childTree { data.getChild (newIndex) };
408 onChildMoved (childTree, oldIndex, newIndex);
409 }
410}
411
412void Object::valueTreeParentChanged (juce::ValueTree& tree)
413{
414 if (tree == data && onParentChanged != nullptr)
415 onParentChanged ();
416}
417
418void Object::valueTreeRedirected (juce::ValueTree& tree)
419{
420 if (tree == data && onTreeRedirected != nullptr)
421 onTreeRedirected ();
422}
423
424} // namespace cello
425
426#if RUN_UNIT_TESTS
427#include "test/test_cello_object.inl"
428#endif
void move(int fromIndex, int toIndex)
Change the position of one of this object's children.
Definition cello_object.cpp:240
void setUndoManager(juce::UndoManager *undo)
Set the undo manager to use in this object (and others created from it).
Definition cello_object.cpp:139
void sort(Comparator &comp, bool stableSort)
Sort this object's children using the provided comparison object.
Definition cello_object.cpp:245
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:104
void onPropertyChange(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:255
~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:637
CreationType wrap(const Object &other)
Wrap another Object's tree after this object is created.
Definition cello_object.cpp:58
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:310
bool redo()
Attempt to redo the last transaction.
Definition cello_object.cpp:165
bool hasattr(const juce::Identifier &attr) const
test the object to see if it has an attribute with this id.
Definition cello_object.cpp:275
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:144
void delattr(const juce::Identifier &attr)
Remove the specified property from this object.
Definition cello_object.cpp:280
int getNumChildren() const
Check how many children this object has.
Definition cello_object.cpp:178
bool canRedo() const
Test whether this object/tree has anything that can be redone.
Definition cello_object.cpp:158
juce::UndoManager * undoManager
The undo manager to use for set() operations.
Definition cello_object.h:640
bool undo()
Attempt to undo the last transaction.
Definition cello_object.cpp:151
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:643
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:250
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
Object * remove(Object *object)
Attempt to remove a child object from this.
Definition cello_object.cpp:221
void clearUndoHistory()
reset the undo manager
Definition cello_object.cpp:172
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:191
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:285
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:183
void insert(Object *object, int index)
add a new child object at a specific index in the list.
Definition cello_object.cpp:196
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:126
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
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:30
juce::Identifier getId() const
Definition cello_value.h:35