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(); } }