Commit b8a051ab authored by Nicolas Carion's avatar Nicolas Carion

various improvements and tests for copy/paste

parent 5f3b30d6
......@@ -1045,25 +1045,24 @@ QPair<QList<int>, QList<int>> TimelineFunctions::getAVTracksIds(const std::share
QString TimelineFunctions::copyClips(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &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<int> allIds;
for (const auto &itemId : itemIds) {
std::unordered_set<int> 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);
std::unordered_set<int> groupRoots;
std::transform(itemIds.begin(), itemIds.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=======";
QDomDocument copiedItems;
int offset = -1;
QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
copiedItems.appendChild(container);
QStringList binIds;
for (int id : itemIds) {
for (int id : allIds) {
if (offset == -1 || timeline->getItemPosition(id) < offset) {
offset = timeline->getItemPosition(id);
}
......@@ -1104,14 +1103,24 @@ QString TimelineFunctions::copyClips(const std::shared_ptr<TimelineItemModel> &t
container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
container.appendChild(grp);
std::unordered_set<int> groupRoots;
std::transform(itemIds.begin(), itemIds.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)));
// TODO: groups
qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
return copiedItems.toString();
}
bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position)
{
timeline->requestClearSelection();
QDomDocument copiedItems;
copiedItems.setContent(pasteString);
if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
......@@ -1134,7 +1143,7 @@ bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &tim
// List of all source video tracks
QList<int> videoTracks;
// List of all audio tracks with their corresponding video mirror
QMap<int, int> audioMirrors;
std::unordered_map<int, int> audioMirrors;
// List of all source audio tracks that don't have video mirror
QList<int> singleAudioTracks;
for (int i = 0; i < clips.count(); i++) {
......@@ -1153,7 +1162,7 @@ bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &tim
singleAudioTracks << trackPos;
continue;
}
audioMirrors.insert(trackPos, videoMirror);
audioMirrors[trackPos] = videoMirror;
if (videoTracks.contains(videoMirror)) {
continue;
}
......@@ -1178,9 +1187,9 @@ bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &tim
videoTracks << atrackPos;
}
// Now we have a list of all source tracks, check that we have enough target tracks
qSort(videoTracks);
qSort(audioTracks);
qSort(singleAudioTracks);
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()) {
......@@ -1213,19 +1222,17 @@ bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &tim
qDebug() << "/// MASTER PASTE: " << masterIx;
for (int tk : videoTracks) {
tracksMap.insert(tk, projectTracks.second.at(masterIx + tk - masterSourceTrack));
qDebug() << "// TK MAP: " << tk << " => " << projectTracks.second.at(masterIx + tk - masterSourceTrack);
qDebug() << "// TK MAP: " << tk << " => " << tracksMap[tk];
}
QMapIterator<int, int> it(audioMirrors);
while (it.hasNext()) {
it.next();
int videoIx = tracksMap.value(it.value());
for (const auto &mirror : audioMirrors) {
int videoIx = tracksMap.value(mirror.second);
// qDebug()<<"// TK AUDIO MAP: "<<it.key()<<" => "<<videoIx<<" ; AUDIO MIRROR: "<<timeline->getMirrorAudioTrackId(videoIx);
tracksMap.insert(it.key(), timeline->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// AUDIO MIRRORS: " << audioMirrors << ", RESULT: " << tracksMap;
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"));
......@@ -1328,6 +1335,10 @@ bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &tim
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(); };
PUSH_FRONT_LAMBDA(unselect, undo);
PUSH_FRONT_LAMBDA(unselect, redo);
pCore->pushUndo(undo, redo, i18n("Paste clips"));
return true;
}
......
......@@ -793,6 +793,187 @@ TEST_CASE("Copy/paste", "[CP]")
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<int>({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({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<int>{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<int>({cid3, cid4}));
int g1 = timeline->m_groups->getDirectAncestor(cid3);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({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<int>{cid1, cid2});
undoStack->undo();
REQUIRE(timeline->requestClearSelection());
state(1, 0);
// potentially annoying selection
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set<int>{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<int>{cid1, cid2, cid3, cid4});
undoStack->undo();
REQUIRE(timeline->requestClearSelection());
state(1, 0);
REQUIRE(timeline->requestSetSelection({cid1}));
REQUIRE(timeline->getCurrentSelection() == std::unordered_set<int>{cid1, cid2});
undoStack->redo();
REQUIRE(timeline->requestClearSelection());
state2(2, 0);
}
SECTION("Paste when tracks get deleted")
{
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 = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(tid2) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1b) == 0);
REQUIRE(timeline->getTrackClipsCount(tid2b) == 0);
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<int>({cid1, cid2}));
int g1 = timeline->m_groups->getDirectAncestor(cid1);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({cid1, cid2}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state();
QString cpy_str = TimelineFunctions::copyClips(timeline, {cid1});
// we delete origin of the copy, paste should still be possible
REQUIRE(timeline->requestItemDeletion(cid1));
state0();
REQUIRE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
int cid3 = timeline->getTrackById(tid1)->getClipByPosition(0);
REQUIRE(cid3 != -1);
int cid4 = timeline->m_groups->getSplitPartner(cid3);
auto state2 = [&](int audio) {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 1);
REQUIRE(timeline->getTrackClipsCount(audio) == 1);
REQUIRE(timeline->getTrackClipsCount(tid1b) == 0);
REQUIRE(timeline->getClipTrackId(cid3) == tid1);
REQUIRE(timeline->getClipTrackId(cid4) == audio);
REQUIRE(timeline->getClipPosition(cid3) == 0);
REQUIRE(timeline->getClipPosition(cid4) == 0);
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<int>({cid3, cid4}));
int g1 = timeline->m_groups->getDirectAncestor(cid3);
REQUIRE(timeline->m_groups->getDirectChildren(g1) == std::unordered_set<int>({cid3, cid4}));
REQUIRE(timeline->m_groups->getType(g1) == GroupType::AVSplit);
};
state2(tid2);
undoStack->undo();
state0();
undoStack->redo();
state2(tid2);
undoStack->undo();
state0();
// now, we remove all audio tracks, making paste impossible
REQUIRE(timeline->requestTrackDeletion(tid2));
REQUIRE(timeline->requestTrackDeletion(tid2b));
REQUIRE_FALSE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
// undo one deletion
undoStack->undo();
// now, tid2b should be a valid audio track
REQUIRE(TimelineFunctions::pasteClips(timeline, cpy_str, tid1, 0));
cid3 = timeline->getTrackById(tid1)->getClipByPosition(0);
REQUIRE(cid3 != -1);
cid4 = timeline->m_groups->getSplitPartner(cid3);
state2(tid2b);
}
binModel->clean();
pCore->m_projectManager = nullptr;
Logger::print_trace();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment