diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp
index ce8b60220..367466c49 100644
--- a/src/timeline2/model/groupsmodel.cpp
+++ b/src/timeline2/model/groupsmodel.cpp
@@ -1,708 +1,711 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.kdenlive.org. *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program 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. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "groupsmodel.hpp"
#include "macros.hpp"
#include "timelineitemmodel.hpp"
#include
#include
#include
#include
#include
#include
#include
GroupsModel::GroupsModel(std::weak_ptr parent)
: m_parent(std::move(parent))
, m_lock(QReadWriteLock::Recursive)
{
}
void GroupsModel::promoteToGroup(int gid, GroupType type)
{
Q_ASSERT(type != GroupType::Leaf);
Q_ASSERT(m_groupIds.count(gid) == 0);
m_groupIds.insert({gid, type});
auto ptr = m_parent.lock();
if (ptr) {
// qDebug() << "Registering group" << gid << "of type" << groupTypeToStr(getType(gid));
ptr->registerGroup(gid);
} else {
qDebug() << "Impossible to create group because the timeline is not available anymore";
Q_ASSERT(false);
}
}
void GroupsModel::downgradeToLeaf(int gid)
{
Q_ASSERT(m_groupIds.count(gid) != 0);
Q_ASSERT(m_downLink.at(gid).size() == 0);
auto ptr = m_parent.lock();
if (ptr) {
// qDebug() << "Deregistering group" << gid << "of type" << groupTypeToStr(getType(gid));
ptr->deregisterGroup(gid);
m_groupIds.erase(gid);
} else {
qDebug() << "Impossible to ungroup item because the timeline is not available anymore";
Q_ASSERT(false);
}
}
Fun GroupsModel::groupItems_lambda(int gid, const std::unordered_set &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) {
setGroup(gid, parent);
}
promoteToGroup(gid, type);
std::unordered_set roots;
std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); });
auto ptr = m_parent.lock();
if (!ptr) Q_ASSERT(false);
for (int id : roots) {
setGroup(getRootId(id), gid);
if (type != GroupType::Selection && ptr->isClip(id)) {
QModelIndex ix = ptr->makeClipIndexFromID(id);
ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole});
}
}
return true;
};
}
int GroupsModel::groupItems(const std::unordered_set &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
return *(ids.begin());
}
int gid = TimelineModel::getNextId();
auto operation = groupItems_lambda(gid, ids, type);
if (operation()) {
auto reverse = destructGroupItem_lambda(gid);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
return gid;
}
return -1;
}
bool GroupsModel::ungroupItem(int id, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
int gid = getRootId(id);
if (m_groupIds.count(gid) == 0) {
// element is not part of a group
return false;
}
return destructGroupItem(gid, true, undo, redo);
}
void GroupsModel::createGroupItem(int id)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) == 0);
Q_ASSERT(m_downLink.count(id) == 0);
m_upLink[id] = -1;
m_downLink[id] = std::unordered_set();
}
Fun GroupsModel::destructGroupItem_lambda(int id)
{
QWriteLocker locker(&m_lock);
return [this, id]() {
removeFromGroup(id);
auto ptr = m_parent.lock();
if (!ptr) Q_ASSERT(false);
for (int child : m_downLink[id]) {
m_upLink[child] = -1;
if (ptr->isClip(child)) {
QModelIndex ix = ptr->makeClipIndexFromID(child);
ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole});
}
}
m_downLink[id].clear();
if (getType(id) != GroupType::Leaf) {
downgradeToLeaf(id);
}
m_downLink.erase(id);
m_upLink.erase(id);
return true;
};
}
bool GroupsModel::destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) > 0);
int parent = m_upLink[id];
auto old_children = m_downLink[id];
auto old_type = GroupType::Selection;
if (m_groupIds.count(id) > 0) {
old_type = m_groupIds[id];
}
auto operation = destructGroupItem_lambda(id);
if (operation()) {
auto reverse = groupItems_lambda(id, old_children, old_type, parent);
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
if (parent != -1 && m_downLink[parent].empty() && deleteOrphan) {
return destructGroupItem(parent, true, undo, redo);
}
return true;
}
return false;
}
bool GroupsModel::destructGroupItem(int id)
{
QWriteLocker locker(&m_lock);
return destructGroupItem_lambda(id)();
}
int GroupsModel::getRootId(int id) const
{
READ_LOCK();
std::unordered_set seen; // we store visited ids to detect cycles
int father = -1;
do {
Q_ASSERT(m_upLink.count(id) > 0);
Q_ASSERT(seen.count(id) == 0);
seen.insert(id);
father = m_upLink.at(id);
if (father != -1) {
id = father;
}
} while (father != -1);
return id;
}
bool GroupsModel::isLeaf(int id) const
{
READ_LOCK();
Q_ASSERT(m_downLink.count(id) > 0);
return m_downLink.at(id).empty();
}
bool GroupsModel::isInGroup(int id) const
{
READ_LOCK();
Q_ASSERT(m_downLink.count(id) > 0);
return getRootId(id) != id;
}
int GroupsModel::getSplitPartner(int id) const
{
READ_LOCK();
Q_ASSERT(m_downLink.count(id) > 0);
int groupId = m_upLink.at(id);
if (groupId == -1 || getType(groupId) != GroupType::AVSplit) {
// clip does not have an AV split partner
return -1;
}
std::unordered_set leaves = getDirectChildren(groupId);
if (leaves.size() != 2) {
// clip does not have an AV split partner
qDebug()<<"WRONG SPLIT GROUP SIZE: "< GroupsModel::getSubtree(int id) const
{
READ_LOCK();
std::unordered_set result;
result.insert(id);
std::queue queue;
queue.push(id);
while (!queue.empty()) {
int current = queue.front();
queue.pop();
for (const int &child : m_downLink.at(current)) {
result.insert(child);
queue.push(child);
}
}
return result;
}
std::unordered_set GroupsModel::getLeaves(int id) const
{
READ_LOCK();
std::unordered_set result;
std::queue queue;
queue.push(id);
while (!queue.empty()) {
int current = queue.front();
queue.pop();
for (const int &child : m_downLink.at(current)) {
queue.push(child);
}
if (m_downLink.at(current).empty()) {
result.insert(current);
}
}
return result;
}
std::unordered_set GroupsModel::getDirectChildren(int id) const
{
READ_LOCK();
Q_ASSERT(m_downLink.count(id) > 0);
return m_downLink.at(id);
}
void GroupsModel::setGroup(int id, int groupId)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) > 0);
Q_ASSERT(groupId == -1 || m_downLink.count(groupId) > 0);
Q_ASSERT(id != groupId);
removeFromGroup(id);
m_upLink[id] = groupId;
if (groupId != -1) {
m_downLink[groupId].insert(id);
if (getType(groupId) == GroupType::Leaf) {
promoteToGroup(groupId, GroupType::Normal);
}
}
}
void GroupsModel::removeFromGroup(int id)
{
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) > 0);
Q_ASSERT(m_downLink.count(id) > 0);
int parent = m_upLink[id];
if (parent != -1) {
Q_ASSERT(getType(parent) != GroupType::Leaf);
m_downLink[parent].erase(id);
if (m_downLink[parent].size() == 0) {
downgradeToLeaf(parent);
}
}
m_upLink[id] = -1;
}
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.
// In the process, if we find a node with only one children, we flag it for deletion
QWriteLocker locker(&m_lock);
Q_ASSERT(m_upLink.count(id) > 0);
auto leaves = getLeaves(id);
std::unordered_map old_parents, new_parents;
std::vector to_delete;
std::unordered_set processed; // to avoid going twice along the same branch
for (int leaf : leaves) {
int current = m_upLink[leaf];
int start = leaf;
while (current != m_upLink[id] && processed.count(current) == 0) {
processed.insert(current);
if (m_downLink[current].size() == 1) {
to_delete.push_back(current);
} else {
if (current != m_upLink[start]) {
old_parents[start] = m_upLink[start];
new_parents[start] = current;
}
start = current;
}
current = m_upLink[current];
}
if (current != m_upLink[start]) {
old_parents[start] = m_upLink[start];
new_parents[start] = current;
}
}
auto parent_changer = [this](const std::unordered_map &parents) {
auto ptr = m_parent.lock();
if (!ptr) {
qDebug() << "Impossible to create group because the timeline is not available anymore";
return false;
}
for (const auto &group : parents) {
int old = m_upLink[group.first];
setGroup(group.first, group.second);
if (old == -1 && group.second != -1 && ptr->isClip(group.first)) {
QModelIndex ix = ptr->makeClipIndexFromID(group.first);
ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole});
}
}
return true;
};
Fun reverse = [this, old_parents, parent_changer]() { return parent_changer(old_parents); };
Fun operation = [this, new_parents, parent_changer]() { return parent_changer(new_parents); };
bool res = operation();
if (!res) {
bool undone = reverse();
Q_ASSERT(undone);
return res;
}
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
for (int gid : to_delete) {
Q_ASSERT(m_downLink[gid].size() == 0);
res = destructGroupItem(gid, false, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return res;
}
}
return true;
}
bool GroupsModel::split(int id, const std::function &criterion, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
+ if (isLeaf(id)) {
+ return true;
+ }
// This function is valid only for roots (otherwise it is not clear what should be the new parent of the created tree)
Q_ASSERT(m_upLink[id] == -1);
bool regroup = m_groupIds[id] != GroupType::Selection;
// We do a BFS on the tree to copy it
// We store corresponding nodes
std::unordered_map corresp; // keys are id in the original tree, values are temporary negative id assigned for creation of the new tree
corresp[-1] = -1;
// These are the nodes to be moved to new tree
std::vector to_move;
// We store the groups (ie the nodes) that are going to be part of the new tree
// Keys are temporary id (negative) and values are the set of children (true ids in the case of leaves and temporary ids for other nodes)
std::unordered_map> new_groups;
std::queue queue;
queue.push(id);
int tempId = -10;
while (!queue.empty()) {
int current = queue.front();
queue.pop();
if (!isLeaf(current) || criterion(current)) {
if (isLeaf(current)) {
if (m_groupIds[getRootId(current)] != GroupType::Selection) {
to_move.push_back(current);
new_groups[corresp[m_upLink[current]]].insert(current);
}
} else {
corresp[current] = tempId;
if (m_upLink[current] != -1) new_groups[corresp[m_upLink[current]]].insert(tempId);
tempId--;
}
}
for (const int &child : m_downLink.at(current)) {
queue.push(child);
}
}
// First, we simulate deletion of elements that we have to remove from the original tree
// A side effect of this is that empty groups will be removed
for (const auto &leaf : to_move) {
destructGroupItem(leaf, true, undo, redo);
}
// we artificially recreate the leaves
Fun operation = [this, to_move]() {
for (const auto &leaf : to_move) {
createGroupItem(leaf);
}
return true;
};
Fun reverse = [this, to_move]() {
for (const auto &group : to_move) {
destructGroupItem(group);
}
return true;
};
bool res = operation();
if (!res) {
return false;
}
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
// We prune the new_groups to remove empty ones
bool finished = false;
while (!finished) {
finished = true;
int selected = INT_MAX;
for (const auto &it : new_groups) {
if (it.second.size() == 0) { // empty group
finished = false;
selected = it.first;
break;
}
for (int it2 : it.second) {
if (it2 < -1 && new_groups.count(it2) == 0) {
// group that has no reference, it is empty too
finished = false;
selected = it2;
break;
}
}
if (!finished) break;
}
if (!finished) {
new_groups.erase(selected);
for (auto it = new_groups.begin(); it != new_groups.end(); ++it) {
(*it).second.erase(selected);
}
}
}
// We now regroup the items of the new tree to recreate hierarchy.
// This is equivalent to creating the tree bottom up (starting from the leaves)
// At each iteration, we create a new node by grouping together elements that are either leaves or already created nodes.
std::unordered_map created_id; // to keep track of node that we create
while (!new_groups.empty()) {
int selected = INT_MAX;
for (const auto &group : new_groups) {
// we check that all children are already created
bool ok = true;
for (int elem : group.second) {
if (elem < -1 && created_id.count(elem) == 0) {
ok = false;
break;
}
}
if (ok) {
selected = group.first;
break;
}
}
Q_ASSERT(selected != INT_MAX);
std::unordered_set group;
for (int elem : new_groups[selected]) {
group.insert(elem < -1 ? created_id[elem] : elem);
}
int gid = groupItems(group, undo, redo, GroupType::Normal, true);
created_id[selected] = gid;
new_groups.erase(selected);
}
if (regroup) {
mergeSingleGroups(id, undo, redo);
mergeSingleGroups(created_id[corresp[id]], undo, redo);
}
return res;
}
void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo)
{
Q_ASSERT(m_upLink.count(targetId) > 0);
Fun operation = [ this, id, group = m_upLink[targetId] ]()
{
setGroup(id, group);
return true;
};
Fun reverse = [ this, id, group = m_upLink[id] ]()
{
setGroup(id, group);
return true;
};
operation();
UPDATE_UNDO_REDO(operation, reverse, undo, redo);
}
bool GroupsModel::processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo)
{
qDebug() << "processCopy" << gid;
if (isLeaf(gid)) {
qDebug() << "it is a leaf";
return true;
}
bool ok = true;
std::unordered_set targetGroup;
for (int child : m_downLink.at(gid)) {
ok = ok && processCopy(child, mapping, undo, redo);
if (!ok) {
break;
}
targetGroup.insert(mapping.at(child));
}
qDebug() << "processCopy" << gid << "success of child" << ok;
if (ok && m_groupIds[gid] != GroupType::Selection) {
int id = groupItems(targetGroup, undo, redo);
qDebug() << "processCopy" << gid << "created id" << id;
if (id != -1) {
mapping[gid] = id;
return true;
}
}
return ok;
}
bool GroupsModel::copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo)
{
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
// destruct old groups for the targets items
for (const auto &corresp : mapping) {
ungroupItem(corresp.second, local_undo, local_redo);
}
std::unordered_set roots;
std::transform(mapping.begin(), mapping.end(), std::inserter(roots, roots.begin()),
[&](decltype(*mapping.begin()) corresp) { return getRootId(corresp.first); });
bool res = true;
qDebug() << "found" << roots.size() << "roots";
for (int r : roots) {
qDebug() << "processing copy for root " << r;
res = res && processCopy(r, mapping, local_undo, local_redo);
if (!res) {
bool undone = local_undo();
Q_ASSERT(undone);
return false;
}
}
UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
return true;
}
GroupType GroupsModel::getType(int id) const
{
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 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 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;
}
diff --git a/tests/groupstest.cpp b/tests/groupstest.cpp
index 9b69cac5f..8a8b2bfc1 100644
--- a/tests/groupstest.cpp
+++ b/tests/groupstest.cpp
@@ -1,1085 +1,1115 @@
#include "bin/model/markerlistmodel.hpp"
#include "catch.hpp"
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
#pragma GCC diagnostic push
#include "fakeit.hpp"
#include
#include
#define private public
#define protected public
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "core.h"
#include "doc/docundostack.hpp"
#include "project/projectmanager.h"
#include "test_utils.hpp"
#include "timeline2/model/clipmodel.hpp"
#include "timeline2/model/groupsmodel.hpp"
#include "timeline2/model/timelineitemmodel.hpp"
#include "timeline2/model/timelinemodel.hpp"
#include "timeline2/model/trackmodel.hpp"
#include
#include
Mlt::Profile profile_group;
TEST_CASE("Functional test of the group hierarchy", "[GroupsModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack);
GroupsModel groups(timeline);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (int i = 0; i < 10; i++) {
groups.createGroupItem(i);
}
SECTION("Test Basic Creation")
{
for (int i = 0; i < 10; i++) {
REQUIRE(groups.getRootId(i) == i);
REQUIRE(groups.isLeaf(i));
REQUIRE(groups.getLeaves(i).size() == 1);
REQUIRE(groups.getSubtree(i).size() == 1);
}
}
groups.setGroup(0, 1);
groups.setGroup(1, 2);
groups.setGroup(3, 2);
groups.setGroup(9, 3);
groups.setGroup(6, 3);
groups.setGroup(4, 3);
groups.setGroup(7, 3);
groups.setGroup(8, 5);
SECTION("Test leaf nodes")
{
std::unordered_set nodes = {1, 2, 3, 5};
for (int i = 0; i < 10; i++) {
REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0));
if (nodes.count(i) == 0) {
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
}
}
SECTION("Test leaves retrieving")
{
REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9}));
REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9}));
REQUIRE(groups.getLeaves(1) == std::unordered_set({0}));
REQUIRE(groups.getLeaves(5) == std::unordered_set({8}));
}
SECTION("Test subtree retrieving")
{
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 6, 7, 9}));
REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9}));
REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8}));
}
SECTION("Test root retieving")
{
std::set first_tree = {0, 1, 2, 3, 4, 6, 7, 9};
for (int n : first_tree) {
CAPTURE(n);
REQUIRE(groups.getRootId(n) == 2);
}
std::unordered_set second_tree = {5, 8};
for (int n : second_tree) {
REQUIRE(groups.getRootId(n) == 5);
}
}
groups.setGroup(3, 8);
SECTION("Test leaf nodes 2")
{
std::unordered_set nodes = {1, 2, 3, 5, 8};
for (int i = 0; i < 10; i++) {
REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0));
if (nodes.count(i) == 0) {
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
}
}
SECTION("Test leaves retrieving 2")
{
REQUIRE(groups.getLeaves(1) == std::unordered_set({0}));
REQUIRE(groups.getLeaves(2) == std::unordered_set({0}));
REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9}));
REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9}));
REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9}));
}
SECTION("Test subtree retrieving 2")
{
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2}));
REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9}));
REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9}));
}
SECTION("Test root retieving 2")
{
std::set first_tree = {0, 1, 2};
for (int n : first_tree) {
CAPTURE(n);
REQUIRE(groups.getRootId(n) == 2);
}
std::unordered_set second_tree = {5, 8, 3, 4, 6, 7, 9};
for (int n : second_tree) {
REQUIRE(groups.getRootId(n) == 5);
}
}
groups.setGroup(5, 2);
SECTION("Test leaf nodes 3")
{
std::unordered_set nodes = {1, 2, 3, 5, 8};
for (int i = 0; i < 10; i++) {
REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0));
if (nodes.count(i) == 0) {
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
}
}
SECTION("Test leaves retrieving 3")
{
REQUIRE(groups.getLeaves(1) == std::unordered_set({0}));
REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 4, 6, 7, 9}));
REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9}));
REQUIRE(groups.getLeaves(5) == std::unordered_set({4, 6, 7, 9}));
REQUIRE(groups.getLeaves(8) == std::unordered_set({4, 6, 7, 9}));
}
SECTION("Test subtree retrieving 3")
{
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}));
REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9}));
REQUIRE(groups.getSubtree(5) == std::unordered_set({5, 8, 3, 4, 6, 7, 9}));
}
SECTION("Test root retieving 3")
{
for (int i = 0; i < 10; i++) {
CAPTURE(i);
REQUIRE(groups.getRootId(i) == 2);
}
}
groups.destructGroupItem(8, false, undo, redo);
SECTION("Test leaf nodes 4")
{
std::unordered_set nodes = {1, 2, 3};
for (int i = 0; i < 10; i++) {
if (i == 8) continue;
REQUIRE(groups.isLeaf(i) != (nodes.count(i) > 0));
if (nodes.count(i) == 0) {
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
}
}
SECTION("Test leaves retrieving 4")
{
REQUIRE(groups.getLeaves(1) == std::unordered_set({0}));
REQUIRE(groups.getLeaves(2) == std::unordered_set({0, 5}));
REQUIRE(groups.getLeaves(3) == std::unordered_set({4, 6, 7, 9}));
}
SECTION("Test subtree retrieving 4")
{
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 5}));
REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6, 7, 9}));
REQUIRE(groups.getSubtree(5) == std::unordered_set({5}));
}
SECTION("Test root retieving 4")
{
std::set first_tree = {0, 1, 2, 5};
for (int n : first_tree) {
CAPTURE(n);
REQUIRE(groups.getRootId(n) == 2);
}
std::unordered_set second_tree = {3, 4, 6, 7, 9};
for (int n : second_tree) {
CAPTURE(n);
REQUIRE(groups.getRootId(n) == 3);
}
}
}
TEST_CASE("Interface test of the group hierarchy", "[GroupsModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack);
GroupsModel groups(timeline);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (int i = 0; i < 10; i++) {
groups.createGroupItem(i);
// the following call shouldn't do anything, but we test that behaviour too.
groups.ungroupItem(i, undo, redo);
REQUIRE(groups.getRootId(i) == i);
REQUIRE(groups.isLeaf(i));
REQUIRE(groups.getLeaves(i).size() == 1);
REQUIRE(groups.getSubtree(i).size() == 1);
}
auto g1 = std::unordered_set({4, 6, 7, 9});
int gid1 = groups.groupItems(g1, undo, redo);
SECTION("One single group")
{
for (int i = 0; i < 10; i++) {
if (g1.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid1);
} else {
REQUIRE(groups.getRootId(i) == i);
}
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
REQUIRE(groups.getLeaves(gid1) == g1);
auto g1b = g1;
g1b.insert(gid1);
REQUIRE(groups.getSubtree(gid1) == g1b);
}
SECTION("Twice the same group")
{
int old_gid1 = gid1;
gid1 = groups.groupItems(g1, undo, redo); // recreate the same group (will create a parent with the old group as only element)
for (int i = 0; i < 10; i++) {
if (g1.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid1);
} else {
REQUIRE(groups.getRootId(i) == i);
}
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
REQUIRE(groups.getLeaves(gid1) == g1);
REQUIRE(groups.getLeaves(old_gid1) == g1);
auto g1b = g1;
g1b.insert(old_gid1);
REQUIRE(groups.getSubtree(old_gid1) == g1b);
g1b.insert(gid1);
REQUIRE(groups.getSubtree(gid1) == g1b);
}
auto g2 = std::unordered_set({3, 5, 7});
int gid2 = groups.groupItems(g2, undo, redo);
auto all_g2 = g2;
all_g2.insert(4);
all_g2.insert(6);
all_g2.insert(9);
SECTION("Heterogeneous group")
{
for (int i = 0; i < 10; i++) {
CAPTURE(i);
if (all_g2.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid2);
} else {
REQUIRE(groups.getRootId(i) == i);
}
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
REQUIRE(groups.getLeaves(gid1) == g1);
REQUIRE(groups.getLeaves(gid2) == all_g2);
}
auto g3 = std::unordered_set({0, 1});
int gid3 = groups.groupItems(g3, undo, redo);
auto g4 = std::unordered_set({0, 4});
int gid4 = groups.groupItems(g4, undo, redo);
auto all_g4 = all_g2;
for (int i : g3)
all_g4.insert(i);
SECTION("Group of group")
{
for (int i = 0; i < 10; i++) {
CAPTURE(i);
if (all_g4.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid4);
} else {
REQUIRE(groups.getRootId(i) == i);
}
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
REQUIRE(groups.getLeaves(gid1) == g1);
REQUIRE(groups.getLeaves(gid2) == all_g2);
REQUIRE(groups.getLeaves(gid3) == g3);
REQUIRE(groups.getLeaves(gid4) == all_g4);
}
// the following should delete g4
groups.ungroupItem(3, undo, redo);
SECTION("Ungroup")
{
for (int i = 0; i < 10; i++) {
CAPTURE(i);
if (all_g2.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid2);
} else if (g3.count(i) > 0) {
REQUIRE(groups.getRootId(i) == gid3);
} else {
REQUIRE(groups.getRootId(i) == i);
}
REQUIRE(groups.getSubtree(i) == std::unordered_set({i}));
REQUIRE(groups.getLeaves(i) == std::unordered_set({i}));
}
REQUIRE(groups.getLeaves(gid1) == g1);
REQUIRE(groups.getLeaves(gid2) == all_g2);
REQUIRE(groups.getLeaves(gid3) == g3);
}
}
TEST_CASE("Orphan groups deletion", "[GroupsModel]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
std::shared_ptr timeline = TimelineItemModel::construct(new Mlt::Profile(), guideModel, undoStack);
GroupsModel groups(timeline);
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (int i = 0; i < 4; i++) {
groups.createGroupItem(i);
}
auto g1 = std::unordered_set({0, 1});
int gid1 = groups.groupItems(g1, undo, redo);
auto g2 = std::unordered_set({2, 3});
int gid2 = groups.groupItems(g2, undo, redo);
auto g3 = std::unordered_set({0, 3});
int gid3 = groups.groupItems(g3, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({0, 1, 2, 3}));
groups.destructGroupItem(0, true, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({1, 2, 3}));
SECTION("Normal deletion")
{
groups.destructGroupItem(1, false, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({gid1, 2, 3}));
groups.destructGroupItem(gid1, true, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3}));
}
SECTION("Cascade deletion")
{
groups.destructGroupItem(1, true, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({2, 3}));
groups.destructGroupItem(2, true, undo, redo);
REQUIRE(groups.getLeaves(gid3) == std::unordered_set({3}));
REQUIRE(groups.m_downLink.count(gid3) > 0);
groups.destructGroupItem(3, true, undo, redo);
REQUIRE(groups.m_downLink.count(gid3) == 0);
REQUIRE(groups.m_downLink.size() == 0);
}
}
TEST_CASE("Undo/redo", "[GroupsModel]")
{
qDebug() << "STARTING PASS";
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr undoStack = std::make_shared(nullptr);
std::shared_ptr guideModel = std::make_shared(undoStack);
// Here we do some trickery to enable testing.
// We mock the project class so that the undoStack function returns our undoStack
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
TimelineItemModel tim(new Mlt::Profile(), undoStack);
Mock timMock(tim);
TimelineItemModel &tt = timMock.get();
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
TimelineItemModel tim2(new Mlt::Profile(), undoStack);
Mock timMock2(tim2);
TimelineItemModel &tt2 = timMock2.get();
auto timeline2 = std::shared_ptr(&timMock2.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline2, guideModel);
RESET(timMock);
RESET(timMock2);
Mlt::Profile *pr = new Mlt::Profile();
QString binId = createProducer(*pr, "red", binModel);
int length = binModel->getClipByBinID(binId)->frameDuration();
GroupsModel groups(timeline);
std::vector clips;
for (int i = 0; i < 4; i++) {
clips.push_back(ClipModel::construct(timeline, binId));
}
std::vector 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 and export/import from json")
{
auto check_roots = [&](int r1, int r2, int r3, int r4) {
REQUIRE(timeline->m_groups->getRootId(clips[0]) == r1);
REQUIRE(timeline->m_groups->getRootId(clips[1]) == r2);
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 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 corresp;
for (int c : children) {
corresp.insert(rec_check(c));
}
if (corresp.count(-1) > 0) {
return -1; // something went wrong
}
std::unordered_set 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);
}
}
// we do the export then import
REQUIRE(timeline2->m_groups->fromJson(timeline->m_groups->toJson()));
std::unordered_map roots;
for (int i = 0; i < 4; i++) {
int r = timeline2->m_groups->getRootId(clips2[0]);
if (roots.count(r) == 0) {
roots[r] = rec_check(r);
REQUIRE(roots[r] != -1);
}
}
for (int i = 0; i < 4; i++) {
int r = timeline->m_groups->getRootId(clips[0]);
int r2 = timeline2->m_groups->getRootId(clips2[0]);
REQUIRE(roots[r2] == r);
}
};
auto g1 = std::unordered_set({clips[0], clips[1]});
int gid1, gid2, gid3;
// this fails because clips are not inserted
REQUIRE(timeline->requestClipsGroup(g1) == -1);
for (int i = 0; i < 4; i++) {
REQUIRE(timeline->requestClipMove(clips[i], (i % 2 == 0) ? tid1 : tid2, i * length));
}
for (int i = 0; i < 4; i++) {
REQUIRE(timeline2->requestClipMove(clips2[i], (i % 2 == 0) ? tid1_2 : tid2_2, i * length));
}
init_index = undoStack->index();
REQUIRE(timeline->requestClipsGroup(g1, true, GroupType::Normal) > 0);
auto state1 = [&]() {
gid1 = timeline->m_groups->getRootId(clips[0]);
check_roots(gid1, gid1, clips[2], clips[3]);
REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal);
REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]}));
REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]}));
REQUIRE(undoStack->index() == init_index + 1);
};
INFO("Test 1");
checkJsonParsing();
state1();
auto g2 = std::unordered_set({clips[2], clips[3]});
REQUIRE(timeline->requestClipsGroup(g2, true, GroupType::AVSplit) > 0);
auto state2 = [&]() {
gid2 = timeline->m_groups->getRootId(clips[2]);
check_roots(gid1, gid1, gid2, gid2);
REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal);
REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]}));
REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]}));
REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]}));
REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]}));
REQUIRE(undoStack->index() == init_index + 2);
};
INFO("Test 2");
checkJsonParsing();
state2();
auto g3 = std::unordered_set({clips[0], clips[3]});
REQUIRE(timeline->requestClipsGroup(g3, true, GroupType::Normal) > 0);
auto state3 = [&]() {
REQUIRE(undoStack->index() == init_index + 3);
gid3 = timeline->m_groups->getRootId(clips[0]);
check_roots(gid3, gid3, gid3, gid3);
REQUIRE(timeline->m_groups->getType(gid1) == GroupType::Normal);
REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal);
REQUIRE(timeline->m_groups->getSubtree(gid3) == std::unordered_set({gid1, clips[0], clips[1], gid3, gid2, clips[2], clips[3]}));
REQUIRE(timeline->m_groups->getLeaves(gid3) == std::unordered_set({clips[2], clips[3], clips[0], clips[1]}));
REQUIRE(timeline->m_groups->getSubtree(gid2) == std::unordered_set({gid2, clips[2], clips[3]}));
REQUIRE(timeline->m_groups->getLeaves(gid2) == std::unordered_set({clips[2], clips[3]}));
REQUIRE(timeline->m_groups->getSubtree(gid1) == std::unordered_set({gid1, clips[0], clips[1]}));
REQUIRE(timeline->m_groups->getLeaves(gid1) == std::unordered_set({clips[0], clips[1]}));
};
INFO("Test 3");
checkJsonParsing();
state3();
undoStack->undo();
INFO("Test 4");
checkJsonParsing();
state2();
undoStack->redo();
INFO("Test 5");
checkJsonParsing();
state3();
undoStack->undo();
INFO("Test 6");
checkJsonParsing();
state2();
undoStack->undo();
INFO("Test 8");
checkJsonParsing();
state1();
undoStack->undo();
INFO("Test 9");
checkJsonParsing();
check_roots(clips[0], clips[1], clips[2], clips[3]);
undoStack->redo();
INFO("Test 10");
checkJsonParsing();
state1();
undoStack->redo();
INFO("Test 11");
checkJsonParsing();
state2();
REQUIRE(timeline->requestClipsGroup(g3) > 0);
checkJsonParsing();
state3();
undoStack->undo();
checkJsonParsing();
state2();
undoStack->undo();
checkJsonParsing();
state1();
undoStack->undo();
checkJsonParsing();
check_roots(clips[0], clips[1], clips[2], clips[3]);
}
SECTION("Group deletion undo")
{
int tid1 = TrackModel::construct(timeline);
CAPTURE(clips[0]);
CAPTURE(clips[1]);
CAPTURE(clips[2]);
CAPTURE(clips[3]);
REQUIRE(timeline->requestClipMove(clips[0], tid1, 10));
REQUIRE(timeline->requestClipMove(clips[1], tid1, 10 + length));
REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length));
REQUIRE(timeline->requestClipMove(clips[3], tid1, 50 + 3 * length));
auto state0 = [&]() {
REQUIRE(timeline->getTrackById(tid1)->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 4);
for (int i = 0; i < 4; i++) {
REQUIRE(timeline->getClipTrackId(clips[i]) == tid1);
}
REQUIRE(timeline->getClipPosition(clips[0]) == 10);
REQUIRE(timeline->getClipPosition(clips[1]) == 10 + length);
REQUIRE(timeline->getClipPosition(clips[2]) == 15 + 2 * length);
REQUIRE(timeline->getClipPosition(clips[3]) == 50 + 3 * length);
};
auto state = [&](int gid1, int gid2, int gid3) {
state0();
REQUIRE(timeline->m_groups->getType(gid1) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(gid2) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(gid3) == GroupType::Normal);
};
state0();
auto g1 = std::unordered_set({clips[0], clips[1]});
int gid1, gid2, gid3;
gid1 = timeline->requestClipsGroup(g1, true, GroupType::AVSplit);
REQUIRE(gid1 > 0);
auto g2 = std::unordered_set({clips[2], clips[3]});
gid2 = timeline->requestClipsGroup(g2, true, GroupType::AVSplit);
REQUIRE(gid2 > 0);
auto g3 = std::unordered_set({clips[0], clips[3]});
gid3 = timeline->requestClipsGroup(g3, true, GroupType::Normal);
REQUIRE(gid3 > 0);
state(gid1, gid2, gid3);
for (int i = 0; i < 4; i++) {
REQUIRE(timeline->requestItemDeletion(clips[i]));
REQUIRE(timeline->getTrackClipsCount(tid1) == 0);
REQUIRE(timeline->getClipsCount() == 0);
REQUIRE(timeline->getTrackById(tid1)->checkConsistency());
undoStack->undo();
state(gid1, gid2, gid3);
undoStack->redo();
REQUIRE(timeline->getTrackClipsCount(tid1) == 0);
REQUIRE(timeline->getClipsCount() == 0);
REQUIRE(timeline->getTrackById(tid1)->checkConsistency());
undoStack->undo();
state(gid1, gid2, gid3);
}
// we undo the three grouping operations
undoStack->undo();
state0();
undoStack->undo();
state0();
undoStack->undo();
state0();
}
SECTION("Group creation and query from timeline")
{
REQUIRE(timeline->requestClipMove(clips[0], tid1, 10));
REQUIRE(timeline->requestClipMove(clips[1], tid1, 10 + length));
REQUIRE(timeline->requestClipMove(clips[2], tid1, 15 + 2 * length));
REQUIRE(timeline->requestClipMove(clips[3], tid1, 50 + 3 * length));
auto state1 = [&]() {
REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]}));
REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]}));
REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]}));
REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]}));
};
state1();
auto g1 = std::unordered_set({clips[0], clips[3]});
int gid1, gid2, gid3;
REQUIRE(timeline->requestClipsGroup(g1) > 0);
auto state2 = [&]() {
REQUIRE(timeline->getGroupElements(clips[0]) == g1);
REQUIRE(timeline->getGroupElements(clips[3]) == g1);
REQUIRE(timeline->getGroupElements(clips[2]) == std::unordered_set({clips[2]}));
REQUIRE(timeline->getGroupElements(clips[1]) == std::unordered_set({clips[1]}));
};
state2();
undoStack->undo();
state1();
undoStack->redo();
state2();
undoStack->undo();
state1();
undoStack->redo();
state2();
auto g2 = std::unordered_set({clips[2], clips[1]});
REQUIRE(timeline->requestClipsGroup(g2) > 0);
auto state3 = [&]() {
REQUIRE(timeline->getGroupElements(clips[0]) == g1);
REQUIRE(timeline->getGroupElements(clips[3]) == g1);
REQUIRE(timeline->getGroupElements(clips[2]) == g2);
REQUIRE(timeline->getGroupElements(clips[1]) == g2);
};
state3();
undoStack->undo();
state2();
undoStack->redo();
state3();
auto g3 = std::unordered_set({clips[0], clips[1]});
REQUIRE(timeline->requestClipsGroup(g3) > 0);
auto all_g = std::unordered_set({clips[0], clips[1], clips[2], clips[3]});
auto state4 = [&]() {
REQUIRE(timeline->getGroupElements(clips[0]) == all_g);
REQUIRE(timeline->getGroupElements(clips[3]) == all_g);
REQUIRE(timeline->getGroupElements(clips[2]) == all_g);
REQUIRE(timeline->getGroupElements(clips[1]) == all_g);
};
state4();
undoStack->undo();
state3();
undoStack->redo();
state4();
REQUIRE(timeline->requestClipUngroup(clips[0]));
state3();
undoStack->undo();
state4();
REQUIRE(timeline->requestClipUngroup(clips[1]));
state3();
undoStack->undo();
state4();
undoStack->redo();
state3();
REQUIRE(timeline->requestClipUngroup(clips[0]));
REQUIRE(timeline->getGroupElements(clips[2]) == g2);
REQUIRE(timeline->getGroupElements(clips[1]) == g2);
REQUIRE(timeline->getGroupElements(clips[3]) == std::unordered_set({clips[3]}));
REQUIRE(timeline->getGroupElements(clips[0]) == std::unordered_set({clips[0]}));
REQUIRE(timeline->requestClipUngroup(clips[1]));
state1();
}
SECTION("MergeSingleGroups")
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(groups.m_upLink.size() == 0);
for (int i = 0; i < 6; i++) {
groups.createGroupItem(i);
}
groups.setGroup(0, 3);
groups.setGroup(2, 4);
groups.setGroup(3, 1);
groups.setGroup(4, 1);
groups.setGroup(5, 0);
auto test_tree = [&]() {
REQUIRE(groups.getSubtree(1) == std::unordered_set({0, 1, 2, 3, 4, 5}));
REQUIRE(groups.getDirectChildren(0) == std::unordered_set({5}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 4}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0}));
REQUIRE(groups.getDirectChildren(4) == std::unordered_set({2}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
};
test_tree();
REQUIRE(groups.mergeSingleGroups(1, undo, redo));
auto test_tree2 = [&]() {
REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 2, 5}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2, 5}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
};
test_tree2();
undo();
test_tree();
redo();
test_tree2();
}
SECTION("MergeSingleGroups2")
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(groups.m_upLink.size() == 0);
for (int i = 0; i < 3; i++) {
groups.createGroupItem(i);
}
groups.setGroup(1, 0);
groups.setGroup(2, 1);
auto test_tree = [&]() {
REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2}));
REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({2}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
};
test_tree();
REQUIRE(groups.mergeSingleGroups(0, undo, redo));
auto test_tree2 = [&]() {
REQUIRE(groups.getSubtree(2) == std::unordered_set({2}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
REQUIRE(groups.getRootId(2) == 2);
};
test_tree2();
undo();
test_tree();
redo();
test_tree2();
}
SECTION("MergeSingleGroups3")
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(groups.m_upLink.size() == 0);
for (int i = 0; i < 6; i++) {
groups.createGroupItem(i);
}
groups.setGroup(0, 2);
groups.setGroup(1, 0);
groups.setGroup(3, 1);
groups.setGroup(4, 1);
groups.setGroup(5, 4);
auto test_tree = [&]() {
for (int i = 0; i < 6; i++) {
REQUIRE(groups.getRootId(i) == 2);
}
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5}));
REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({4, 3}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({0}));
REQUIRE(groups.getDirectChildren(3) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(4) == std::unordered_set({5}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
};
test_tree();
REQUIRE(groups.mergeSingleGroups(2, undo, redo));
auto test_tree2 = [&]() {
REQUIRE(groups.getRootId(1) == 1);
REQUIRE(groups.getRootId(3) == 1);
REQUIRE(groups.getRootId(5) == 1);
REQUIRE(groups.getSubtree(1) == std::unordered_set({1, 3, 5}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({3, 5}));
REQUIRE(groups.getDirectChildren(3) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
};
test_tree2();
undo();
test_tree();
redo();
test_tree2();
}
+ SECTION("Split leaf")
+ {
+ Fun undo = []() { return true; };
+ Fun redo = []() { return true; };
+ REQUIRE(groups.m_upLink.size() == 0);
+
+ // This is a dummy split criterion
+ auto criterion = [](int a) { return a % 2 == 0; };
+ auto criterion2 = [](int a) { return a % 2 != 0; };
+
+ // We create a leaf
+ groups.createGroupItem(1);
+ auto test_leaf = [&]() {
+ REQUIRE(groups.getRootId(1) == 1);
+ REQUIRE(groups.isLeaf(1));
+ REQUIRE(groups.m_upLink.size() == 1);
+ };
+ test_leaf();
+
+ REQUIRE(groups.split(1, criterion, undo, redo));
+ test_leaf();
+ undo();
+ test_leaf();
+ redo();
+ REQUIRE(groups.split(1, criterion2, undo, redo));
+ test_leaf();
+ undo();
+ test_leaf();
+ redo();
+ }
SECTION("Simple split Tree")
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(groups.m_upLink.size() == 0);
// This is a dummy split criterion
auto criterion = [](int a) { return a % 2 == 0; };
// We create a very simple tree
for (int i = 0; i < 3; i++) {
groups.createGroupItem(i);
}
groups.setGroup(1, 0);
groups.setGroup(2, 0);
auto test_tree = [&]() {
REQUIRE(groups.getRootId(0) == 0);
REQUIRE(groups.getRootId(1) == 0);
REQUIRE(groups.getRootId(2) == 0);
REQUIRE(groups.getSubtree(0) == std::unordered_set({0, 1, 2}));
REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 2}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
};
test_tree();
REQUIRE(groups.split(0, criterion, undo, redo));
auto test_tree2 = [&]() {
REQUIRE(groups.getRootId(1) == 1);
REQUIRE(groups.getRootId(2) == 2);
REQUIRE(groups.getSubtree(2) == std::unordered_set({2}));
REQUIRE(groups.getSubtree(1) == std::unordered_set({1}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({}));
};
test_tree2();
undo();
test_tree();
redo();
test_tree2();
}
SECTION("complex split Tree")
{
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(groups.m_upLink.size() == 0);
// This is a dummy split criterion
auto criterion = [](int a) { return a % 2 != 0; };
for (int i = 0; i < 9; i++) {
groups.createGroupItem(i);
}
groups.setGroup(0, 3);
groups.setGroup(1, 0);
groups.setGroup(3, 2);
groups.setGroup(4, 3);
groups.setGroup(5, 8);
groups.setGroup(6, 0);
groups.setGroup(7, 8);
groups.setGroup(8, 2);
auto test_tree = [&]() {
for (int i = 0; i < 9; i++) {
REQUIRE(groups.getRootId(i) == 2);
}
REQUIRE(groups.getSubtree(2) == std::unordered_set({0, 1, 2, 3, 4, 5, 6, 7, 8}));
REQUIRE(groups.getDirectChildren(0) == std::unordered_set({1, 6}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(2) == std::unordered_set({3, 8}));
REQUIRE(groups.getDirectChildren(3) == std::unordered_set({0, 4}));
REQUIRE(groups.getDirectChildren(4) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(6) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(7) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(8) == std::unordered_set({5, 7}));
};
test_tree();
REQUIRE(groups.split(2, criterion, undo, redo));
auto test_tree2 = [&]() {
REQUIRE(groups.getRootId(6) == 3);
REQUIRE(groups.getRootId(3) == 3);
REQUIRE(groups.getRootId(4) == 3);
REQUIRE(groups.getSubtree(3) == std::unordered_set({3, 4, 6}));
REQUIRE(groups.getDirectChildren(6) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(4) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(3) == std::unordered_set({6, 4}));
// new tree
int newRoot = groups.getRootId(1);
REQUIRE(groups.getRootId(1) == newRoot);
REQUIRE(groups.getRootId(5) == newRoot);
REQUIRE(groups.getRootId(7) == newRoot);
int other = -1;
REQUIRE(groups.getDirectChildren(newRoot).size() == 2);
for (int c : groups.getDirectChildren(newRoot))
if (c != 1) other = c;
REQUIRE(other != -1);
REQUIRE(groups.getSubtree(newRoot) == std::unordered_set({1, 5, 7, newRoot, other}));
REQUIRE(groups.getDirectChildren(1) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(5) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(7) == std::unordered_set({}));
REQUIRE(groups.getDirectChildren(newRoot) == std::unordered_set({1, other}));
REQUIRE(groups.getDirectChildren(other) == std::unordered_set({5, 7}));
};
test_tree2();
undo();
test_tree();
redo();
test_tree2();
}
}