Commit c0738086 authored by Nicolas Carion's avatar Nicolas Carion

Json export of groups now better represent the tree hierarchy and supports compositions. Add tests

parent 76e085e0
......@@ -31,7 +31,6 @@ QDebug operator<<(QDebug qd, const ItemInfo &info)
return qd.maybeSpace();
}
CommentedTime::CommentedTime()
: m_time(GenTime(0))
, m_type(0)
......@@ -132,3 +131,30 @@ bool CommentedTime::operator!=(const CommentedTime &op) const
{
return m_time != op.time();
}
const QString groupTypeToStr(GroupType t)
{
switch (t) {
case GroupType::Normal:
return QStringLiteral("Normal");
case GroupType::Selection:
return QStringLiteral("Selection");
case GroupType::AVSplit:
return QStringLiteral("AVSplit");
case GroupType::Leaf:
return QStringLiteral("Leaf");
}
Q_ASSERT(false);
return QString();
}
GroupType groupTypeFromStr(const QString &s)
{
std::vector<GroupType> types{GroupType::Selection, GroupType::Normal, GroupType::AVSplit, GroupType::Leaf};
for (const auto &t : types) {
if (s == groupTypeToStr(t)) {
return t;
}
}
Q_ASSERT(false);
return GroupType::Normal;
}
......@@ -26,8 +26,8 @@
#include "kdenlive_debug.h"
#include <QHash>
#include <QString>
#include <QPersistentModelIndex>
#include <QString>
#include <QTreeWidgetItem>
#include <memory>
#include <cassert>
......@@ -39,15 +39,17 @@ namespace Kdenlive {
enum MonitorId { NoMonitor = 0x01, ClipMonitor = 0x02, ProjectMonitor = 0x04, RecordMonitor = 0x08, StopMotionMonitor = 0x10, DvdMonitor = 0x20 };
const int DefaultThumbHeight = 100;
}
} // namespace Kdenlive
enum class GroupType {
Normal,
Selection, // in that case, the group is used to emulate a selection
AVSplit // in that case, the group links the audio and video of the same clip
AVSplit, // in that case, the group links the audio and video of the same clip
Leaf // This is a leaf (clip or composition)
};
const QString groupTypeToStr(GroupType t);
GroupType groupTypeFromStr(const QString &s);
enum class ObjectType { TimelineClip, TimelineComposition, TimelineTrack, BinClip, NoItem };
using ObjectId = std::pair<ObjectType, int>;
......@@ -148,7 +150,6 @@ public:
}
};
struct requestClipInfo
{
QDomElement xml;
......@@ -212,7 +213,6 @@ public:
}
};
class CommentedTime
{
public:
......@@ -260,69 +260,61 @@ template <> struct hash<QPersistentModelIndex>
{
std::size_t operator()(const QPersistentModelIndex &k) const { return qHash(k); }
};
}
} // namespace std
// The following is a hack that allows to use shared_from_this in the case of a multiple inheritance.
// Credit: https://stackoverflow.com/questions/14939190/boost-shared-from-this-and-multiple-inheritance
template<typename T> struct enable_shared_from_this_virtual;
template <typename T> struct enable_shared_from_this_virtual;
class enable_shared_from_this_virtual_base : public std::enable_shared_from_this<enable_shared_from_this_virtual_base>
{
typedef std::enable_shared_from_this<enable_shared_from_this_virtual_base> base_type;
template<typename T>
friend struct enable_shared_from_this_virtual;
template <typename T> friend struct enable_shared_from_this_virtual;
std::shared_ptr<enable_shared_from_this_virtual_base> shared_from_this()
{
return base_type::shared_from_this();
}
std::shared_ptr<enable_shared_from_this_virtual_base const> shared_from_this() const
{
return base_type::shared_from_this();
}
std::shared_ptr<enable_shared_from_this_virtual_base> shared_from_this() { return base_type::shared_from_this(); }
std::shared_ptr<enable_shared_from_this_virtual_base const> shared_from_this() const { return base_type::shared_from_this(); }
};
template<typename T>
struct enable_shared_from_this_virtual: virtual enable_shared_from_this_virtual_base
template <typename T> struct enable_shared_from_this_virtual : virtual enable_shared_from_this_virtual_base
{
typedef enable_shared_from_this_virtual_base base_type;
public:
std::shared_ptr<T> shared_from_this()
{
std::shared_ptr<T> result(base_type::shared_from_this(), static_cast<T*>(this));
return result;
std::shared_ptr<T> result(base_type::shared_from_this(), static_cast<T *>(this));
return result;
}
std::shared_ptr<T const> shared_from_this() const
{
std::shared_ptr<T const> result(base_type::shared_from_this(), static_cast<T const*>(this));
std::shared_ptr<T const> result(base_type::shared_from_this(), static_cast<T const *>(this));
return result;
}
};
// This is a small trick to have a QAbstractItemModel with shared_from_this enabled without multiple inheritance
// Be careful, if you use this class, you have to make sure to init weak_this_ when you construct a shared_ptr to your object
template<class T>
class QAbstractItemModel_shared_from_this : public QAbstractItemModel
template <class T> class QAbstractItemModel_shared_from_this : public QAbstractItemModel
{
protected:
QAbstractItemModel_shared_from_this() : QAbstractItemModel() {}
QAbstractItemModel_shared_from_this()
: QAbstractItemModel()
{
}
public:
std::shared_ptr<T> shared_from_this()
{
std::shared_ptr<T> p( weak_this_ );
assert( p.get() == this );
std::shared_ptr<T> p(weak_this_);
assert(p.get() == this);
return p;
}
std::shared_ptr<T const> shared_from_this() const
{
std::shared_ptr<T const> p( weak_this_ );
assert( p.get() == this );
std::shared_ptr<T const> p(weak_this_);
assert(p.get() == this);
return p;
}
......
......@@ -23,6 +23,9 @@
#include "macros.hpp"
#include "timelineitemmodel.hpp"
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QModelIndex>
#include <queue>
#include <utility>
......@@ -36,6 +39,7 @@ GroupsModel::GroupsModel(std::weak_ptr<TimelineItemModel> parent)
Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set<int> &ids, GroupType type, int parent)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(type != GroupType::Leaf);
return [gid, ids, parent, type, this]() {
createGroupItem(gid);
if (parent != -1) {
......@@ -68,6 +72,7 @@ Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set<int> &ids,
int GroupsModel::groupItems(const std::unordered_set<int> &ids, Fun &undo, Fun &redo, GroupType type, bool force)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(type != GroupType::Leaf);
Q_ASSERT(!ids.empty());
if (ids.size() == 1 && !force) {
// We do not create a group with only one element. Instead, we return the id of that element
......@@ -258,16 +263,6 @@ void GroupsModel::removeFromGroup(int id)
m_upLink[id] = -1;
}
std::unordered_map<int, int> GroupsModel::groupsData()
{
return m_upLink;
}
std::unordered_map<int, std::unordered_set<int>> GroupsModel::groupsDataDownlink()
{
return m_downLink;
}
bool GroupsModel::mergeSingleGroups(int id, Fun &undo, Fun &redo)
{
// The idea is as follow: we start from the leaves, and go up to the root.
......@@ -534,6 +529,125 @@ bool GroupsModel::copyGroups(std::unordered_map<int, int> &mapping, Fun &undo, F
GroupType GroupsModel::getType(int id) const
{
Q_ASSERT(m_groupIds.count(id) > 0);
return m_groupIds.at(id);
if (m_groupIds.count(id) > 0) {
return m_groupIds.at(id);
}
return GroupType::Leaf;
}
QJsonObject GroupsModel::toJson(int gid) const
{
QJsonObject currentGroup;
currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(getType(gid))));
if (m_groupIds.count(gid) > 0) {
// in that case, we have a proper group
QJsonArray array;
Q_ASSERT(m_downLink.count(gid) > 0);
for (int c : m_downLink.at(gid)) {
array.push_back(toJson(c));
}
currentGroup.insert(QLatin1String("children"), array);
} else {
// in that case we have a clip or composition
if (auto ptr = m_parent.lock()) {
Q_ASSERT(ptr->isClip(gid) || ptr->isComposition(gid));
currentGroup.insert(QLatin1String("leaf"), QJsonValue(QLatin1String(ptr->isClip(gid) ? "clip" : "composition")));
int track = ptr->getTrackPosition(ptr->getItemTrackId(gid));
int pos = ptr->getItemPosition(gid);
currentGroup.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(track).arg(pos)));
} else {
qDebug() << "Impossible to create group because the timeline is not available anymore";
Q_ASSERT(false);
}
}
return currentGroup;
}
const QString GroupsModel::toJson() const
{
std::unordered_set<int> roots;
std::transform(m_groupIds.begin(), m_groupIds.end(), std::inserter(roots, roots.begin()),
[&](decltype(*m_groupIds.begin()) g) { return getRootId(g.first); });
QJsonArray list;
for (int r : roots) {
list.push_back(toJson(r));
}
QJsonDocument json(list);
return QString(json.toJson());
}
int GroupsModel::fromJson(const QJsonObject &o, Fun &undo, Fun &redo)
{
if (!o.contains(QLatin1String("type"))) {
return -1;
}
auto type = groupTypeFromStr(o.value(QLatin1String("type")).toString());
if (type == GroupType::Leaf) {
if (auto ptr = m_parent.lock()) {
if (!o.contains(QLatin1String("data")) || !o.contains(QLatin1String("leaf"))) {
qDebug() << "Error: missing info in the group structure while parsing json";
return -1;
}
QString data = o.value(QLatin1String("data")).toString();
QString leaf = o.value(QLatin1String("leaf")).toString();
int trackId = ptr->getTrackIndexFromPosition(data.section(":", 0, 0).toInt());
int pos = data.section(":", 1, 1).toInt();
int id = -1;
if (leaf == QLatin1String("clip")) {
id = ptr->getClipByPosition(trackId, pos);
} else if (leaf == QLatin1String("clip")) {
id = ptr->getCompositionByPosition(trackId, pos);
}
return id;
} else {
qDebug() << "Impossible to create group because the timeline is not available anymore";
Q_ASSERT(false);
}
} else {
if (!o.contains(QLatin1String("children"))) {
qDebug() << "Error: missing info in the group structure while parsing json";
return -1;
}
auto value = o.value(QLatin1String("children"));
if (!value.isArray()) {
qDebug() << "Error : Expected json array of children while parsing groups";
return -1;
}
const auto children = value.toArray();
std::unordered_set<int> ids;
for (const auto &c : children) {
if (!c.isObject()) {
qDebug() << "Error : Expected json object while parsing groups";
return -1;
}
ids.insert(fromJson(c.toObject(), undo, redo));
}
if (ids.count(-1) > 0) {
return -1;
}
return groupItems(ids, undo, redo, type);
}
return -1;
}
bool GroupsModel::fromJson(const QString &data)
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
auto json = QJsonDocument::fromJson(data.toUtf8());
if (!json.isArray()) {
qDebug() << "Error : Json file should be an array";
return false;
}
const auto list = json.array();
bool ok = true;
for (const auto &elem : list) {
if (!elem.isObject()) {
qDebug() << "Error : Expected json object while parsing groups";
undo();
return false;
}
ok = ok && fromJson(elem.toObject(), undo, redo);
}
return ok;
}
......@@ -143,10 +143,12 @@ public:
*/
GroupType getType(int id) const;
/* @brief Returns group data for saving
*/
std::unordered_map<int, int> groupsData();
std::unordered_map<int, std::unordered_set<int>> groupsDataDownlink();
/* @brief Convert the group hierarchy to json.
Note that we cannot expect clipId nor groupId to be the same on project reopening, thus we cannot rely on them for saving.
To workaround that, we currently identify clips by their position + track
*/
const QString toJson() const;
bool fromJson(const QString &data);
protected:
/* @brief Destruct a groupItem in the hierarchy.
......@@ -175,6 +177,14 @@ protected:
/* @brief This is the actual recursive implementation of the copy function. */
bool processCopy(int gid, std::unordered_map<int, int> &mapping, Fun &undo, Fun &redo);
/* @brief This is the actual recursive implementation of the conversion to json */
QJsonObject toJson(int gid) const;
/* @brief This is the actual recursive implementation of the parsing from json
Returns the id of the created group
*/
int fromJson(const QJsonObject &o, Fun &undo, Fun &redo);
private:
std::weak_ptr<TimelineItemModel> m_parent;
......
......@@ -38,10 +38,6 @@
#include <mlt++/MltTransition.h>
#include <mlt++/MltField.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <utility>
TimelineItemModel::TimelineItemModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack> undo_stack)
......@@ -400,101 +396,12 @@ void TimelineItemModel::buildTrackCompositing()
const QString TimelineItemModel::groupsData()
{
std::unordered_map<int, int>upLinks = m_groups->groupsData();
std::unordered_map<int, std::unordered_set<int>>downLinks = m_groups->groupsDataDownlink();
QJsonArray list;
for (const auto &uplink : upLinks) {
QJsonObject currentGroup;
int cid = uplink.first;
if (isClip(cid) || isComposition(cid)) {
continue;
} else {
// encountering a group
currentGroup.insert(QLatin1String("id"), QJsonValue(cid));
currentGroup.insert(QLatin1String("parent"), QJsonValue(uplink.second));
std::unordered_set<int> children = downLinks[cid];
QJsonArray array;
for (const int &child : children) {
if (isClip(child) || isComposition(child)) {
array.append(QString("%1:%2").arg(getTrackMltIndex(getItemTrackId(child))).arg(getItemPosition(child)));
} else {
// this is a subgroup
array.append(child);
}
}
currentGroup.insert(QLatin1String("leaves"), QJsonValue(array));
}
list.push_back(currentGroup);
}
QJsonDocument json(list);
return QString(json.toJson());
return m_groups->toJson();
}
bool TimelineItemModel::loadGroups(const QString &groupsData)
{
auto json = QJsonDocument::fromJson(groupsData.toUtf8());
if (!json.isArray()) {
qDebug() << "Error : Json file should be an array";
return false;
}
QMap <int, int> processedIds;
auto list = json.array();
int i = 0;
while (processedIds.count() < list.count()) {
if (i >= list.count()) {
// loop
i = 0;
}
auto entry = list.at(i);
if (!entry.isObject()) {
qDebug() << "Warning : Skipping invalid marker data";
continue;
}
auto entryObj = entry.toObject();
if (!entryObj.contains(QLatin1String("leaves"))) {
qDebug() << "Warning : Skipping invalid empty group";
continue;
}
int previousId = entryObj[QLatin1String("id")].toInt();
int parentId = entryObj[QLatin1String("parent")].toInt();
if (processedIds.contains(previousId)) {
//already processed
i++;
continue;
}
auto clipList = entryObj[QLatin1String("leaves")].toArray();
std::unordered_set<int> ids;
bool validGroup = true;
for (int j = 0; j < clipList.count(); j++) {
if (clipList[j].isString()) {
QString clip = clipList[j].toString();
if (clip.contains(QStringLiteral(":"))) {
int track = getTrackIndexFromPosition(clip.section(":", 0, 0).toInt() - 1);
int position = clip.section(":", 1, 1).toInt();
int cid = getClipByPosition(track, position);
ids.insert(cid);
} else {
qDebug()<<"// PARSING UNKNOWN OBJECT IN GROUP: "<<parentId<<" = "<<clip;
}
} else {
// subgroup
int clip = clipList[j].toInt();
if (!processedIds.contains(clip)) {
// subgroup has not yet been created, so wait until it is
validGroup = false;
break;
}
ids.insert(processedIds.value(clip));
}
}
if (validGroup) {
int newGroupId = requestClipsGroup(ids, false, GroupType::Normal);
processedIds.insert(previousId, newGroupId);
}
i++;
}
return true;
return m_groups->fromJson(groupsData);
}
bool TimelineItemModel::isInSelection(int cid) const
......
......@@ -227,6 +227,13 @@ int TimelineModel::getClipByPosition(int trackId, int position) const
return getTrackById_const(trackId)->getClipByPosition(position);
}
int TimelineModel::getCompositionByPosition(int trackId, int position) const
{
READ_LOCK();
Q_ASSERT(isTrack(trackId));
return getTrackById_const(trackId)->getCompositionByPosition(position);
}
int TimelineModel::getTrackPosition(int trackId) const
{
READ_LOCK();
......
......@@ -460,10 +460,14 @@ public:
*/
void setTimelineEffectsEnabled(bool enabled);
/* @brief Get a timeline clip id by its position
/* @brief Get a timeline clip id by its position or -1 if not found
*/
int getClipByPosition(int trackId, int position) const;
/* @brief Get a timeline composition id by its starting position or -1 if not found
*/
int getCompositionByPosition(int trackId, int position) const;
/* @brief Returns a list of all items that are at or after a given position.
* @param trackId is the id of the track for concerned items. Setting trackId to -1 returns items on all tracks
* @param position is the position where we the items should start
......
......@@ -22,9 +22,9 @@
#include "trackmodel.hpp"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "kdenlivesettings.h"
#include "snapmodel.hpp"
#include "timelinemodel.hpp"
#include "kdenlivesettings.h"
#include <QDebug>
#include <QModelIndex>
#include <mlt++/MltProfile.h>
......@@ -140,7 +140,7 @@ Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updat
ptr->checkRefresh(new_in, new_out);
}
if (!audioOnly && finalMove) {
qDebug()<<"/// INVALIDATE CLIP ON INSERTT!!!!!!";
qDebug() << "/// INVALIDATE CLIP ON INSERTT!!!!!!";
ptr->invalidateClip(clip->getId());
}
}
......@@ -231,7 +231,7 @@ Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool fin
if (prod != nullptr) {
if (finalMove && !audioOnly) {
if (auto ptr = m_parent.lock()) {
qDebug()<<"/// INVALIDATE CLIP ON DELETE!!!!!!";
qDebug() << "/// INVALIDATE CLIP ON DELETE!!!!!!";
ptr->invalidateClip(clipId);
}
}
......@@ -493,6 +493,17 @@ int TrackModel::getClipByPosition(int position)
return prod->get_int("_kdenlive_cid");
}
int TrackModel::getCompositionByPosition(int position)
{
READ_LOCK();
for (const auto &comp : m_compoPos) {
if (comp.second == position) {
return comp.first;
}
}
return -1;
}
int TrackModel::getClipByRow(int row) const
{
READ_LOCK();
......
......@@ -190,6 +190,8 @@ protected:
/* @brief Returns the clip id on this track at position requested, or -1 if no clip */
int getClipByPosition(int position);
/* @brief Returns the composition id on this track starting position requested, or -1 if not found */
int getCompositionByPosition(int position);
public slots:
/*Delete the current track and all its associated clips */
void slotDelete();
......
......@@ -465,7 +465,14 @@ TEST_CASE("Undo/redo", "[GroupsModel]")
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
RESET();
TimelineItemModel tim2(new Mlt::Profile(), undoStack);
Mock<TimelineItemModel> timMock2(tim2);
TimelineItemModel &tt2 = timMock2.get();
auto timeline2 = std::shared_ptr<TimelineItemModel>(&timMock2.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline2, guideModel);
RESET(timMock);
RESET(timMock2);
Mlt::Profile *pr = new Mlt::Profile();
QString binId = createProducer(*pr, "red", binModel);
......@@ -477,10 +484,17 @@ TEST_CASE("Undo/redo", "[GroupsModel]")
for (int i = 0; i < 4; i++) {
clips.push_back(ClipModel::construct(timeline, binId));
}
std::vector<int> clips2;
for (int i = 0; i < 4; i++) {
clips2.push_back(ClipModel::construct(timeline2, binId));
}
int tid1 = TrackModel::construct(timeline);
int tid2 = TrackModel::construct(timeline);
int tid1_2 = TrackModel::construct(timeline2);
int tid2_2 = TrackModel::construct(timeline2);
int init_index = undoStack->index();
SECTION("Basic Creation")
SECTION("Basic Creation and export/import from json")
{
auto check_roots = [&](int r1, int r2, int r3, int r4) {
REQUIRE(timeline->m_groups->getRootId(clips[0]) == r1);
......@@ -488,13 +502,85 @@ TEST_CASE("Undo/redo", "[GroupsModel]")
REQUIRE(timeline->m_groups->getRootId(clips[2]) == r3);
REQUIRE(timeline->m_groups->getRootId(clips[3]) == r4);
};
// the following function is a recursive function to check the correctness of a json import
// Basically, it takes as input a groupId in the imported (target) group hierarchy, and outputs the corresponding groupId from the original one. If no
// match is found, it returns -1
std::function<int(int)> rec_check;
rec_check = [&](int gid) {
// we first check if the gid is a leaf
if (timeline2->m_groups->isLeaf(gid)) {
// then it must be a clip/composition
int found = -1;
for (int i = 0; i < 4; i++) {
if (clips2[i] == gid) {
found = i;
break;
}
}
if (found != -1) {
return clips[found];
} else {
qDebug() << "ERROR: did not find correspondance for group" << gid;
}
} else {
// we find correspondances of all the children
auto children = timeline2->m_groups->getDirectChildren(gid);
std::unordered_set<int> corresp;
for (int c : children) {
corresp.insert(rec_check(c));
}
if (corresp.count(-1) > 0) {
return -1; // something went wrong
}
std::unordered_set<int> parents;
for (int c : corresp) {
// we find the parents of the corresponding groups in the original hierarchy
parents.insert(timeline->m_groups->m_upLink[c]);
}
// if the matching is correct, we should have found only one parent
if (parents.size() != 1) {
return -1; // something went wrong
}
return *parents.begin();
}
return -1;
};
auto checkJsonParsing = [&]() {
// we first destroy all groups in target timeline
Fun undo = []() { return true; };
Fun redo = []() { return true; };
for (int i = 0; i < 4; i++) {
while (timeline2->m_groups->getRootId(clips2[i]) != clips2[i]) {
timeline2->m_groups->ungroupItem(clips2[i], undo, redo);
}
}