diff --git a/ChangeList.txt b/ChangeList.txt index ebf8964717..8eb727d63a 100644 --- a/ChangeList.txt +++ b/ChangeList.txt @@ -19,3 +19,4 @@ Version 3.1.2 (not yet released) - Various minor additions to the Introjucer - Added parameters to the ValueTree::Listener::valueTreeChildRemoved() and valueTreeChildOrderChanged() methods to include more info about exactly what changed - this is essential for some applications + - New class ValueTreeSynchroniser, for remote-syncing multiple ValueTrees \ No newline at end of file diff --git a/modules/juce_data_structures/juce_data_structures.cpp b/modules/juce_data_structures/juce_data_structures.cpp index fd1d12aaca..e2e8e8189b 100644 --- a/modules/juce_data_structures/juce_data_structures.cpp +++ b/modules/juce_data_structures/juce_data_structures.cpp @@ -42,6 +42,7 @@ namespace juce #include "values/juce_Value.cpp" #include "values/juce_ValueTree.cpp" +#include "values/juce_ValueTreeSynchroniser.cpp" #include "undomanager/juce_UndoManager.cpp" #include "app_properties/juce_ApplicationProperties.cpp" #include "app_properties/juce_PropertiesFile.cpp" diff --git a/modules/juce_data_structures/juce_data_structures.h b/modules/juce_data_structures/juce_data_structures.h index 629b88ddf6..e80d1cea5f 100644 --- a/modules/juce_data_structures/juce_data_structures.h +++ b/modules/juce_data_structures/juce_data_structures.h @@ -35,6 +35,7 @@ namespace juce #include "undomanager/juce_UndoManager.h" #include "values/juce_Value.h" #include "values/juce_ValueTree.h" +#include "values/juce_ValueTreeSynchroniser.h" #include "app_properties/juce_PropertiesFile.h" #include "app_properties/juce_ApplicationProperties.h" diff --git a/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.cpp b/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.cpp new file mode 100644 index 0000000000..027620786a --- /dev/null +++ b/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.cpp @@ -0,0 +1,217 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +namespace ValueTreeSynchroniserHelpers +{ + enum ChangeType + { + propertyChanged = 1, + fullSync = 2, + childAdded = 3, + childRemoved = 4, + childMoved = 5 + }; + + static void getValueTreePath (ValueTree v, const ValueTree& topLevelTree, Array& path) + { + while (v != topLevelTree) + { + ValueTree parent (v.getParent()); + + if (! parent.isValid()) + break; + + path.add (parent.indexOf (v)); + v = parent; + } + } + + static void writeHeader (MemoryOutputStream& stream, ChangeType type) + { + stream.writeByte ((char) type); + } + + static void writeHeader (ValueTreeSynchroniser& target, MemoryOutputStream& stream, + ChangeType type, ValueTree v) + { + writeHeader (stream, type); + + Array path; + getValueTreePath (v, target.getRoot(), path); + + stream.writeCompressedInt (path.size()); + + for (int i = path.size(); --i >= 0;) + stream.writeCompressedInt (path.getUnchecked(i)); + } + + static ValueTree readSubTreeLocation (MemoryInputStream& input, ValueTree v) + { + const int numLevels = input.readCompressedInt(); + + if (! isPositiveAndBelow (numLevels, 65536)) // sanity-check + return ValueTree(); + + for (int i = numLevels; --i >= 0;) + { + const int index = input.readCompressedInt(); + + if (! isPositiveAndBelow (index, v.getNumChildren())) + return ValueTree(); + + v = v.getChild (index); + } + + return v; + } +} + +ValueTreeSynchroniser::ValueTreeSynchroniser (const ValueTree& tree) : valueTree (tree) +{ + valueTree.addListener (this); +} + +ValueTreeSynchroniser::~ValueTreeSynchroniser() +{ + valueTree.removeListener (this); +} + +void ValueTreeSynchroniser::sendFullSyncCallback() +{ + MemoryOutputStream m; + writeHeader (m, ValueTreeSynchroniserHelpers::fullSync); + valueTree.writeToStream (m); + stateChanged (m.getData(), m.getDataSize()); +} + +void ValueTreeSynchroniser::valueTreePropertyChanged (ValueTree& vt, const Identifier& property) +{ + MemoryOutputStream m; + ValueTreeSynchroniserHelpers::writeHeader (*this, m, ValueTreeSynchroniserHelpers::propertyChanged, vt); + m.writeString (property.toString()); + vt.getProperty (property).writeToStream (m); + stateChanged (m.getData(), m.getDataSize()); +} + +void ValueTreeSynchroniser::valueTreeChildAdded (ValueTree& parentTree, ValueTree& childTree) +{ + const int index = parentTree.indexOf (childTree); + jassert (index >= 0); + + MemoryOutputStream m; + ValueTreeSynchroniserHelpers::writeHeader (*this, m, ValueTreeSynchroniserHelpers::childAdded, parentTree); + m.writeCompressedInt (index); + childTree.writeToStream (m); + stateChanged (m.getData(), m.getDataSize()); +} + +void ValueTreeSynchroniser::valueTreeChildRemoved (ValueTree& parentTree, ValueTree&, int oldIndex) +{ + MemoryOutputStream m; + ValueTreeSynchroniserHelpers::writeHeader (*this, m, ValueTreeSynchroniserHelpers::childRemoved, parentTree); + m.writeCompressedInt (oldIndex); + stateChanged (m.getData(), m.getDataSize()); +} + +void ValueTreeSynchroniser::valueTreeChildOrderChanged (ValueTree& parent, int oldIndex, int newIndex) +{ + MemoryOutputStream m; + ValueTreeSynchroniserHelpers::writeHeader (*this, m, ValueTreeSynchroniserHelpers::childMoved, parent); + m.writeCompressedInt (oldIndex); + m.writeCompressedInt (newIndex); + stateChanged (m.getData(), m.getDataSize()); +} + +void ValueTreeSynchroniser::valueTreeParentChanged (ValueTree&) {} // (No action needed here) + +bool ValueTreeSynchroniser::applyChange (ValueTree& root, const void* data, size_t dataSize, UndoManager* undoManager) +{ + MemoryInputStream input (data, dataSize, false); + + const ValueTreeSynchroniserHelpers::ChangeType type = (ValueTreeSynchroniserHelpers::ChangeType) input.readByte(); + + if (type == ValueTreeSynchroniserHelpers::fullSync) + { + root = ValueTree::readFromStream (input); + return true; + } + + ValueTree v (ValueTreeSynchroniserHelpers::readSubTreeLocation (input, root)); + + if (! v.isValid()) + return false; + + switch (type) + { + case ValueTreeSynchroniserHelpers::propertyChanged: + { + Identifier property (input.readString()); + v.setProperty (property, var::readFromStream (input), undoManager); + return true; + } + + case ValueTreeSynchroniserHelpers::childAdded: + { + const int index = input.readCompressedInt(); + v.addChild (ValueTree::readFromStream (input), index, undoManager); + return true; + } + + case ValueTreeSynchroniserHelpers::childRemoved: + { + const int index = input.readCompressedInt(); + + if (isPositiveAndBelow (index, v.getNumChildren())) + { + v.removeChild (index, undoManager); + return true; + } + + jassertfalse; // Either received some corrupt data, or the trees have drifted out of sync + break; + } + + case ValueTreeSynchroniserHelpers::childMoved: + { + const int oldIndex = input.readCompressedInt(); + const int newIndex = input.readCompressedInt(); + + if (isPositiveAndBelow (oldIndex, v.getNumChildren()) + && isPositiveAndBelow (newIndex, v.getNumChildren())) + { + v.moveChild (oldIndex, newIndex, undoManager); + return true; + } + + jassertfalse; // Either received some corrupt data, or the trees have drifted out of sync + break; + } + + default: + jassertfalse; // Seem to have received some corrupt data? + break; + } + + return false; +} diff --git a/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.h b/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.h new file mode 100644 index 0000000000..8cb4fbef6d --- /dev/null +++ b/modules/juce_data_structures/values/juce_ValueTreeSynchroniser.h @@ -0,0 +1,98 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2013 - Raw Material Software Ltd. + + Permission is granted to use this software under the terms of either: + a) the GPL v2 (or any later version) + b) the Affero GPL v3 + + Details of these licenses can be found at: www.gnu.org/licenses + + JUCE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + ------------------------------------------------------------------------------ + + To release a closed-source product which uses JUCE, commercial licenses are + available: visit www.juce.com for more information. + + ============================================================================== +*/ + +#ifndef JUCE_VALUETREESYNCHRONISER_H_INCLUDED +#define JUCE_VALUETREESYNCHRONISER_H_INCLUDED + + +//============================================================================== +/** + This class can be used to watch for all changes to the state of a ValueTree, + and to convert them to a transmittable binary encoding. + + The purpose of this class is to allow two or more ValueTrees to be remotely + synchronised by transmitting encoded changes over some kind of transport + mechanism. + + To use it, you'll need to implement a subclass of ValueTreeSynchroniser + and implement the stateChanged() method to transmit the encoded change (maybe + via a network or other means) to a remote destination, where it can be + applied to a target tree. +*/ +class JUCE_API ValueTreeSynchroniser : private ValueTree::Listener +{ +public: + /** Creates a ValueTreeSynchroniser that watches the given tree. + + After creating an instance of this class and somehow attaching it to + a target tree, you probably want to call sendFullSyncCallback() to + get them into a common starting state. + */ + ValueTreeSynchroniser (const ValueTree& tree); + + /** Destructor. */ + virtual ~ValueTreeSynchroniser(); + + /** This callback happens when the ValueTree changes and the given state-change message + needs to be applied to any other trees that need to stay in sync with it. + The data is an opaque blob of binary that you should transmit to wherever your + target tree lives, and use applyChange() to apply this to the target tree. + */ + virtual void stateChanged (const void* encodedChange, size_t encodedChangeSize) = 0; + + /** Forces the sending of a full state message, which may be large, as it + encodes the entire ValueTree. + + This will internally invoke stateChanged() with the encoded version of the state. + */ + void sendFullSyncCallback(); + + /** Applies an encoded change to the given destination tree. + + When you implement a receiver for changes that were sent by the stateChanged() + message, this is the function that you'll need to call to apply them to the + target tree that you want to be synced. + */ + static bool applyChange (ValueTree& target, + const void* encodedChangeData, size_t encodedChangeDataSize, + UndoManager* undoManager); + + /** Returns the root ValueTree that is being observed. */ + const ValueTree& getRoot() noexcept { return valueTree; } + +private: + ValueTree valueTree; + + void valueTreePropertyChanged (ValueTree&, const Identifier&) override; + void valueTreeChildAdded (ValueTree&, ValueTree&) override; + void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override; + void valueTreeChildOrderChanged (ValueTree&, int, int) override; + void valueTreeParentChanged (ValueTree&) override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ValueTreeSynchroniser) +}; + + + +#endif // JUCE_VALUETREESYNCHRONISER_H_INCLUDED