diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp index 809712522..8e27538e3 100644 --- a/src/timeline2/model/groupsmodel.cpp +++ b/src/timeline2/model/groupsmodel.cpp @@ -1,1051 +1,1075 @@ /*************************************************************************** * 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 "trackmodel.hpp" #include #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(ids.size() == 0 || type != GroupType::Leaf); return [gid, ids, parent, type, this]() { createGroupItem(gid); if (parent != -1) { setGroup(gid, parent); } if (ids.size() > 0) { 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, type != GroupType::Selection); } } 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()); std::unordered_set roots; std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return getRootId(id); }); if (roots.size() == 1 && !force) { // We do not create a group with only one element. Instead, we return the id of that element return *(roots.begin()); } if (type == GroupType::AVSplit && !force) { // additional checks for AVSplit if (roots.size() != 2) { // must group exactly two items return -1; } auto it = roots.begin(); int cid1 = *it; ++it; int cid2 = *it; auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); if (cid1 == cid2 || !ptr->isClip(cid1) || !ptr->isClip(cid2)) { // invalid: we must get two different clips return -1; } int tid1 = ptr->getClipTrackId(cid1); bool isAudio1 = ptr->getTrackById(tid1)->isAudioTrack(); int tid2 = ptr->getClipTrackId(cid2); bool isAudio2 = ptr->getTrackById(tid2)->isAudioTrack(); if (isAudio1 == isAudio2) { // invalid: we must insert one in video the other in audio return -1; } } int gid = TimelineModel::getNextId(); auto operation = groupItems_lambda(gid, roots, 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; QModelIndex ix; if (ptr->isClip(child)) { ix = ptr->makeClipIndexFromID(child); } else if (ptr->isComposition(child)) { ix = ptr->makeCompositionIndexFromID(child); } if (ix.isValid()) { 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 = getType(id); auto old_parent_type = GroupType::Normal; if (parent != -1) { old_parent_type = getType(parent); } auto operation = destructGroupItem_lambda(id); if (operation()) { auto reverse = groupItems_lambda(id, old_children, old_type, parent); // we may need to reset the group of the parent if (parent != -1) { auto setParent = [&, old_parent_type, parent]() { setType(parent, old_parent_type); return true; }; PUSH_LAMBDA(setParent, reverse); } 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: " << leaves.size(); return -1; } for (const int &child : leaves) { if (child != id) { return child; } } return -1; } std::unordered_set 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); } int GroupsModel::getDirectAncestor(int id) const { READ_LOCK(); Q_ASSERT(m_upLink.count(id) > 0); return m_upLink.at(id); } void GroupsModel::setGroup(int id, int groupId, bool changeState) { 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); auto ptr = m_parent.lock(); if (changeState && ptr) { QModelIndex ix; if (ptr->isClip(id)) { ix = ptr->makeClipIndexFromID(id); } else if (ptr->isComposition(id)) { ix = ptr->makeCompositionIndexFromID(id); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } } 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); QModelIndex ix; auto ptr = m_parent.lock(); if (!ptr) Q_ASSERT(false); if (ptr->isClip(id)) { ix = ptr->makeClipIndexFromID(id); } else if (ptr->isComposition(id)) { ix = ptr->makeCompositionIndexFromID(id); } if (ix.isValid()) { ptr->dataChanged(ix, ix, {TimelineModel::GroupedRole}); } 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) { setGroup(group.first, group.second); } return true; }; Fun reverse = [old_parents, parent_changer]() { return parent_changer(old_parents); }; Fun operation = [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); if (getType(gid) == GroupType::Selection) { continue; } 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); Q_ASSERT(m_groupIds[id] != GroupType::Selection); bool regroup = true; // we don't support splitting if selection group is active // 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; // We store also the target type of the new groups std::unordered_map new_types; 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)) { to_move.push_back(current); new_groups[corresp[m_upLink[current]]].insert(current); } else { corresp[current] = tempId; new_types[tempId] = getType(current); 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 &new_group : new_groups) { new_group.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); } Q_ASSERT(new_types.count(selected) != 0); int gid = groupItems(group, undo, redo, new_types[selected], true); created_id[selected] = gid; new_groups.erase(selected); } if (regroup) { if (m_groupIds.count(id) > 0) { mergeSingleGroups(id, undo, redo); } if (created_id[corresp[id]]) { mergeSingleGroups(created_id[corresp[id]], undo, redo); } } return res; } void GroupsModel::setInGroupOf(int id, int targetId, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); 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::createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo) { QWriteLocker locker(&m_lock); Q_ASSERT(m_upLink.count(id) > 0); Q_ASSERT(isLeaf(id)); if (to_add.size() == 0) { return true; } int gid = TimelineModel::getNextId(); std::unordered_map old_parents; to_add.insert(id); for (int g : to_add) { Q_ASSERT(m_upLink.count(g) > 0); old_parents[g] = m_upLink[g]; } Fun operation = [this, gid, type, to_add, parent = m_upLink.at(id)]() { createGroupItem(gid); setGroup(gid, parent); for (const auto &g : to_add) { setGroup(g, gid); } setType(gid, type); return true; }; Fun reverse = [this, old_parents, gid]() { for (const auto &g : old_parents) { setGroup(g.first, g.second); } setGroup(gid, -1); destructGroupItem_lambda(gid)(); return true; }; bool success = operation(); if (success) { UPDATE_UNDO_REDO(operation, reverse, undo, redo); } return success; } 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) { if (getType(r) != GroupType::Selection) list.push_back(toJson(r)); } QJsonDocument json(list); return QString(json.toJson()); } const QString GroupsModel::toJson(std::unordered_set roots) const { QJsonArray list; for (int r : roots) { if (getType(r) != GroupType::Selection) 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"))) { qDebug() << "CANNOT PARSE GROUP DATA"; 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("composition")) { id = ptr->getCompositionByPosition(trackId, pos); } else { qDebug() << " * * *UNKNOWN ITEM: " << leaf; } 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; } +void GroupsModel::adjustOffset(QJsonArray &updatedNodes, QJsonObject childObject, int offset, const QMap &trackMap) +{ + auto value = childObject.value(QLatin1String("children")); + auto children = value.toArray(); + for (auto c : children) { + if (!c.isObject()) { + continue; + } + auto child = c.toObject(); + auto type = groupTypeFromStr(child.value(QLatin1String("type")).toString()); + if (child.contains(QLatin1String("data"))) { + if (auto ptr = m_parent.lock()) { + QString cur_data = child.value(QLatin1String("data")).toString(); + int trackId = cur_data.section(":", 0, 0).toInt(); + int pos = cur_data.section(":", 1, 1).toInt(); + int trackPos = ptr->getTrackPosition(trackMap.value(trackId)); + pos += offset; + child.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(trackPos).arg(pos))); + updatedNodes.append(QJsonValue(child)); + } + } else if (type != GroupType::Leaf) { + QJsonObject currentGroup; + currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(type))); + QJsonArray array; + adjustOffset(array, child, offset, trackMap); + currentGroup.insert(QLatin1String("children"), array); + updatedNodes.append(QJsonValue(currentGroup)); + } + } +} + bool GroupsModel::fromJsonWithOffset(const QString &data, const QMap &trackMap, int offset, Fun &undo, Fun &redo) { Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; auto json = QJsonDocument::fromJson(data.toUtf8()); if (!json.isArray()) { qDebug() << "Error : Json file should be an array"; return false; } auto list = json.array(); + QJsonArray newGroups; bool ok = true; for (auto elem : list) { if (!elem.isObject()) { qDebug() << "Error : Expected json object while parsing groups"; local_undo(); return false; } QJsonObject obj = elem.toObject(); + QJsonArray updatedNodes; + auto type = groupTypeFromStr(obj.value(QLatin1String("type")).toString()); auto value = obj.value(QLatin1String("children")); if (!value.isArray()) { qDebug() << "Error : Expected json array of children while parsing groups"; continue; } - QJsonArray updatedNodes; + // Adjust offset auto children = value.toArray(); - std::unordered_set ids; - for (auto c : children) { - if (!c.isObject()) { - continue; - } - QJsonObject child = c.toObject(); - if (child.contains(QLatin1String("data"))) { - if (auto ptr = m_parent.lock()) { - QString cur_data = child.value(QLatin1String("data")).toString(); - int trackId = cur_data.section(":", 0, 0).toInt(); - int pos = cur_data.section(":", 1, 1).toInt(); - int trackPos = ptr->getTrackPosition(trackMap.value(trackId)); - pos += offset; - child.insert(QLatin1String("data"), QJsonValue(QString("%1:%2").arg(trackPos).arg(pos))); - } - updatedNodes.append(QJsonValue(child)); - } - } - qDebug() << "* ** * UPDATED JSON NODES: " << updatedNodes; - obj.insert(QLatin1String("children"), QJsonValue(updatedNodes)); - qDebug() << "* ** * UPDATED JSON NODES: " << obj; - ok = ok && (fromJson(obj, local_undo, local_redo) > 0); - if (!ok) { + adjustOffset(updatedNodes, obj, offset, trackMap); + QJsonObject currentGroup; + currentGroup.insert(QLatin1String("children"), QJsonValue(updatedNodes)); + currentGroup.insert(QLatin1String("type"), QJsonValue(groupTypeToStr(type))); + newGroups.append(QJsonValue(currentGroup)); + } + + // Group + for (const auto &elem : newGroups) { + if (!elem.isObject()) { + qDebug() << "Error : Expected json object while parsing groups"; break; } + ok = ok && fromJson(elem.toObject(), local_undo, local_redo); } + if (ok) { UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); } else { bool undone = local_undo(); Q_ASSERT(undone); } return ok; } void GroupsModel::setType(int gid, GroupType type) { Q_ASSERT(m_groupIds.count(gid) != 0); if (type == GroupType::Leaf) { Q_ASSERT(m_downLink[gid].size() == 0); if (m_groupIds.count(gid) > 0) { m_groupIds.erase(gid); } } else { m_groupIds[gid] = type; } } bool GroupsModel::checkConsistency(bool failOnSingleGroups, bool checkTimelineConsistency) { // check that all element with up link have a down link for (const auto &elem : m_upLink) { if (m_downLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } // check that all element with down link have a up link for (const auto &elem : m_downLink) { if (m_upLink.count(elem.first) == 0) { qDebug() << "ERROR: Group model has missing up/down links"; return false; } } int selectionCount = 0; for (const auto &elem : m_upLink) { // iterate through children to check links for (const auto &child : m_downLink[elem.first]) { if (m_upLink[child] != elem.first) { qDebug() << "ERROR: Group model has inconsistent up/down links"; return false; } } bool isLeaf = m_downLink[elem.first].empty(); if (isLeaf) { if (m_groupIds.count(elem.first) > 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } } else { if (m_groupIds.count(elem.first) == 0) { qDebug() << "ERROR: Group model has wrong tracking of non-leaf groups"; return false; } if (m_downLink[elem.first].size() == 1 && failOnSingleGroups) { qDebug() << "ERROR: Group model contains groups with single element"; return false; } if (getType(elem.first) == GroupType::Selection) { selectionCount++; } if (elem.second != -1 && getType(elem.first) == GroupType::Selection) { qDebug() << "ERROR: Group model contains inner groups of selection type"; return false; } if (getType(elem.first) == GroupType::Leaf) { qDebug() << "ERROR: Group model contains groups of Leaf type"; return false; } } } if (selectionCount > 1) { qDebug() << "ERROR: Found too many selections: " << selectionCount; return false; } // Finally, we do a depth first visit of the tree to check for loops std::unordered_set visited; for (const auto &elem : m_upLink) { if (elem.second == -1) { // this is a root, traverse the tree from here std::stack stack; stack.push(elem.first); while (!stack.empty()) { int cur = stack.top(); stack.pop(); if (visited.count(cur) > 0) { qDebug() << "ERROR: Group model contains a cycle"; return false; } visited.insert(cur); for (int child : m_downLink[cur]) { stack.push(child); } } } } // Do a last pass to check everybody was visited for (const auto &elem : m_upLink) { if (visited.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains unreachable elements"; return false; } } if (checkTimelineConsistency) { if (auto ptr = m_parent.lock()) { auto isTimelineObject = [&](int cid) { return ptr->isClip(cid) || ptr->isComposition(cid); }; for (int g : ptr->m_allGroups) { if (m_upLink.count(g) == 0 || getType(g) == GroupType::Leaf) { qDebug() << "ERROR: Timeline contains inconsistent group data"; return false; } } for (const auto &elem : m_upLink) { if (getType(elem.first) == GroupType::Leaf) { if (!isTimelineObject(elem.first)) { qDebug() << "ERROR: Group model contains leaf element that is not a clip nor a composition"; return false; } } else { if (ptr->m_allGroups.count(elem.first) == 0) { qDebug() << "ERROR: Group model contains group element that is not registered on timeline"; Q_ASSERT(false); return false; } if (getType(elem.first) == GroupType::AVSplit) { if (m_downLink[elem.first].size() != 2) { qDebug() << "ERROR: Group model contains a AVSplit group with a children count != 2"; return false; } auto it = m_downLink[elem.first].begin(); int cid1 = (*it); ++it; int cid2 = (*it); if (!isTimelineObject(cid1) || !isTimelineObject(cid2)) { qDebug() << "ERROR: Group model contains an AVSplit group with invalid members"; return false; } int tid1 = ptr->getClipTrackId(cid1); bool isAudio1 = ptr->getTrackById(tid1)->isAudioTrack(); int tid2 = ptr->getClipTrackId(cid2); bool isAudio2 = ptr->getTrackById(tid2)->isAudioTrack(); if (isAudio1 == isAudio2) { qDebug() << "ERROR: Group model contains an AVSplit formed with members that are both on an audio track or on a video track"; return false; } } } } } } return true; } diff --git a/src/timeline2/model/groupsmodel.hpp b/src/timeline2/model/groupsmodel.hpp index 9f09ddd95..530c95c6f 100644 --- a/src/timeline2/model/groupsmodel.hpp +++ b/src/timeline2/model/groupsmodel.hpp @@ -1,233 +1,235 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef GROUPMODEL_H #define GROUPMODEL_H #include "definitions.h" #include "undohelper.hpp" #include #include #include #include class TimelineItemModel; /* @brief This class represents the group hierarchy. This is basically a tree structure In this class, we consider that a groupItem is either a clip or a group */ class GroupsModel { public: GroupsModel() = delete; GroupsModel(std::weak_ptr parent); /* @brief Create a group that contains all the given items and returns the id of the created group. Note that if an item is already part of a group, its topmost group will be considered instead and added in the newly created group. If only one id is provided, no group is created, unless force = true. @param ids set containing the items to group. @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation @param type indicates the type of group we create Returns the id of the new group, or -1 on error. */ int groupItems(const std::unordered_set &ids, Fun &undo, Fun &redo, GroupType type = GroupType::Normal, bool force = false); protected: /* Lambda version */ Fun groupItems_lambda(int gid, const std::unordered_set &ids, GroupType type = GroupType::Normal, int parent = -1); public: /* Deletes the topmost group containing given element Note that if the element is not in a group, then it will not be touched. Return true on success @param id id of the groupitem @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool ungroupItem(int id, Fun &undo, Fun &redo); /* @brief Create a groupItem in the hierarchy. Initially it is not part of a group @param id id of the groupItem */ void createGroupItem(int id); /* @brief Destruct a group item Note that this public function expects that the given id is an orphan element. @param id id of the groupItem */ bool destructGroupItem(int id); /* @brief Merges group with only one child to parent Ex: . . / \ / \ . . becomes a b / \ a b @param id id of the tree to consider */ bool mergeSingleGroups(int id, Fun &undo, Fun &redo); /* @brief Split the group tree according to a given criterion All the leaves satisfying the criterion are moved to the new tree, the other stay Both tree are subsequently simplified to avoid weird structure. @param id is the root of the tree */ bool split(int id, const std::function &criterion, Fun &undo, Fun &redo); /* @brief Copy a group hierarchy. @param mapping describes the correspondence between the ids of the items in the source group hierarchy, and their counterpart in the hierarchy that we create. It will also be used as a return parameter, by adding the mapping between the groups of the hierarchy Note that if the target items should not belong to a group. */ bool copyGroups(std::unordered_map &mapping, Fun &undo, Fun &redo); /* @brief Get the overall father of a given groupItem If the element has no father, it is returned as is. @param id id of the groupitem */ int getRootId(int id) const; /* @brief Returns true if the groupItem has no descendant @param id of the groupItem */ bool isLeaf(int id) const; /* @brief Returns true if the element is in a non-trivial group @param id of the groupItem */ bool isInGroup(int id) const; /* @brief Move element id in the same group as targetId */ void setInGroupOf(int id, int targetId, Fun &undo, Fun &redo); /* @brief We replace the leaf node given by id with a group that contains the leaf plus all the clips in to_add. * The created group type is given in parameter * Returns true on success */ bool createGroupAtSameLevel(int id, std::unordered_set to_add, GroupType type, Fun &undo, Fun &redo); /* @brief Returns the id of all the descendant of given item (including item) @param id of the groupItem */ std::unordered_set getSubtree(int id) const; /* @brief Returns the id of all the leaves in the subtree of the given item This should correspond to the ids of the clips, since they should be the only items with no descendants @param id of the groupItem */ std::unordered_set getLeaves(int id) const; /* @brief Gets direct children of a given group item @param id of the groupItem */ std::unordered_set getDirectChildren(int id) const; /* @brief Gets direct ancestor of a given group item. Returns -1 if not in a group @param id of the groupItem */ int getDirectAncestor(int id) const; /* @brief Get the type of the group @param id of the groupItem. Must be a proper group, not a leaf */ GroupType getType(int id) const; /* @brief Convert the group hierarchy to json. Note that we cannot expect clipId nor groupId to be the same on project reopening, thus we cannot rely on them for saving. To workaround that, we currently identify clips by their position + track */ const QString toJson() const; const QString toJson(std::unordered_set roots) const; bool fromJson(const QString &data); bool fromJsonWithOffset(const QString &data, const QMap &trackMap, int offset, Fun &undo, Fun &redo); /* @brief if the clip belongs to a AVSplit group, then return the id of the other corresponding clip. Otherwise, returns -1 */ int getSplitPartner(int id) const; /* @brief Check the internal consistency of the model. Returns false if something is wrong @param failOnSingleGroups: if true, we make sure that a non-leaf node has at least two children @param checkTimelineConsistency: if true, we make sure that the group data of the parent timeline are consistent */ bool checkConsistency(bool failOnSingleGroups = true, bool checkTimelineConsistency = false); protected: /* @brief Destruct a groupItem in the hierarchy. All its children will become their own roots Return true on success @param id id of the groupitem @param deleteOrphan If this parameter is true, we recursively delete any group that become empty following the destruction @param undo Lambda function containing the current undo stack. Will be updated with current operation @param redo Lambda function containing the current redo queue. Will be updated with current operation */ bool destructGroupItem(int id, bool deleteOrphan, Fun &undo, Fun &redo); /* Lambda version */ Fun destructGroupItem_lambda(int id); /* @brief change the group of a given item @param id of the groupItem @param groupId id of the group to assign it to @param changeState when false, the grouped role for item won't be updated (for selection) */ void setGroup(int id, int groupId, bool changeState = true); /* @brief Remove an item from all the groups it belongs to. @param id of the groupItem */ void removeFromGroup(int id); /* @brief This is the actual recursive implementation of the copy function. */ bool processCopy(int gid, std::unordered_map &mapping, Fun &undo, Fun &redo); /* @brief This is the actual recursive implementation of the conversion to json */ QJsonObject toJson(int gid) const; /* @brief This is the actual recursive implementation of the parsing from json Returns the id of the created group */ int fromJson(const QJsonObject &o, Fun &undo, Fun &redo); /* @brief Transform a leaf node into a group node of given type. This implies doing the registration to the timeline */ void promoteToGroup(int gid, GroupType type); /* @brief Transform a group node with no children into a leaf. This implies doing the deregistration to the timeline */ void downgradeToLeaf(int gid); /* @Brief helper function to change the type of a group. @param id of the groupItem @param type: new type of the group */ void setType(int gid, GroupType type); + + void adjustOffset(QJsonArray &updatedNodes, QJsonObject childObject, int offset, const QMap &trackMap); private: std::weak_ptr m_parent; std::unordered_map m_upLink; // edges toward parent std::unordered_map> m_downLink; // edges toward children std::unordered_map m_groupIds; // this keeps track of "real" groups (non-leaf elements), and their types mutable QReadWriteLock m_lock; // This is a lock that ensures safety in case of concurrent access }; #endif