diff --git a/src/timeline2/model/groupsmodel.cpp b/src/timeline2/model/groupsmodel.cpp
index 2d1ba45a6..809712522 100644
--- a/src/timeline2/model/groupsmodel.cpp
+++ b/src/timeline2/model/groupsmodel.cpp
@@ -1,1045 +1,1051 @@
/***************************************************************************
* 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;
}
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();
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();
auto value = obj.value(QLatin1String("children"));
if (!value.isArray()) {
qDebug() << "Error : Expected json array of children while parsing groups";
continue;
}
QJsonArray updatedNodes;
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 = (fromJson(obj, local_undo, local_redo) > 0);
- if (ok) {
- UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
+ ok = ok && (fromJson(obj, local_undo, local_redo) > 0);
+ if (!ok) {
+ break;
}
}
+ 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/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp
index 8273fd8c5..a5fee19bc 100644
--- a/src/timeline2/model/timelinefunctions.cpp
+++ b/src/timeline2/model/timelinefunctions.cpp
@@ -1,1357 +1,1362 @@
/*
Copyright (C) 2017 Jean-Baptiste Mardelle
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 "timelinefunctions.hpp"
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
#include "clipmodel.hpp"
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "effects/effectstack/model/effectstackmodel.hpp"
#include "groupsmodel.hpp"
#include "logger.hpp"
#include "timelineitemmodel.hpp"
#include "trackmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include
#include
#include
#include
#include
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-equal"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wpedantic"
#include
#pragma GCC diagnostic pop
RTTR_REGISTRATION
{
using namespace rttr;
registration::class_("TimelineFunctions")
.method("requestClipCut", select_overload, int, int)>(&TimelineFunctions::requestClipCut))(
parameter_names("timeline", "clipId", "position"));
}
bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
Fun &redo)
{
// Special case: slowmotion clips
double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, undo, redo);
timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
// copy useful timeline properties
timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);
int duration = timeline->getClipPlaytime(clipId);
int init_duration = timeline->getClipPlaytime(newId);
if (duration != init_duration) {
int in = timeline->m_allClips[clipId]->getIn();
res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo);
res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
}
if (!res) {
return false;
}
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->importEffects(sourceStack, state);
return res;
}
bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position,
QList &clipIds, bool logUndo, bool refreshView)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
for (const QString &binId : binIds) {
int clipId;
if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
clipIds.append(clipId);
position += timeline->getItemPlaytime(clipId);
} else {
undo();
clipIds.clear();
return false;
}
}
if (logUndo) {
pCore->pushUndo(undo, redo, i18n("Insert Clips"));
}
return true;
}
bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
{
int trackId = timeline->getClipTrackId(clipId);
int trackDuration = timeline->getTrackById_const(trackId)->trackDuration();
int start = timeline->getClipPosition(clipId);
int duration = timeline->getClipPlaytime(clipId);
if (start > position || (start + duration) < position) {
return false;
}
PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
timeline->m_blockRefresh = true;
res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo);
int newDuration = timeline->getClipPlaytime(clipId);
// parse effects
std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId);
sourceStack->cleanFadeEffects(true, undo, redo);
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->cleanFadeEffects(false, undo, redo);
res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo);
// The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now
bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration();
res = res && timeline->requestClipMove(newId, trackId, position, true, false, undo, redo);
if (durationChanged) {
// Track length changed, check project duration
Fun updateDuration = [timeline]() {
timeline->updateDuration();
return true;
};
updateDuration();
PUSH_LAMBDA(updateDuration, redo);
}
timeline->m_blockRefresh = false;
return res;
}
bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
TRACE_STATIC(timeline, clipId, position);
bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, i18n("Cut clip"));
}
TRACE_RES(result);
return result;
}
bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo)
{
const std::unordered_set clipselect = timeline->getGroupElements(clipId);
// Remove locked items
std::unordered_set clips;
for (int cid : clipselect) {
int tk = timeline->getClipTrackId(cid);
if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
clips.insert(cid);
}
}
timeline->requestClearSelection();
std::unordered_set topElements;
std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
// We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
timeline->requestClearSelection();
int count = 0;
QList newIds;
int mainId = -1;
QList clipsToCut;
for (int cid : clips) {
int start = timeline->getClipPosition(cid);
int duration = timeline->getClipPlaytime(cid);
if (start < position && (start + duration) > position) {
clipsToCut << cid;
}
}
for (int cid : clipsToCut) {
count++;
int newId;
bool res = processClipCut(timeline, cid, position, newId, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
if (cid == clipId) {
mainId = newId;
}
// splitted elements go temporarily in the same group as original ones.
timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
newIds << newId;
}
if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
// we now split the group hierarchy.
// As a splitting criterion, we compare start point with split position
auto criterion = [timeline, position](int cid) { return timeline->getClipPosition(cid) < position; };
bool res = true;
for (const int topId : topElements) {
res = res && timeline->m_groups->split(topId, criterion, undo, redo);
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
}
return count > 0;
}
int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position)
{
std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1);
if (!clips.empty()) {
timeline->requestSetSelection(clips);
return (*clips.cbegin());
}
return -1;
}
bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition)
{
// Move group back to original position
int track = timeline->getItemTrackId(itemId);
bool isClip = timeline->isClip(itemId);
if (isClip) {
timeline->requestClipMove(itemId, track, startPosition, false, false);
} else {
timeline->requestCompositionMove(itemId, track, startPosition, false, false);
}
std::unordered_set clips = timeline->getGroupElements(itemId);
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
int res = timeline->requestClipsGroup(clips, undo, redo);
bool final = false;
if (res > -1) {
if (clips.size() > 1) {
final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo);
} else {
// only 1 clip to be moved
if (isClip) {
final = timeline->requestClipMove(itemId, track, endPosition, true, true, undo, redo);
} else {
final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
}
}
}
timeline->requestClearSelection();
if (final) {
if (startPosition < endPosition) {
pCore->pushUndo(undo, redo, i18n("Insert space"));
} else {
pCore->pushUndo(undo, redo, i18n("Remove space"));
}
return true;
}
return false;
}
bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
for (int trackId : tracks) {
if (timeline->getTrackById_const(trackId)->isLocked()) {
continue;
}
result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
}
if (result && !liftOnly) {
result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo);
}
pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
return result;
}
bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone,
bool overwrite)
{
// Start undoable command
std::function undo = []() { return true; };
std::function redo = []() { return true; };
bool result = true;
int trackId = trackIds.takeFirst();
if (overwrite) {
// Cut all tracks
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
if (!result) {
break;
}
++it;
}
} else {
// Cut all tracks
auto it = timeline->m_allTracks.cbegin();
while (it != timeline->m_allTracks.cend()) {
int target_track = (*it)->getId();
if (!trackIds.contains(target_track) && !timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
++it;
continue;
}
int startClipId = timeline->getClipByPosition(target_track, insertFrame);
if (startClipId > -1) {
// There is a clip, cut it
result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
}
++it;
}
result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
}
if (result) {
int newId = -1;
QString binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
result = timeline->requestClipInsertion(binClipId, trackId, insertFrame, newId, true, true, true, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
}
}
if (!result) {
undo();
}
return result;
}
bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
// Check if there is a clip at start point
int startClipId = timeline->getClipByPosition(trackId, zone.x());
if (startClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(startClipId) < zone.x()) {
qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId;
TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
qDebug() << "/// CUTTING AT START DONE";
}
}
int endClipId = timeline->getClipByPosition(trackId, zone.y());
if (endClipId > -1) {
// There is a clip, cut it
if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId;
TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
qDebug() << "/// CUTTING AT END DONE";
}
}
std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
for (const auto &clipId : clips) {
timeline->requestItemDeletion(clipId, undo, redo);
}
return true;
}
bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
{
Q_UNUSED(trackId)
std::unordered_set clips = timeline->getItemsInRange(-1, zone.y() - 1, -1, true);
bool result = false;
if (!clips.empty()) {
int clipId = *clips.begin();
if (clips.size() > 1) {
int res = timeline->requestClipsGroup(clips, undo, redo);
if (res > -1) {
result = timeline->requestGroupMove(clipId, res, 0, zone.x() - zone.y(), true, true, undo, redo);
if (result) {
result = timeline->requestClipUngroup(clipId, undo, redo);
}
if (!result) {
undo();
}
}
} else {
// only 1 clip to be moved
int clipStart = timeline->getItemPosition(clipId);
result = timeline->requestClipMove(clipId, timeline->getItemTrackId(clipId), clipStart - (zone.y() - zone.x()), true, true, undo, redo);
}
}
return result;
}
bool TimelineFunctions::requestInsertSpace(const std::shared_ptr &timeline, QPoint zone, Fun &undo, Fun &redo)
{
timeline->requestClearSelection();
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
std::unordered_set items = timeline->getItemsInRange(-1, zone.x(), -1, true);
if (items.empty()) {
return true;
}
timeline->requestSetSelection(items);
bool result = true;
int itemId = *(items.begin());
int targetTrackId = timeline->getItemTrackId(itemId);
int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x();
// TODO the three move functions should be unified in a "requestItemMove" function
if (timeline->m_groups->isInGroup(itemId)) {
result =
result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo);
} else if (timeline->isClip(itemId)) {
result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, local_undo, local_redo);
} else {
result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true,
local_undo, local_redo);
}
timeline->requestClearSelection();
if (!result) {
bool undone = local_undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage);
}
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
return result;
}
bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position)
{
Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
Fun undo = []() { return true; };
Fun redo = []() { return true; };
int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
int deltaPos = position - timeline->getItemPosition(clipId);
std::unordered_set allIds = timeline->getGroupElements(clipId);
std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips
bool res = true;
for (int id : allIds) {
int newId = -1;
if (timeline->isClip(id)) {
PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
res = cloneClip(timeline, id, newId, state, undo, redo);
res = res && (newId != -1);
}
int target_position = timeline->getItemPosition(id) + deltaPos;
int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) {
auto it = timeline->m_allTracks.cbegin();
std::advance(it, target_track_position);
int target_track = (*it)->getId();
if (timeline->isClip(id)) {
res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, undo, redo);
} else {
const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
std::unique_ptr transProps(timeline->m_allCompositions[id]->properties());
res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
}
} else {
res = false;
}
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
mapping[id] = newId;
}
qDebug() << "Successful copy, coping groups...";
res = timeline->m_groups->copyGroups(mapping, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
return false;
}
return true;
}
void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value)
{
timeline->m_allClips[clipId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value)
{
timeline->m_allCompositions[compoId]->setShowKeyframes(value);
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
}
bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, int clipId)
{
PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
PlaylistState::ClipState state = PlaylistState::Disabled;
bool disable = true;
if (oldState == PlaylistState::Disabled) {
state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
disable = false;
}
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = changeClipState(timeline, clipId, state, undo, redo);
if (result) {
pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
}
return result;
}
bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
{
int track = timeline->getClipTrackId(clipId);
int start = -1;
int end = -1;
if (track > -1) {
if (!timeline->getTrackById_const(track)->isAudioTrack()) {
start = timeline->getItemPosition(clipId);
end = start + timeline->getItemPlaytime(clipId);
}
}
Fun local_undo = []() { return true; };
Fun local_redo = []() { return true; };
bool result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
Fun local_update = [start, end, timeline]() {
if (start > -1) {
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
}
return true;
};
if (start > -1) {
local_update();
PUSH_LAMBDA(local_update, local_redo);
PUSH_LAMBDA(local_update, local_undo);
}
UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
return result;
}
bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection(false, undo, redo);
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
// clip without audio or audio only, skip
pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
return false;
}
int position = timeline->getClipPosition(cid);
int track = timeline->getClipTrackId(cid);
QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack);
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage);
return false;
}
int newId;
bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Audio split failed"), ErrorMessage);
return false;
}
done = true;
}
if (done) {
timeline->requestSetSelection(clips, undo, redo);
pCore->pushUndo(undo, redo, i18n("Split Audio"));
}
return done;
}
bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const std::unordered_set clips = timeline->getGroupElements(clipId);
bool done = false;
// Now clear selection so we don't mess with groups
timeline->requestClearSelection();
for (int cid : clips) {
if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) {
// clip without audio or audio only, skip
continue;
}
int position = timeline->getClipPosition(cid);
QList possibleTracks = QList() << videoTarget;
if (possibleTracks.isEmpty()) {
// No available audio track for splitting, abort
undo();
pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage);
return false;
}
int newId;
bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
if (!res) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
return false;
}
bool success = false;
while (!success && !possibleTracks.isEmpty()) {
int newTrack = possibleTracks.takeFirst();
success = timeline->requestClipMove(newId, newTrack, position, true, false, undo, redo);
}
TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo);
if (!success) {
bool undone = undo();
Q_ASSERT(undone);
pCore->displayMessage(i18n("Video split failed"), ErrorMessage);
return false;
}
done = true;
}
if (done) {
pCore->pushUndo(undo, redo, i18n("Split Video"));
}
return done;
}
void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack)
{
std::function undo = []() { return true; };
std::function redo = []() { return true; };
std::shared_ptr compo = timeline->getCompositionPtr(cid);
int previousATrack = compo->getATrack();
int previousAutoTrack = static_cast(compo->getForcedTrack() == -1);
bool autoTrack = aTrack < 0;
if (autoTrack) {
// Automatic track compositing, find lower video track
aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId());
}
int start = timeline->getItemPosition(cid);
int end = start + timeline->getItemPlaytime(cid);
Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
QScopedPointer field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack);
timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1));
field->unlock();
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
QScopedPointer field(timeline->m_tractor->field());
field->lock();
timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
field->unlock();
QModelIndex modelIndex = timeline->makeCompositionIndexFromID(cid);
timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ItemATrack});
timeline->invalidateZone(start, end);
timeline->checkRefresh(start, end);
return true;
};
if (local_redo()) {
PUSH_LAMBDA(local_undo, undo);
PUSH_LAMBDA(local_redo, redo);
}
pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
}
void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable)
{
QList videoTracks;
for (const auto &track : timeline->m_iteratorTable) {
if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) {
continue;
}
videoTracks << track.first;
}
if (videoTracks.size() < 2) {
pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage);
}
// First, dis/enable track compositing
QScopedPointer service(timeline->m_tractor->field());
Mlt::Field *field = timeline->m_tractor->field();
field->lock();
while ((service != nullptr) && service->is_valid()) {
if (service->type() == transition_type) {
Mlt::Transition t((mlt_transition)service->get_service());
QString serviceName = t.get("mlt_service");
int added = t.get_int("internal_added");
if (added == 237 && serviceName != QLatin1String("mix")) {
// remove all compositing transitions
t.set("disable", enable ? "1" : nullptr);
} else if (!enable && added == 200) {
field->disconnect_service(t);
}
}
service.reset(service->producer());
}
if (enable) {
for (int i = 0; i < videoTracks.size(); ++i) {
Mlt::Transition transition(*timeline->m_tractor->profile(), "composite");
transition.set("mlt_service", "composite");
transition.set("a_track", 0);
transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i)));
transition.set("distort", 0);
transition.set("aligned", 0);
// 200 is an arbitrary number so we can easily remove these transition later
transition.set("internal_added", 200);
QString geometry;
switch (i) {
case 0:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("0 0 50% 100%");
break;
case 3:
geometry = QStringLiteral("0 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("0 0 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("0 0 33% 50%");
break;
default:
geometry = QStringLiteral("0 0 33% 33%");
break;
}
break;
case 1:
switch (videoTracks.size()) {
case 2:
geometry = QStringLiteral("50% 0 50% 100%");
break;
case 3:
geometry = QStringLiteral("33% 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("50% 0 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("33% 0 33% 50%");
break;
default:
geometry = QStringLiteral("33% 0 33% 33%");
break;
}
break;
case 2:
switch (videoTracks.size()) {
case 3:
geometry = QStringLiteral("66% 0 33% 100%");
break;
case 4:
geometry = QStringLiteral("0 50% 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("66% 0 33% 50%");
break;
default:
geometry = QStringLiteral("66% 0 33% 33%");
break;
}
break;
case 3:
switch (videoTracks.size()) {
case 4:
geometry = QStringLiteral("50% 50% 50% 50%");
break;
case 5:
case 6:
geometry = QStringLiteral("0 50% 33% 50%");
break;
default:
geometry = QStringLiteral("0 33% 33% 33%");
break;
}
break;
case 4:
switch (videoTracks.size()) {
case 5:
case 6:
geometry = QStringLiteral("33% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("33% 33% 33% 33%");
break;
}
break;
case 5:
switch (videoTracks.size()) {
case 6:
geometry = QStringLiteral("66% 50% 33% 50%");
break;
default:
geometry = QStringLiteral("66% 33% 33% 33%");
break;
}
break;
case 6:
geometry = QStringLiteral("0 66% 33% 33%");
break;
case 7:
geometry = QStringLiteral("33% 66% 33% 33%");
break;
default:
geometry = QStringLiteral("66% 66% 33% 33%");
break;
}
// Add transition to track:
transition.set("geometry", geometry.toUtf8().constData());
transition.set("always_active", 1);
field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i)));
}
}
field->unlock();
timeline->requestMonitorRefresh();
}
void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection,
const QDir &targetDir)
{
bool ok;
QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
QString(), &ok);
if (name.isEmpty() || !ok) {
return;
}
if (targetDir.exists(name + QStringLiteral(".mlt"))) {
// TODO: warn and ask for overwrite / rename
}
int offset = -1;
int lowerAudioTrack = -1;
int lowerVideoTrack = -1;
QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
// Build a copy of selected tracks.
QMap sourceTracks;
for (int i : selection) {
int sourceTrack = timeline->getItemTrackId(i);
int clipPos = timeline->getItemPosition(i);
if (offset < 0 || clipPos < offset) {
offset = clipPos;
}
int trackPos = timeline->getTrackMltIndex(sourceTrack);
if (!sourceTracks.contains(trackPos)) {
sourceTracks.insert(trackPos, sourceTrack);
}
}
// Build target timeline
Mlt::Tractor newTractor(*timeline->m_tractor->profile());
QScopedPointer field(newTractor.field());
int ix = 0;
QString composite = TransitionsRepository::get()->getCompositingTransition();
QMapIterator i(sourceTracks);
QList compositions;
while (i.hasNext()) {
i.next();
QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
newTractor.set_track(*newTrackPlaylist, ix);
// QScopedPointer trackProducer(newTractor.track(ix));
int trackId = i.value();
sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
std::shared_ptr track = timeline->getTrackById_const(trackId);
bool isAudio = track->isAudioTrack();
if (isAudio) {
newTrackPlaylist->set("hide", 1);
if (lowerAudioTrack < 0) {
lowerAudioTrack = ix;
}
} else {
newTrackPlaylist->set("hide", 2);
if (lowerVideoTrack < 0) {
lowerVideoTrack = ix;
}
}
for (int itemId : selection) {
if (timeline->getItemTrackId(itemId) == trackId) {
// Copy clip on the destination track
if (timeline->isClip(itemId)) {
int clip_position = timeline->m_allClips[itemId]->getPosition();
auto clip_loc = track->getClipIndexAt(clip_position);
int target_clip = clip_loc.second;
QSharedPointer clip = track->getClipProducer(target_clip);
newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
} else if (timeline->isComposition(itemId)) {
// Composition
auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
QString id(t->get("kdenlive_id"));
QString internal(t->get("internal_added"));
if (internal.isEmpty()) {
compositions << t;
if (id.isEmpty()) {
qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service");
t->set("kdenlive_id", t->get("mlt_service"));
}
}
}
}
}
ix++;
}
// Sort compositions and insert
if (!compositions.isEmpty()) {
std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
while (!compositions.isEmpty()) {
QScopedPointer t(compositions.takeFirst());
if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) {
Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service"));
Mlt::Properties sourceProps(t->get_properties());
newComposition.inherit(sourceProps);
QString id(t->get("kdenlive_id"));
int in = qMax(0, t->get_in() - offset);
int out = t->get_out() - offset;
newComposition.set_in_and_out(in, out);
int a_track = sourceTracks.value(t->get_a_track());
int b_track = sourceTracks.value(t->get_b_track());
field->plant_transition(newComposition, a_track, b_track);
}
}
}
// Track compositing
i.toFront();
ix = 0;
while (i.hasNext()) {
i.next();
int trackId = i.value();
std::shared_ptr track = timeline->getTrackById_const(trackId);
bool isAudio = track->isAudioTrack();
if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) {
// add track compositing / mix
Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData());
if (isAudio) {
t.set("sum", 1);
}
t.set("always_active", 1);
t.set("internal_added", 237);
field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
}
ix++;
}
Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData());
xmlConsumer.set("terminate_on_pause", 1);
xmlConsumer.connect(newTractor);
xmlConsumer.run();
}
int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack)
{
qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack;
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
int destTrackMltIndex = timeline->getTrackMltIndex(destTrack);
int offset = 0;
qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex;
if (masterTrackMltIndex == destTrackMltIndex) {
return offset;
}
int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1;
bool isAudio = timeline->isAudioTrack(startTrack);
int track = masterTrackMltIndex;
while (track != destTrackMltIndex) {
track += step;
qDebug() << "+ + +TESTING TRACK: " << track;
int trackId = timeline->getTrackIndexFromPosition(track - 1);
if (isAudio == timeline->isAudioTrack(trackId)) {
offset += step;
}
}
return offset;
}
int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset)
{
int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
bool isAudio = timeline->isAudioTrack(startTrack);
if (isAudio != audioOffset) {
offset = -offset;
}
qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset;
while (offset != 0) {
masterTrackMltIndex += offset > 0 ? 1 : -1;
qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex;
if (masterTrackMltIndex < 0) {
masterTrackMltIndex = 0;
break;
}
if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) {
masterTrackMltIndex = (int)timeline->m_allTracks.size();
break;
}
int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
if (timeline->isAudioTrack(trackId) == isAudio) {
offset += offset > 0 ? -1 : 1;
}
}
return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
}
QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline)
{
QList audioTracks;
QList videoTracks;
for (const auto &track : timeline->m_allTracks) {
if (track->isAudioTrack()) {
audioTracks << track->getId();
} else {
videoTracks << track->getId();
}
}
return {audioTracks, videoTracks};
}
QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set &itemIds)
{
int clipId = *(itemIds.begin());
// We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips
std::unordered_set allIds;
for (const auto &itemId : itemIds) {
std::unordered_set siblings = timeline->getGroupElements(itemId);
allIds.insert(siblings.begin(), siblings.end());
}
timeline->requestClearSelection();
// TODO better guess for master track
int masterTid = timeline->getItemTrackId(clipId);
bool audioCopy = timeline->isAudioTrack(masterTid);
int masterTrack = timeline->getTrackPosition(masterTid);
QDomDocument copiedItems;
int offset = -1;
QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
copiedItems.appendChild(container);
QStringList binIds;
for (int id : allIds) {
if (offset == -1 || timeline->getItemPosition(id) < offset) {
offset = timeline->getItemPosition(id);
}
if (timeline->isClip(id)) {
container.appendChild(timeline->m_allClips[id]->toXml(copiedItems));
const QString bid = timeline->m_allClips[id]->binId();
if (!binIds.contains(bid)) {
binIds << bid;
}
} else if (timeline->isComposition(id)) {
container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems));
} else {
Q_ASSERT(false);
}
}
QDomElement container2 = copiedItems.createElement(QStringLiteral("bin"));
container.appendChild(container2);
for (const QString &id : binIds) {
std::shared_ptr clip = pCore->projectItemModel()->getClipByBinID(id);
QDomDocument tmp;
container2.appendChild(clip->toXml(tmp));
}
container.setAttribute(QStringLiteral("offset"), offset);
if (audioCopy) {
int masterMirror = timeline->getMirrorVideoTrackId(masterTid);
if (masterMirror == -1) {
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
if (!projectTracks.second.isEmpty()) {
masterTrack = timeline->getTrackPosition(projectTracks.second.first());
}
} else {
masterTrack = timeline->getTrackPosition(masterMirror);
}
}
/* masterTrack contains the reference track over which we want to paste.
this is a video track, unless audioCopy is defined */
container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
container.appendChild(grp);
std::unordered_set groupRoots;
- std::transform(itemIds.begin(), itemIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
+ std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
qDebug() << "==============\n GROUP ROOTS: ";
for (int gp : groupRoots) {
qDebug() << "GROUP: " << gp;
}
qDebug() << "\n=======";
grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots)));
qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
return copiedItems.toString();
}
bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position)
{
timeline->requestClearSelection();
QDomDocument copiedItems;
copiedItems.setContent(pasteString);
if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
qDebug() << " / / READING CLIPS FROM CLIPBOARD";
} else {
return false;
}
std::function undo = []() { return true; };
std::function redo = []() { return true; };
const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid"));
QMap mappedIds;
// Check available tracks
QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt();
QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
// find paste tracks
// List of all source audio tracks
QList audioTracks;
// List of all source video tracks
QList videoTracks;
// List of all audio tracks with their corresponding video mirror
std::unordered_map audioMirrors;
// List of all source audio tracks that don't have video mirror
QList singleAudioTracks;
for (int i = 0; i < clips.count(); i++) {
QDomElement prod = clips.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack"));
if (audioTrack) {
if (!audioTracks.contains(trackPos)) {
audioTracks << trackPos;
}
int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt();
if (videoMirror == -1) {
if (singleAudioTracks.contains(trackPos)) {
continue;
}
singleAudioTracks << trackPos;
continue;
}
audioMirrors[trackPos] = videoMirror;
if (videoTracks.contains(videoMirror)) {
continue;
}
videoTracks << videoMirror;
} else {
if (videoTracks.contains(trackPos)) {
continue;
}
videoTracks << trackPos;
}
}
for (int i = 0; i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
int trackPos = prod.attribute(QStringLiteral("track")).toInt();
if (!videoTracks.contains(trackPos)) {
videoTracks << trackPos;
}
int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt();
if (atrackPos == 0 || videoTracks.contains(atrackPos)) {
continue;
}
videoTracks << atrackPos;
}
// Now we have a list of all source tracks, check that we have enough target tracks
std::sort(videoTracks.begin(), videoTracks.end());
std::sort(audioTracks.begin(), audioTracks.end());
std::sort(singleAudioTracks.begin(), singleAudioTracks.end());
int requestedVideoTracks = videoTracks.isEmpty() ? 0 : videoTracks.last() - videoTracks.first() + 1;
int requestedAudioTracks = audioTracks.isEmpty() ? 0 : audioTracks.last() - audioTracks.first() + 1;
if (requestedVideoTracks > projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) {
pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500);
return false;
}
// Check we have enough tracks above/below
if (requestedVideoTracks > 0) {
qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks;
int tracksBelow = masterSourceTrack - videoTracks.first();
int tracksAbove = videoTracks.last() - masterSourceTrack;
qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove;
qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId;
qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId);
if (projectTracks.second.indexOf(trackId) < tracksBelow) {
qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
// not enough tracks below, try to paste on upper track
trackId = projectTracks.second.at(tracksBelow);
} else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) {
// not enough tracks above, try to paste on lower track
qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove);
trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1);
}
}
QMap tracksMap;
int masterIx = projectTracks.second.indexOf(trackId);
qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId;
qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack;
qDebug() << "/// MASTER PASTE: " << masterIx;
for (int tk : videoTracks) {
tracksMap.insert(tk, projectTracks.second.at(masterIx + tk - masterSourceTrack));
qDebug() << "// TK MAP: " << tk << " => " << tracksMap[tk];
}
for (const auto &mirror : audioMirrors) {
int videoIx = tracksMap.value(mirror.second);
// qDebug()<<"// TK AUDIO MAP: "< "<getMirrorAudioTrackId(videoIx);
tracksMap.insert(mirror.first, timeline->getMirrorAudioTrackId(videoIx));
}
for (int i = 0; i < singleAudioTracks.size(); i++) {
tracksMap.insert(singleAudioTracks.at(i), projectTracks.first.at(i));
}
qDebug() << "++++++++++++++++++++++++++\n\n\n// TRACK MAP: " << tracksMap;
if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
// paste from another document, import bin clips
QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
if (folderId.isEmpty()) {
// Folder doe not exist
const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
}
QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
for (int i = 0; i < binClips.count(); ++i) {
QDomElement currentProd = binClips.item(i).toElement();
QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
if (!pCore->projectItemModel()->isIdFree(clipId)) {
QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
mappedIds.insert(clipId, updatedId);
clipId = updatedId;
}
pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo);
}
}
int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
bool res = true;
QLocale locale;
std::unordered_map correspondingIds;
QList waitingIds;
for (int i = 0; i < clips.count(); i++) {
waitingIds << i;
}
for (int i = 0; res && !waitingIds.isEmpty();) {
if (i >= waitingIds.size()) {
i = 0;
}
QDomElement prod = clips.at(waitingIds.at(i)).toElement();
QString originalId = prod.attribute(QStringLiteral("binid"));
if (mappedIds.contains(originalId)) {
// Map id
originalId = mappedIds.value(originalId);
}
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
double speed = locale.toDouble(prod.attribute(QStringLiteral("speed")));
int newId;
bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, undo, redo);
if (created) {
// Master producer is ready
// ids.removeAll(originalId);
waitingIds.removeAt(i);
} else {
i++;
qApp->processEvents();
continue;
}
if (timeline->m_allClips[newId]->m_endlessResize) {
out = out - in;
in = 0;
timeline->m_allClips[newId]->m_producer->set("length", out + 1);
}
timeline->m_allClips[newId]->setInOut(in, out);
int targetId = prod.attribute(QStringLiteral("id")).toInt();
correspondingIds[targetId] = newId;
res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, undo, redo);
// paste effects
if (res) {
std::shared_ptr destStack = timeline->getClipEffectStackModel(newId);
destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), undo, redo);
}
}
// Compositions
for (int i = 0; res && i < compositions.count(); i++) {
QDomElement prod = compositions.at(i).toElement();
QString originalId = prod.attribute(QStringLiteral("composition"));
int in = prod.attribute(QStringLiteral("in")).toInt();
int out = prod.attribute(QStringLiteral("out")).toInt();
int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt();
if (aTrackId > 0) {
aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId));
}
int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
int newId;
auto transProps = std::make_unique();
QDomNodeList props = prod.elementsByTagName(QStringLiteral("property"));
for (int j = 0; j < props.count(); j++) {
transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(),
props.at(j).toElement().text().toUtf8().constData());
}
res = timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in, std::move(transProps), newId, undo, redo);
}
if (!res) {
undo();
return false;
}
const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
// Rebuild groups
timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, undo, redo);
// unsure to clear selection in undo/redo too.
- Fun unselect = [&]() { return timeline->requestClearSelection(); };
+ Fun unselect = [&]() {
+ qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection;
+ timeline->requestClearSelection();
+ qDebug() << "after Selection " << timeline->m_currentSelection;
+ return true;
+ };
PUSH_FRONT_LAMBDA(unselect, undo);
PUSH_FRONT_LAMBDA(unselect, redo);
pCore->pushUndo(undo, redo, i18n("Paste clips"));
return true;
}
bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr &timeline, int trackId, int position, bool affectAllTracks)
{
// find blank duration
int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(position);
int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position);
if (cid == -1) {
return false;
}
int start = timeline->getItemPosition(cid);
requestSpacerEndOperation(timeline, cid, start, start - spaceDuration);
return true;
}
diff --git a/tests/trimmingtest.cpp b/tests/trimmingtest.cpp
index 91e37ef92..0c440452b 100644
--- a/tests/trimmingtest.cpp
+++ b/tests/trimmingtest.cpp
@@ -1,980 +1,1046 @@
#include "doc/kdenlivedoc.h"
#include "test_utils.hpp"
using namespace fakeit;
Mlt::Profile profile_trimming;
TEST_CASE("Advanced trimming operations", "[Trimming]")
{
Logger::clear();
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;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_trimming, undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
RESET(timMock);
QString binId = createProducer(profile_trimming, "red", binModel);
QString binId2 = createProducer(profile_trimming, "blue", binModel);
QString binId3 = createProducerWithSound(profile_trimming, binModel);
int cid1 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int tid1 = TrackModel::construct(timeline);
int tid2 = TrackModel::construct(timeline);
int tid3 = TrackModel::construct(timeline);
// Add an audio track
int tid4 = TrackModel::construct(timeline, -1, -1, QString(), true);
int cid2 = ClipModel::construct(timeline, binId2, -1, PlaylistState::VideoOnly);
int cid3 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int cid4 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int cid5 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int cid6 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int cid7 = ClipModel::construct(timeline, binId, -1, PlaylistState::VideoOnly);
int audio1 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly);
int audio2 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly);
int audio3 = ClipModel::construct(timeline, binId3, -1, PlaylistState::VideoOnly);
timeline->m_allClips[cid1]->m_endlessResize = false;
timeline->m_allClips[cid2]->m_endlessResize = false;
timeline->m_allClips[cid3]->m_endlessResize = false;
timeline->m_allClips[cid4]->m_endlessResize = false;
timeline->m_allClips[cid5]->m_endlessResize = false;
timeline->m_allClips[cid6]->m_endlessResize = false;
timeline->m_allClips[cid7]->m_endlessResize = false;
SECTION("Clip cutting")
{
// Trivial split
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
int l = timeline->getClipPlaytime(cid2);
REQUIRE(timeline->requestItemResize(cid2, l - 3, true) == l - 3);
REQUIRE(timeline->requestItemResize(cid2, l - 5, false) == l - 5);
REQUIRE(timeline->requestClipMove(cid2, tid1, l));
REQUIRE(timeline->requestClipMove(cid3, tid1, l + l - 5));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid1) == l);
REQUIRE(timeline->getClipPlaytime(cid2) == l - 5);
REQUIRE(timeline->getClipPlaytime(cid3) == l);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == l);
REQUIRE(timeline->getClipPosition(cid3) == l + l - 5);
REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2);
REQUIRE(timeline->getClipPtr(cid2)->getOut() == l - 4);
};
state();
// require cut position outside the clip
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0));
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l));
// can't cut on edges either
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l));
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l + l - 5));
state();
REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4));
int splitted = timeline->getClipByPosition(tid1, l + 5);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(cid1) == l);
REQUIRE(timeline->getClipPlaytime(cid2) == 4);
REQUIRE(timeline->getClipPlaytime(splitted) == l - 9);
REQUIRE(timeline->getClipPlaytime(cid3) == l);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == l);
REQUIRE(timeline->getClipPosition(splitted) == l + 4);
REQUIRE(timeline->getClipPosition(cid3) == l + l - 5);
REQUIRE(timeline->getClipPtr(cid2)->getIn() == 2);
REQUIRE(timeline->getClipPtr(cid2)->getOut() == 5);
REQUIRE(timeline->getClipPtr(splitted)->getIn() == 6);
REQUIRE(timeline->getClipPtr(splitted)->getOut() == l - 4);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
}
SECTION("Cut and resize")
{
REQUIRE(timeline->requestClipMove(cid1, tid1, 5));
int l = timeline->getClipPlaytime(cid1);
timeline->m_allClips[cid1]->m_endlessResize = false;
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipPlaytime(cid1) == l);
REQUIRE(timeline->getClipPosition(cid1) == 5);
};
state();
REQUIRE(TimelineFunctions::requestClipCut(timeline, cid1, 9));
int splitted = timeline->getClipByPosition(tid1, 10);
timeline->m_allClips[splitted]->m_endlessResize = false;
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(splitted) == tid1);
REQUIRE(timeline->getClipPlaytime(cid1) == 4);
REQUIRE(timeline->getClipPlaytime(splitted) == l - 4);
REQUIRE(timeline->getClipPosition(cid1) == 5);
REQUIRE(timeline->getClipPosition(splitted) == 9);
};
state2();
REQUIRE(timeline->requestClipMove(splitted, tid2, 9, true, true));
REQUIRE(timeline->requestItemResize(splitted, l - 3, true, true) == -1);
REQUIRE(timeline->requestItemResize(splitted, l, false, true) == l);
REQUIRE(timeline->requestItemResize(cid1, 5, false, true) == -1);
REQUIRE(timeline->requestItemResize(cid1, l, true, true) == l);
auto state3 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(splitted) == tid2);
REQUIRE(timeline->getClipPlaytime(cid1) == l);
REQUIRE(timeline->getClipPlaytime(splitted) == l);
REQUIRE(timeline->getClipPosition(cid1) == 5);
REQUIRE(timeline->getClipPosition(splitted) == 5);
};
state3();
undoStack->undo();
undoStack->undo();
undoStack->undo();
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
undoStack->redo();
undoStack->redo();
undoStack->redo();
state3();
}
SECTION("Clip cutting 2")
{
// More complex group structure split split
int l = timeline->getClipPlaytime(cid2);
REQUIRE(timeline->requestClipMove(cid1, tid1, 0));
REQUIRE(timeline->requestClipMove(cid2, tid1, l));
REQUIRE(timeline->requestClipMove(cid3, tid1, 2 * l));
REQUIRE(timeline->requestClipMove(cid4, tid2, 0));
REQUIRE(timeline->requestClipMove(cid5, tid2, l));
REQUIRE(timeline->requestClipMove(cid6, tid2, 2 * l));
REQUIRE(timeline->requestClipMove(cid7, tid1, 200));
int gid1 = timeline->requestClipsGroup(std::unordered_set({cid1, cid4}), true, GroupType::Normal);
int gid2 = timeline->requestClipsGroup(std::unordered_set({cid2, cid5}), true, GroupType::Normal);
int gid3 = timeline->requestClipsGroup(std::unordered_set({cid3, cid6}), true, GroupType::Normal);
int gid4 = timeline->requestClipsGroup(std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7}), true, GroupType::Normal);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
int p = 0;
for (int c : std::vector({cid1, cid2, cid3})) {
REQUIRE(timeline->getClipPlaytime(c) == l);
REQUIRE(timeline->getClipTrackId(c) == tid1);
REQUIRE(timeline->getClipPosition(c) == p);
p += l;
}
p = 0;
for (int c : std::vector({cid4, cid5, cid6})) {
REQUIRE(timeline->getClipPlaytime(c) == l);
REQUIRE(timeline->getClipTrackId(c) == tid2);
REQUIRE(timeline->getClipPosition(c) == p);
p += l;
}
REQUIRE(timeline->getClipPosition(cid7) == 200);
REQUIRE(timeline->getClipTrackId(cid7) == tid1);
REQUIRE(timeline->m_groups->getDirectChildren(gid1) == std::unordered_set({cid1, cid4}));
REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({cid2, cid5}));
REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6}));
REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid1, gid2, gid3, cid7}));
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid3, cid4, cid5, cid6, cid7}));
};
state();
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 0));
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 5 * l));
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, l));
REQUIRE_FALSE(TimelineFunctions::requestClipCut(timeline, cid2, 2 * l));
state();
REQUIRE(TimelineFunctions::requestClipCut(timeline, cid2, l + 4));
int splitted = timeline->getClipByPosition(tid1, l + 5);
int splitted2 = timeline->getClipByPosition(tid2, l + 5);
REQUIRE(splitted != splitted2);
auto check_groups = [&]() {
REQUIRE(timeline->m_groups->getDirectChildren(gid2) == std::unordered_set({splitted, splitted2}));
REQUIRE(timeline->m_groups->getDirectChildren(gid3) == std::unordered_set({cid3, cid6}));
REQUIRE(timeline->m_groups->getDirectChildren(gid4) == std::unordered_set({gid2, gid3, cid7}));
REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({splitted, splitted2, cid3, cid6, cid7}));
int g1b = timeline->m_groups->m_upLink[cid1];
int g2b = timeline->m_groups->m_upLink[cid2];
int g4b = timeline->m_groups->getRootId(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1b) == std::unordered_set({cid1, cid4}));
REQUIRE(timeline->m_groups->getDirectChildren(g2b) == std::unordered_set({cid2, cid5}));
REQUIRE(timeline->m_groups->getDirectChildren(g4b) == std::unordered_set({g1b, g2b}));
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2, cid4, cid5}));
};
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
int p = 0;
for (int c : std::vector({cid1, cid2, cid3})) {
REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l));
REQUIRE(timeline->getClipTrackId(c) == tid1);
REQUIRE(timeline->getClipPosition(c) == p);
p += l;
}
p = 0;
for (int c : std::vector({cid4, cid5, cid6})) {
REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l));
REQUIRE(timeline->getClipTrackId(c) == tid2);
REQUIRE(timeline->getClipPosition(c) == p);
p += l;
}
REQUIRE(timeline->getClipPosition(cid7) == 200);
REQUIRE(timeline->getClipTrackId(cid7) == tid1);
REQUIRE(timeline->getClipPosition(splitted) == l + 4);
REQUIRE(timeline->getClipPlaytime(splitted) == l - 4);
REQUIRE(timeline->getClipTrackId(splitted) == tid1);
REQUIRE(timeline->getClipPosition(splitted2) == l + 4);
REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4);
REQUIRE(timeline->getClipTrackId(splitted2) == tid2);
check_groups();
};
state2();
REQUIRE(timeline->requestClipMove(splitted, tid1, l + 4 + 10, true, true));
REQUIRE(timeline->requestClipMove(cid1, tid2, 10, true, true));
auto state3 = [&]() {
REQUIRE(timeline->checkConsistency());
int p = 0;
for (int c : std::vector({cid1, cid2, cid3})) {
REQUIRE(timeline->getClipPlaytime(c) == (c == cid2 ? 4 : l));
REQUIRE(timeline->getClipTrackId(c) == (c == cid3 ? tid1 : tid2));
REQUIRE(timeline->getClipPosition(c) == p + 10);
p += l;
}
p = 0;
for (int c : std::vector({cid4, cid5, cid6})) {
REQUIRE(timeline->getClipPlaytime(c) == (c == cid5 ? 4 : l));
REQUIRE(timeline->getClipTrackId(c) == (c == cid6 ? tid2 : tid3));
REQUIRE(timeline->getClipPosition(c) == p + 10);
p += l;
}
REQUIRE(timeline->getClipPosition(cid7) == 210);
REQUIRE(timeline->getClipTrackId(cid7) == tid1);
REQUIRE(timeline->getClipPosition(splitted) == l + 4 + 10);
REQUIRE(timeline->getClipPlaytime(splitted) == l - 4);
REQUIRE(timeline->getClipTrackId(splitted) == tid1);
REQUIRE(timeline->getClipPosition(splitted2) == l + 4 + 10);
REQUIRE(timeline->getClipPlaytime(splitted2) == l - 4);
REQUIRE(timeline->getClipTrackId(splitted2) == tid2);
check_groups();
};
state3();
undoStack->undo();
undoStack->undo();
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
undoStack->redo();
undoStack->redo();
state3();
}
SECTION("Simple audio split")
{
int l = timeline->getClipPlaytime(audio1);
REQUIRE(timeline->requestClipMove(audio1, tid1, 3));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPosition(audio1) == 3);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1}));
};
state();
REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4));
int splitted1 = timeline->getClipByPosition(tid4, 3);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPosition(audio1) == 3);
REQUIRE(timeline->getClipPlaytime(splitted1) == l);
REQUIRE(timeline->getClipPosition(splitted1) == 3);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(splitted1) == tid4);
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid4) == 1);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1}));
int g1 = timeline->m_groups->getDirectAncestor(audio1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
// We also make sure that clips that are audio only cannot be further splitted
REQUIRE(timeline->requestClipMove(cid1, tid1, l + 30));
// This is a color clip, shouldn't be splittable
REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, cid1, tid2));
// Check we cannot split audio on a video track
REQUIRE_FALSE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid2));
}
SECTION("Split audio on a selection")
{
int l = timeline->getClipPlaytime(audio2);
REQUIRE(timeline->requestClipMove(audio1, tid1, 0));
REQUIRE(timeline->requestClipMove(audio2, tid1, l));
REQUIRE(timeline->requestClipMove(audio3, tid1, 2 * l));
std::unordered_set selection{audio1, audio3, audio2};
timeline->requestSetSelection(selection);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPlaytime(audio2) == l);
REQUIRE(timeline->getClipPlaytime(audio3) == l);
REQUIRE(timeline->getClipPosition(audio1) == 0);
REQUIRE(timeline->getClipPosition(audio2) == l);
REQUIRE(timeline->getClipPosition(audio3) == l + l);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(audio2) == tid1);
REQUIRE(timeline->getClipTrackId(audio3) == tid1);
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, audio2, audio3}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set({audio1, audio3, audio2}));
};
state();
REQUIRE(TimelineFunctions::requestSplitAudio(timeline, audio1, tid4));
int splitted1 = timeline->getClipByPosition(tid4, 0);
int splitted2 = timeline->getClipByPosition(tid4, l);
int splitted3 = timeline->getClipByPosition(tid4, 2 * l);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getClipPlaytime(audio1) == l);
REQUIRE(timeline->getClipPlaytime(audio2) == l);
REQUIRE(timeline->getClipPlaytime(audio3) == l);
REQUIRE(timeline->getClipPosition(audio1) == 0);
REQUIRE(timeline->getClipPosition(audio2) == l);
REQUIRE(timeline->getClipPosition(audio3) == l + l);
REQUIRE(timeline->getClipPlaytime(splitted1) == l);
REQUIRE(timeline->getClipPlaytime(splitted2) == l);
REQUIRE(timeline->getClipPlaytime(splitted3) == l);
REQUIRE(timeline->getClipPosition(splitted1) == 0);
REQUIRE(timeline->getClipPosition(splitted2) == l);
REQUIRE(timeline->getClipPosition(splitted3) == l + l);
REQUIRE(timeline->getClipTrackId(audio1) == tid1);
REQUIRE(timeline->getClipTrackId(audio2) == tid1);
REQUIRE(timeline->getClipTrackId(audio3) == tid1);
REQUIRE(timeline->getClipTrackId(splitted1) == tid4);
REQUIRE(timeline->getClipTrackId(splitted2) == tid4);
REQUIRE(timeline->getClipTrackId(splitted3) == tid4);
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid4) == 3);
REQUIRE(timeline->getGroupElements(audio1) == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set({audio1, splitted1, audio2, audio3, splitted2, splitted3}));
int g1 = timeline->m_groups->getDirectAncestor(audio1);
int g2 = timeline->m_groups->getDirectAncestor(audio2);
int g3 = timeline->m_groups->getDirectAncestor(audio3);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({audio1, splitted1}));
REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({audio2, splitted2}));
REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set({audio3, splitted3}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit);
REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
}
SECTION("Cut should preserve AV groups")
{
QString binId3 = createProducerWithSound(profile_trimming, binModel);
int tid6 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid5 = TrackModel::construct(timeline);
int cid6 = -1;
int pos = 3;
REQUIRE(timeline->requestClipInsertion(binId3, tid5, pos, cid6, true, true, false));
int cid7 = timeline->m_groups->getSplitPartner(cid6);
int l = timeline->getClipPlaytime(cid6);
REQUIRE(l >= 10);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5) == 1);
REQUIRE(timeline->getTrackClipsCount(tid6) == 1);
REQUIRE(timeline->getClipTrackId(cid6) == tid5);
REQUIRE(timeline->getClipTrackId(cid7) == tid6);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid6) == std::unordered_set({cid6, cid7}));
int g1 = timeline->m_groups->getDirectAncestor(cid6);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid6, cid7}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state();
REQUIRE(TimelineFunctions::requestClipCut(timeline, cid6, pos + 4));
int cid8 = timeline->getClipByPosition(tid5, pos + 5);
int cid9 = timeline->getClipByPosition(tid6, pos + 5);
REQUIRE(cid8 >= 0);
REQUIRE(cid9 >= 0);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid5) == 2);
REQUIRE(timeline->getTrackClipsCount(tid6) == 2);
REQUIRE(timeline->getClipTrackId(cid6) == tid5);
REQUIRE(timeline->getClipTrackId(cid7) == tid6);
REQUIRE(timeline->getClipTrackId(cid8) == tid5);
REQUIRE(timeline->getClipTrackId(cid9) == tid6);
REQUIRE(timeline->getClipPosition(cid6) == pos);
REQUIRE(timeline->getClipPosition(cid7) == pos);
REQUIRE(timeline->getClipPosition(cid8) == pos + 4);
REQUIRE(timeline->getClipPosition(cid9) == pos + 4);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid7)->clipState() == PlaylistState::AudioOnly);
REQUIRE(timeline->getClipPtr(cid8)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid9)->clipState() == PlaylistState::AudioOnly);
// original AV group
REQUIRE(timeline->getGroupElements(cid6) == std::unordered_set({cid6, cid7}));
int g1 = timeline->m_groups->getDirectAncestor(cid6);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid6, cid7}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
// new AV group
REQUIRE(timeline->getGroupElements(cid8) == std::unordered_set({cid8, cid9}));
int g2 = timeline->m_groups->getDirectAncestor(cid8);
REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({cid8, cid9}));
REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
}
binModel->clean();
pCore->m_projectManager = nullptr;
Logger::print_trace();
}
TEST_CASE("Insert/delete", "[Trimming2]")
{
Logger::clear();
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;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_trimming, undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
RESET(timMock);
QString binId = createProducerWithSound(profile_trimming, binModel);
int tid2b = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid2 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid1 = TrackModel::construct(timeline);
int tid1b = TrackModel::construct(timeline);
SECTION("Remove Space should preserve groups")
{
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId, tid1, 3, cid1, true, true, false));
int cid2 = timeline->m_groups->getSplitPartner(cid1);
auto state = [&](int pos) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == pos);
REQUIRE(timeline->getClipPosition(cid2) == pos);
REQUIRE(timeline->getClipPtr(cid1)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid2)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid1, cid2}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state(3);
REQUIRE(TimelineFunctions::requestDeleteBlankAt(timeline, tid1, 1, true));
state(0);
undoStack->undo();
state(3);
undoStack->redo();
state(0);
}
SECTION("Insert zone should preserve groups")
{
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId, tid1, 3, cid1, true, true, false));
int cid2 = timeline->m_groups->getSplitPartner(cid1);
int l = timeline->getClipPlaytime(cid2);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == 3);
REQUIRE(timeline->getClipPtr(cid1)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid2)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid1, cid2}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state();
REQUIRE(TimelineFunctions::insertZone(timeline, {tid1, tid2}, binId, 3 + 2, {l / 4, 3 * l / 4}, false));
int small_length = 3 * l / 4 - l / 4;
int cid3 = timeline->getClipByPosition(tid1, 3 + 2);
int cid4 = timeline->getClipByPosition(tid2, 3 + 2);
int cid5 = timeline->getClipByPosition(tid1, 3 + 2 + small_length);
int cid6 = timeline->getClipByPosition(tid2, 3 + 2 + small_length);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getTrackClipsCount(tid2) == 3);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid2);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == tid2);
REQUIRE(timeline->getClipTrackId(cid5) == tid1);
REQUIRE(timeline->getClipTrackId(cid6) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == 3);
REQUIRE(timeline->getClipPosition(cid3) == 3 + 2);
REQUIRE(timeline->getClipPosition(cid4) == 3 + 2);
REQUIRE(timeline->getClipPosition(cid5) == 3 + 2 + small_length);
REQUIRE(timeline->getClipPosition(cid6) == 3 + 2 + small_length);
REQUIRE(timeline->getClipPlaytime(cid1) + timeline->getClipPlaytime(cid5) == l);
REQUIRE(timeline->getClipPlaytime(cid1) == 2);
REQUIRE(timeline->getClipPlaytime(cid3) == small_length);
REQUIRE(timeline->getClipPtr(cid1)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid2)->clipState() == PlaylistState::AudioOnly);
REQUIRE(timeline->getClipPtr(cid3)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid4)->clipState() == PlaylistState::AudioOnly);
REQUIRE(timeline->getClipPtr(cid5)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid6)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid1, cid2}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({cid3, cid4}));
int g2 = timeline->m_groups->getDirectAncestor(cid3);
REQUIRE(timeline->m_groups->getDirectChildren(g2) == std::unordered_set({cid3, cid4}));
REQUIRE(timeline->m_groups->getType(g2) == GroupType::AVSplit);
int g3 = timeline->m_groups->getDirectAncestor(cid5);
REQUIRE(timeline->m_groups->getDirectChildren(g3) == std::unordered_set({cid5, cid6}));
REQUIRE(timeline->m_groups->getType(g3) == GroupType::AVSplit);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
}
binModel->clean();
pCore->m_projectManager = nullptr;
Logger::print_trace();
}
TEST_CASE("Copy/paste", "[CP]")
{
Logger::clear();
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 a doc to stub the getDocumentProperty
Mock docMock;
When(Method(docMock, getDocumentProperty)).AlwaysDo([](const QString &name, const QString &defaultValue) {
qDebug() << "Intercepted call";
return QStringLiteral("dummyId");
});
KdenliveDoc &mockedDoc = docMock.get();
// We mock the project class so that the undoStack function returns our undoStack, and our mocked document
Mock pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, current)).AlwaysReturn(&mockedDoc);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_trimming, undoStack);
Mock timMock(tim);
auto timeline = std::shared_ptr(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
RESET(timMock);
QString binId = createProducerWithSound(profile_trimming, binModel);
QString binId2 = createProducer(profile_trimming, "red", binModel);
int tid2b = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid2 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid1 = TrackModel::construct(timeline);
int tid1b = TrackModel::construct(timeline);
SECTION("Simple copy paste of one clip")
{
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId2, tid1, 3, cid1, true, true, false));
int l = timeline->getClipPlaytime(cid1);
int cid2 = -1;
REQUIRE(timeline->requestClipInsertion(binId2, tid1, 3 + l, cid2, true, true, false));
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == 3 + l);
};
state();
QString cpy_str = TimelineFunctions::copyClips(timeline, {cid1});
// Try to paste in invalid positions
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
state();
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 4));
state();
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 4 + l));
state();
// Paste in audio track
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid2, 0));
state();
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid2b, 0));
state();
// Paste after the last clip
REQUIRE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 3 + 2 * l));
int cid3 = timeline->getTrackById(tid1)->getClipByPosition(3 + 2 * l + 1);
REQUIRE(cid3 != -1);
auto state2 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 3);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == 3 + l);
REQUIRE(timeline->getClipPosition(cid3) == 3 + 2 * l);
};
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
// Paste in different track
REQUIRE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1b, 0));
int cid4 = timeline->getTrackById(tid1b)->getClipByPosition(0);
REQUIRE(cid4 != -1);
auto state3 = [&]() {
state2();
REQUIRE(timeline->getTrackClipsCount(tid1b) == 1);
REQUIRE(timeline->getClipTrackId(cid4) == tid1b);
REQUIRE(timeline->getClipPosition(cid4) == 0);
};
state3();
undoStack->undo();
state2();
undoStack->undo();
state();
undoStack->redo();
state2();
undoStack->redo();
state3();
}
SECTION("Copy paste groups")
{
auto state0 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 0);
REQUIRE(timeline->getTrackClipsCount(tid2) == 0);
REQUIRE(timeline->getTrackClipsCount(tid1b) == 0);
REQUIRE(timeline->getTrackClipsCount(tid2b) == 0);
};
state0();
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId, tid1, 3, cid1, true, true, false));
int l = timeline->getClipPlaytime(cid1);
int cid2 = timeline->m_groups->getSplitPartner(cid1);
auto state = [&](int count1, int count2) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == count1);
REQUIRE(timeline->getTrackClipsCount(tid2) == count1);
REQUIRE(timeline->getTrackClipsCount(tid1b) == count2);
REQUIRE(timeline->getTrackClipsCount(tid2b) == count2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid2);
REQUIRE(timeline->getClipPosition(cid1) == 3);
REQUIRE(timeline->getClipPosition(cid2) == 3);
REQUIRE(timeline->getClipPtr(cid1)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid2)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid1) == std::unordered_set({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid1, cid2}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state(1, 0);
QString cpy_str = TimelineFunctions::copyClips(timeline, {cid1});
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
state(1, 0);
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 4));
state(1, 0);
// potentially annoying selection
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2});
// paste on same track, after clip
REQUIRE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 3 + 2 * l));
int cid3 = timeline->getTrackById(tid1)->getClipByPosition(3 + 2 * l + 1);
REQUIRE(cid3 != -1);
int cid4 = timeline->m_groups->getSplitPartner(cid3);
auto state2 = [&](int count1, int count2) {
state(count1, count2);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == tid2);
REQUIRE(timeline->getClipPosition(cid3) == 3 + 2 * l);
REQUIRE(timeline->getClipPosition(cid4) == 3 + 2 * l);
REQUIRE(timeline->getClipPtr(cid3)->clipState() == PlaylistState::VideoOnly);
REQUIRE(timeline->getClipPtr(cid4)->clipState() == PlaylistState::AudioOnly);
// we check that the av group was correctly created
REQUIRE(timeline->getGroupElements(cid3) == std::unordered_set({cid3, cid4}));
int g1 = timeline->m_groups->getDirectAncestor(cid3);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set({cid3, cid4}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state2(2, 0);
// potentially annoying selection
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2});
undoStack->undo();
REQUIRE(timeline->requestClearSelection());
state(1, 0);
// potentially annoying selection
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2});
undoStack->redo();
REQUIRE(timeline->requestClearSelection());
state2(2, 0);
// another potentially annoying selection
REQUIRE(timeline->requestSetSelection({cid1, cid3}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2, cid3, cid4});
undoStack->undo();
REQUIRE(timeline->requestClearSelection());
state(1, 0);
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2});
undoStack->redo();
REQUIRE(timeline->requestClearSelection());
state2(2, 0);
+
+ // now, we copy the old clips as well as those we just pasted. Let's do it using a selection
+ REQUIRE(timeline->requestSetSelection({cid1, cid3}));
+ REQUIRE(timeline->getCurrentSelection() == std::unordered_set{cid1, cid2, cid3, cid4});
+ // since everything is selected, everything should be copied here
+ cpy_str = TimelineFunctions::copyClips(timeline, {cid1});
+
+ REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
+ state2(2, 0);
+
+ // parasitic selection
+ REQUIRE(timeline->requestSetSelection({cid1, cid3}));
+ REQUIRE(timeline->getCurrentSelection() == std::unordered_set