I’m happy to share what I have in use:
namespace IDs
{
#define DECLARE_ID(name) const juce::Identifier name(#name);
DECLARE_ID(SONG);
DECLARE_ID(associatedPrgNr);
DECLARE_ID(songNumber);
DECLARE_ID(songName);
DECLARE_ID(isAttacaFromSongBefore);
DECLARE_ID(isSongExtension);
#undef DECLARE_ID
}
class Song
: private ValueTree::Listener
{
public:
class Listener
{
public:
virtual ~Listener() noexcept = default;
virtual void associatedPrgNrChanged(juce::TPrgNr);
virtual void songNumberChanged(const juce::String&);
virtual void songNameChanged(const juce::String&);
virtual void isAttacaFromSongBeforeChanged(bool);
virtual void isSongExtensionChanged(bool);
};
Song();
explicit Song(const ValueTree& vt);
Song(const Song& other);
Song& operator=(const Song& inlay);
void swap(Song& inlay) noexcept;
juce::TPrgNr getAssociatedPrgNr() const;
juce::String getSongNumber() const;
juce::String getSongName() const;
bool getIsAttacaFromSongBefore() const;
bool getIsSongExtension() const;
void setAssociatedPrgNr(juce::TPrgNr value, UndoManager* undo = nullptr);
void setSongNumber(const juce::String& value, UndoManager* undo = nullptr);
void setSongName(const juce::String& value, UndoManager* undo = nullptr);
void setIsAttacaFromSongBefore(bool value, UndoManager* undo = nullptr);
void setIsSongExtension(bool value, UndoManager* undo = nullptr);
bool isValid() const;
void deleteSelf(UndoManager* undo = nullptr);
void addListener(Listener& listener);
void removeListener(Listener& listener);
private:
void valueTreePropertyChanged(ValueTree&, const Identifier& property) override;
void valueTreePropertyChanged_associatedPrgNr();
void valueTreePropertyChanged_songNumber();
void valueTreePropertyChanged_songName();
void valueTreePropertyChanged_isAttacaFromSongBefore();
void valueTreePropertyChanged_isSongExtension();
void referValues();
ValueTree valueTree;
ListenerList<Listener> listeners;
CachedValue<int> associatedPrgNr;
CachedValue<juce::String> songNumber;
CachedValue<juce::String> songName;
CachedValue<bool> isAttacaFromSongBefore;
CachedValue<bool> isSongExtension;
friend class SongTable;
JUCE_LEAK_DETECTOR(Song);
};
#include "Song.h"
#include "SongTable.h"
#include "ValueTreeUtils.h"
void Song::Listener::associatedPrgNrChanged(juce::TPrgNr) {}
void Song::Listener::songNumberChanged(const juce::String&) {}
void Song::Listener::songNameChanged(const juce::String&) {}
void Song::Listener::isAttacaFromSongBeforeChanged(bool) {}
void Song::Listener::isSongExtensionChanged(bool) {}
Song::Song()
: Song(ValueTree(IDs::SONG))
{}
Song::Song(const Song& other)
: Song(other.valueTree)
{}
Song::Song(const ValueTree& vt)
: valueTree(vt)
{
jassert(valueTree.hasType(IDs::SONG));
referValues();
valueTree.addListener(this);
}
Song& Song::operator=(const Song& other)
{
auto copy(other);
swap(copy);
return *this;
}
void Song::swap(Song& other) noexcept
{
std::swap(valueTree, other.valueTree);
referValues();
}
void Song::referValues()
{
associatedPrgNr.referTo(valueTree, IDs::associatedPrgNr, nullptr, (int) juce::TProgram_Min);
songNumber.referTo(valueTree, IDs::songNumber, nullptr, "");
songName.referTo(valueTree, IDs::songName, nullptr, "");
isAttacaFromSongBefore.referTo(valueTree, IDs::isAttacaFromSongBefore, nullptr, false);
isSongExtension.referTo(valueTree, IDs::isSongExtension, nullptr, false);
}
void Song::valueTreePropertyChanged(ValueTree&, const Identifier& property)
{
if (property == IDs::associatedPrgNr)
valueTreePropertyChanged_associatedPrgNr();
else if (property == IDs::songNumber)
valueTreePropertyChanged_songNumber();
else if (property == IDs::songName)
valueTreePropertyChanged_songName();
else if (property == IDs::isAttacaFromSongBefore)
valueTreePropertyChanged_isAttacaFromSongBefore();
else if (property == IDs::isSongExtension)
valueTreePropertyChanged_isSongExtension();
else
jassertfalse;
}
#define IMPL_Song_valueTreePropertyChanged(__field, __listenerFunc) \
__field .forceUpdateOfCachedValue(); \
listeners.call([this](auto& listener) { listener. __listenerFunc ( __field ); });
void Song::valueTreePropertyChanged_associatedPrgNr() { IMPL_Song_valueTreePropertyChanged(associatedPrgNr, associatedPrgNrChanged); }
void Song::valueTreePropertyChanged_songNumber() { IMPL_Song_valueTreePropertyChanged(songNumber, songNumberChanged); }
void Song::valueTreePropertyChanged_songName() { IMPL_Song_valueTreePropertyChanged(songName, songNameChanged); }
void Song::valueTreePropertyChanged_isAttacaFromSongBefore() { IMPL_Song_valueTreePropertyChanged(isAttacaFromSongBefore, isAttacaFromSongBeforeChanged); }
void Song::valueTreePropertyChanged_isSongExtension() { IMPL_Song_valueTreePropertyChanged(isSongExtension, isSongExtensionChanged); }
void Song::setAssociatedPrgNr(juce::TPrgNr value, UndoManager* undoManager)
{
associatedPrgNr.setValue((int) value, undoManager);
sortValueTreeChild<int>(valueTree, [](auto vt) { return vt.getProperty(IDs::associatedPrgNr).operator int(); }, undoManager);
}
void Song::deleteSelf(UndoManager* undoManager)
{
jassert(valueTree.getParent().isValid());
valueTree.getParent().removeChild(valueTree, undoManager);
}
void Song::setSongNumber(const juce::String& value, UndoManager* undoManager) { songNumber.setValue(value, undoManager); }
void Song::setSongName(const juce::String& value, UndoManager* undoManager) { songName.setValue(value, undoManager); }
void Song::setIsAttacaFromSongBefore(bool value, UndoManager* undoManager) { isAttacaFromSongBefore.setValue(value, undoManager); }
void Song::setIsSongExtension(bool value, UndoManager* undoManager) { isSongExtension.setValue(value, undoManager); }
juce::TPrgNr Song::getAssociatedPrgNr() const { return associatedPrgNr; }
juce::String Song::getSongNumber() const { return songNumber; }
juce::String Song::getSongName() const { return songName; }
bool Song::getIsAttacaFromSongBefore() const { return isAttacaFromSongBefore; }
bool Song::getIsSongExtension() const { return isSongExtension; }
bool Song::isValid() const { return valueTree.getParent().isValid(); }
void Song::addListener(Listener& listener) { listeners.add(&listener); }
void Song::removeListener(Listener& listener) { listeners.remove(&listener); }
#pragma once
#include "JuceHeader.h"
#include "Song.h"
namespace IDs
{
#define DECLARE_ID(name) const juce::Identifier name(#name);
DECLARE_ID(SONG_TABLE);
#undef DECLARE_ID
}
class SongTable
: private ValueTree::Listener
{
public:
class Listener
{
public:
virtual ~Listener() noexcept = default;
virtual void songAdded(const Song& song);
virtual void songOrderChanged(int ixFrom, int ixTo);
virtual void songRemoved(const Song& song, int index);
virtual void songChanged(const Song& song);
};
SongTable();
explicit SongTable(const ValueTree& vt);
SongTable(const SongTable& other);
SongTable& operator=(const SongTable& other);
void swap(SongTable& other) noexcept;
Song createNewSong(UndoManager* undoManager = nullptr);
void deleteSong(const Song& inlay, UndoManager* undoManager = nullptr);
size_t getNumSongs() const;
Song getSong(size_t ix);
Song findSongByProgram(TPrgNr program);
Song findSongByNumber(const juce::String& numberPattern);
int indexOfSong(const Song& inlay) const;
void addListener(Listener& listener);
void removeListener(Listener& listener);
private:
void valueTreePropertyChanged(ValueTree& vt, const Identifier& property) override;
void valueTreeChildOrderChanged(ValueTree&, int oldIndex, int newIndex) override;
void valueTreeChildAdded(ValueTree&, ValueTree& child) override;
void valueTreeChildRemoved(ValueTree&, ValueTree& child, int ixChild) override;
Song findSongByNumber_exact(const juce::String& number);
ValueTree valueTree;
ListenerList<Listener> listeners;
JUCE_LEAK_DETECTOR(SongTable);
};
void SongTable::Listener::songAdded(const Song&) {}
void SongTable::Listener::songRemoved(const Song&, int) {}
void SongTable::Listener::songOrderChanged(int, int) {}
void SongTable::Listener::songChanged(const Song&) {}
SongTable::SongTable()
: SongTable(ValueTree(IDs::SONG_TABLE))
{}
SongTable::SongTable(const SongTable& other)
: SongTable(other.valueTree)
{}
SongTable::SongTable(const ValueTree& vt)
: valueTree(vt)
{
jassert(valueTree.hasType(IDs::SONG_TABLE));
valueTree.addListener(this);
}
SongTable& SongTable::operator=(const SongTable& other)
{
auto copy(other);
swap(copy);
return *this;
}
void SongTable::swap(SongTable& other) noexcept
{
std::swap(valueTree, other.valueTree);
}
Song SongTable::createNewSong(UndoManager* undoManager)
{
Song song;
if (valueTree.getNumChildren() != 0)
song.setAssociatedPrgNr(jmin((juce::TPrgNr) (getSong(getNumSongs() - 1).getAssociatedPrgNr() + 1), juce::TProgram_Max));
valueTree.addChild(song.valueTree, valueTree_appendChild, undoManager);
return song;
}
Song SongTable::findSongByProgram(TPrgNr program)
{
for (auto i = 0; i < valueTree.getNumChildren(); ++i)
if (auto child = valueTree.getChild(i); child.hasType(IDs::SONG) and child.getProperty(IDs::associatedPrgNr).operator int() == program)
return Song(child);
return Song();
}
Song SongTable::findSongByNumber(const juce::String& number)
{
// omitted
}
Song SongTable::findSongByNumber_exact(const juce::String& number)
{
// omitted
}
void SongTable::deleteSong(const Song& song, UndoManager* undoManager)
{
jassert(song.valueTree.getParent() == valueTree);
valueTree.removeChild(song.valueTree, undoManager);
}
void SongTable::valueTreeChildAdded(ValueTree&, ValueTree& child)
{
jassert(child.hasType(IDs::SONG));
listeners.call([song = Song(child)](auto& listener) { listener.songAdded(song); });
}
void SongTable::valueTreeChildRemoved(ValueTree&, ValueTree& child, int ixChild)
{
jassert(child.hasType(IDs::SONG));
listeners.call([song = Song(child), ixChild](auto& listener) { listener.songRemoved(song, ixChild); });
}
void SongTable::valueTreeChildOrderChanged(ValueTree&, int oldIndex, int newIndex)
{
listeners.call([oldIndex, newIndex](auto& listener) { listener.songOrderChanged(oldIndex, newIndex); });
}
void SongTable::valueTreePropertyChanged(ValueTree& vt, const Identifier& property)
{
if (vt.hasType(IDs::SONG))
listeners.call([song = Song(vt)](auto& listener) { listener.songChanged(song); });
}
size_t SongTable::getNumSongs() const { return valueTree.getNumChildren(); }
Song SongTable::getSong(size_t ix) { return ix < getNumSongs() ? Song(valueTree.getChild((int) ix)) : Song(); }
int SongTable::indexOfSong(const Song& song) const { return valueTree.indexOf(song.valueTree); }
void SongTable::addListener(Listener& listener) { listeners.add(&listener); }
void SongTable::removeListener(Listener& listener) { listeners.remove(&listener); }
You pass those wrappers same way around as you would ValueTrees. I however recently discovered a major flaw when using ValueTree: when you subscribe to SongTable for changes and the SongTable is part of bigger ValueTree structure, you probably can’t no longer load your data by calling ::copyPropertiesAndChildren because that gets ride of the SongTable entry and readds a new one. Your Song list will then mostliekly refer to an unattached SongTable that is just floating around in the RAM.