File indexing completed on 2024-04-28 04:52:29

0001 /*
0002 SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 This file is part of Kdenlive. See www.kdenlive.org.
0004 
0005 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "timelinefunctions.hpp"
0009 #include "bin/bin.h"
0010 #include "bin/model/markerlistmodel.hpp"
0011 #include "bin/model/subtitlemodel.hpp"
0012 #include "bin/projectclip.h"
0013 #include "bin/projectfolder.h"
0014 #include "bin/projectitemmodel.h"
0015 #include "clipmodel.hpp"
0016 #include "compositionmodel.hpp"
0017 #include "core.h"
0018 #include "doc/kdenlivedoc.h"
0019 #include "effects/effectstack/model/effectstackmodel.hpp"
0020 #include "groupsmodel.hpp"
0021 #include "mainwindow.h"
0022 #include "monitor/monitor.h"
0023 #include "project/projectmanager.h"
0024 #include "timelineitemmodel.hpp"
0025 #include "trackmodel.hpp"
0026 #include "transitions/transitionsrepository.hpp"
0027 
0028 #include "utils/KMessageBox_KdenliveCompat.h"
0029 #include <KIO/RenameDialog>
0030 #include <KLocalizedString>
0031 #include <KMessageBox>
0032 #include <QApplication>
0033 #include <QDebug>
0034 #include <QInputDialog>
0035 #include <QSemaphore>
0036 #include <unordered_map>
0037 
0038 #ifdef CRASH_AUTO_TEST
0039 #include "logger.hpp"
0040 #pragma GCC diagnostic push
0041 #pragma GCC diagnostic ignored "-Wunused-parameter"
0042 #pragma GCC diagnostic ignored "-Wsign-conversion"
0043 #pragma GCC diagnostic ignored "-Wfloat-equal"
0044 #pragma GCC diagnostic ignored "-Wshadow"
0045 #pragma GCC diagnostic ignored "-Wpedantic"
0046 #include <rttr/registration>
0047 #pragma GCC diagnostic pop
0048 
0049 RTTR_REGISTRATION
0050 {
0051     using namespace rttr;
0052     registration::class_<TimelineFunctions>("TimelineFunctions")
0053         .method("requestClipCut", select_overload<bool(std::shared_ptr<TimelineItemModel>, int, int)>(&TimelineFunctions::requestClipCut))(
0054             parameter_names("timeline", "clipId", "position"))
0055         .method("requestDeleteBlankAt", select_overload<bool(const std::shared_ptr<TimelineItemModel> &, int, int, bool)>(
0056                                             &TimelineFunctions::requestDeleteBlankAt))(parameter_names("timeline", "trackId", "position", "affectAllTracks"));
0057 }
0058 #else
0059 #define TRACE_STATIC(...)
0060 #define TRACE_RES(...)
0061 #endif
0062 
0063 QStringList waitingBinIds;
0064 QMap<QString, QString> mappedIds;
0065 QMap<int, int> tracksMap;
0066 QMap<int, int> spacerUngroupedItems;
0067 int spacerMinPosition;
0068 QSemaphore semaphore(1);
0069 
0070 bool TimelineFunctions::cloneClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
0071                                   Fun &redo)
0072 {
0073     // Special case: slowmotion clips
0074     double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
0075     bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
0076     int audioStream = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index"));
0077     bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, audioStream, clipSpeed, warp_pitch, undo, redo);
0078     timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
0079 
0080     // copy useful timeline properties
0081     timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);
0082 
0083     int duration = timeline->getClipPlaytime(clipId);
0084     int init_duration = timeline->getClipPlaytime(newId);
0085     if (duration != init_duration) {
0086         init_duration -= timeline->m_allClips[clipId]->getIn();
0087         res = res && timeline->requestItemResize(newId, init_duration, false, true, undo, redo);
0088         res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
0089     }
0090     if (!res) {
0091         return false;
0092     }
0093     std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
0094     std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
0095     destStack->importEffects(sourceStack, state);
0096     return res;
0097 }
0098 
0099 bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> &timeline, const QStringList &binIds, int trackId, int position,
0100                                                       QList<int> &clipIds, bool logUndo, bool refreshView)
0101 {
0102     std::function<bool(void)> undo = []() { return true; };
0103     std::function<bool(void)> redo = []() { return true; };
0104     for (const QString &binId : binIds) {
0105         int clipId;
0106         if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
0107             clipIds.append(clipId);
0108             position += timeline->getItemPlaytime(clipId);
0109         } else {
0110             undo();
0111             clipIds.clear();
0112             return false;
0113         }
0114     }
0115 
0116     if (logUndo) {
0117         pCore->pushUndo(undo, redo, i18n("Insert Clips"));
0118     }
0119 
0120     return true;
0121 }
0122 
0123 bool TimelineFunctions::processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
0124 {
0125     bool isSubtitle = timeline->isSubTitle(clipId);
0126     int trackId = isSubtitle ? -1 : timeline->getClipTrackId(clipId);
0127     int trackDuration = isSubtitle ? -1 : timeline->getTrackById_const(trackId)->trackDuration();
0128     int start = timeline->getItemPosition(clipId);
0129     int duration = timeline->getItemPlaytime(clipId);
0130     if (start > position || (start + duration) < position) {
0131         return false;
0132     }
0133     if (isSubtitle) {
0134         newId = timeline->cutSubtitle(position, undo, redo);
0135         return newId > -1;
0136     }
0137     bool hasEndMix = timeline->getTrackById_const(trackId)->hasEndMix(clipId);
0138     bool hasStartMix = timeline->getTrackById_const(trackId)->hasStartMix(clipId);
0139     int subplaylist = timeline->m_allClips[clipId]->getSubPlaylistIndex();
0140     PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
0141     // Check if clip has an end Mix
0142     bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
0143     timeline->m_blockRefresh = true;
0144 
0145     int updatedDuration = position - start;
0146     // Resize original clip
0147     res = timeline->m_allClips[clipId]->requestResize(updatedDuration, true, undo, redo, true, hasEndMix || hasStartMix);
0148 
0149     if (hasEndMix) {
0150         // Assing end mix to new clone clip
0151         Fun local_redo = [timeline, trackId, clipId, newId]() { return timeline->getTrackById_const(trackId)->reAssignEndMix(clipId, newId); };
0152         local_redo();
0153         PUSH_LAMBDA(local_redo, redo);
0154         // Reassing end mix to original clip on undo
0155         Fun local_undo = [timeline, trackId, clipId, newId]() {
0156             timeline->getTrackById_const(trackId)->reAssignEndMix(newId, clipId);
0157             return true;
0158         };
0159         PUSH_LAMBDA(local_undo, undo);
0160         // Assing end mix to new clone clip
0161         if (!hasStartMix && subplaylist != 1) {
0162             Fun local_redo2 = [timeline, trackId, clipId, start]() {
0163                 // If the clip has no start mix, move to playlist 1
0164                 return timeline->getTrackById_const(trackId)->switchPlaylist(clipId, start, 0, 1);
0165             };
0166             // Restore initial subplaylist on undo
0167             Fun local_undo2 = [timeline, trackId, clipId, start]() {
0168                 // If the clip has no start mix, move back to playlist 0
0169                 return timeline->getTrackById_const(trackId)->switchPlaylist(clipId, start, 1, 0);
0170             };
0171             res = res && local_redo2();
0172             if (res) {
0173                 UPDATE_UNDO_REDO_NOLOCK(local_redo2, local_undo2, undo, redo);
0174             }
0175         }
0176     }
0177     int newDuration = timeline->getClipPlaytime(clipId);
0178     // parse effects
0179     if (res) {
0180         std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
0181         sourceStack->cleanFadeEffects(true, undo, redo);
0182         std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
0183         destStack->cleanFadeEffects(false, undo, redo);
0184     }
0185     updatedDuration = duration - newDuration;
0186     res = res && timeline->requestItemResize(newId, updatedDuration, false, true, undo, redo);
0187     // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now
0188     bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration();
0189     if (hasEndMix) {
0190         timeline->m_allClips[newId]->setSubPlaylistIndex(subplaylist, trackId);
0191     }
0192     res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo);
0193 
0194     if (durationChanged) {
0195         // Track length changed, check project duration
0196         Fun updateDuration = [timeline]() {
0197             timeline->updateDuration();
0198             return true;
0199         };
0200         updateDuration();
0201         PUSH_LAMBDA(updateDuration, redo);
0202     }
0203     timeline->m_blockRefresh = false;
0204     return res;
0205 }
0206 
0207 bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position)
0208 {
0209     std::function<bool(void)> undo = []() { return true; };
0210     std::function<bool(void)> redo = []() { return true; };
0211     TRACE_STATIC(timeline, clipId, position);
0212     bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
0213     if (result) {
0214         pCore->pushUndo(undo, redo, i18n("Cut clip"));
0215     }
0216     TRACE_RES(result);
0217     return result;
0218 }
0219 
0220 bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo)
0221 {
0222     const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId);
0223     // Remove locked items
0224     std::unordered_set<int> clips;
0225     for (int cid : clipselect) {
0226         if (timeline->isSubTitle(cid)) {
0227             clips.insert(cid);
0228             continue;
0229         }
0230         if (!timeline->isClip(cid)) {
0231             continue;
0232         }
0233         int tk = timeline->getClipTrackId(cid);
0234         if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
0235             clips.insert(cid);
0236         }
0237     }
0238     // Shall we reselect after the split
0239     int trackToSelect = -1;
0240     if (timeline->isClip(clipId) && timeline->m_allClips[clipId]->selected) {
0241         int mainIn = timeline->getItemPosition(clipId);
0242         int mainOut = mainIn + timeline->getItemPlaytime(clipId);
0243         if (position > mainIn && position < mainOut) {
0244             trackToSelect = timeline->getItemTrackId(clipId);
0245         }
0246     }
0247 
0248     std::unordered_set<int> topElements;
0249     std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
0250 
0251     int count = 0;
0252     QList<int> newIds;
0253     QList<int> clipsToCut;
0254     bool subtitleItemSelected = false;
0255     for (int cid : clips) {
0256         if (!timeline->isClip(cid) && !timeline->isSubTitle(cid)) {
0257             continue;
0258         }
0259         int start = timeline->getItemPosition(cid);
0260         int duration = timeline->getItemPlaytime(cid);
0261         if (start < position && (start + duration) > position) {
0262             clipsToCut << cid;
0263             if (timeline->isSubTitle(cid)) {
0264                 if (subtitleItemSelected) {
0265                     // We cannot cut 2 overlapping subtitles at the same position
0266                     pCore->displayMessage(i18nc("@info:status", "Cannot cut overlapping subtitles"), ErrorMessage, 500);
0267                     bool undone = undo();
0268                     Q_ASSERT(undone);
0269                     return false;
0270                 }
0271                 subtitleItemSelected = true;
0272             }
0273         }
0274     }
0275     if (clipsToCut.isEmpty()) {
0276         return true;
0277     }
0278 
0279     // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
0280     timeline->requestClearSelection();
0281 
0282     for (int cid : qAsConst(clipsToCut)) {
0283         count++;
0284         int newId = -1;
0285         bool res = processClipCut(timeline, cid, position, newId, undo, redo);
0286         if (!res) {
0287             bool undone = undo();
0288             Q_ASSERT(undone);
0289             return false;
0290         }
0291         // splitted elements go temporarily in the same group as original ones.
0292         timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
0293         newIds << newId;
0294     }
0295     if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
0296         // we now split the group hierarchy.
0297         // As a splitting criterion, we compare start point with split position
0298         auto criterion = [timeline, position](int cid) { return timeline->getItemPosition(cid) < position; };
0299         bool res = true;
0300         for (const int topId : topElements) {
0301             qDebug() << "// CHECKING REGROUP ELEMENT: " << topId << ", ISCLIP: " << timeline->isClip(topId) << timeline->isGroup(topId);
0302             res = res && timeline->m_groups->split(topId, criterion, undo, redo);
0303         }
0304         if (!res) {
0305             bool undone = undo();
0306             Q_ASSERT(undone);
0307             return false;
0308         }
0309     }
0310     if (count > 0 && trackToSelect > -1) {
0311         int newClip = timeline->getClipByPosition(trackToSelect, position);
0312         if (newClip > -1) {
0313             timeline->requestSetSelection({newClip});
0314         }
0315     }
0316     return count > 0;
0317 }
0318 
0319 bool TimelineFunctions::requestClipCutAll(std::shared_ptr<TimelineItemModel> timeline, int position)
0320 {
0321     QVector<std::shared_ptr<TrackModel>> affectedTracks;
0322     std::function<bool(void)> undo = []() { return true; };
0323     std::function<bool(void)> redo = []() { return true; };
0324 
0325     for (const auto &track : timeline->m_allTracks) {
0326         if (track->shouldReceiveTimelineOp()) {
0327             affectedTracks << track;
0328         }
0329     }
0330 
0331     unsigned count = 0;
0332     auto subModel = timeline->getSubtitleModel();
0333     if (subModel && !subModel->isLocked()) {
0334         int clipId = timeline->getClipByPosition(-2, position);
0335         if (clipId > -1) {
0336             // Found subtitle clip at position in track, cut it. Update undo/redo as we go.
0337             if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) {
0338                 qWarning() << "Failed to cut clip " << clipId << " at " << position;
0339                 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500);
0340                 // Undo all cuts made, assert successful undo.
0341                 bool undone = undo();
0342                 Q_ASSERT(undone);
0343                 return false;
0344             }
0345             count++;
0346         }
0347     }
0348     if (affectedTracks.isEmpty() && count == 0) {
0349         pCore->displayMessage(i18n("All tracks are locked"), ErrorMessage, 500);
0350         return false;
0351     }
0352     for (auto track : qAsConst(affectedTracks)) {
0353         int clipId = track->getClipByPosition(position);
0354         if (clipId > -1) {
0355             // Found clip at position in track, cut it. Update undo/redo as we go.
0356             if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) {
0357                 qWarning() << "Failed to cut clip " << clipId << " at " << position;
0358                 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500);
0359                 // Undo all cuts made, assert successful undo.
0360                 bool undone = undo();
0361                 Q_ASSERT(undone);
0362                 return false;
0363             }
0364             count++;
0365         }
0366     }
0367 
0368     if (!count) {
0369         pCore->displayMessage(i18n("No clips to cut"), ErrorMessage);
0370     } else {
0371         pCore->pushUndo(undo, redo, i18n("Cut all clips"));
0372     }
0373 
0374     return count > 0;
0375 }
0376 
0377 std::pair<int, int> TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position,
0378                                                                    bool ignoreMultiTrackGroups, bool allowGroupBreaking)
0379 {
0380     if (trackId != -1 && timeline->trackIsLocked(trackId)) {
0381         timeline->flashLock(trackId);
0382         return {-1, -1};
0383     }
0384     std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1);
0385     timeline->requestClearSelection();
0386     // Find the first clip on each track to calculate the minimum space operation
0387     QMap<int, int> firstClipOnTrack;
0388     // Find the maximum space allowed by grouped clips placed before the operation start {trackid,blank_duration}
0389     QMap<int, int> relatedMaxSpace;
0390     spacerMinPosition = -1;
0391     if (!clips.empty()) {
0392         // Remove grouped items that are before the click position
0393         // First get top groups ids
0394         std::unordered_set<int> roots;
0395         spacerUngroupedItems.clear();
0396         std::transform(clips.begin(), clips.end(), std::inserter(roots, roots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
0397         std::unordered_set<int> groupsToRemove;
0398         int firstCid = -1;
0399         int spaceDuration = -1;
0400         std::unordered_set<int> toSelect;
0401         //  List all clips involved in the spacer operation
0402         std::unordered_set<int> allClips;
0403         for (int r : roots) {
0404             std::unordered_set<int> children = timeline->m_groups->getLeaves(r);
0405             allClips.insert(children.begin(), children.end());
0406         }
0407         for (int r : roots) {
0408             if (timeline->isGroup(r)) {
0409                 std::unordered_set<int> leaves = timeline->m_groups->getLeaves(r);
0410                 std::unordered_set<int> leavesToRemove;
0411                 std::unordered_set<int> leavesToKeep;
0412                 for (int l : leaves) {
0413                     int pos = timeline->getItemPosition(l);
0414                     bool outOfRange = timeline->getItemEnd(l) < position;
0415                     int tid = timeline->getItemTrackId(l);
0416                     bool unaffectedTrack = ignoreMultiTrackGroups && trackId > -1 && tid != trackId;
0417                     if (allowGroupBreaking) {
0418                         if (outOfRange || unaffectedTrack) {
0419                             leavesToRemove.insert(l);
0420                         } else {
0421                             leavesToKeep.insert(l);
0422                         }
0423                     } else if (outOfRange) {
0424                         // This is a grouped clip positionned before the spacer operation position, check maximum space before
0425                         std::unordered_set<int> beforeOnTrack = timeline->getItemsInRange(tid, 0, pos - 1);
0426                         for (auto &c : allClips) {
0427                             beforeOnTrack.erase(c);
0428                         }
0429                         int lastPos = 0;
0430                         for (int c : beforeOnTrack) {
0431                             int p = timeline->getClipEnd(c);
0432                             if (p >= pos - 1) {
0433                                 lastPos = pos;
0434                                 break;
0435                             }
0436                             if (p > lastPos) {
0437                                 lastPos = p;
0438                             }
0439                         }
0440                         if (relatedMaxSpace.contains(trackId)) {
0441                             if (relatedMaxSpace.value(trackId) > (pos - lastPos)) {
0442                                 relatedMaxSpace.insert(trackId, pos - lastPos);
0443                             }
0444                         } else {
0445                             relatedMaxSpace.insert(trackId, pos - lastPos);
0446                         }
0447                     }
0448                     if (!outOfRange && !unaffectedTrack) {
0449                         // Find first item
0450                         if (!firstClipOnTrack.contains(tid)) {
0451                             firstClipOnTrack.insert(tid, l);
0452                         } else if (timeline->getItemPosition(firstClipOnTrack.value(tid)) > pos) {
0453                             firstClipOnTrack.insert(tid, l);
0454                         }
0455                     }
0456                 }
0457                 for (int l : leavesToRemove) {
0458                     int checkedParent = timeline->m_groups->getDirectAncestor(l);
0459                     if (checkedParent < 0) {
0460                         checkedParent = l;
0461                     }
0462                     spacerUngroupedItems.insert(l, checkedParent);
0463                 }
0464                 if (leavesToKeep.size() == 1) {
0465                     toSelect.insert(*leavesToKeep.begin());
0466                     groupsToRemove.insert(r);
0467                 }
0468             } else {
0469                 // Find first clip on track
0470                 int pos = timeline->getItemPosition(r);
0471                 int tid = timeline->getItemTrackId(r);
0472                 if (!firstClipOnTrack.contains(tid)) {
0473                     firstClipOnTrack.insert(tid, r);
0474                 } else if (timeline->getItemPosition(firstClipOnTrack.value(tid)) > pos) {
0475                     firstClipOnTrack.insert(tid, r);
0476                 }
0477             }
0478         }
0479         toSelect.insert(roots.begin(), roots.end());
0480         for (int r : groupsToRemove) {
0481             toSelect.erase(r);
0482         }
0483 
0484         Fun undo = []() { return true; };
0485         Fun redo = []() { return true; };
0486         QMapIterator<int, int> i(spacerUngroupedItems);
0487         while (i.hasNext()) {
0488             i.next();
0489             timeline->m_groups->removeFromGroup(i.key());
0490         }
0491 
0492         timeline->requestSetSelection(toSelect);
0493 
0494         QMapIterator<int, int> it(firstClipOnTrack);
0495         int firstPos = -1;
0496         if (firstClipOnTrack.isEmpty() && firstCid > -1) {
0497             int clipPos = timeline->getItemPosition(firstCid);
0498             spaceDuration = timeline->getTrackById_const(timeline->getItemTrackId(firstCid))->getBlankSizeAtPos(clipPos - 1);
0499         }
0500         while (it.hasNext()) {
0501             it.next();
0502             int clipPos = timeline->getItemPosition(it.value());
0503             if (trackId > -1) {
0504                 if (it.key() == trackId) {
0505                     firstCid = it.value();
0506                 }
0507             } else {
0508                 if (firstPos == -1) {
0509                     firstCid = it.value();
0510                     firstPos = clipPos;
0511                 } else if (firstPos < clipPos) {
0512                     firstCid = it.value();
0513                 }
0514             }
0515             if (timeline->isSubtitleTrack(it.key())) {
0516                 if (timeline->getSubtitleModel()->isBlankAt(clipPos - 1)) {
0517                     if (spaceDuration == -1) {
0518                         spaceDuration = timeline->getSubtitleModel()->getBlankSizeAtPos(clipPos - 1);
0519                     } else {
0520                         int blank = timeline->getSubtitleModel()->getBlankSizeAtPos(clipPos - 1);
0521                         spaceDuration = qMin(blank, spaceDuration);
0522                     }
0523                 }
0524             } else {
0525                 if (timeline->getTrackById_const(it.key())->isBlankAt(clipPos - 1)) {
0526                     if (spaceDuration == -1) {
0527                         spaceDuration = timeline->getTrackById_const(it.key())->getBlankSizeAtPos(clipPos - 1);
0528                     } else {
0529                         int blank = timeline->getTrackById_const(it.key())->getBlankSizeAtPos(clipPos - 1);
0530                         spaceDuration = qMin(blank, spaceDuration);
0531                     }
0532                 }
0533             }
0534             if (relatedMaxSpace.contains(it.key())) {
0535                 spaceDuration = qMin(spaceDuration, relatedMaxSpace.value(it.key()));
0536             }
0537         }
0538         spacerMinPosition = timeline->getItemPosition(firstCid) - spaceDuration;
0539         return {firstCid, spaceDuration};
0540     }
0541     return {-1, -1};
0542 }
0543 
0544 bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition,
0545                                                   int affectedTrack, int moveGuidesPosition, Fun &undo, Fun &redo, bool pushUndo)
0546 {
0547     // Move guides if needed
0548     if (moveGuidesPosition > -1) {
0549         moveGuidesPosition = qMin(moveGuidesPosition, startPosition);
0550         GenTime fromPos(moveGuidesPosition, pCore->getCurrentFps());
0551         GenTime toPos(endPosition - startPosition, pCore->getCurrentFps());
0552         QList<CommentedTime> guides = timeline->getGuideModel()->getMarkersInRange(moveGuidesPosition, -1);
0553         if (!guides.isEmpty()) {
0554             timeline->getGuideModel()->moveMarkers(guides, fromPos, fromPos + toPos, undo, redo);
0555         }
0556     }
0557 
0558     // Move group back to original position
0559     spacerMinPosition = -1;
0560     int track = timeline->getItemTrackId(itemId);
0561     bool isClip = timeline->isClip(itemId);
0562     if (isClip) {
0563         timeline->requestClipMove(itemId, track, startPosition, true, false, false, false, true);
0564     } else if (timeline->isComposition(itemId)) {
0565         timeline->requestCompositionMove(itemId, track, startPosition, false, false);
0566     } else {
0567         timeline->requestSubtitleMove(itemId, startPosition, false, false);
0568     }
0569 
0570     std::unordered_set<int> clips = timeline->getGroupElements(itemId);
0571     int mainGroup = timeline->m_groups->getRootId(itemId);
0572     bool final = false;
0573     bool liftOk = true;
0574     if (timeline->m_editMode == TimelineMode::OverwriteEdit && endPosition < startPosition) {
0575         // Remove zone between end and start pos
0576         if (affectedTrack == -1) {
0577             // touch all tracks
0578             auto it = timeline->m_allTracks.cbegin();
0579             while (it != timeline->m_allTracks.cend()) {
0580                 int target_track = (*it)->getId();
0581                 if (!timeline->getTrackById_const(target_track)->isLocked()) {
0582                     liftOk = liftOk && TimelineFunctions::liftZone(timeline, target_track, QPoint(endPosition, startPosition), undo, redo);
0583                 }
0584                 ++it;
0585             }
0586         } else if (timeline->isTrack(affectedTrack)) {
0587             liftOk = TimelineFunctions::liftZone(timeline, affectedTrack, QPoint(endPosition, startPosition), undo, redo);
0588         }
0589         // The lift operation destroys selection group, so regroup now
0590         if (clips.size() > 1) {
0591             timeline->requestSetSelection(clips);
0592             mainGroup = timeline->m_groups->getRootId(itemId);
0593         }
0594     }
0595     if (liftOk && (mainGroup > -1 || clips.size() == 1)) {
0596         if (clips.size() > 1) {
0597             final = timeline->requestGroupMove(itemId, mainGroup, 0, endPosition - startPosition, true, true, undo, redo);
0598         } else {
0599             // only 1 clip to be moved
0600             if (isClip) {
0601                 final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo);
0602             } else if (timeline->isComposition(itemId)) {
0603                 final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
0604             } else {
0605                 final = timeline->requestSubtitleMove(itemId, endPosition, true, true, true, true, undo, redo);
0606             }
0607         }
0608     }
0609     timeline->requestClearSelection();
0610     if (final) {
0611         if (pushUndo) {
0612             if (startPosition < endPosition) {
0613                 pCore->pushUndo(undo, redo, i18n("Insert space"));
0614             } else {
0615                 pCore->pushUndo(undo, redo, i18n("Remove space"));
0616             }
0617         }
0618         // Regroup temporarily ungrouped items
0619         QMapIterator<int, int> i(spacerUngroupedItems);
0620         Fun local_undo = []() { return true; };
0621         Fun local_redo = []() { return true; };
0622         std::unordered_set<int> newlyGrouped;
0623         while (i.hasNext()) {
0624             i.next();
0625             if (timeline->isItem(i.value())) {
0626                 if (newlyGrouped.count(i.value()) > 0) {
0627                     Q_ASSERT(timeline->m_groups->isInGroup(i.value()));
0628                     timeline->m_groups->setInGroupOf(i.key(), i.value(), local_undo, local_redo);
0629                 } else {
0630                     std::unordered_set<int> items = {i.key(), i.value()};
0631                     timeline->m_groups->groupItems(items, local_undo, local_redo);
0632                     newlyGrouped.insert(i.value());
0633                 }
0634             } else {
0635                 // i.value() is either a group (detectable via timeline->isGroup) or an empty group
0636                 if (timeline->isGroup(i.key())) {
0637                     std::unordered_set<int> items = {i.key(), i.value()};
0638                     timeline->m_groups->groupItems(items, local_undo, local_redo);
0639                 } else {
0640                     timeline->m_groups->setGroup(i.key(), i.value());
0641                 }
0642             }
0643         }
0644         spacerUngroupedItems.clear();
0645         return true;
0646     } else {
0647         undo();
0648     }
0649     return false;
0650 }
0651 
0652 bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, Fun &undo, Fun &redo)
0653 {
0654     // Check if we have grouped clips that are on unaffected tracks, and ungroup them
0655     bool result = true;
0656     std::unordered_set<int> affectedItems;
0657     // First find all affected items
0658     for (auto trackId : tracks) {
0659         std::unordered_set<int> items = timeline->getItemsInRange(trackId, zone.x(), zone.y());
0660         affectedItems.insert(items.begin(), items.end());
0661     }
0662     for (int item : affectedItems) {
0663         if (timeline->m_groups->isInGroup(item)) {
0664             int groupId = timeline->m_groups->getRootId(item);
0665             std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
0666             for (int child : all_children) {
0667                 int childTrackId = timeline->getItemTrackId(child);
0668                 if (!tracks.contains(childTrackId) && timeline->m_groups->isInGroup(child)) {
0669                     // This item should not be affected by the operation, ungroup it
0670                     result = result && timeline->requestClipUngroup(child, undo, redo);
0671                 }
0672             }
0673         }
0674     }
0675     return result;
0676 }
0677 
0678 bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, bool liftOnly,
0679                                     int clipToUnGroup, std::unordered_set<int> clipsToRegroup)
0680 {
0681     std::function<bool(void)> undo = []() { return true; };
0682     std::function<bool(void)> redo = []() { return true; };
0683     bool res = extractZoneWithUndo(timeline, tracks, zone, liftOnly, clipToUnGroup, clipsToRegroup, undo, redo);
0684     pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
0685     return res;
0686 }
0687 
0688 bool TimelineFunctions::extractZoneWithUndo(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, bool liftOnly,
0689                                             int clipToUnGroup, std::unordered_set<int> clipsToRegroup, Fun &undo, Fun &redo)
0690 {
0691     // Start undoable command
0692     bool result = true;
0693     if (clipToUnGroup > -1) {
0694         result = timeline->requestClipUngroup(clipToUnGroup, undo, redo);
0695     }
0696     result = breakAffectedGroups(timeline, tracks, zone, undo, redo);
0697     for (auto trackId : tracks) {
0698         if (timeline->getTrackById_const(trackId)->isLocked()) {
0699             continue;
0700         }
0701         result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
0702     }
0703     if (result && !liftOnly) {
0704         result = TimelineFunctions::removeSpace(timeline, zone, undo, redo, tracks);
0705     }
0706     if (clipsToRegroup.size() > 1) {
0707         result = timeline->requestClipsGroup(clipsToRegroup, undo, redo);
0708     }
0709     return result;
0710 }
0711 
0712 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, const QList<int> &trackIds, const QString &binId, int insertFrame,
0713                                    QPoint zone, bool overwrite, bool useTargets)
0714 {
0715     std::function<bool(void)> undo = []() { return true; };
0716     std::function<bool(void)> redo = []() { return true; };
0717     bool res = TimelineFunctions::insertZone(timeline, trackIds, binId, insertFrame, zone, overwrite, useTargets, undo, redo);
0718     if (res) {
0719         pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
0720     } else {
0721         pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
0722         undo();
0723     }
0724     return res;
0725 }
0726 
0727 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
0728                                    bool overwrite, bool useTargets, Fun &undo, Fun &redo)
0729 {
0730     // Start undoable command
0731     bool result = true;
0732     QVector<int> affectedTracks;
0733     auto it = timeline->m_allTracks.cbegin();
0734     if (!useTargets) {
0735         // Timeline drop in overwrite mode
0736         for (int target_track : trackIds) {
0737             if (!timeline->getTrackById_const(target_track)->isLocked()) {
0738                 affectedTracks << target_track;
0739             }
0740         }
0741     } else {
0742         while (it != timeline->m_allTracks.cend()) {
0743             int target_track = (*it)->getId();
0744             if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
0745                 affectedTracks << target_track;
0746             } else if (trackIds.contains(target_track)) {
0747                 // Track is marked as target but not active, remove it
0748                 trackIds.removeAll(target_track);
0749             }
0750             ++it;
0751         }
0752     }
0753     if (affectedTracks.isEmpty()) {
0754         pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), ErrorMessage);
0755         return false;
0756     }
0757     result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
0758     if (overwrite) {
0759         // Cut all tracks
0760         for (int target_track : qAsConst(affectedTracks)) {
0761             result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
0762             if (!result) {
0763                 qDebug() << "// LIFTING ZONE FAILED\n";
0764                 break;
0765             }
0766         }
0767     } else {
0768         // Cut all tracks
0769         for (int target_track : qAsConst(affectedTracks)) {
0770             int startClipId = timeline->getClipByPosition(target_track, insertFrame);
0771             if (startClipId > -1) {
0772                 // There is a clip, cut it
0773                 result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
0774             }
0775         }
0776         result =
0777             result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks);
0778     }
0779     if (result) {
0780         if (!trackIds.isEmpty()) {
0781             int newId = -1;
0782             QString binClipId;
0783             if (binId.contains(QLatin1Char('/'))) {
0784                 binClipId = QString("%1/%2/%3").arg(binId.section(QLatin1Char('/'), 0, 0)).arg(zone.x()).arg(zone.y() - 1);
0785             } else {
0786                 binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
0787             }
0788             result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks);
0789         }
0790     }
0791     return result;
0792 }
0793 
0794 bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
0795 {
0796     // Check if there is a clip at start point
0797     int startClipId = timeline->getClipByPosition(trackId, zone.x());
0798     if (startClipId > -1) {
0799         // There is a clip, cut it
0800         if (timeline->getClipPosition(startClipId) < zone.x()) {
0801             // Check if we have a mix
0802             std::pair<MixInfo, MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(startClipId);
0803             bool abortCut = false;
0804             if (mixData.first.firstClipId > -1) {
0805                 // Clip has a start mix
0806                 if (mixData.first.secondClipInOut.first + (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first) -
0807                         mixData.first.mixOffset >=
0808                     zone.x()) {
0809                     // Cut pos is in the mix zone before clip cut, completely remove clip
0810                     abortCut = true;
0811                 }
0812             }
0813             if (!abortCut) {
0814                 TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
0815             } else {
0816                 // Remove the clip now, so that the mix is deleted before checking items in range
0817                 timeline->requestClipUngroup(startClipId, undo, redo);
0818                 timeline->requestItemDeletion(startClipId, undo, redo);
0819             }
0820         }
0821     }
0822     int endClipId = timeline->getClipByPosition(trackId, zone.y());
0823     if (endClipId > -1) {
0824         // There is a clip, cut it
0825         if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
0826             // Check if we have a mix
0827             std::pair<MixInfo, MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(endClipId);
0828             bool abortCut = false;
0829             if (mixData.second.firstClipId > -1) {
0830                 // Clip has an end mix
0831                 if (mixData.second.firstClipInOut.second - (mixData.second.firstClipInOut.second - mixData.second.secondClipInOut.first) -
0832                         mixData.first.mixOffset <=
0833                     zone.y()) {
0834                     // Cut pos is in the mix zone after clip cut, completely remove clip
0835                     abortCut = true;
0836                 }
0837             }
0838             if (!abortCut) {
0839                 TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
0840             } else {
0841                 // Remove the clip now, so that the mix is deleted before checking items in range
0842                 timeline->requestClipUngroup(endClipId, undo, redo);
0843                 timeline->requestItemDeletion(endClipId, undo, redo);
0844             }
0845         }
0846     }
0847     std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
0848     for (const auto &clipId : clips) {
0849         timeline->requestClipUngroup(clipId, undo, redo);
0850         timeline->requestItemDeletion(clipId, undo, redo);
0851     }
0852     return true;
0853 }
0854 
0855 bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, const QVector<int> &allowedTracks,
0856                                     bool useTargets)
0857 {
0858     std::unordered_set<int> clips;
0859     if (useTargets) {
0860         auto it = timeline->m_allTracks.cbegin();
0861         while (it != timeline->m_allTracks.cend()) {
0862             int target_track = (*it)->getId();
0863             if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
0864                 std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true);
0865                 clips.insert(subs.begin(), subs.end());
0866             }
0867             ++it;
0868         }
0869     } else {
0870         for (auto tid : allowedTracks) {
0871             std::unordered_set<int> subs = timeline->getItemsInRange(tid, zone.y() - 1, -1, true);
0872             clips.insert(subs.begin(), subs.end());
0873         }
0874     }
0875     if (clips.size() == 0) {
0876         // TODO: inform user no change will be performed
0877         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
0878         return true;
0879     }
0880     bool result = false;
0881     timeline->requestSetSelection(clips);
0882     int itemId = *clips.begin();
0883     int targetTrackId = timeline->getItemTrackId(itemId);
0884     int targetPos = timeline->getItemPosition(itemId) + zone.x() - zone.y();
0885 
0886     if (timeline->m_groups->isInGroup(itemId)) {
0887         result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, true,
0888                                             allowedTracks);
0889     } else if (timeline->isClip(itemId)) {
0890         result = timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, undo, redo);
0891     } else {
0892         result =
0893             timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, undo, redo);
0894     }
0895     timeline->requestClearSelection();
0896     if (!result) {
0897         undo();
0898     }
0899     return result;
0900 }
0901 
0902 bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo,
0903                                            const QVector<int> &allowedTracks)
0904 {
0905     timeline->requestClearSelection();
0906     Fun local_undo = []() { return true; };
0907     Fun local_redo = []() { return true; };
0908     std::unordered_set<int> items;
0909     if (allowedTracks.isEmpty()) {
0910         // Select clips in all tracks
0911         items = timeline->getItemsInRange(-1, zone.x(), -1, true);
0912     } else {
0913         // Select clips in target and active tracks only
0914         for (int target_track : allowedTracks) {
0915             std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
0916             items.insert(subs.begin(), subs.end());
0917         }
0918     }
0919     if (items.empty()) {
0920         return true;
0921     }
0922     timeline->requestSetSelection(items);
0923     bool result = true;
0924     int itemId = *(items.begin());
0925     int targetTrackId = timeline->getItemTrackId(itemId);
0926     int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x();
0927 
0928     // TODO the three move functions should be unified in a "requestItemMove" function
0929     if (timeline->m_groups->isInGroup(itemId)) {
0930         result = result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo,
0931                                                       true, true, true, allowedTracks);
0932     } else if (timeline->isClip(itemId)) {
0933         result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo);
0934     } else {
0935         result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true,
0936                                                             local_undo, local_redo);
0937     }
0938     timeline->requestClearSelection();
0939     if (!result) {
0940         bool undone = local_undo();
0941         Q_ASSERT(undone);
0942         pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage);
0943     }
0944     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
0945     return result;
0946 }
0947 
0948 bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
0949 {
0950     Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
0951     Fun undo = []() { return true; };
0952     Fun redo = []() { return true; };
0953     int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
0954     int deltaPos = position - timeline->getItemPosition(clipId);
0955     std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
0956     std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
0957     bool res = true;
0958     for (int id : allIds) {
0959         int newId = -1;
0960         if (timeline->isClip(id)) {
0961             PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
0962             res = cloneClip(timeline, id, newId, state, undo, redo);
0963             res = res && (newId != -1);
0964         }
0965         int target_position = timeline->getItemPosition(id) + deltaPos;
0966         int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
0967         if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) {
0968             auto it = timeline->m_allTracks.cbegin();
0969             std::advance(it, target_track_position);
0970             int target_track = (*it)->getId();
0971             if (timeline->isClip(id)) {
0972                 res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo);
0973             } else {
0974                 const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
0975                 std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
0976                 res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
0977                                                                    timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
0978             }
0979         } else {
0980             res = false;
0981         }
0982         if (!res) {
0983             bool undone = undo();
0984             Q_ASSERT(undone);
0985             return false;
0986         }
0987         mapping[id] = newId;
0988     }
0989     qDebug() << "Successful copy, copying groups...";
0990     res = timeline->m_groups->copyGroups(mapping, undo, redo);
0991     if (!res) {
0992         bool undone = undo();
0993         Q_ASSERT(undone);
0994         return false;
0995     }
0996     return true;
0997 }
0998 
0999 void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
1000 {
1001     timeline->m_allClips[clipId]->setShowKeyframes(value);
1002     QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
1003     Q_EMIT timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
1004 }
1005 
1006 void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
1007 {
1008     timeline->m_allCompositions[compoId]->setShowKeyframes(value);
1009     QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
1010     Q_EMIT timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
1011 }
1012 
1013 bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, std::unordered_set<int> selection)
1014 {
1015     Fun undo = []() { return true; };
1016     Fun redo = []() { return true; };
1017     bool result = false;
1018     bool disable = true;
1019     for (int clipId : selection) {
1020         if (!timeline->isClip(clipId)) {
1021             continue;
1022         }
1023         PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
1024         PlaylistState::ClipState state = PlaylistState::Disabled;
1025         disable = true;
1026         if (oldState == PlaylistState::Disabled) {
1027             state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
1028             disable = false;
1029         }
1030         result = changeClipState(timeline, clipId, state, undo, redo);
1031         if (!result) {
1032             break;
1033         }
1034     }
1035     // Update action name since clip will be switched
1036     int id = *selection.begin();
1037     Fun local_redo = []() { return true; };
1038     Fun local_undo = []() { return true; };
1039     if (timeline->isClip(id)) {
1040         bool disabled = timeline->m_allClips[id]->clipState() == PlaylistState::Disabled;
1041         QAction *action = pCore->window()->actionCollection()->action(QStringLiteral("clip_switch"));
1042         local_redo = [disabled, action]() {
1043             action->setText(disabled ? i18n("Enable clip") : i18n("Disable clip"));
1044             return true;
1045         };
1046         local_undo = [disabled, action]() {
1047             action->setText(disabled ? i18n("Disable clip") : i18n("Enable clip"));
1048             return true;
1049         };
1050     }
1051     if (result) {
1052         local_redo();
1053         UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
1054         pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
1055     }
1056     return result;
1057 }
1058 
1059 bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
1060 {
1061     int track = timeline->getClipTrackId(clipId);
1062     int start = -1;
1063     bool invalidate = false;
1064     if (track > -1) {
1065         if (!timeline->getTrackById_const(track)->isAudioTrack()) {
1066             invalidate = true;
1067         }
1068         start = timeline->getItemPosition(clipId);
1069     }
1070     Fun local_undo = []() { return true; };
1071     Fun local_redo = []() { return true; };
1072     // For the state change to work, we need to unplant/replant the clip
1073     bool result = true;
1074     if (track > -1) {
1075         result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false);
1076     }
1077     result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
1078     if (result && track > -1) {
1079         result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo, false, false);
1080     }
1081     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
1082     return result;
1083 }
1084 
1085 bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
1086 {
1087     std::function<bool(void)> undo = []() { return true; };
1088     std::function<bool(void)> redo = []() { return true; };
1089     const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
1090     bool done = false;
1091     // Now clear selection so we don't mess with groups
1092     timeline->requestClearSelection(false, undo, redo);
1093     for (int cid : clips) {
1094         if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
1095             // clip without audio or audio only, skip
1096             pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
1097             return false;
1098         }
1099         int position = timeline->getClipPosition(cid);
1100         int track = timeline->getClipTrackId(cid);
1101         QList<int> possibleTracks;
1102         // Try inserting in target track first, then mirror track
1103         if (audioTarget >= 0) {
1104             possibleTracks = {audioTarget};
1105         }
1106         int mirror = timeline->getMirrorAudioTrackId(track);
1107         if (mirror > -1) {
1108             possibleTracks << mirror;
1109         }
1110         if (possibleTracks.isEmpty()) {
1111             // No available audio track for splitting, abort
1112             undo();
1113             pCore->displayMessage(i18n("No available audio track for restore operation"), ErrorMessage);
1114             return false;
1115         }
1116         int newId;
1117         bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
1118         if (!res) {
1119             bool undone = undo();
1120             Q_ASSERT(undone);
1121             pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage);
1122             return false;
1123         }
1124         bool success = false;
1125         while (!success && !possibleTracks.isEmpty()) {
1126             int newTrack = possibleTracks.takeFirst();
1127             success = timeline->requestClipMove(newId, newTrack, position, true, true, false, true, undo, redo);
1128         }
1129         TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
1130         success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
1131         if (!success) {
1132             bool undone = undo();
1133             Q_ASSERT(undone);
1134             pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage);
1135             return false;
1136         }
1137         done = true;
1138     }
1139     if (done) {
1140         timeline->requestSetSelection(clips, undo, redo);
1141         pCore->pushUndo(undo, redo, i18n("Restore Audio"));
1142     }
1143     return done;
1144 }
1145 
1146 bool TimelineFunctions::requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget)
1147 {
1148     std::function<bool(void)> undo = []() { return true; };
1149     std::function<bool(void)> redo = []() { return true; };
1150     const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
1151     bool done = false;
1152     // Now clear selection so we don't mess with groups
1153     timeline->requestClearSelection();
1154     for (int cid : clips) {
1155         if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) {
1156             // clip without audio or audio only, skip
1157             continue;
1158         }
1159         int position = timeline->getClipPosition(cid);
1160         int track = timeline->getClipTrackId(cid);
1161         QList<int> possibleTracks;
1162         // Try inserting in target track first, then mirror track
1163         if (videoTarget >= 0) {
1164             possibleTracks = {videoTarget};
1165         }
1166         int mirror = timeline->getMirrorVideoTrackId(track);
1167         if (mirror > -1) {
1168             possibleTracks << mirror;
1169         }
1170         if (possibleTracks.isEmpty()) {
1171             // No available audio track for splitting, abort
1172             undo();
1173             pCore->displayMessage(i18n("No available video track for restore operation"), ErrorMessage);
1174             return false;
1175         }
1176         int newId;
1177         bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
1178         if (!res) {
1179             bool undone = undo();
1180             Q_ASSERT(undone);
1181             pCore->displayMessage(i18n("Video restore failed"), ErrorMessage);
1182             return false;
1183         }
1184         bool success = false;
1185         while (!success && !possibleTracks.isEmpty()) {
1186             int newTrack = possibleTracks.takeFirst();
1187             success = timeline->requestClipMove(newId, newTrack, position, true, true, true, true, undo, redo);
1188         }
1189         TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
1190         success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
1191         if (!success) {
1192             bool undone = undo();
1193             Q_ASSERT(undone);
1194             pCore->displayMessage(i18n("Video restore failed"), ErrorMessage);
1195             return false;
1196         }
1197         done = true;
1198     }
1199     if (done) {
1200         pCore->pushUndo(undo, redo, i18n("Restore Video"));
1201     }
1202     return done;
1203 }
1204 
1205 void TimelineFunctions::setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack)
1206 {
1207     std::function<bool(void)> undo = []() { return true; };
1208     std::function<bool(void)> redo = []() { return true; };
1209     std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid);
1210     int previousATrack = compo->getATrack();
1211     int previousAutoTrack = static_cast<int>(compo->getForcedTrack() == -1);
1212     bool autoTrack = aTrack < 0;
1213     if (autoTrack) {
1214         // Automatic track compositing, find lower video track
1215         aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId());
1216     }
1217     int start = timeline->getItemPosition(cid);
1218     int end = start + timeline->getItemPlaytime(cid);
1219     Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
1220         timeline->unplantComposition(cid);
1221         QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
1222         field->lock();
1223         timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack);
1224         timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack < 1 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1));
1225         field->unlock();
1226         timeline->replantCompositions(cid, true);
1227         Q_EMIT timeline->invalidateZone(start, end);
1228         timeline->checkRefresh(start, end);
1229         return true;
1230     };
1231     Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
1232         timeline->unplantComposition(cid);
1233         QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
1234         field->lock();
1235         timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
1236         timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack < 1 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
1237         field->unlock();
1238         timeline->replantCompositions(cid, true);
1239         Q_EMIT timeline->invalidateZone(start, end);
1240         timeline->checkRefresh(start, end);
1241         return true;
1242     };
1243     if (local_redo()) {
1244         PUSH_LAMBDA(local_undo, undo);
1245         PUSH_LAMBDA(local_redo, redo);
1246     }
1247     pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
1248 }
1249 
1250 QStringList TimelineFunctions::enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable, bool refresh)
1251 {
1252     QStringList trackNames;
1253     std::vector<int> videoTracks;
1254     for (int i = 0; i < int(timeline->m_allTracks.size()); i++) {
1255         int tid = timeline->getTrackIndexFromPosition(i);
1256         if (timeline->getTrackById_const(tid)->isAudioTrack() || timeline->getTrackById_const(tid)->isHidden()) {
1257             continue;
1258         }
1259         videoTracks.push_back(tid);
1260     }
1261     if (videoTracks.size() < 2) {
1262         pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), ErrorMessage);
1263     }
1264     // First, dis/enable track compositing
1265     QScopedPointer<Mlt::Service> service(timeline->m_tractor->field());
1266     Mlt::Field *field = timeline->m_tractor->field();
1267     field->lock();
1268     while ((service != nullptr) && service->is_valid()) {
1269         if (service->type() == mlt_service_transition_type) {
1270             Mlt::Transition t(mlt_transition(service->get_service()));
1271             service.reset(service->producer());
1272             QString serviceName = t.get("mlt_service");
1273             int added = t.get_int("internal_added");
1274             if (added == 237 && serviceName != QLatin1String("mix")) {
1275                 // Disable all compositing transitions
1276                 t.set("disable", enable ? "1" : nullptr);
1277             } else if (added == 200) {
1278                 field->disconnect_service(t);
1279                 t.disconnect_all_producers();
1280             }
1281         } else {
1282             service.reset(service->producer());
1283         }
1284     }
1285     if (enable) {
1286         int count = 0;
1287 
1288         for (int tid : videoTracks) {
1289             int b_track = timeline->getTrackMltIndex(tid);
1290             Mlt::Transition transition(timeline->m_tractor->get_profile(), "qtblend");
1291             // transition.set("mlt_service", "composite");
1292             transition.set("a_track", 0);
1293             transition.set("b_track", b_track);
1294             // 200 is an arbitrary number so we can easily remove these transition later
1295             transition.set("internal_added", 200);
1296             QString geometry;
1297             trackNames << timeline->getTrackFullName(tid);
1298             switch (count) {
1299             case 0:
1300                 switch (videoTracks.size()) {
1301                 case 1:
1302                     geometry = QStringLiteral("0 0 100% 100% 100%");
1303                     break;
1304                 case 2:
1305                     geometry = QStringLiteral("0 0 50% 100% 100%");
1306                     break;
1307                 case 3:
1308                 case 4:
1309                     geometry = QStringLiteral("0 0 50% 50% 100%");
1310                     break;
1311                 case 5:
1312                 case 6:
1313                     geometry = QStringLiteral("0 0 33% 50% 100%");
1314                     break;
1315                 default:
1316                     geometry = QStringLiteral("0 0 33% 33% 100%");
1317                     break;
1318                 }
1319                 break;
1320             case 1:
1321                 switch (videoTracks.size()) {
1322                 case 2:
1323                     geometry = QStringLiteral("50% 0 50% 100% 100%");
1324                     break;
1325                 case 3:
1326                 case 4:
1327                     geometry = QStringLiteral("50% 0 50% 50% 100%");
1328                     break;
1329                 case 5:
1330                 case 6:
1331                     geometry = QStringLiteral("33% 0 33% 50% 100%");
1332                     break;
1333                 default:
1334                     geometry = QStringLiteral("33% 0 33% 33% 100%");
1335                     break;
1336                 }
1337                 break;
1338             case 2:
1339                 switch (videoTracks.size()) {
1340                 case 3:
1341                 case 4:
1342                     geometry = QStringLiteral("0 50% 50% 50% 100%");
1343                     break;
1344                 case 5:
1345                 case 6:
1346                     geometry = QStringLiteral("66% 0 33% 50% 100%");
1347                     break;
1348                 default:
1349                     geometry = QStringLiteral("66% 0 33% 33% 100%");
1350                     break;
1351                 }
1352                 break;
1353             case 3:
1354                 switch (videoTracks.size()) {
1355                 case 4:
1356                     geometry = QStringLiteral("50% 50% 50% 50% 100%");
1357                     break;
1358                 case 5:
1359                 case 6:
1360                     geometry = QStringLiteral("0 50% 33% 50% 100%");
1361                     break;
1362                 default:
1363                     geometry = QStringLiteral("0 33% 33% 33% 100%");
1364                     break;
1365                 }
1366                 break;
1367             case 4:
1368                 switch (videoTracks.size()) {
1369                 case 5:
1370                 case 6:
1371                     geometry = QStringLiteral("33% 50% 33% 50% 100%");
1372                     break;
1373                 default:
1374                     geometry = QStringLiteral("33% 33% 33% 33% 100%");
1375                     break;
1376                 }
1377                 break;
1378             case 5:
1379                 switch (videoTracks.size()) {
1380                 case 6:
1381                     geometry = QStringLiteral("66% 50% 33% 50% 100%");
1382                     break;
1383                 default:
1384                     geometry = QStringLiteral("66% 33% 33% 33% 100%");
1385                     break;
1386                 }
1387                 break;
1388             case 6:
1389                 geometry = QStringLiteral("0 66% 33% 33% 100%");
1390                 break;
1391             case 7:
1392                 geometry = QStringLiteral("33% 66% 33% 33% 100%");
1393                 break;
1394             default:
1395                 geometry = QStringLiteral("66% 66% 33% 33% 100%");
1396                 break;
1397             }
1398             count++;
1399             // Add transition to track:
1400             transition.set("rect", geometry.toUtf8().constData());
1401             transition.set("always_active", 1);
1402             field->plant_transition(transition, 0, b_track);
1403         }
1404     }
1405     field->unlock();
1406     if (refresh) {
1407         Q_EMIT timeline->requestMonitorRefresh();
1408     }
1409     return trackNames;
1410 }
1411 
1412 void TimelineFunctions::saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection,
1413                                               const QDir &targetDir, int duration)
1414 {
1415     bool ok;
1416     QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
1417                                          QString(), &ok);
1418     if (name.isEmpty() || !ok) {
1419         return;
1420     }
1421     QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
1422     if (QFile::exists(fullPath)) {
1423         QUrl url = QUrl::fromLocalFile(targetDir.absoluteFilePath(name + QStringLiteral(".mlt")));
1424         KIO::RenameDialog renameDialog(QApplication::activeWindow(), i18n("File already exists"), url, url, KIO::RenameDialog_Option::RenameDialog_Overwrite);
1425         if (renameDialog.exec() != QDialog::Rejected) {
1426             QUrl final = renameDialog.newDestUrl();
1427             if (final.isValid()) {
1428                 fullPath = final.toLocalFile();
1429             }
1430         } else {
1431             return;
1432         }
1433     }
1434     int offset = -1;
1435     int lowerAudioTrack = -1;
1436     int lowerVideoTrack = -1;
1437     // Build a copy of selected tracks.
1438     QMap<int, int> sourceTracks;
1439     for (int i : selection) {
1440         int sourceTrack = timeline->getItemTrackId(i);
1441         int clipPos = timeline->getItemPosition(i);
1442         if (offset < 0 || clipPos < offset) {
1443             offset = clipPos;
1444         }
1445         int trackPos = timeline->getTrackMltIndex(sourceTrack);
1446         if (!sourceTracks.contains(trackPos)) {
1447             sourceTracks.insert(trackPos, sourceTrack);
1448         }
1449         // Check if we have a composition with a track not yet listed
1450         if (timeline->isComposition(i)) {
1451             std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(i);
1452             int aTrack = compo->getATrack();
1453             if (!sourceTracks.contains(aTrack)) {
1454                 if (aTrack == 0) {
1455                     sourceTracks.insert(0, -1);
1456                 } else {
1457                     sourceTracks.insert(aTrack, timeline->getTrackIndexFromPosition(aTrack - 1));
1458                 }
1459             }
1460         }
1461     }
1462     qDebug() << "==========\nGOT SOUREC TRACKS: " << sourceTracks << "\n\nGGGGGGGGGGGGGGGGGGGGGGG";
1463     // Build target timeline
1464     Mlt::Tractor newTractor(pCore->getProjectProfile());
1465     QScopedPointer<Mlt::Field> field(newTractor.field());
1466     int ix = 0;
1467     QString composite = TransitionsRepository::get()->getCompositingTransition();
1468     QMapIterator<int, int> i(sourceTracks);
1469     QList<Mlt::Transition *> compositions;
1470     while (i.hasNext()) {
1471         i.next();
1472         if (i.value() == -1) {
1473             // Insert a black background track
1474             QScopedPointer<Mlt::Producer> newTrackPlaylist(new Mlt::Producer(*newTractor.profile(), "color:black"));
1475             newTrackPlaylist->set("kdenlive:playlistid", "black_track");
1476             newTrackPlaylist->set("mlt_type", "producer");
1477             newTrackPlaylist->set("aspect_ratio", 1);
1478             newTrackPlaylist->set("length", INT_MAX);
1479             newTrackPlaylist->set("mlt_image_format", "rgba");
1480             newTrackPlaylist->set("set.test_audio", 0);
1481             newTrackPlaylist->set_in_and_out(0, duration);
1482             newTractor.set_track(*newTrackPlaylist, ix);
1483             sourceTracks.insert(0, ix);
1484             ix++;
1485             continue;
1486         }
1487         QScopedPointer<Mlt::Playlist> newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
1488         newTractor.set_track(*newTrackPlaylist, ix);
1489         // QScopedPointer<Mlt::Producer> trackProducer(newTractor.track(ix));
1490         int trackId = i.value();
1491         sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
1492         std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
1493         bool isAudio = track->isAudioTrack();
1494         if (isAudio) {
1495             newTrackPlaylist->set("hide", 1);
1496             if (lowerAudioTrack < 0) {
1497                 lowerAudioTrack = ix;
1498             }
1499         } else {
1500             newTrackPlaylist->set("hide", 2);
1501             if (lowerVideoTrack < 0) {
1502                 lowerVideoTrack = ix;
1503             }
1504         }
1505         for (int itemId : selection) {
1506             if (timeline->getItemTrackId(itemId) == trackId) {
1507                 // Copy clip on the destination track
1508                 if (timeline->isClip(itemId)) {
1509                     int clip_position = timeline->m_allClips[itemId]->getPosition();
1510                     auto clip_loc = track->getClipIndexAt(clip_position);
1511                     int target_clip = clip_loc.second;
1512                     QSharedPointer<Mlt::Producer> clip = track->getClipProducer(target_clip);
1513                     newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
1514                 } else if (timeline->isComposition(itemId)) {
1515                     // Composition
1516                     auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
1517                     QString id(t->get("kdenlive_id"));
1518                     QString internal(t->get("internal_added"));
1519                     if (internal.isEmpty()) {
1520                         compositions << t;
1521                         if (id.isEmpty()) {
1522                             qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service");
1523                             t->set("kdenlive_id", t->get("mlt_service"));
1524                         }
1525                     }
1526                 }
1527             }
1528         }
1529         ix++;
1530     }
1531     // Sort compositions and insert
1532     if (!compositions.isEmpty()) {
1533         std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() > b->get_b_track(); });
1534         while (!compositions.isEmpty()) {
1535             QScopedPointer<Mlt::Transition> t(compositions.takeFirst());
1536             int a_track = t->get_a_track();
1537             if ((sourceTracks.contains(a_track) || a_track == 0) && sourceTracks.contains(t->get_b_track())) {
1538                 Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service"));
1539                 Mlt::Properties sourceProps(t->get_properties());
1540                 newComposition.inherit(sourceProps);
1541                 int in = qMax(0, t->get_in() - offset);
1542                 int out = t->get_out() - offset;
1543                 newComposition.set_in_and_out(in, out);
1544                 if (sourceTracks.contains(a_track)) {
1545                     a_track = sourceTracks.value(a_track);
1546                 }
1547                 int b_track = sourceTracks.value(t->get_b_track());
1548                 field->plant_transition(newComposition, a_track, b_track);
1549             }
1550         }
1551     }
1552     // Track compositing
1553     i.toFront();
1554     ix = 0;
1555     while (i.hasNext()) {
1556         i.next();
1557         int trackId = i.value();
1558         if (trackId < 0) {
1559             // Black background
1560             continue;
1561         }
1562         std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
1563         bool isAudio = track->isAudioTrack();
1564         if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) {
1565             // add track compositing / mix
1566             Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData());
1567             if (isAudio) {
1568                 t.set("sum", 1);
1569                 t.set("accepts_blanks", 1);
1570             }
1571             t.set("always_active", 1);
1572             t.set("internal_added", 237);
1573             t.set_tracks(isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
1574             field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
1575         }
1576         ix++;
1577     }
1578     Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData());
1579     xmlConsumer.set("terminate_on_pause", 1);
1580     xmlConsumer.connect(newTractor);
1581     xmlConsumer.run();
1582 }
1583 
1584 int TimelineFunctions::getTrackOffset(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int destTrack)
1585 {
1586     qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack;
1587     int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
1588     int destTrackMltIndex = timeline->getTrackMltIndex(destTrack);
1589     int offset = 0;
1590     qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex;
1591     if (masterTrackMltIndex == destTrackMltIndex) {
1592         return offset;
1593     }
1594     int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1;
1595     bool isAudio = timeline->isAudioTrack(startTrack);
1596     int track = masterTrackMltIndex;
1597     while (track != destTrackMltIndex) {
1598         track += step;
1599         qDebug() << "+ + +TESTING TRACK: " << track;
1600         if (track < 1) {
1601             continue;
1602         }
1603         int trackId = timeline->getTrackIndexFromPosition(track - 1);
1604         if (isAudio == timeline->isAudioTrack(trackId)) {
1605             offset += step;
1606         }
1607     }
1608     return offset;
1609 }
1610 
1611 int TimelineFunctions::getOffsetTrackId(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int offset, bool audioOffset)
1612 {
1613     int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
1614     bool isAudio = timeline->isAudioTrack(startTrack);
1615     if (isAudio != audioOffset) {
1616         offset = -offset;
1617     }
1618     qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset;
1619     while (offset != 0) {
1620         masterTrackMltIndex += offset > 0 ? 1 : -1;
1621         qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex;
1622         if (masterTrackMltIndex < 1) {
1623             masterTrackMltIndex = 1;
1624             break;
1625         }
1626         if (masterTrackMltIndex > int(timeline->m_allTracks.size())) {
1627             masterTrackMltIndex = int(timeline->m_allTracks.size());
1628             break;
1629         }
1630         int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
1631         if (timeline->isAudioTrack(trackId) == isAudio) {
1632             offset += offset > 0 ? -1 : 1;
1633         }
1634     }
1635     return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
1636 }
1637 
1638 TimelineFunctions::TimelineTracksInfo TimelineFunctions::getAVTracksIds(const std::shared_ptr<TimelineItemModel> &timeline)
1639 {
1640     TimelineTracksInfo tracks;
1641     for (const auto &track : timeline->m_allTracks) {
1642         if (track->isAudioTrack()) {
1643             tracks.audioIds << track->getId();
1644         } else {
1645             tracks.videoIds << track->getId();
1646         }
1647     }
1648     return tracks;
1649 }
1650 
1651 QString TimelineFunctions::copyClips(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &itemIds)
1652 {
1653     int mainId = *(itemIds.begin());
1654     // We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips
1655     std::unordered_set<int> allIds;
1656     if (timeline->singleSelectionMode()) {
1657         allIds = itemIds;
1658     } else {
1659         for (const auto &itemId : itemIds) {
1660             std::unordered_set<int> siblings = timeline->getGroupElements(itemId);
1661             allIds.insert(siblings.begin(), siblings.end());
1662         }
1663     }
1664     // Avoid using a subtitle item as reference since it doesn't work with track offset
1665     if (timeline->isSubTitle(mainId)) {
1666         for (const auto &id : allIds) {
1667             if (!timeline->isSubTitle(id)) {
1668                 mainId = id;
1669                 break;
1670             }
1671         }
1672     }
1673     bool subtitleOnlyCopy = false;
1674     if (timeline->isSubTitle(mainId)) {
1675         subtitleOnlyCopy = true;
1676     }
1677 
1678     // TODO better guess for master track
1679     int masterTid = timeline->getItemTrackId(mainId);
1680     bool audioCopy = subtitleOnlyCopy ? false : timeline->isAudioTrack(masterTid);
1681     int masterTrack = subtitleOnlyCopy ? -1 : timeline->getTrackPosition(masterTid);
1682     QDomDocument copiedItems;
1683     int offset = -1;
1684     int lastFrame = -1;
1685     QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
1686     container.setAttribute(QStringLiteral("fps"), QString::number(pCore->getCurrentFps()));
1687     copiedItems.appendChild(container);
1688     QStringList binIds;
1689     for (int id : allIds) {
1690         int startPos = timeline->getItemPosition(id);
1691         if (offset == -1 || startPos < offset) {
1692             offset = timeline->getItemPosition(id);
1693         }
1694         if (startPos + timeline->getItemPlaytime(id) > lastFrame) {
1695             lastFrame = startPos + timeline->getItemPlaytime(id);
1696         }
1697         if (timeline->isClip(id)) {
1698             QDomElement clipXml = timeline->m_allClips[id]->toXml(copiedItems);
1699             container.appendChild(clipXml);
1700             const QString bid = timeline->m_allClips[id]->binId();
1701             if (!binIds.contains(bid)) {
1702                 binIds << bid;
1703             }
1704             int tid = timeline->getItemTrackId(id);
1705             if (timeline->getTrackById_const(tid)->hasStartMix(id)) {
1706                 QDomElement mix = timeline->getTrackById_const(tid)->mixXml(copiedItems, id);
1707                 clipXml.appendChild(mix);
1708             }
1709         } else if (timeline->isComposition(id)) {
1710             container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems));
1711         } else if (timeline->isSubTitle(id)) {
1712             container.appendChild(timeline->getSubtitleModel()->toXml(id, copiedItems));
1713         } else {
1714             Q_ASSERT(false);
1715         }
1716     }
1717     QDomElement container2 = copiedItems.createElement(QStringLiteral("bin"));
1718     container.appendChild(container2);
1719     for (const QString &id : qAsConst(binIds)) {
1720         std::shared_ptr<ProjectClip> clip = pCore->projectItemModel()->getClipByBinID(id);
1721         QDomDocument tmp;
1722         container2.appendChild(clip->toXml(tmp));
1723     }
1724     container.setAttribute(QStringLiteral("offset"), offset);
1725     container.setAttribute(QStringLiteral("duration"), lastFrame - offset);
1726     if (audioCopy) {
1727         container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack);
1728         int masterMirror = timeline->getMirrorVideoTrackId(masterTid);
1729         if (masterMirror == -1) {
1730             TimelineTracksInfo timelineTracks = TimelineFunctions::getAVTracksIds(timeline);
1731             if (!timelineTracks.videoIds.isEmpty()) {
1732                 masterTrack = timeline->getTrackPosition(timelineTracks.videoIds.first());
1733             }
1734         } else {
1735             masterTrack = timeline->getTrackPosition(masterMirror);
1736         }
1737     }
1738     /* masterTrack contains the reference track over which we want to paste.
1739        this is a video track, unless audioCopy is defined */
1740     container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
1741     container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
1742     QPair<int, int> avTracks = timeline->getAVtracksCount();
1743     container.setAttribute(QStringLiteral("audioTracks"), avTracks.first);
1744     container.setAttribute(QStringLiteral("videoTracks"), avTracks.second);
1745     QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
1746     container.appendChild(grp);
1747     std::unordered_set<int> groupRoots;
1748     std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) {
1749         int parent = timeline->m_groups->getRootId(id);
1750         if (timeline->m_groups->getType(parent) == GroupType::Selection) {
1751             std::unordered_set<int> children = timeline->m_groups->getDirectChildren(parent);
1752             for (const auto &gid : children) {
1753                 std::unordered_set<int> leaves = timeline->m_groups->getLeaves(gid);
1754                 if (leaves.count(id) == 1) {
1755                     return gid;
1756                 }
1757             }
1758             // This should not happen
1759             qDebug() << "INCORRECT GROUP ID FOUND";
1760             return -1;
1761         } else {
1762             return parent;
1763         }
1764     });
1765 
1766     qDebug() << "==============\n GROUP ROOTS: ";
1767     for (int gp : groupRoots) {
1768         qDebug() << "GROUP: " << gp;
1769     }
1770     qDebug() << "\n=======";
1771     grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots)));
1772 
1773     qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
1774     return copiedItems.toString();
1775 }
1776 
1777 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position)
1778 {
1779     std::function<bool(void)> undo = []() { return true; };
1780     std::function<bool(void)> redo = []() { return true; };
1781     if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, undo, redo)) {
1782         pCore->pushUndo(undo, redo, i18n("Paste clips"));
1783         return true;
1784     }
1785     return false;
1786 }
1787 
1788 bool TimelineFunctions::getUsedTracks(const QDomNodeList &clips, const QDomNodeList &compositions, int sourceMasterTrack, int &topAudioMirror, TimelineTracksInfo &allTracks, QList<int> &singleAudioTracks, std::unordered_map<int, int> &audioMirrors)
1789 {
1790     // Tracks used by clips
1791     int max = clips.count();
1792     for (int i = 0; i < max; i++) {
1793         QDomElement clipProducer = clips.at(i).toElement();
1794         int trackPos = clipProducer.attribute(QStringLiteral("track")).toInt();
1795         if (trackPos < 0) {
1796             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1797             semaphore.release(1);
1798             return false;
1799         }
1800         bool audioTrack = clipProducer.hasAttribute(QStringLiteral("audioTrack"));
1801         if (audioTrack) {
1802             if (!allTracks.audioIds.contains(trackPos)) {
1803                 allTracks.audioIds << trackPos;
1804             }
1805             int videoMirror = clipProducer.attribute(QStringLiteral("mirrorTrack")).toInt();
1806             if (videoMirror == -1 || sourceMasterTrack == -1) {
1807                 // The clip has no mirror track
1808                 if (!singleAudioTracks.contains(trackPos)) {
1809                     singleAudioTracks << trackPos;
1810                 }
1811                 continue;
1812             }
1813             // The clip has mirror track
1814             audioMirrors[trackPos] = videoMirror;
1815             if (videoMirror > topAudioMirror) {
1816                 // We have to check how many video tracks with mirror are needed
1817                 topAudioMirror = videoMirror;
1818             }
1819             if (!allTracks.videoIds.contains(videoMirror)) {
1820                 allTracks.videoIds << videoMirror;
1821             }
1822         } else {
1823             // Video clip
1824             if (!allTracks.videoIds.contains(trackPos)) {
1825                 allTracks.videoIds << trackPos;
1826             }
1827         }
1828     }
1829 
1830     // Tracks used by compositions
1831     max = compositions.count();
1832     for (int i = 0; i < max; i++) {
1833         QDomElement composition = compositions.at(i).toElement();
1834         int trackPos = composition.attribute(QStringLiteral("track")).toInt();
1835         if (!allTracks.videoIds.contains(trackPos)) {
1836             allTracks.videoIds << trackPos;
1837         }
1838         int atrackPos = composition.attribute(QStringLiteral("a_track")).toInt();
1839         if (atrackPos != 0 && !allTracks.videoIds.contains(atrackPos)) {
1840             allTracks.videoIds << atrackPos;
1841         }
1842     }
1843 
1844     return true;
1845 }
1846 
1847 bool TimelineFunctions::pasteClipsWithUndo(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position, Fun &undo,
1848                                            Fun &redo)
1849 {
1850     std::function<bool(void)> paste_undo = []() { return true; };
1851     std::function<bool(void)> paste_redo = []() { return true; };
1852     if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, paste_undo, paste_redo)) {
1853         PUSH_FRONT_LAMBDA(paste_undo, undo);
1854         PUSH_FRONT_LAMBDA(paste_redo, redo);
1855         return true;
1856     }
1857     return false;
1858 }
1859 
1860 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position, Fun &undo,
1861                                    Fun &redo, int inPos, int duration)
1862 {
1863     timeline->requestClearSelection();
1864     if (!semaphore.tryAcquire(1)) {
1865         pCore->displayMessage(i18n("Another paste operation is in progress"), ErrorMessage, 500);
1866         while (!semaphore.tryAcquire(1)) {
1867             qApp->processEvents();
1868         }
1869     }
1870     waitingBinIds.clear();
1871     QDomDocument copiedItems;
1872     copiedItems.setContent(pasteString);
1873     if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
1874         qDebug() << " / / READING CLIPS FROM CLIPBOARD";
1875     } else {
1876         semaphore.release(1);
1877         pCore->displayMessage(i18n("No valid data in clipboard"), ErrorMessage, 500);
1878         return false;
1879     }
1880     const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid"));
1881     mappedIds.clear();
1882     // Check available tracks
1883     TimelineTracksInfo timelineTracks = TimelineFunctions::getAVTracksIds(timeline);
1884     int sourceMasterTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack"), QStringLiteral("-1")).toInt();
1885     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
1886     QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
1887     QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle"));
1888     // find paste tracks
1889     // Info about all source tracks
1890     TimelineTracksInfo sourceTracks;
1891     // List of all audio tracks with their corresponding video mirror
1892     std::unordered_map<int, int> audioMirrors;
1893     // List of all source audio tracks that don't have video mirror
1894     QList<int> singleAudioTracks;
1895     // Number of required video tracks with mirror
1896     int topAudioMirror = 0;
1897 
1898     if(!getUsedTracks(clips, compositions, sourceMasterTrack, topAudioMirror, sourceTracks, singleAudioTracks, audioMirrors)) {
1899         return false;
1900     }
1901 
1902     if (sourceTracks.audioIds.isEmpty() && sourceTracks.videoIds.isEmpty() && subtitles.isEmpty()) {
1903         // playlist does not have any tracks, exit
1904         semaphore.release(1);
1905         return true;
1906     }
1907     // Now we have a list of all source tracks, check that we have enough target tracks
1908     std::sort(sourceTracks.videoIds.begin(), sourceTracks.videoIds.end());
1909     std::sort(sourceTracks.audioIds.begin(), sourceTracks.audioIds.end());
1910     std::sort(singleAudioTracks.begin(), singleAudioTracks.end());
1911 
1912     // qDebug()<<"== GOT WANTED TKS\n VIDEO: "<<videoTracks<<"\n AUDIO TKS: "<<audioTracks<<"\n SINGLE AUDIO: "<<singleAudioTracks;
1913     int requestedVideoTracks = sourceTracks.videoIds.isEmpty() ? 0 : sourceTracks.videoIds.last() - sourceTracks.videoIds.first() + 1;
1914     int requestedAudioTracks = sourceTracks.audioIds.isEmpty() ? 0 : sourceTracks.audioIds.last() - sourceTracks.audioIds.first() + 1;
1915     if (requestedVideoTracks > timelineTracks.videoIds.size() || requestedAudioTracks > timelineTracks.audioIds.size()) {
1916         pCore->displayMessage(i18n("Not enough tracks to paste clipboard (requires %1 audio, %2 video tracks)", requestedAudioTracks, requestedVideoTracks),
1917                               ErrorMessage, 500);
1918         semaphore.release(1);
1919         return false;
1920     }
1921 
1922     auto findPerfectTracks = [](int &sourceTrackId, const QList<int> &sourceTracks, int &targetTrackId, const QList<int> &targetTracks) {
1923         const int neededTracksBelow = sourceTrackId - sourceTracks.first();
1924         const int neededTracksAbove = sourceTracks.last() - sourceTrackId;
1925 
1926         const int existingTracksBelow = targetTracks.indexOf(targetTrackId);
1927         const int existingTracksAbove = targetTracks.size() - (targetTracks.indexOf(targetTrackId) + 1);
1928 
1929         int sourceOffset = 0;
1930         if (neededTracksBelow < 0) {
1931             sourceOffset = neededTracksBelow;
1932         } else if (neededTracksAbove < 0) {
1933             sourceOffset = neededTracksAbove;
1934         }
1935 
1936         sourceTrackId += qMax(0, sourceTracks.count() - targetTracks.count() - sourceOffset);
1937 
1938         if (existingTracksBelow < neededTracksBelow) {
1939             qDebug() << "// UPDATING BELOW TID IX TO:" << neededTracksBelow;
1940             // not enough tracks below, try to paste on upper track
1941             targetTrackId = targetTracks.at(qMin(neededTracksBelow, targetTracks.length() - 1));
1942             return;
1943         }
1944 
1945         if (existingTracksAbove < neededTracksAbove) {
1946             // not enough tracks above, try to paste on lower track
1947             qDebug() << "// UPDATING ABOVE TID IX TO:" << (targetTracks.size() - neededTracksAbove);
1948             targetTrackId = targetTracks.at(qMax(0, targetTracks.size() - neededTracksAbove - 1));
1949             return;
1950         }
1951 
1952         // enough tracks above and below, keep the current
1953         // ensure it is one of the existing tracks
1954         targetTrackId = qBound(targetTracks.first(), targetTrackId, targetTracks.last());
1955     };
1956 
1957     // Find destination master track
1958     // Check we have enough tracks above/below
1959     if (requestedVideoTracks > 0) {
1960         findPerfectTracks(sourceMasterTrack, sourceTracks.videoIds, trackId, timelineTracks.videoIds);
1961 
1962         // Find top-most video track that requires an audio mirror
1963         int topAudioOffset = sourceTracks.videoIds.indexOf(topAudioMirror) - sourceTracks.videoIds.indexOf(sourceMasterTrack);
1964         // Check if we have enough video tracks with mirror at paste track position
1965         if (requestedAudioTracks > 0 && timelineTracks.audioIds.size() <= (timelineTracks.videoIds.indexOf(trackId) + topAudioOffset)) {
1966             int updatedPos = sourceTracks.audioIds.size() - topAudioOffset - 1;
1967             if (updatedPos < 0 || updatedPos >= timelineTracks.videoIds.size()) {
1968                 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1969                 semaphore.release(1);
1970                 return false;
1971             }
1972             trackId = timelineTracks.videoIds.at(updatedPos);
1973         }
1974     } else if (requestedAudioTracks > 0) {
1975         // Audio only
1976         sourceMasterTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt();
1977         findPerfectTracks(sourceMasterTrack, sourceTracks.audioIds, trackId, timelineTracks.audioIds);
1978     }
1979     tracksMap.clear();
1980     bool audioMaster = false;
1981     int targetMasterIx = timelineTracks.videoIds.indexOf(trackId);
1982     if (targetMasterIx == -1) {
1983         targetMasterIx = timelineTracks.audioIds.indexOf(trackId);
1984         audioMaster = true;
1985     }
1986 
1987     int masterOffset = targetMasterIx - sourceMasterTrack;
1988     for (int tk : qAsConst(sourceTracks.videoIds)) {
1989         int newPos = masterOffset + tk;
1990         if (newPos < 0 || newPos >= timelineTracks.videoIds.size()) {
1991             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1992             semaphore.release(1);
1993             return false;
1994         }
1995         tracksMap.insert(tk, timelineTracks.videoIds.at(newPos));
1996         // qDebug() << "/// MAPPING SOURCE TRACK: "<<tk<<" TO PROJECT TK: "<<timelineTracks.videoIds.at(newPos)<<" =
1997         // "<<timeline->getTrackMltIndex(timelineTracks.videoIds.at(newPos));
1998     }
1999     bool audioOffsetCalculated = false;
2000     int audioOffset = 0;
2001     for (const auto &mirror : audioMirrors) {
2002         int videoIx = tracksMap.value(mirror.second);
2003         int mirrorIx = timeline->getMirrorAudioTrackId(videoIx);
2004         if (mirrorIx > 0) {
2005             tracksMap.insert(mirror.first, mirrorIx);
2006             if (!audioOffsetCalculated) {
2007                 int oldPosition = mirror.first;
2008                 int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition));
2009                 audioOffset = currentPosition - oldPosition;
2010                 audioOffsetCalculated = true;
2011             }
2012         }
2013     }
2014     if (!audioOffsetCalculated && audioMaster) {
2015         audioOffset = masterOffset;
2016         audioOffsetCalculated = true;
2017     } else if (audioMirrors.size() == 0) {
2018         // We are passing ungrouped audio clips, calculate offset
2019         int sourceAudioTracks = copiedItems.documentElement().attribute(QStringLiteral("audioTracks")).toInt();
2020         if (sourceAudioTracks > 0) {
2021             audioOffset = timelineTracks.audioIds.count() - sourceAudioTracks;
2022         }
2023     }
2024     for (int oldPos : qAsConst(singleAudioTracks)) {
2025         if (tracksMap.contains(oldPos)) {
2026             continue;
2027         }
2028         int offsetId = oldPos + audioOffset;
2029         if (offsetId < 0 || offsetId >= timelineTracks.audioIds.size()) {
2030             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
2031             semaphore.release(1);
2032             return false;
2033         }
2034         tracksMap.insert(oldPos, timelineTracks.audioIds.at(offsetId));
2035     }
2036     std::function<void(const QString &)> callBack = [timeline, copiedItems, position, inPos, duration](const QString &binId) {
2037         waitingBinIds.removeAll(binId);
2038         if (waitingBinIds.isEmpty()) {
2039             TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, inPos, duration);
2040         }
2041     };
2042     bool clipsImported = false;
2043     int updatedPosition = 0;
2044     int pasteDuration = copiedItems.documentElement().attribute(QStringLiteral("duration")).toInt();
2045     if (docId == pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
2046         // Check that the bin clips exists in case we try to paste in a copy of original project
2047         QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
2048         QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
2049         for (int i = 0; i < binClips.count(); ++i) {
2050             QDomElement currentProd = binClips.item(i).toElement();
2051             QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
2052             if (clipId.isEmpty()) {
2053                 // Invalid clip, maybe black track from a sequence, ignore
2054                 continue;
2055             }
2056             QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash"));
2057             if (!pCore->projectItemModel()->validateClip(clipId, clipHash)) {
2058                 // This clip is different in project and in paste data, create a copy
2059                 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
2060                 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
2061                 mappedIds.insert(clipId, updatedId);
2062                 if (folderId.isEmpty()) {
2063                     // Folder does not exist
2064                     const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
2065                     folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
2066                     pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
2067                 }
2068                 waitingBinIds << updatedId;
2069                 clipsImported = true;
2070                 pCore->projectItemModel()->requestAddBinClip(updatedId, currentProd, folderId, undo, redo, callBack);
2071             }
2072         }
2073         updatedPosition = position + pasteDuration;
2074     }
2075 
2076     if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
2077         // paste from another document, import bin clips
2078 
2079         // Check if the fps matches
2080         QString currentFps = QString::number(pCore->getCurrentFps());
2081         QString sourceFps = copiedItems.documentElement().attribute(QStringLiteral("fps"));
2082         double ratio = 1.;
2083         if (currentFps != sourceFps && !sourceFps.isEmpty()) {
2084             if (KMessageBox::questionTwoActions(
2085                     pCore->window(),
2086                     i18n("The source project has a different framerate (%1fps) than your current project.<br/>Clips or keyframes might be messed up.",
2087                          sourceFps),
2088                     i18n("Pasting Warning"), KGuiItem(i18n("Paste")), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
2089                 semaphore.release(1);
2090                 return false;
2091             }
2092             ratio = pCore->getCurrentFps() / sourceFps.toDouble();
2093             copiedItems.documentElement().setAttribute(QStringLiteral("fps-ratio"), ratio);
2094         }
2095 
2096         // Folder in the project for the pasted clips
2097         QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
2098         if (folderId.isEmpty()) {
2099             // Folder does not exist
2100             const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
2101             folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
2102             pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
2103         }
2104         updatedPosition = position + (pasteDuration * ratio);
2105 
2106         auto disableProxy = [](QDomElement &producer) {
2107             const QString proxy = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:proxy"));
2108             if (proxy.length() < 4) {
2109                 return;
2110             }
2111             const QString resource = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:originalurl"));
2112             if (!resource.isEmpty()) {
2113                 Xml::setXmlProperty(producer, QStringLiteral("resource"), resource);
2114                 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:proxy"), QStringLiteral("-"));
2115             }
2116         };
2117 
2118         auto useFreeBinId = [](QDomElement &producer, const QString &clipId, QMap<QString, QString> &mappedIds) {
2119             if (!pCore->projectItemModel()->isIdFree(clipId)) {
2120                 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
2121                 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:id"), updatedId);
2122                 mappedIds.insert(clipId, updatedId);
2123                 return updatedId;
2124             }
2125             return clipId;
2126         };
2127 
2128         auto pasteClip = [disableProxy, callBack, useFreeBinId](const QDomNodeList &clips, int ratio, const QString &folderId, bool &clipsImported, Fun &undo,
2129                                                                 Fun &redo){
2130             for (int i = 0; i < clips.count(); ++i) {
2131                 QDomElement currentProd = clips.item(i).toElement();
2132                 QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
2133                 if (clipId.isEmpty()) {
2134                     // Not a bin clip
2135                     continue;
2136                 }
2137 
2138                 // Adjust duration in case of different fps on source and target
2139                 if (ratio != 1.) {
2140                     int out = currentProd.attribute(QStringLiteral("out")).toInt() * ratio;
2141                     int length = Xml::getXmlProperty(currentProd, QStringLiteral("length")).toInt() * ratio;
2142                     currentProd.setAttribute(QStringLiteral("out"), out);
2143                     Xml::setXmlProperty(currentProd, QStringLiteral("length"), QString::number(length));
2144                 }
2145 
2146                 // Check if we already have a clip with same hash in pasted clips folder
2147                 QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash"));
2148                 QString existingId = pCore->projectItemModel()->validateClipInFolder(folderId, clipHash);
2149                 if (!existingId.isEmpty()) {
2150                     mappedIds.insert(clipId, existingId);
2151                     continue;
2152                 }
2153                 clipId = useFreeBinId(currentProd, clipId, mappedIds);
2154 
2155                 // Disable proxy if any when pasting to another document
2156                 disableProxy(currentProd);
2157 
2158                 waitingBinIds << clipId;
2159                 clipsImported = true;
2160                 bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo, callBack);
2161                 if (!insert) {
2162                     return false;
2163                 }
2164             }
2165             return true;
2166         };
2167 
2168         QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
2169         if (!pasteClip(binClips, ratio, folderId, clipsImported, undo, redo)) {
2170             pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500);
2171             undo();
2172             semaphore.release(1);
2173             return false;
2174         }
2175 
2176         QDomNodeList chainClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("chain"));
2177         if (!pasteClip(chainClips, ratio, folderId, clipsImported, undo, redo)) {
2178             pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500);
2179             undo();
2180             semaphore.release(1);
2181             return false;
2182         }
2183 
2184         auto remapClipIds = [](QDomNodeList &elements, const QMap<QString, QString> &map) {
2185             int max = elements.count();
2186             for (int i = 0; i < max; i++) {
2187                 QDomElement e = elements.item(i).toElement();
2188                 const QString currentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id"));
2189                 if (map.contains(currentId)) {
2190                     Xml::setXmlProperty(e, QStringLiteral("kdenlive:id"), map.value(currentId));
2191                 }
2192             }
2193         };
2194 
2195         QDomNodeList sequenceClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("mlt"));
2196         for (int i = 0; i < sequenceClips.count(); ++i) {
2197             QDomElement currentProd = sequenceClips.item(i).toElement();
2198             QString clipId = currentProd.attribute(QStringLiteral("kdenlive:id"));
2199             const QString uuid = currentProd.attribute(QStringLiteral("kdenlive:uuid"));
2200             int duration = currentProd.attribute(QStringLiteral("kdenlive:duration")).toInt();
2201             const QString clipname = currentProd.attribute(QStringLiteral("kdenlive:clipname"));
2202             if (clipId.isEmpty()) {
2203                 // Not a bin clip
2204                 continue;
2205             }
2206 
2207             QDomDocument doc;
2208             doc.appendChild(doc.importNode(currentProd, true));
2209             clipId = useFreeBinId(currentProd, clipId, mappedIds);
2210 
2211             // update all bin ids
2212             QDomNodeList prods = doc.documentElement().elementsByTagName(QStringLiteral("producer"));
2213             remapClipIds(prods, mappedIds);
2214             QDomNodeList chains = doc.documentElement().elementsByTagName(QStringLiteral("chain"));
2215             remapClipIds(chains, mappedIds);
2216             QDomNodeList entries = doc.documentElement().elementsByTagName(QStringLiteral("entry"));
2217             remapClipIds(entries, mappedIds);
2218 
2219             waitingBinIds << clipId;
2220             clipsImported = true;
2221             std::shared_ptr<Mlt::Producer> xmlProd(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", doc.toString().toUtf8().constData()));
2222             if (!xmlProd->is_valid()) {
2223                 qDebug() << ":::: CANNOT IMPORT SEQUENCE: " << clipId;
2224                 continue;
2225             }
2226             xmlProd->set("kdenlive:id", clipId.toUtf8().constData());
2227             xmlProd->set("kdenlive:producer_type", ClipType::Timeline);
2228             xmlProd->set("kdenlive:uuid", uuid.toUtf8().constData());
2229             xmlProd->set("kdenlive:duration", xmlProd->frames_to_time(duration));
2230             xmlProd->set("kdenlive:clipname", clipname.toUtf8().constData());
2231             xmlProd->set("_kdenlive_processed", 1);
2232             Mlt::Service s(*xmlProd.get());
2233             Mlt::Tractor tractor(s);
2234             std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(tractor.cut()));
2235             prod->set("id", uuid.toUtf8().constData());
2236             prod->set("kdenlive:id", clipId.toUtf8().constData());
2237             prod->set("kdenlive:producer_type", ClipType::Timeline);
2238             prod->set("kdenlive:uuid", uuid.toUtf8().constData());
2239             prod->set("kdenlive:duration", xmlProd->frames_to_time(duration));
2240             prod->set("kdenlive:clipname", clipname.toUtf8().constData());
2241             prod->set("_kdenlive_processed", 1);
2242             bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, prod, folderId, undo, redo, callBack);
2243             if (!insert) {
2244                 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500);
2245                 undo();
2246                 semaphore.release(1);
2247                 return false;
2248             }
2249         }
2250     }
2251 
2252     if (!clipsImported) {
2253         // Clips from same document, directly proceed to pasting
2254         bool result = TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, undo, redo, false, inPos, duration);
2255         if (result && updatedPosition > 0) {
2256             pCore->seekMonitor(Kdenlive::ProjectMonitor, updatedPosition);
2257         }
2258         return result;
2259     }
2260     qDebug() << "++++++++++++\nWAITIND FOR BIN INSERTION: " << waitingBinIds << "\n\n+++++++++++++";
2261     return true;
2262 }
2263 
2264 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, const QDomDocument &copiedItems, int position, int inPos,
2265                                            int duration)
2266 {
2267     std::function<bool(void)> timeline_undo = []() { return true; };
2268     std::function<bool(void)> timeline_redo = []() { return true; };
2269     return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, timeline_undo, timeline_redo, true, inPos, duration);
2270 }
2271 
2272 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, QDomDocument copiedItems, int position, Fun &timeline_undo,
2273                                            Fun &timeline_redo, bool pushToStack, int inPos, int duration)
2274 {
2275     // Wait until all bin clips are inserted
2276     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
2277     QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
2278     QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle"));
2279     int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
2280     bool res = true;
2281     std::unordered_map<int, int> correspondingIds;
2282     double ratio = 1.0;
2283     if (copiedItems.documentElement().hasAttribute(QStringLiteral("fps-ratio"))) {
2284         ratio = copiedItems.documentElement().attribute(QStringLiteral("fps-ratio")).toDouble();
2285         offset *= ratio;
2286     }
2287 
2288     QDomElement documentMixes = copiedItems.createElement(QStringLiteral("mixes"));
2289     for (int i = 0; i < clips.count(); i++) {
2290         QDomElement prod = clips.at(i).toElement();
2291         QString originalId = prod.attribute(QStringLiteral("binid"));
2292         if (mappedIds.contains(originalId)) {
2293             // Map id
2294             originalId = mappedIds.value(originalId);
2295         }
2296         if (!pCore->projectItemModel()->hasClip(originalId)) {
2297             // Clip import was not successful, continue
2298             pCore->displayMessage(i18n("All clips were not successfully copied"), ErrorMessage, 500);
2299             continue;
2300         }
2301         int in = prod.attribute(QStringLiteral("in")).toInt();
2302         int out = prod.attribute(QStringLiteral("out")).toInt();
2303         int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
2304         if (!timeline->isTrack(curTrackId)) {
2305             // Something is broken
2306             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
2307             timeline_undo();
2308             semaphore.release(1);
2309             return false;
2310         }
2311         int pos = prod.attribute(QStringLiteral("position")).toInt();
2312         if (ratio != 1.0) {
2313             in = in * ratio;
2314             out = out * ratio;
2315             pos = pos * ratio;
2316         }
2317         int newIn = in;
2318         int newOut = out;
2319         if ((inPos > 0 && pos + (out - in) < inPos + offset) || (duration > -1 && (pos > inPos + duration + offset))) {
2320             // Clip outside paste range
2321             continue;
2322         }
2323         if (inPos > 0) {
2324             pos -= inPos;
2325             if (pos < offset) {
2326                 newIn = in + (offset - pos);
2327                 pos = offset;
2328             }
2329         }
2330         if (duration > -1) {
2331             if (pos + (out - in) > inPos + duration + offset) {
2332                 newOut = out - (pos + (out - in) - (inPos + duration + offset));
2333             }
2334         }
2335 
2336         pos -= offset;
2337         double speed = prod.attribute(QStringLiteral("speed")).toDouble();
2338         bool warp_pitch = false;
2339         if (!qFuzzyCompare(speed, 1.)) {
2340             warp_pitch = prod.attribute(QStringLiteral("warp_pitch")).toInt();
2341         }
2342         int audioStream = prod.attribute(QStringLiteral("audioStream")).toInt();
2343         int newId;
2344         bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), audioStream, speed, warp_pitch,
2345                                                      timeline_undo, timeline_redo);
2346         if (!created) {
2347             // Something is broken
2348             pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500);
2349             timeline_undo();
2350             semaphore.release(1);
2351             return false;
2352         }
2353         if (prod.hasAttribute(QStringLiteral("timemap"))) {
2354             // This is a timeremap
2355             timeline->m_allClips[newId]->useTimeRemapProducer(true, timeline_undo, timeline_redo);
2356             if (timeline->m_allClips[newId]->m_producer->parent().type() == mlt_service_chain_type) {
2357                 Mlt::Chain fromChain(timeline->m_allClips[newId]->m_producer->parent());
2358                 int count = fromChain.link_count();
2359                 for (int i = 0; i < count; i++) {
2360                     QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
2361                     if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
2362                         if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
2363                             // Found a timeremap effect, read params
2364                             fromLink->set("time_map", prod.attribute(QStringLiteral("timemap")).toUtf8().constData());
2365                             fromLink->set("pitch", prod.attribute(QStringLiteral("timepitch")).toInt());
2366                             fromLink->set("image_mode", prod.attribute(QStringLiteral("timeblend")).toUtf8().constData());
2367                             break;
2368                         }
2369                     }
2370                 }
2371             }
2372         }
2373         if (timeline->m_allClips[newId]->m_endlessResize) {
2374             out = out - in;
2375             in = 0;
2376             timeline->m_allClips[newId]->m_producer->set("length", out + 1);
2377             timeline->m_allClips[newId]->m_producer->set("out", out);
2378         }
2379         timeline->m_allClips[newId]->setInOut(in, out);
2380         int targetId = prod.attribute(QStringLiteral("id")).toInt();
2381         int targetPlaylist = prod.attribute(QStringLiteral("playlist")).toInt();
2382         if (targetPlaylist > 0) {
2383             timeline->m_allClips[newId]->setSubPlaylistIndex(targetPlaylist, curTrackId);
2384         }
2385         correspondingIds[targetId] = newId;
2386         std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
2387         destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), timeline_undo, timeline_redo);
2388         if (newIn != in) {
2389             int newSize = out - newIn + 1;
2390             res = res && timeline->requestItemResize(newId, newSize, false, true, timeline_undo, timeline_redo);
2391             if (res) {
2392                 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId);
2393                 sourceStack->cleanFadeEffects(true, timeline_undo, timeline_redo);
2394             }
2395             // TODO manage mixes
2396         }
2397         if (newOut != out) {
2398             int newSize = newOut - newIn;
2399             res = res && timeline->requestItemResize(newId, newSize, true, true, timeline_undo, timeline_redo);
2400             if (res) {
2401                 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId);
2402                 sourceStack->cleanFadeEffects(false, timeline_undo, timeline_redo);
2403             }
2404             // TODO manage mixes
2405         }
2406         res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, timeline_undo, timeline_redo);
2407         // paste effects
2408         if (!res) {
2409             qDebug() << "=== COULD NOT PASTE CLIP: " << newId << " ON TRACK: " << curTrackId << " AT: " << position;
2410             break;
2411         }
2412         // Mixes (same track transitions)
2413         if (prod.hasChildNodes()) {
2414             // TODO: adjust position/duration with inPos / duration
2415             QDomNodeList mixes = prod.elementsByTagName(QLatin1String("mix"));
2416             if (!mixes.isEmpty()) {
2417                 QDomElement mix = mixes.at(0).toElement();
2418                 if (mix.tagName() == QLatin1String("mix")) {
2419                     mix.setAttribute(QStringLiteral("tid"), curTrackId);
2420                     documentMixes.appendChild(mix);
2421                 }
2422             }
2423         }
2424     }
2425     // Process mix insertion
2426     QDomNodeList mixes = documentMixes.childNodes();
2427     for (int k = 0; k < mixes.count(); k++) {
2428         QDomElement mix = mixes.at(k).toElement();
2429         int originalFirstClipId = mix.attribute(QLatin1String("firstClip")).toInt();
2430         int originalSecondClipId = mix.attribute(QLatin1String("secondClip")).toInt();
2431         if (correspondingIds.count(originalFirstClipId) > 0 && correspondingIds.count(originalSecondClipId) > 0) {
2432             QVector<QPair<QString, QVariant>> params;
2433             QDomNodeList paramsXml = mix.elementsByTagName(QLatin1String("param"));
2434             for (int j = 0; j < paramsXml.count(); j++) {
2435                 QDomElement e = paramsXml.at(j).toElement();
2436                 params.append({e.attribute(QLatin1String("name")), e.text()});
2437             }
2438             std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = {mix.attribute(QLatin1String("asset")), params};
2439             MixInfo mixData;
2440             mixData.firstClipId = correspondingIds[originalFirstClipId];
2441             mixData.secondClipId = correspondingIds[originalSecondClipId];
2442             mixData.firstClipInOut.second = mix.attribute(QLatin1String("mixEnd")).toInt() * ratio;
2443             mixData.secondClipInOut.first = mix.attribute(QLatin1String("mixStart")).toInt() * ratio;
2444             mixData.mixOffset = mix.attribute(QLatin1String("mixOffset")).toInt() * ratio;
2445             std::pair<int, int> tracks = {mix.attribute(QLatin1String("a_track")).toInt(), mix.attribute(QLatin1String("b_track")).toInt()};
2446             if (tracks.first == tracks.second) {
2447                 tracks = {0, 1};
2448             }
2449             timeline->getTrackById_const(mix.attribute(QLatin1String("tid")).toInt())->createMix(mixData, mixParams, tracks, true);
2450         }
2451     }
2452     // Compositions
2453     if (res) {
2454         for (int i = 0; res && i < compositions.count(); i++) {
2455             QDomElement prod = compositions.at(i).toElement();
2456             QString originalId = prod.attribute(QStringLiteral("composition"));
2457             int in = prod.attribute(QStringLiteral("in")).toInt() * ratio;
2458             int out = prod.attribute(QStringLiteral("out")).toInt() * ratio;
2459             int pos = prod.attribute(QStringLiteral("position")).toInt() * ratio - offset;
2460             int newPos = pos;
2461             if (inPos > 0) {
2462                 newPos -= inPos;
2463             }
2464             int compoDuration = out - in + 1;
2465             int compoDuration2 = out - in + 1;
2466             if (newPos < 0) {
2467                 // resize composition
2468                 compoDuration += newPos;
2469                 newPos = 0;
2470             }
2471             if (duration > -1 && (newPos + compoDuration > inPos + duration)) {
2472                 compoDuration2 = inPos + duration - newPos;
2473             }
2474             if (compoDuration2 <= 0) {
2475                 continue;
2476             }
2477             int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
2478             int trackOffset = Xml::getXmlProperty(prod, QStringLiteral("b_track")).toInt() - Xml::getXmlProperty(prod, QStringLiteral("a_track")).toInt();
2479             // Add 1 to account for the black track
2480             int aTrackPos = timeline->getTrackPosition(curTrackId) - trackOffset + 1;
2481             int atrackId = -1;
2482             if (aTrackPos > 0 && aTrackPos < timeline->getTracksCount()) {
2483                 atrackId = timeline->getTrackIndexFromPosition(aTrackPos - 1);
2484             }
2485             if (atrackId > -1 && !timeline->isAudioTrack(atrackId)) {
2486                 // Ok, track found
2487             } else {
2488                 aTrackPos = 0;
2489             }
2490 
2491             int newId;
2492             auto transProps = std::make_unique<Mlt::Properties>();
2493             QDomNodeList props = prod.elementsByTagName(QStringLiteral("property"));
2494             for (int j = 0; j < props.count(); j++) {
2495                 transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(),
2496                                 props.at(j).toElement().text().toUtf8().constData());
2497             }
2498             res = res && timeline->requestCompositionCreation(originalId, out - in + 1, std::move(transProps), newId, timeline_undo, timeline_redo);
2499             if (newPos != pos) {
2500                 // transition start resized
2501                 timeline->requestItemResize(newId, compoDuration, false, true, timeline_undo, timeline_redo, false);
2502             }
2503             if (compoDuration != compoDuration2) {
2504                 timeline->requestItemResize(newId, compoDuration2, true, true, timeline_undo, timeline_redo, false);
2505             }
2506             res = res && timeline->requestCompositionMove(newId, curTrackId, aTrackPos, position + newPos, true, true, timeline_undo, timeline_redo);
2507         }
2508     }
2509     if (res && !subtitles.isEmpty()) {
2510         auto subModel = timeline->getSubtitleModel();
2511         if (!subModel) {
2512             // This timeline doesn't yet have subtitles, initiate
2513             pCore->window()->slotShowSubtitles(true);
2514             subModel = timeline->getSubtitleModel();
2515         }
2516         for (int i = 0; res && i < subtitles.count(); i++) {
2517             QDomElement prod = subtitles.at(i).toElement();
2518             int in = prod.attribute(QStringLiteral("in")).toInt() * ratio - offset;
2519             int out = prod.attribute(QStringLiteral("out")).toInt() * ratio - offset;
2520             QString text = prod.attribute(QStringLiteral("text"));
2521             res = res && subModel->addSubtitle(GenTime(position + in, pCore->getCurrentFps()), GenTime(position + out, pCore->getCurrentFps()), text,
2522                                                timeline_undo, timeline_redo);
2523         }
2524     }
2525     if (!res) {
2526         timeline_undo();
2527         pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500);
2528         semaphore.release(1);
2529         return false;
2530     }
2531     // Rebuild groups
2532     const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
2533     if (!groupsData.isEmpty()) {
2534         timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, ratio, timeline_undo, timeline_redo);
2535     }
2536     // Ensure to clear selection in undo/redo too.
2537     Fun unselect = [timeline]() {
2538         qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection.size();
2539         timeline->requestClearSelection();
2540         qDebug() << "after Selection " << timeline->m_currentSelection.size();
2541         return true;
2542     };
2543     PUSH_FRONT_LAMBDA(unselect, timeline_undo);
2544     PUSH_FRONT_LAMBDA(unselect, timeline_redo);
2545     // UPDATE_UNDO_REDO_NOLOCK(timeline_redo, timeline_undo, undo, redo);
2546     if (pushToStack) {
2547         pCore->pushUndo(timeline_undo, timeline_redo, i18n("Paste timeline clips"));
2548     }
2549     semaphore.release(1);
2550     return true;
2551 }
2552 
2553 bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool affectAllTracks)
2554 {
2555     // Check we have blank at position
2556     int startPos = -1;
2557     int endPos = -1;
2558     if (affectAllTracks) {
2559         for (const auto &track : timeline->m_allTracks) {
2560             if (!track->isLocked()) {
2561                 if (!track->isBlankAt(position)) {
2562                     return false;
2563                 }
2564                 startPos = track->getBlankStart(position) - 1;
2565                 endPos = track->getBlankEnd(position) + 2;
2566                 if (startPos > -1) {
2567                     std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos);
2568                     if (clips.size() == 2) {
2569                         auto it = clips.begin();
2570                         int firstCid = *it;
2571                         ++it;
2572                         int lastCid = *it;
2573                         if (timeline->m_groups->isInGroup(firstCid)) {
2574                             int groupId = timeline->m_groups->getRootId(firstCid);
2575                             std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
2576                             if (all_children.find(lastCid) != all_children.end()) {
2577                                 return false;
2578                             }
2579                         }
2580                     }
2581                 }
2582             }
2583         }
2584         // check subtitle track
2585         if (timeline->getSubtitleModel() && !timeline->getSubtitleModel()->isLocked()) {
2586             if (!timeline->getSubtitleModel()->isBlankAt(position)) {
2587                 return false;
2588             }
2589             startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1;
2590             endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1;
2591             if (startPos > -1) {
2592                 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos);
2593                 if (clips.size() == 2) {
2594                     auto it = clips.begin();
2595                     int firstCid = *it;
2596                     ++it;
2597                     int lastCid = *it;
2598                     if (timeline->m_groups->isInGroup(firstCid)) {
2599                         int groupId = timeline->m_groups->getRootId(firstCid);
2600                         std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
2601                         if (all_children.find(lastCid) != all_children.end()) {
2602                             return false;
2603                         }
2604                     }
2605                 }
2606             }
2607         }
2608     } else {
2609         // Check we have a blank and that it is in not between 2 grouped clips
2610         if (timeline->trackIsLocked(trackId)) {
2611             timeline->flashLock(trackId);
2612             return false;
2613         }
2614         if (timeline->isSubtitleTrack(trackId)) {
2615             // Subtitle track
2616             if (!timeline->getSubtitleModel()->isBlankAt(position)) {
2617                 return false;
2618             }
2619             startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1;
2620             endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1;
2621         } else {
2622             if (!timeline->getTrackById_const(trackId)->isBlankAt(position)) {
2623                 return false;
2624             }
2625             startPos = timeline->getTrackById_const(trackId)->getBlankStart(position) - 1;
2626             endPos = timeline->getTrackById_const(trackId)->getBlankEnd(position) + 2;
2627         }
2628         if (startPos > -1) {
2629             std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos);
2630             if (clips.size() == 2) {
2631                 auto it = clips.begin();
2632                 int firstCid = *it;
2633                 ++it;
2634                 int lastCid = *it;
2635                 if (timeline->m_groups->isInGroup(firstCid)) {
2636                     int groupId = timeline->m_groups->getRootId(firstCid);
2637                     std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
2638                     if (all_children.find(lastCid) != all_children.end()) {
2639                         return false;
2640                     }
2641                 }
2642             }
2643         }
2644     }
2645     std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position);
2646     int cid = spacerOp.first;
2647     if (cid == -1 || spacerOp.second == -1) {
2648         return false;
2649     }
2650     int start = timeline->getItemPosition(cid);
2651     int spaceStart = start - spacerOp.second;
2652     if (spaceStart >= start) {
2653         return false;
2654     }
2655     // Start undoable command
2656     std::function<bool(void)> undo = []() { return true; };
2657     std::function<bool(void)> redo = []() { return true; };
2658     requestSpacerEndOperation(timeline, cid, start, spaceStart, affectAllTracks ? -1 : trackId, KdenliveSettings::lockedGuides() ? -1 : position, undo, redo);
2659     return true;
2660 }
2661 
2662 bool TimelineFunctions::requestDeleteAllBlanksFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
2663 {
2664     // Abort if track is locked
2665     if (timeline->trackIsLocked(trackId)) {
2666         timeline->flashLock(trackId);
2667         return false;
2668     }
2669     // Start undoable command
2670     std::function<bool(void)> undo = []() { return true; };
2671     std::function<bool(void)> redo = []() { return true; };
2672     if (timeline->isSubtitleTrack(trackId)) {
2673         // Subtitle track
2674         int blankStart = timeline->getSubtitleModel()->getNextBlankStart(position);
2675         if (blankStart == -1) {
2676             return false;
2677         }
2678         while (blankStart != -1) {
2679             std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true);
2680             int cid = spacerOp.first;
2681             if (cid == -1) {
2682                 break;
2683             }
2684             int start = timeline->getItemPosition(cid);
2685             // Start undoable command
2686             std::function<bool(void)> local_undo = []() { return true; };
2687             std::function<bool(void)> local_redo = []() { return true; };
2688             if (blankStart < start) {
2689                 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo,
2690                                                local_redo, false)) {
2691                     // Failed to remove blank, maybe blocked because of a group. Pass to the next one
2692                     blankStart = start;
2693                 } else {
2694                     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
2695                 }
2696             } else {
2697                 if (timeline->getSubtitleModel()->isBlankAt(blankStart)) {
2698                     blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1;
2699                     if (blankStart == 1) {
2700                         break;
2701                     }
2702                 } else {
2703                     blankStart = start + timeline->getItemPlaytime(cid) + 1;
2704                 }
2705             }
2706             int nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart);
2707             if (nextBlank == blankStart) {
2708                 blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1;
2709                 nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart);
2710                 if (nextBlank == blankStart) {
2711                     break;
2712                 }
2713             }
2714             if (nextBlank < blankStart) {
2715                 // Done
2716                 break;
2717             }
2718             blankStart = nextBlank;
2719         }
2720     } else {
2721         int blankStart = timeline->getTrackById_const(trackId)->getNextBlankStart(position);
2722         if (blankStart == -1) {
2723             return false;
2724         }
2725         while (blankStart != -1) {
2726             std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true);
2727             int cid = spacerOp.first;
2728             if (cid == -1) {
2729                 break;
2730             }
2731             int start = timeline->getItemPosition(cid);
2732             // Start undoable command
2733             std::function<bool(void)> local_undo = []() { return true; };
2734             std::function<bool(void)> local_redo = []() { return true; };
2735             if (blankStart < start) {
2736                 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo,
2737                                                local_redo, false)) {
2738                     // Failed to remove blank, maybe blocked because of a group. Pass to the next one
2739                     blankStart = start;
2740                 } else {
2741                     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
2742                 }
2743             } else {
2744                 if (timeline->getTrackById_const(trackId)->isBlankAt(blankStart)) {
2745                     blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1;
2746                 } else {
2747                     blankStart = start + timeline->getItemPlaytime(cid);
2748                 }
2749             }
2750             int nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart);
2751             if (nextBlank == blankStart) {
2752                 blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1;
2753                 nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart);
2754                 if (nextBlank == blankStart) {
2755                     break;
2756                 }
2757             }
2758             if (nextBlank < blankStart) {
2759                 // Done
2760                 break;
2761             }
2762             blankStart = nextBlank;
2763         }
2764     }
2765     pCore->pushUndo(undo, redo, i18n("Remove space on track"));
2766     return true;
2767 }
2768 
2769 bool TimelineFunctions::requestDeleteAllClipsFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position)
2770 {
2771     // Abort if track is locked
2772     if (timeline->trackIsLocked(trackId)) {
2773         timeline->flashLock(trackId);
2774         return false;
2775     }
2776     // Start undoable command
2777     std::function<bool(void)> undo = []() { return true; };
2778     std::function<bool(void)> redo = []() { return true; };
2779     std::unordered_set<int> items;
2780     if (timeline->isSubtitleTrack(trackId)) {
2781         // Subtitle track
2782         items = timeline->getSubtitleModel()->getItemsInRange(position, -1);
2783     } else {
2784         items = timeline->getTrackById_const(trackId)->getClipsInRange(position, -1);
2785     }
2786     if (items.size() == 0) {
2787         return false;
2788     }
2789     for (int id : items) {
2790         timeline->requestItemDeletion(id, undo, redo);
2791     }
2792     pCore->pushUndo(undo, redo, i18n("Delete clips on track"));
2793     return true;
2794 }
2795 
2796 QDomDocument TimelineFunctions::extractClip(const std::shared_ptr<TimelineItemModel> &timeline, int cid, const QString &binId)
2797 {
2798     int tid = timeline->getClipTrackId(cid);
2799     int pos = timeline->getClipPosition(cid);
2800     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
2801     QDomDocument sourceDoc;
2802     QDomDocument destDoc;
2803     if (!Xml::docContentFromFile(sourceDoc, clip->clipUrl(), false)) {
2804         return destDoc;
2805     }
2806     QDomElement container = destDoc.createElement(QStringLiteral("kdenlive-scene"));
2807     destDoc.appendChild(container);
2808     QDomElement bin = destDoc.createElement(QStringLiteral("bin"));
2809     container.appendChild(bin);
2810     bool isAudio = timeline->isAudioTrack(tid);
2811     container.setAttribute(QStringLiteral("offset"), pos);
2812     container.setAttribute(QStringLiteral("documentid"), QStringLiteral("000000"));
2813     // Process producers
2814     QList<int> processedProducers;
2815     QString blackBg;
2816     QMap<QString, int> producerMap;
2817     QMap<QString, double> producerSpeed;
2818     QMap<QString, int> producerSpeedResource;
2819     QDomNodeList producers = sourceDoc.elementsByTagName(QLatin1String("producer"));
2820     for (int i = 0; i < producers.count(); ++i) {
2821         QDomElement currentProd = producers.item(i).toElement();
2822         bool ok;
2823         int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok);
2824         if (!ok) {
2825             // Check if this is a black bg track
2826             if (Xml::hasXmlProperty(currentProd, QLatin1String("kdenlive:playlistid"))) {
2827                 // This is the black bg track
2828                 blackBg = currentProd.attribute(QStringLiteral("id"));
2829                 continue;
2830             }
2831             const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2832             qDebug() << "===== CLIP NOT FOUND: " << resource;
2833             if (producerSpeedResource.contains(resource)) {
2834                 clipId = producerSpeedResource.value(resource);
2835                 qDebug() << "===== GOT PREVIOUS ID: " << clipId;
2836                 QString baseProducerId;
2837                 int baseProducerClipId = 0;
2838                 QMapIterator<QString, int> m(producerMap);
2839                 while (m.hasNext()) {
2840                     m.next();
2841                     if (m.value() == clipId) {
2842                         baseProducerId = m.key();
2843                         baseProducerClipId = m.value();
2844                         qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key();
2845                         break;
2846                     }
2847                 }
2848                 if (!baseProducerId.isEmpty()) {
2849                     producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId));
2850                     producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId);
2851                     qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId;
2852                 }
2853                 // Producer already processed;
2854                 continue;
2855             } else {
2856                 clipId = pCore->projectItemModel()->getFreeClipId();
2857             }
2858             Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId));
2859             qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2860         }
2861         producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId);
2862         qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId;
2863         QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service"));
2864         if (mltService == QLatin1String("timewarp")) {
2865             // Speed producer
2866             double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble();
2867             Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat"));
2868             producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId);
2869             qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId;
2870             QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource"));
2871             Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource);
2872             producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed);
2873         }
2874         if (processedProducers.contains(clipId)) {
2875             // Producer already processed
2876             continue;
2877         }
2878         processedProducers << clipId;
2879         // This could be a timeline track producer, reset custom audio/video setting
2880         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio"));
2881         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image"));
2882         bin.appendChild(destDoc.importNode(currentProd, true));
2883     }
2884     // Same for chains
2885     QDomNodeList chains = sourceDoc.elementsByTagName(QStringLiteral("chain"));
2886     for (int i = 0; i < chains.count(); ++i) {
2887         QDomElement currentProd = chains.item(i).toElement();
2888         bool ok;
2889         int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok);
2890         if (!ok) {
2891             const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2892             qDebug() << "===== CLIP NOT FOUND: " << resource;
2893             if (producerSpeedResource.contains(resource)) {
2894                 clipId = producerSpeedResource.value(resource);
2895                 qDebug() << "===== GOT PREVIOUS ID: " << clipId;
2896                 QString baseProducerId;
2897                 int baseProducerClipId = 0;
2898                 QMapIterator<QString, int> m(producerMap);
2899                 while (m.hasNext()) {
2900                     m.next();
2901                     if (m.value() == clipId) {
2902                         baseProducerId = m.key();
2903                         baseProducerClipId = m.value();
2904                         qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key();
2905                         break;
2906                     }
2907                 }
2908                 if (!baseProducerId.isEmpty()) {
2909                     producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId));
2910                     producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId);
2911                     qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId;
2912                 }
2913                 // Producer already processed;
2914                 continue;
2915             } else {
2916                 clipId = pCore->projectItemModel()->getFreeClipId();
2917             }
2918             Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId));
2919             qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2920         }
2921         producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId);
2922         qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId;
2923         QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service"));
2924         if (mltService == QLatin1String("timewarp")) {
2925             // Speed producer
2926             double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble();
2927             Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat"));
2928             producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId);
2929             qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId;
2930             QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource"));
2931             Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource);
2932             producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed);
2933         }
2934         if (processedProducers.contains(clipId)) {
2935             // Producer already processed
2936             continue;
2937         }
2938         processedProducers << clipId;
2939         // This could be a timeline track producer, reset custom audio/video setting
2940         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio"));
2941         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image"));
2942         bin.appendChild(destDoc.importNode(currentProd, true));
2943     }
2944     // Check for audio tracks
2945     QMap<QString, bool> tracksType;
2946     int audioTracks = 0;
2947     int videoTracks = 0;
2948     QDomNodeList tracks = sourceDoc.elementsByTagName(QLatin1String("track"));
2949     for (int i = 0; i < tracks.count(); ++i) {
2950         QDomElement currentTrack = tracks.item(i).toElement();
2951         if (currentTrack.attribute(QLatin1String("hide")) == QLatin1String("video")) {
2952             // Audio track
2953             tracksType.insert(currentTrack.attribute(QLatin1String("producer")), true);
2954             audioTracks++;
2955         } else {
2956             // Video track
2957             if (!blackBg.isEmpty() && blackBg == currentTrack.attribute(QLatin1String("producer"))) {
2958                 continue;
2959             }
2960             tracksType.insert(currentTrack.attribute(QLatin1String("producer")), false);
2961             videoTracks++;
2962         }
2963     }
2964     int track = 1;
2965     if (isAudio) {
2966         container.setAttribute(QStringLiteral("masterAudioTrack"), 0);
2967     } else {
2968         track = audioTracks;
2969         container.setAttribute(QStringLiteral("masterTrack"), track);
2970     }
2971     // Process playlists
2972     QDomNodeList playlists = sourceDoc.elementsByTagName(QLatin1String("playlist"));
2973     for (int i = 0; i < playlists.count(); ++i) {
2974         QDomElement currentPlay = playlists.item(i).toElement();
2975         int position = 0;
2976         bool audioTrack = tracksType.value(currentPlay.attribute("id"));
2977         if (audioTrack != isAudio) {
2978             continue;
2979         }
2980         QDomNodeList elements = currentPlay.childNodes();
2981         for (int j = 0; j < elements.count(); ++j) {
2982             QDomElement currentElement = elements.item(j).toElement();
2983             if (currentElement.tagName() == QLatin1String("blank")) {
2984                 position += currentElement.attribute(QLatin1String("length")).toInt();
2985                 continue;
2986             }
2987             if (currentElement.tagName() == QLatin1String("entry")) {
2988                 QDomElement clipElement = destDoc.createElement(QStringLiteral("clip"));
2989                 container.appendChild(clipElement);
2990                 int in = currentElement.attribute(QLatin1String("in")).toInt();
2991                 int out = currentElement.attribute(QLatin1String("out")).toInt();
2992                 const QString originalProducer = currentElement.attribute(QLatin1String("producer"));
2993                 clipElement.setAttribute(QLatin1String("binid"), producerMap.value(originalProducer));
2994                 clipElement.setAttribute(QLatin1String("in"), in);
2995                 clipElement.setAttribute(QLatin1String("out"), out);
2996                 clipElement.setAttribute(QLatin1String("position"), position + pos);
2997                 clipElement.setAttribute(QLatin1String("track"), track);
2998                 // clipElement.setAttribute(QStringLiteral("state"), (int)m_currentState);
2999                 clipElement.setAttribute(QStringLiteral("state"), audioTrack ? 2 : 1);
3000                 if (audioTrack) {
3001                     clipElement.setAttribute(QLatin1String("audioTrack"), 1);
3002                     int mirror = audioTrack + videoTracks - track - 1;
3003                     if (track <= videoTracks) {
3004                         clipElement.setAttribute(QLatin1String("mirrorTrack"), mirror);
3005                     } else {
3006                         clipElement.setAttribute(QLatin1String("mirrorTrack"), -1);
3007                     }
3008                 }
3009                 if (producerSpeed.contains(originalProducer)) {
3010                     clipElement.setAttribute(QStringLiteral("speed"), producerSpeed.value(originalProducer));
3011                 } else {
3012                     clipElement.setAttribute(QStringLiteral("speed"), 1);
3013                 }
3014                 position += (out - in + 1);
3015                 QDomNodeList effects = currentElement.elementsByTagName(QLatin1String("filter"));
3016                 if (effects.count() == 0) {
3017                     continue;
3018                 }
3019                 QDomElement effectsList = destDoc.createElement(QStringLiteral("effects"));
3020                 clipElement.appendChild(effectsList);
3021                 effectsList.setAttribute(QStringLiteral("parentIn"), in);
3022                 for (int k = 0; k < effects.count(); ++k) {
3023                     QDomElement effect = effects.item(k).toElement();
3024                     QString filterId = Xml::getXmlProperty(effect, QLatin1String("kdenlive_id"));
3025                     QDomElement clipEffect = destDoc.createElement(QStringLiteral("effect"));
3026                     effectsList.appendChild(clipEffect);
3027                     clipEffect.setAttribute(QStringLiteral("id"), filterId);
3028                     QDomNodeList properties = effect.childNodes();
3029                     if (effect.hasAttribute(QStringLiteral("in"))) {
3030                         clipEffect.setAttribute(QStringLiteral("in"), effect.attribute(QStringLiteral("in")));
3031                     }
3032                     if (effect.hasAttribute(QStringLiteral("out"))) {
3033                         clipEffect.setAttribute(QStringLiteral("out"), effect.attribute(QStringLiteral("out")));
3034                     }
3035                     for (int l = 0; l < properties.count(); ++l) {
3036                         QDomElement prop = properties.item(l).toElement();
3037                         const QString propName = prop.attribute(QLatin1String("name"));
3038                         if (propName == QLatin1String("mlt_service") || propName == QLatin1String("kdenlive_id")) {
3039                             continue;
3040                         }
3041                         Xml::setXmlProperty(clipEffect, propName, prop.text());
3042                     }
3043                 }
3044             }
3045         }
3046         track++;
3047     }
3048     if (!isAudio) {
3049         // Compositions
3050         QDomNodeList compositions = sourceDoc.elementsByTagName(QLatin1String("transition"));
3051         for (int i = 0; i < compositions.count(); ++i) {
3052             QDomElement currentCompo = compositions.item(i).toElement();
3053             if (Xml::getXmlProperty(currentCompo, QLatin1String("internal_added")).toInt() > 0) {
3054                 // Track compositing, discard
3055                 continue;
3056             }
3057             QDomElement composition = destDoc.createElement(QStringLiteral("composition"));
3058             container.appendChild(composition);
3059             int in = currentCompo.attribute(QLatin1String("in")).toInt();
3060             int out = currentCompo.attribute(QLatin1String("out")).toInt();
3061             const QString compoId = Xml::getXmlProperty(currentCompo, QLatin1String("kdenlive_id"));
3062             composition.setAttribute(QLatin1String("position"), in + pos);
3063             composition.setAttribute(QLatin1String("in"), in);
3064             composition.setAttribute(QLatin1String("out"), out);
3065             composition.setAttribute(QLatin1String("composition"), compoId);
3066             int a_track = Xml::getXmlProperty(currentCompo, QLatin1String("a_track")).toInt();
3067             int b_track = Xml::getXmlProperty(currentCompo, QLatin1String("b_track")).toInt();
3068             if (!blackBg.isEmpty()) {
3069                 a_track--;
3070                 b_track--;
3071             }
3072             composition.setAttribute(QLatin1String("a_track"), a_track);
3073             composition.setAttribute(QLatin1String("track"), b_track);
3074             QDomNodeList properties = currentCompo.childNodes();
3075             for (int l = 0; l < properties.count(); ++l) {
3076                 QDomElement prop = properties.item(l).toElement();
3077                 const QString &propName = prop.attribute(QLatin1String("name"));
3078                 Xml::setXmlProperty(composition, propName, prop.text());
3079             }
3080         }
3081     }
3082     qDebug() << "=== GOT CONVERTED DOCUMENT\n\n" << destDoc.toString();
3083     return destDoc;
3084 }
3085 
3086 int TimelineFunctions::spacerMinPos()
3087 {
3088     return spacerMinPosition;
3089 }