File indexing completed on 2024-05-12 08:54:42

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "timelinemodel.hpp"
0007 #include "assets/model/assetparametermodel.hpp"
0008 #include "bin/model/markerlistmodel.hpp"
0009 #include "bin/model/markersortmodel.h"
0010 #include "bin/model/subtitlemodel.hpp"
0011 #include "bin/projectclip.h"
0012 #include "bin/projectitemmodel.h"
0013 #include "clipmodel.hpp"
0014 #include "compositionmodel.hpp"
0015 #include "core.h"
0016 #include "doc/docundostack.hpp"
0017 #include "doc/kdenlivedoc.h"
0018 #include "effects/effectsrepository.hpp"
0019 #include "effects/effectstack/model/effectstackmodel.hpp"
0020 #include "groupsmodel.hpp"
0021 #include "kdenlivesettings.h"
0022 #include "profiles/profilemodel.hpp"
0023 #include "snapmodel.hpp"
0024 #include "timeline2/view/previewmanager.h"
0025 #include "timelinefunctions.hpp"
0026 
0027 #include "monitor/monitormanager.h"
0028 
0029 #include <KLocalizedString>
0030 #include <QCryptographicHash>
0031 #include <QDebug>
0032 #include <QModelIndex>
0033 #include <QThread>
0034 #include <mlt++/MltConsumer.h>
0035 #include <mlt++/MltField.h>
0036 #include <mlt++/MltProfile.h>
0037 #include <mlt++/MltTractor.h>
0038 #include <mlt++/MltTransition.h>
0039 #include <queue>
0040 #include <set>
0041 
0042 #include "macros.hpp"
0043 #include <localeHandling.h>
0044 
0045 #ifdef CRASH_AUTO_TEST
0046 #include "logger.hpp"
0047 #pragma GCC diagnostic push
0048 #pragma GCC diagnostic ignored "-Wunused-parameter"
0049 #pragma GCC diagnostic ignored "-Wsign-conversion"
0050 #pragma GCC diagnostic ignored "-Wfloat-equal"
0051 #pragma GCC diagnostic ignored "-Wshadow"
0052 #pragma GCC diagnostic ignored "-Wpedantic"
0053 #include <rttr/registration>
0054 #pragma GCC diagnostic pop
0055 RTTR_REGISTRATION
0056 {
0057     using namespace rttr;
0058     registration::class_<TimelineModel>("TimelineModel")
0059         .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
0060         .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool, bool, bool)>(&TimelineModel::requestClipMove))(
0061             parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline", "revertMove"))
0062         .method("requestCompositionMove", select_overload<bool(int, int, int, bool, bool)>(&TimelineModel::requestCompositionMove))(
0063             parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
0064         .method("requestClipInsertion", select_overload<bool(const QString &, int, int, int &, bool, bool, bool)>(&TimelineModel::requestClipInsertion))(
0065             parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
0066         .method("requestItemDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
0067         .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool, bool, bool)>(&TimelineModel::requestGroupMove))(
0068             parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo", "revertMove"))
0069         .method("requestGroupDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
0070         .method("requestItemResize", select_overload<int(int, int, bool, bool, int, bool)>(&TimelineModel::requestItemResize))(
0071             parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
0072         .method("requestClipsGroup", select_overload<int(const std::unordered_set<int> &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
0073             parameter_names("itemIds", "logUndo", "type"))
0074         .method("requestClipUngroup", select_overload<bool(int, bool)>(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
0075         .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
0076         .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
0077             parameter_names("pos", "id", "trackName", "audioTrack"))
0078         .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
0079         .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
0080         .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear", "singleSelect"))
0081         .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
0082         .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
0083         .method("requestFakeClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestFakeClipMove))(
0084             parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
0085         .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
0086             parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
0087         .method("suggestClipMove",
0088                 &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
0089         .method("suggestCompositionMove",
0090                 &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
0091         // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
0092         // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
0093         // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>(
0094         //                                            &TimelineModel::requestCompositionInsertion))(
0095         //     parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
0096         .method("requestClipTimeWarp", select_overload<bool(int, double, bool, bool)>(&TimelineModel::requestClipTimeWarp))(
0097             parameter_names("clipId", "speed", "pitchCompensate", "changeDuration"));
0098 }
0099 #else
0100 #define TRACE_CONSTR(...)
0101 #define TRACE_STATIC(...)
0102 #define TRACE_RES(...)
0103 #define TRACE(...)
0104 #endif
0105 
0106 int TimelineModel::seekDuration = 30000;
0107 
0108 TimelineModel::TimelineModel(const QUuid &uuid, std::weak_ptr<DocUndoStack> undo_stack)
0109     : QAbstractItemModel_shared_from_this()
0110     , m_blockRefresh(false)
0111     , m_uuid(uuid)
0112     , m_tractor(new Mlt::Tractor(pCore->getProjectProfile()))
0113     , m_masterStack(nullptr)
0114     , m_masterService(nullptr)
0115     , m_timelinePreview(nullptr)
0116     , m_snaps(new SnapModel())
0117     , m_undoStack(std::move(undo_stack))
0118     , m_blackClip(new Mlt::Producer(pCore->getProjectProfile(), "color:black"))
0119     , m_lock(QReadWriteLock::Recursive)
0120     , m_timelineEffectsEnabled(true)
0121     , m_id(getNextId())
0122     , m_overlayTrackCount(-1)
0123     , m_videoTarget(-1)
0124     , m_editMode(TimelineMode::NormalEdit)
0125     , m_closing(false)
0126     , m_softDelete(false)
0127 {
0128     // Create black background track
0129     m_blackClip->set("kdenlive:playlistid", "black_track");
0130     m_blackClip->set("mlt_type", "producer");
0131     m_blackClip->set("aspect_ratio", 1);
0132     m_blackClip->set("length", INT_MAX);
0133     m_blackClip->set("mlt_image_format", "rgba");
0134     m_blackClip->set("set.test_audio", 0);
0135     m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
0136     m_tractor->insert_track(*m_blackClip, 0);
0137     if (uuid != pCore->currentDoc()->uuid()) {
0138         // This is not the main tractor
0139         m_tractor->set("id", uuid.toString().toUtf8().constData());
0140     }
0141 
0142     TRACE_CONSTR(this);
0143 }
0144 
0145 void TimelineModel::prepareClose(bool softDelete)
0146 {
0147     requestClearSelection(true);
0148     QWriteLocker locker(&m_lock);
0149     // Unlock all tracks to allow deleting clip from tracks
0150     m_closing = true;
0151     m_blockRefresh = true;
0152     if (softDelete) {
0153         m_softDelete = true;
0154     }
0155     if (!m_softDelete) {
0156         auto it = m_allTracks.begin();
0157         while (it != m_allTracks.end()) {
0158             (*it)->unlock();
0159             ++it;
0160         }
0161         m_subtitleModel.reset();
0162     } else {
0163         auto it = m_allTracks.begin();
0164         while (it != m_allTracks.end()) {
0165             (*it)->m_softDelete = true;
0166             ++it;
0167         }
0168     }
0169     // m_subtitleModel->removeAllSubtitles();
0170 }
0171 
0172 TimelineModel::~TimelineModel()
0173 {
0174     m_closing = true;
0175     if (!m_softDelete) {
0176         qDebug() << "::::::==\n\nCLOSING TIMELINE MODEL\n\n::::::::";
0177         QScopedPointer<Mlt::Service> service(m_tractor->field());
0178         QScopedPointer<Mlt::Field> field(m_tractor->field());
0179         field->lock();
0180         // Make sure all previous track compositing is removed
0181         while (service != nullptr && service->is_valid()) {
0182             if (service->type() == mlt_service_transition_type) {
0183                 Mlt::Transition t(mlt_transition(service->get_service()));
0184                 service.reset(service->producer());
0185                 // remove all compositing
0186                 field->disconnect_service(t);
0187                 t.disconnect_all_producers();
0188             } else {
0189                 service.reset(service->producer());
0190             }
0191         }
0192         field->unlock();
0193         m_allTracks.clear();
0194         if (pCore && pCore->currentDoc() && !pCore->currentDoc()->closing) {
0195             // If we are not closing the project, unregister this timeline clips from bin
0196             for (const auto &clip : m_allClips) {
0197                 clip.second->deregisterClipToBin(m_uuid);
0198             }
0199         }
0200     }
0201 }
0202 
0203 void TimelineModel::setMarkerModel(std::shared_ptr<MarkerListModel> markerModel)
0204 {
0205     if (m_guidesModel) {
0206         return;
0207     }
0208     m_guidesModel = markerModel;
0209     m_guidesModel->registerSnapModel(std::static_pointer_cast<SnapInterface>(m_snaps));
0210     m_guidesFilterModel.reset(new MarkerSortModel(this));
0211     m_guidesFilterModel->setSourceModel(m_guidesModel.get());
0212     m_guidesFilterModel->setSortRole(MarkerListModel::PosRole);
0213     m_guidesFilterModel->sort(0, Qt::AscendingOrder);
0214     m_guidesModel->loadCategories(KdenliveSettings::guidesCategories());
0215 }
0216 
0217 int TimelineModel::getTracksCount() const
0218 {
0219     READ_LOCK();
0220     int count = m_tractor->count();
0221     if (m_overlayTrackCount > -1) {
0222         count -= m_overlayTrackCount;
0223     }
0224     Q_ASSERT(count >= 0);
0225     // don't count the black background track
0226     Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
0227     return count - 1;
0228 }
0229 
0230 QPair<int, int> TimelineModel::getAVtracksCount() const
0231 {
0232     QPair<int, int> tracks{0, 0};
0233     auto it = m_allTracks.cbegin();
0234     while (it != m_allTracks.cend()) {
0235         if ((*it)->isAudioTrack()) {
0236             tracks.first++;
0237         } else {
0238             tracks.second++;
0239         }
0240         ++it;
0241     }
0242     if (m_overlayTrackCount > -1) {
0243         tracks.first -= m_overlayTrackCount;
0244     }
0245     return tracks;
0246 }
0247 
0248 QList<int> TimelineModel::getTracksIds(bool audio) const
0249 {
0250     QList<int> trackIds;
0251     auto it = m_allTracks.cbegin();
0252     while (it != m_allTracks.cend()) {
0253         if ((*it)->isAudioTrack() == audio) {
0254             trackIds.insert(0, (*it)->getId());
0255         }
0256         ++it;
0257     }
0258     return trackIds;
0259 }
0260 
0261 int TimelineModel::getTrackIndexFromPosition(int pos) const
0262 {
0263     Q_ASSERT(pos >= 0 && pos < int(m_allTracks.size()));
0264     READ_LOCK();
0265     auto it = m_allTracks.cbegin();
0266     while (pos > 0) {
0267         it++;
0268         pos--;
0269     }
0270     return (*it)->getId();
0271 }
0272 
0273 int TimelineModel::getClipsCount() const
0274 {
0275     READ_LOCK();
0276     int size = int(m_allClips.size());
0277     return size;
0278 }
0279 
0280 int TimelineModel::getCompositionsCount() const
0281 {
0282     READ_LOCK();
0283     int size = int(m_allCompositions.size());
0284     return size;
0285 }
0286 
0287 int TimelineModel::getClipTrackId(int clipId) const
0288 {
0289     READ_LOCK();
0290     Q_ASSERT(m_allClips.count(clipId) > 0);
0291     const auto clip = m_allClips.at(clipId);
0292     return clip->getCurrentTrackId();
0293 }
0294 
0295 int TimelineModel::getCompositionTrackId(int compoId) const
0296 {
0297     Q_ASSERT(m_allCompositions.count(compoId) > 0);
0298     const auto trans = m_allCompositions.at(compoId);
0299     return trans->getCurrentTrackId();
0300 }
0301 
0302 int TimelineModel::getItemTrackId(int itemId) const
0303 {
0304     READ_LOCK();
0305     Q_ASSERT(isItem(itemId));
0306     if (isClip(itemId)) {
0307         return getClipTrackId(itemId);
0308     }
0309     if (isComposition(itemId)) {
0310         return getCompositionTrackId(itemId);
0311     }
0312     if (isSubTitle(itemId)) {
0313         // TODO: fix when introducing multiple subtitle tracks
0314         return -2;
0315     }
0316     return -1;
0317 }
0318 
0319 bool TimelineModel::hasClipEndMix(int clipId) const
0320 {
0321     if (!isClip(clipId)) return false;
0322     int tid = getClipTrackId(clipId);
0323     if (tid < 0) return false;
0324 
0325     return getTrackById_const(tid)->hasEndMix(clipId);
0326 }
0327 
0328 int TimelineModel::getClipPosition(int clipId) const
0329 {
0330     READ_LOCK();
0331     Q_ASSERT(m_allClips.count(clipId) > 0);
0332     const auto clip = m_allClips.at(clipId);
0333     int pos = clip->getPosition();
0334     return pos;
0335 }
0336 
0337 int TimelineModel::getClipEnd(int clipId) const
0338 {
0339     READ_LOCK();
0340     Q_ASSERT(m_allClips.count(clipId) > 0);
0341     const auto clip = m_allClips.at(clipId);
0342     int pos = clip->getPosition() + clip->getPlaytime();
0343     return pos;
0344 }
0345 
0346 double TimelineModel::getClipSpeed(int clipId) const
0347 {
0348     READ_LOCK();
0349     Q_ASSERT(m_allClips.count(clipId) > 0);
0350     return m_allClips.at(clipId)->getSpeed();
0351 }
0352 
0353 int TimelineModel::getClipSplitPartner(int clipId) const
0354 {
0355     READ_LOCK();
0356     Q_ASSERT(m_allClips.count(clipId) > 0);
0357     return m_groups->getSplitPartner(clipId);
0358 }
0359 
0360 int TimelineModel::getClipIn(int clipId) const
0361 {
0362     READ_LOCK();
0363     Q_ASSERT(m_allClips.count(clipId) > 0);
0364     const auto clip = m_allClips.at(clipId);
0365     return clip->getIn();
0366 }
0367 
0368 QPoint TimelineModel::getClipInDuration(int clipId) const
0369 {
0370     READ_LOCK();
0371     Q_ASSERT(m_allClips.count(clipId) > 0);
0372     const auto clip = m_allClips.at(clipId);
0373     return {clip->getIn(), clip->getPlaytime()};
0374 }
0375 
0376 PlaylistState::ClipState TimelineModel::getClipState(int clipId) const
0377 {
0378     READ_LOCK();
0379     Q_ASSERT(m_allClips.count(clipId) > 0);
0380     const auto clip = m_allClips.at(clipId);
0381     return clip->clipState();
0382 }
0383 
0384 const QString TimelineModel::getClipBinId(int clipId) const
0385 {
0386     READ_LOCK();
0387     Q_ASSERT(m_allClips.count(clipId) > 0);
0388     const auto clip = m_allClips.at(clipId);
0389     QString id = clip->binId();
0390     return id;
0391 }
0392 
0393 int TimelineModel::getClipPlaytime(int clipId) const
0394 {
0395     READ_LOCK();
0396     Q_ASSERT(isClip(clipId));
0397     const auto clip = m_allClips.at(clipId);
0398     int playtime = clip->getPlaytime();
0399     return playtime;
0400 }
0401 
0402 QSize TimelineModel::getClipFrameSize(int clipId) const
0403 {
0404     READ_LOCK();
0405     Q_ASSERT(isClip(clipId));
0406     const auto clip = m_allClips.at(clipId);
0407     return clip->getFrameSize();
0408 }
0409 
0410 int TimelineModel::getTrackClipsCount(int trackId) const
0411 {
0412     READ_LOCK();
0413     Q_ASSERT(isTrack(trackId));
0414     int count = getTrackById_const(trackId)->getClipsCount();
0415     return count;
0416 }
0417 
0418 int TimelineModel::getClipByStartPosition(int trackId, int position) const
0419 {
0420     READ_LOCK();
0421     Q_ASSERT(isTrack(trackId));
0422     return getTrackById_const(trackId)->getClipByStartPosition(position);
0423 }
0424 
0425 int TimelineModel::getClipByPosition(int trackId, int position, int playlist) const
0426 {
0427     READ_LOCK();
0428     if (isSubtitleTrack(trackId)) {
0429         return getSubtitleByPosition(position);
0430     }
0431     Q_ASSERT(isTrack(trackId));
0432     return getTrackById_const(trackId)->getClipByPosition(position, playlist);
0433 }
0434 
0435 int TimelineModel::getCompositionByPosition(int trackId, int position) const
0436 {
0437     READ_LOCK();
0438     Q_ASSERT(isTrack(trackId));
0439     return getTrackById_const(trackId)->getCompositionByPosition(position);
0440 }
0441 
0442 int TimelineModel::getSubtitleByStartPosition(int position) const
0443 {
0444     READ_LOCK();
0445     GenTime startTime(position, pCore->getCurrentFps());
0446     auto findResult =
0447         std::find_if(std::begin(m_allSubtitles), std::end(m_allSubtitles), [&](const std::pair<int, GenTime> &pair) { return pair.second == startTime; });
0448     if (findResult != std::end(m_allSubtitles)) {
0449         return findResult->first;
0450     }
0451     return -1;
0452 }
0453 
0454 int TimelineModel::getSubtitleByPosition(int position) const
0455 {
0456     READ_LOCK();
0457     GenTime startTime(position, pCore->getCurrentFps());
0458     if (m_subtitleModel) {
0459         std::unordered_set<int> sids = m_subtitleModel->getItemsInRange(position, position);
0460         if (!sids.empty()) {
0461             return *sids.begin();
0462         }
0463     }
0464     return -1;
0465 }
0466 
0467 int TimelineModel::getTrackPosition(int trackId) const
0468 {
0469     READ_LOCK();
0470     Q_ASSERT(isTrack(trackId));
0471     auto it = m_allTracks.cbegin();
0472     int pos = int(std::distance(it, static_cast<decltype(it)>(m_iteratorTable.at(trackId))));
0473     return pos;
0474 }
0475 
0476 int TimelineModel::getTrackMltIndex(int trackId) const
0477 {
0478     READ_LOCK();
0479     // Because of the black track that we insert in first position, the mlt index is the position + 1
0480     return getTrackPosition(trackId) + 1;
0481 }
0482 
0483 int TimelineModel::getTrackSortValue(int trackId, int separated) const
0484 {
0485     if (separated == 1) {
0486         // This will be A2, A1, V1, V2
0487         return getTrackPosition(trackId) + 1;
0488     }
0489     if (separated == 2) {
0490         // This will be A1, A2, V1, V2
0491         // Count audio/video tracks
0492         auto it = m_allTracks.cbegin();
0493         int aCount = 0;
0494         int vCount = 0;
0495         int refPos = 0;
0496         bool isVideo = true;
0497         while (it != m_allTracks.cend()) {
0498             if ((*it)->isAudioTrack()) {
0499                 if ((*it)->getId() == trackId) {
0500                     refPos = aCount;
0501                     isVideo = false;
0502                 }
0503                 aCount++;
0504             } else {
0505                 // video track
0506                 if ((*it)->getId() == trackId) {
0507                     refPos = vCount;
0508                 }
0509                 vCount++;
0510             }
0511             ++it;
0512         }
0513         return isVideo ? aCount + refPos + 1 : aCount - refPos;
0514     }
0515     // This will be A1, V1, A2, V2
0516     auto it = m_allTracks.cend();
0517     int aCount = 0;
0518     int vCount = 0;
0519     bool isAudio = false;
0520     int trackPos = 0;
0521     while (it != m_allTracks.begin()) {
0522         --it;
0523         bool audioTrack = (*it)->isAudioTrack();
0524         if (audioTrack) {
0525             aCount++;
0526         } else {
0527             vCount++;
0528         }
0529         if (trackId == (*it)->getId()) {
0530             isAudio = audioTrack;
0531             trackPos = audioTrack ? aCount : vCount;
0532         }
0533     }
0534     if (isAudio) {
0535         if (aCount > vCount) {
0536             if (trackPos - 1 > aCount - vCount) {
0537                 // We have more audio tracks than video tracks
0538                 return (aCount - vCount + 1) + 2 * (trackPos - (aCount - vCount + 1));
0539             }
0540             return trackPos;
0541         }
0542         return 2 * trackPos;
0543     }
0544     return 2 * (vCount + 1 - trackPos) + 1;
0545 }
0546 
0547 QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
0548 {
0549     READ_LOCK();
0550     Q_ASSERT(isTrack(trackId));
0551     QList<int> results;
0552     auto it = m_iteratorTable.at(trackId);
0553     while (it != m_allTracks.cbegin()) {
0554         --it;
0555         if (type == TrackType::AnyTrack) {
0556             results << (*it)->getId();
0557             continue;
0558         }
0559         bool audioTrack = (*it)->isAudioTrack();
0560         if (type == TrackType::AudioTrack && audioTrack) {
0561             results << (*it)->getId();
0562         } else if (type == TrackType::VideoTrack && !audioTrack) {
0563             results << (*it)->getId();
0564         }
0565     }
0566     return results;
0567 }
0568 
0569 int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
0570 {
0571     READ_LOCK();
0572     Q_ASSERT(isTrack(trackId));
0573     auto it = m_iteratorTable.at(trackId);
0574     while (it != m_allTracks.cbegin()) {
0575         --it;
0576         if (!(*it)->isAudioTrack()) {
0577             return (*it)->getId();
0578         }
0579     }
0580     return 0;
0581 }
0582 
0583 int TimelineModel::getPreviousVideoTrackPos(int trackId) const
0584 {
0585     READ_LOCK();
0586     Q_ASSERT(isTrack(trackId));
0587     auto it = m_iteratorTable.at(trackId);
0588     while (it != m_allTracks.cbegin()) {
0589         --it;
0590         if (!(*it)->isAudioTrack()) {
0591             return getTrackMltIndex((*it)->getId());
0592         }
0593     }
0594     return 0;
0595 }
0596 
0597 int TimelineModel::getMirrorVideoTrackId(int trackId) const
0598 {
0599     READ_LOCK();
0600     Q_ASSERT(isTrack(trackId));
0601     auto it = m_iteratorTable.at(trackId);
0602     if (!(*it)->isAudioTrack()) {
0603         // we expected an audio track...
0604         return -1;
0605     }
0606     int count = 0;
0607     while (it != m_allTracks.cend()) {
0608         if ((*it)->isAudioTrack()) {
0609             count++;
0610         } else {
0611             count--;
0612             if (count == 0) {
0613                 return (*it)->getId();
0614             }
0615         }
0616         ++it;
0617     }
0618     return -1;
0619 }
0620 
0621 int TimelineModel::getMirrorTrackId(int trackId) const
0622 {
0623     if (isAudioTrack(trackId)) {
0624         return getMirrorVideoTrackId(trackId);
0625     }
0626     return getMirrorAudioTrackId(trackId);
0627 }
0628 
0629 int TimelineModel::getMirrorAudioTrackId(int trackId) const
0630 {
0631     READ_LOCK();
0632     Q_ASSERT(isTrack(trackId));
0633     auto it = m_iteratorTable.at(trackId);
0634     if ((*it)->isAudioTrack()) {
0635         // we expected a video track...
0636         qWarning() << "requesting mirror audio track for audio track";
0637         return -1;
0638     }
0639     int count = 0;
0640     while (it != m_allTracks.cbegin()) {
0641         if (!(*it)->isAudioTrack()) {
0642             count++;
0643         } else {
0644             count--;
0645             if (count == 0) {
0646                 return (*it)->getId();
0647             }
0648         }
0649         --it;
0650     }
0651     if ((*it)->isAudioTrack() && count == 1) {
0652         return (*it)->getId();
0653     }
0654     return -1;
0655 }
0656 
0657 void TimelineModel::setEditMode(TimelineMode::EditMode mode)
0658 {
0659     m_editMode = mode;
0660 }
0661 
0662 TimelineMode::EditMode TimelineModel::editMode() const
0663 {
0664     return m_editMode;
0665 }
0666 
0667 bool TimelineModel::normalEdit() const
0668 {
0669     return m_editMode == TimelineMode::NormalEdit;
0670 }
0671 
0672 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
0673 {
0674     Q_UNUSED(updateView);
0675     Q_UNUSED(invalidateTimeline);
0676     Q_UNUSED(undo);
0677     Q_UNUSED(redo);
0678     Q_ASSERT(isClip(clipId));
0679     m_allClips[clipId]->setFakePosition(position);
0680     bool trackChanged = false;
0681     if (trackId > -1) {
0682         if (trackId != m_allClips[clipId]->getFakeTrackId()) {
0683             if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
0684                 m_allClips[clipId]->setFakeTrackId(trackId);
0685                 trackChanged = true;
0686             }
0687         }
0688     }
0689     QModelIndex modelIndex = makeClipIndexFromID(clipId);
0690     if (modelIndex.isValid()) {
0691         QVector<int> roles{FakePositionRole};
0692         if (trackChanged) {
0693             roles << FakeTrackIdRole;
0694         }
0695         notifyChange(modelIndex, modelIndex, roles);
0696         return true;
0697     }
0698     return false;
0699 }
0700 
0701 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove,
0702                                     Fun &undo, Fun &redo, bool revertMove, bool groupMove, const QMap<int, int> &moving_clips,
0703                                     std::pair<MixInfo, MixInfo> mixData)
0704 {
0705     Q_UNUSED(moveMirrorTracks)
0706     if (trackId == -1) {
0707         qWarning() << "clip is not on a track";
0708         return false;
0709     }
0710     Q_ASSERT(isClip(clipId));
0711     if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
0712         if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
0713             qWarning() << "clip type mismatch 1";
0714             return false;
0715         }
0716         if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
0717             qWarning() << "clip type mismatch 2";
0718             return false;
0719         }
0720     } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
0721         // Move not allowed (audio / video mismatch)
0722         qWarning() << "clip type mismatch 3";
0723         return false;
0724     }
0725     std::function<bool(void)> local_undo = []() { return true; };
0726     std::function<bool(void)> local_redo = []() { return true; };
0727     bool ok = true;
0728     int old_trackId = getClipTrackId(clipId);
0729     int previous_track = moving_clips.value(clipId, -1);
0730     if (old_trackId == -1) {
0731         // old_trackId = previous_track;
0732     }
0733     bool notifyViewOnly = false;
0734     Fun update_model = []() { return true; };
0735     if (old_trackId == trackId) {
0736         // Move on same track, simply inform the view
0737         updateView = false;
0738         notifyViewOnly = true;
0739         update_model = [clipId, this, trackId, invalidateTimeline]() {
0740             QModelIndex modelIndex = makeClipIndexFromID(clipId);
0741             notifyChange(modelIndex, modelIndex, StartRole);
0742             if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
0743                 int in = getClipPosition(clipId);
0744                 Q_EMIT invalidateZone(in, in + getClipPlaytime(clipId));
0745             }
0746             return true;
0747         };
0748     }
0749     Fun sync_mix = []() { return true; };
0750     Fun simple_move_mix = []() { return true; };
0751     Fun simple_restore_mix = []() { return true; };
0752     QList<int> allowedClipMixes;
0753     if (!groupMove && old_trackId > -1) {
0754         mixData = getTrackById_const(old_trackId)->getMixInfo(clipId);
0755     }
0756     if (old_trackId == trackId && !finalMove && !revertMove) {
0757         if (mixData.first.firstClipId > -1 && !moving_clips.contains(mixData.first.firstClipId)) {
0758             // Mix at clip start, don't allow moving left
0759             if (position < (mixData.first.firstClipInOut.second - mixData.first.mixOffset) &&
0760                 (position + m_allClips[clipId]->getPlaytime() >= mixData.first.firstClipInOut.first)) {
0761                 qDebug() << "==== ABORTING GROUP MOVE ON START MIX";
0762                 return false;
0763             }
0764         }
0765         if (mixData.second.firstClipId > -1 && !moving_clips.contains(mixData.second.secondClipId)) {
0766             // Mix at clip end, don't allow moving right
0767             if (position + getClipPlaytime(clipId) > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) {
0768                 qDebug() << "==== ABORTING GROUP MOVE ON END MIX: " << position;
0769                 return false;
0770             }
0771         }
0772     }
0773     bool hadMix = mixData.first.firstClipId > -1 || mixData.second.firstClipId > -1;
0774     if (!finalMove && !revertMove) {
0775         QVector<int> exceptions = {clipId};
0776         if (mixData.first.firstClipId > -1) {
0777             exceptions << mixData.first.firstClipId;
0778         }
0779         if (mixData.second.secondClipId > -1) {
0780             exceptions << mixData.second.secondClipId;
0781         }
0782         if (!getTrackById_const(trackId)->isAvailableWithExceptions(position, getClipPlaytime(clipId), exceptions)) {
0783             // No space for clip insert operation, abort
0784             qWarning() << "No free space for clip move";
0785             return false;
0786         }
0787     }
0788     if (old_trackId == -1 && isTrack(previous_track) && hadMix && previous_track != trackId) {
0789         // Clip is moved to another track
0790         bool mixGroupMove = false;
0791         if (mixData.first.firstClipId > 0) {
0792             allowedClipMixes << mixData.first.firstClipId;
0793             if (moving_clips.contains(mixData.first.firstClipId)) {
0794                 allowedClipMixes << mixData.first.firstClipId;
0795             } else if (finalMove) {
0796                 position += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
0797                 removeMixWithUndo(clipId, local_undo, local_redo);
0798             }
0799         }
0800         if (mixData.second.firstClipId > 0) {
0801             allowedClipMixes << mixData.second.secondClipId;
0802             if (moving_clips.contains(mixData.second.secondClipId)) {
0803                 allowedClipMixes << mixData.second.secondClipId;
0804             } else if (finalMove) {
0805                 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
0806             }
0807         }
0808         if (m_groups->isInGroup(clipId) && mixData.first.firstClipId > 0) {
0809             int parentGroup = m_groups->getRootId(clipId);
0810             if (parentGroup > -1) {
0811                 std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
0812                 if (sub.count(mixData.first.firstClipId) > 0 && sub.count(mixData.first.secondClipId) > 0) {
0813                     mixGroupMove = true;
0814                 }
0815             }
0816         }
0817         if (mixGroupMove) {
0818             // We are moving a group on another track, delete and re-add
0819             // Get mix properties
0820             std::pair<int, int> tracks = getTrackById_const(previous_track)->getMixTracks(mixData.first.secondClipId);
0821             std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = getTrackById_const(previous_track)->getMixParams(mixData.first.secondClipId);
0822             simple_move_mix = [this, previous_track, trackId, finalMove, mixData, tracks, mixParams]() {
0823                 // Insert mix on new track
0824                 bool result = getTrackById_const(trackId)->createMix(mixData.first, mixParams, tracks, finalMove);
0825                 // Remove mix on old track
0826                 getTrackById_const(previous_track)->removeMix(mixData.first);
0827                 return result;
0828             };
0829             simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, tracks, mixParams]() {
0830                 bool result = getTrackById_const(previous_track)->createMix(mixData.first, mixParams, tracks, finalMove);
0831                 // Remove mix on old track
0832                 getTrackById_const(trackId)->removeMix(mixData.first);
0833 
0834                 return result;
0835             };
0836         }
0837     } else if (finalMove && !groupMove && isTrack(old_trackId) && hadMix) {
0838         // Clip has a mix
0839         if (mixData.first.firstClipId > -1) {
0840             if (old_trackId == trackId) {
0841                 int mixCut = m_allClips[clipId]->getMixCutPosition();
0842                 // We are moving a clip on same track
0843                 if (position > mixData.first.secondClipInOut.first - mixCut || position < mixData.first.firstClipInOut.first) {
0844                     position += m_allClips[clipId]->getMixDuration() - mixCut;
0845                     removeMixWithUndo(clipId, local_undo, local_redo);
0846                 }
0847             } else {
0848                 // Clip moved to another track, delete mix
0849                 position += (m_allClips[clipId]->getMixDuration() - m_allClips[clipId]->getMixCutPosition());
0850                 removeMixWithUndo(clipId, local_undo, local_redo);
0851             }
0852         }
0853         if (mixData.second.firstClipId > -1) {
0854             // We have a mix at clip end
0855             if (old_trackId == trackId) {
0856                 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration();
0857                 if (position > mixEnd || position < m_allClips[mixData.second.secondClipId]->getPosition()) {
0858                     // Moved outside mix zone
0859                     removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
0860                 }
0861             } else {
0862                 // Clip moved to another track, delete mix
0863                 // Mix will be deleted by syncronizeMixes operation, only
0864                 // re-add it on undo
0865                 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
0866             }
0867         }
0868     } else if (finalMove && groupMove && isTrack(old_trackId) && hadMix && old_trackId == trackId) {
0869         // Group move on same track with mix
0870         if (mixData.first.firstClipId > -1) {
0871             // Mix on clip start, check if mix is still in range
0872             if (!moving_clips.contains(mixData.first.firstClipId)) {
0873                 int mixCut = m_allClips[clipId]->getMixCutPosition();
0874                 // We are moving a clip on same track
0875                 if (position > mixData.first.secondClipInOut.first - mixCut || position < mixData.first.firstClipInOut.first) {
0876                     // Mix will be deleted, recreate on undo
0877                     position += m_allClips[mixData.first.secondClipId]->getMixDuration() - m_allClips[mixData.first.secondClipId]->getMixCutPosition();
0878                     removeMixWithUndo(mixData.first.secondClipId, local_undo, local_redo);
0879                 }
0880             } else {
0881                 allowedClipMixes << mixData.first.firstClipId;
0882             }
0883         }
0884         if (mixData.second.firstClipId > -1) {
0885             // Mix on clip end, check if mix is still in range
0886             if (!moving_clips.contains(mixData.second.secondClipId)) {
0887                 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration();
0888                 if (mixEnd > position + m_allClips[clipId]->getPlaytime() || position > mixEnd) {
0889                     // Mix will be deleted, recreate on undo
0890                     removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
0891                 }
0892             } else {
0893                 allowedClipMixes << mixData.second.secondClipId;
0894             }
0895         }
0896     } else {
0897     }
0898     if (old_trackId != -1) {
0899         if (notifyViewOnly) {
0900             PUSH_LAMBDA(update_model, local_undo);
0901         }
0902         ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false, allowedClipMixes);
0903         if (!ok) {
0904             bool undone = local_undo();
0905             Q_ASSERT(undone);
0906             qWarning() << "clip deletion failed";
0907             return false;
0908         } else {
0909         }
0910     }
0911     ok = ok && getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove, old_trackId == -1,
0912                                                            allowedClipMixes);
0913 
0914     if (!ok) {
0915         qWarning() << "clip insertion failed";
0916         bool undone = local_undo();
0917         Q_ASSERT(undone);
0918         return false;
0919     }
0920 
0921     sync_mix();
0922     update_model();
0923     simple_move_mix();
0924 
0925     if (finalMove) {
0926         PUSH_LAMBDA(simple_restore_mix, undo);
0927         PUSH_LAMBDA(simple_move_mix, local_redo);
0928         // PUSH_LAMBDA(sync_mix, local_redo);
0929     }
0930     if (notifyViewOnly) {
0931         PUSH_LAMBDA(update_model, local_redo);
0932     }
0933     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
0934     return true;
0935 }
0936 
0937 bool TimelineModel::mixClip(int idToMove, const QString &mixId, int delta)
0938 {
0939     std::unordered_set<int> initialSelection = getCurrentSelection();
0940     if (idToMove == -1 && initialSelection.empty()) {
0941         pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
0942         return false;
0943     }
0944     struct mixStructure
0945     {
0946         int clipId;
0947         std::pair<int, int> clips;
0948         std::pair<int, int> spaces;
0949         std::pair<int, int> durations;
0950         int mixPosition;
0951         int selectedTrack;
0952     };
0953     QList<mixStructure> clipsToMixList;
0954     std::vector<int> clipIds;
0955     std::unordered_set<int> selectionToSort;
0956     if (idToMove != -1) {
0957         selectionToSort = {idToMove};
0958         // Check for audio / video partner
0959         if (isClip(idToMove) && m_groups->isInGroup(idToMove)) {
0960             int partnerId = getClipSplitPartner(idToMove);
0961             if (partnerId > -1) {
0962                 selectionToSort.insert(partnerId);
0963             }
0964         }
0965     } else {
0966         selectionToSort = initialSelection;
0967     }
0968     // Sort selection chronologically to limit reordering
0969     std::vector<int> ordered(selectionToSort.begin(), selectionToSort.end());
0970     std::sort(ordered.begin(), ordered.end(), [this](int cidA, int cidB) {
0971         if (!isClip(cidA) || !isClip(cidB)) {
0972             return false;
0973         }
0974         return m_allClips[cidA]->getPosition() < m_allClips[cidB]->getPosition();
0975     });
0976     clipIds = ordered;
0977 
0978     int noSpaceInClip = 0;
0979     int mixDuration = pCore->getDurationFromString(KdenliveSettings::mix_duration());
0980     std::function<bool(void)> undo = []() { return true; };
0981     std::function<bool(void)> redo = []() { return true; };
0982 
0983     auto processMix = [this, &undo, &redo, mixDuration, mixId](mixStructure mInfo) {
0984         if (mInfo.spaces.first > -1) {
0985             if (mInfo.spaces.second > -1) {
0986                 // Both clips have limited durations
0987                 mInfo.durations.first = qMin(mixDuration / 2, mInfo.spaces.first);
0988                 mInfo.durations.first = qMin(mInfo.durations.first, mInfo.spaces.first);
0989                 mInfo.durations.second = qMin(mixDuration - mixDuration / 2, mInfo.spaces.second);
0990                 int offset = mixDuration - (mInfo.durations.first + mInfo.durations.second);
0991                 if (offset > 0) {
0992                     if (mInfo.spaces.first > mInfo.durations.first) {
0993                         mInfo.durations.first = qMin(mInfo.spaces.first, mInfo.durations.first + offset);
0994                     } else if (mInfo.spaces.second > mInfo.durations.second) {
0995                         mInfo.durations.second = qMin(mInfo.spaces.second, mInfo.durations.second + offset);
0996                     }
0997                 }
0998             } else {
0999                 mInfo.durations.first = qMin(mixDuration - mixDuration / 2, mInfo.spaces.first);
1000                 mInfo.durations.second = mixDuration - mInfo.durations.second;
1001             }
1002         } else {
1003             if (mInfo.spaces.second > -1) {
1004                 mInfo.durations.second = qMin(mixDuration - mixDuration / 2, mInfo.spaces.second);
1005                 mInfo.durations.first = mixDuration - mInfo.durations.second;
1006             } else {
1007                 mInfo.durations.first = mixDuration / 2;
1008                 mInfo.durations.second = mixDuration - mInfo.durations.first;
1009             }
1010         }
1011         return requestClipMix(mixId, mInfo.clips, mInfo.durations, mInfo.selectedTrack, mInfo.mixPosition, true, true, true, undo, redo, false);
1012     };
1013 
1014     for (int s : clipIds) {
1015         if (!isClip(s)) {
1016             continue;
1017         }
1018         mixStructure mixInfo;
1019         mixInfo.clipId = s;
1020         mixInfo.clips = {-1, -1};
1021         mixInfo.spaces = {0, 0};
1022         mixInfo.durations = {0, 0};
1023         mixInfo.mixPosition = 0;
1024         mixInfo.selectedTrack = getClipTrackId(s);
1025         if (mixInfo.selectedTrack == -1 || !isTrack(mixInfo.selectedTrack) || !getTrackById_const(mixInfo.selectedTrack)->shouldReceiveTimelineOp()) {
1026             continue;
1027         }
1028         mixInfo.mixPosition = getItemPosition(s);
1029         int clipDuration = getItemPlaytime(s);
1030         // Check if clip already has a mix
1031         if (delta > -1 && getTrackById_const(mixInfo.selectedTrack)->hasStartMix(s)) {
1032             if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) {
1033                 continue;
1034             }
1035             mixInfo.clips.second = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1);
1036             // Check if previous clip was selected, and not next clip. In that case we stop processing
1037             int previousClip = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1);
1038             if (std::find(clipIds.begin(), clipIds.end(), previousClip) != clipIds.end() &&
1039                 std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) {
1040                 continue;
1041             }
1042         } else if (delta < 1 && getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) {
1043             mixInfo.clips.first = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1);
1044             // Check if next clip was selected, and not previous clip. In that case we stop processing
1045             int nextClip = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1);
1046             if (std::find(clipIds.begin(), clipIds.end(), nextClip) != clipIds.end() &&
1047                 std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) {
1048                 continue;
1049             }
1050             if (mixInfo.clips.first > -1 && getTrackById_const(mixInfo.selectedTrack)->hasEndMix(mixInfo.clips.first)) {
1051                 // Could happen if 2 clips before are mixed to full length
1052                 mixInfo.clips.first = -1;
1053             }
1054         } else {
1055             if (delta < 1) {
1056                 mixInfo.clips.first = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1);
1057             }
1058             if (delta > -1) {
1059                 mixInfo.clips.second = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1);
1060             }
1061         }
1062         if (mixInfo.clips.first > -1 && mixInfo.clips.second > -1) {
1063             // We have a clip before and a clip after, check if both are selected
1064             if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) {
1065                 if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) != clipIds.end()) {
1066                     mixInfo.clips.second = -1;
1067                 }
1068             } else if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) {
1069                 mixInfo.clips.first = -1;
1070             }
1071         }
1072         if (mixInfo.clips.second != -1) {
1073             // Mix at end of selected clip
1074             // Make sure we have enough space in clip to resize
1075             // mixInfo.spaces.first is the maximum frames we have to expand first clip on the right
1076             mixInfo.spaces.first = m_allClips[s]->m_endlessResize
1077                                        ? m_allClips[mixInfo.clips.second]->getPlaytime()
1078                                        : qMin(m_allClips[mixInfo.clips.second]->getPlaytime(), m_allClips[s]->getMaxDuration() - m_allClips[s]->getOut() - 1);
1079             // mixInfo.spaces.second is the maximum frames we have to expand second clip on the left
1080             mixInfo.spaces.second = m_allClips[mixInfo.clips.second]->m_endlessResize
1081                                         ? m_allClips[s]->getPlaytime()
1082                                         : qMin(m_allClips[s]->getPlaytime(), m_allClips[mixInfo.clips.second]->getIn());
1083             if (getTrackById_const(mixInfo.selectedTrack)->hasStartMix(s)) {
1084                 int spaceBeforeMix = m_allClips[mixInfo.clips.second]->getPosition() - (m_allClips[s]->getPosition() + m_allClips[s]->getMixDuration());
1085                 mixInfo.spaces.second = mixInfo.spaces.second == -1 ? spaceBeforeMix : qMin(mixInfo.spaces.second, spaceBeforeMix);
1086             }
1087             if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(mixInfo.clips.second)) {
1088                 MixInfo mixData = getTrackById_const(mixInfo.selectedTrack)->getMixInfo(mixInfo.clips.second).second;
1089                 if (mixData.secondClipId > -1) {
1090                     int spaceAfterMix = m_allClips[mixInfo.clips.second]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration();
1091                     mixInfo.spaces.first = mixInfo.spaces.first == -1 ? spaceAfterMix : qMin(mixInfo.spaces.first, spaceAfterMix);
1092                 }
1093             }
1094             if (mixInfo.spaces.first > -1 && mixInfo.spaces.second > -1 && (mixInfo.spaces.first + mixInfo.spaces.second < 3)) {
1095                 noSpaceInClip = 2;
1096                 continue;
1097             }
1098             mixInfo.mixPosition += clipDuration;
1099             mixInfo.clips.first = s;
1100             mixInfo.clipId = s;
1101             processMix(mixInfo);
1102             clipsToMixList << mixInfo;
1103             continue;
1104         } else {
1105             if (mixInfo.clips.first == -1) {
1106                 // No clip to mix, abort
1107                 continue;
1108             }
1109             // Make sure we have enough space in clip to resize
1110             // mixInfo.spaces.first is the maximum frames we have to expand first clip on the right
1111             mixInfo.spaces.first =
1112                 m_allClips[mixInfo.clips.first]->m_endlessResize
1113                     ? m_allClips[s]->getPlaytime()
1114                     : qMin(m_allClips[s]->getPlaytime(), m_allClips[mixInfo.clips.first]->getMaxDuration() - m_allClips[mixInfo.clips.first]->getOut() - 1);
1115             // mixInfo.spaces.second is the maximum frames we have to expand second clip on the left
1116             mixInfo.spaces.second = m_allClips[s]->m_endlessResize ? m_allClips[mixInfo.clips.first]->getPlaytime()
1117                                                                    : qMin(m_allClips[mixInfo.clips.first]->getPlaytime(), m_allClips[s]->getIn());
1118             if (getTrackById_const(mixInfo.selectedTrack)->hasStartMix(mixInfo.clips.first)) {
1119                 int spaceBeforeMix =
1120                     m_allClips[s]->getPosition() - (m_allClips[mixInfo.clips.first]->getPosition() + m_allClips[mixInfo.clips.first]->getMixDuration());
1121                 mixInfo.spaces.second = mixInfo.spaces.second == -1 ? spaceBeforeMix : qMin(mixInfo.spaces.second, spaceBeforeMix);
1122             }
1123             if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) {
1124                 MixInfo mixData = getTrackById_const(mixInfo.selectedTrack)->getMixInfo(s).second;
1125                 if (mixData.secondClipId > -1) {
1126                     int spaceAfterMix = m_allClips[s]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration();
1127                     mixInfo.spaces.first = mixInfo.spaces.first == -1 ? spaceAfterMix : qMin(mixInfo.spaces.first, spaceAfterMix);
1128                 }
1129             }
1130             if (mixInfo.spaces.first > -1 && mixInfo.spaces.second > -1 && (mixInfo.spaces.first + mixInfo.spaces.second < 3)) {
1131                 noSpaceInClip = 1;
1132                 continue;
1133             }
1134             // Create Mix at start of selected clip
1135             mixInfo.clips.second = s;
1136             mixInfo.clipId = s;
1137             processMix(mixInfo);
1138             clipsToMixList << mixInfo;
1139             continue;
1140         }
1141     }
1142     if (clipsToMixList.isEmpty()) {
1143         if (noSpaceInClip > 0) {
1144             pCore->displayMessage(i18n("Not enough frames at clip %1 to apply the mix", noSpaceInClip == 1 ? i18n("start") : i18n("end")), ErrorMessage, 500);
1145         } else {
1146             pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
1147         }
1148         return false;
1149     }
1150     pCore->pushUndo(undo, redo, i18n("Create mix"));
1151     // Reselect clips
1152     if (!initialSelection.empty()) {
1153         requestSetSelection(initialSelection);
1154     }
1155     return true;
1156 }
1157 
1158 bool TimelineModel::requestClipMix(const QString &mixId, std::pair<int, int> clipIds, std::pair<int, int> mixDurations, int trackId, int position,
1159                                    bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
1160 {
1161     if (trackId == -1) {
1162         return false;
1163     }
1164     Q_ASSERT(isClip(clipIds.first));
1165     std::function<bool(void)> local_undo = []() { return true; };
1166     std::function<bool(void)> local_redo = []() { return true; };
1167     bool ok = true;
1168     Fun update_model = []() { return true; };
1169     // Move on same track, simply inform the view
1170     updateView = false;
1171     bool notifyViewOnly = true;
1172     update_model = [clipIds, this, trackId, position, invalidateTimeline, mixDurations]() {
1173         QModelIndex modelIndex = makeClipIndexFromID(clipIds.second);
1174         notifyChange(modelIndex, modelIndex, {StartRole, DurationRole});
1175         QModelIndex modelIndex2 = makeClipIndexFromID(clipIds.first);
1176         notifyChange(modelIndex2, modelIndex2, DurationRole);
1177         if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
1178             Q_EMIT invalidateZone(position - mixDurations.second, position + mixDurations.first);
1179         }
1180         return true;
1181     };
1182     if (notifyViewOnly) {
1183         PUSH_LAMBDA(update_model, local_undo);
1184     }
1185     ok = getTrackById(trackId)->requestClipMix(mixId, clipIds, mixDurations, updateView, finalMove, local_undo, local_redo, groupMove);
1186     if (!ok) {
1187         qWarning() << "mix failed, reverting";
1188         bool undone = local_undo();
1189         Q_ASSERT(undone);
1190         return false;
1191     }
1192     update_model();
1193     if (notifyViewOnly) {
1194         PUSH_LAMBDA(update_model, local_redo);
1195     }
1196     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1197     return ok;
1198 }
1199 
1200 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
1201 {
1202     QWriteLocker locker(&m_lock);
1203     TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
1204     Q_ASSERT(m_allClips.count(clipId) > 0);
1205     if (m_groups->isInGroup(clipId)) {
1206         // element is in a group.
1207         int groupId = m_groups->getRootId(clipId);
1208         int current_trackId = getClipTrackId(clipId);
1209         int track_pos1 = getTrackPosition(trackId);
1210         int track_pos2 = getTrackPosition(current_trackId);
1211         int delta_track = track_pos1 - track_pos2;
1212         int delta_pos = position - m_allClips[clipId]->getPosition();
1213         bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
1214         TRACE_RES(res);
1215         return res;
1216     }
1217     std::function<bool(void)> undo = []() { return true; };
1218     std::function<bool(void)> redo = []() { return true; };
1219     bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
1220     if (res && logUndo) {
1221         PUSH_UNDO(undo, redo, i18n("Move clip"));
1222     }
1223     TRACE_RES(res);
1224     return res;
1225 }
1226 
1227 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline,
1228                                     bool revertMove)
1229 {
1230     QWriteLocker locker(&m_lock);
1231     TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
1232     Q_ASSERT(m_allClips.count(clipId) > 0);
1233     if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
1234         TRACE_RES(true);
1235         return true;
1236     }
1237     if (m_groups->isInGroup(clipId) && moveMirrorTracks) {
1238         // element is in a group.
1239         int groupId = m_groups->getRootId(clipId);
1240         int current_trackId = getClipTrackId(clipId);
1241         int track_pos1 = getTrackPosition(trackId);
1242         int track_pos2 = getTrackPosition(current_trackId);
1243         int delta_track = track_pos1 - track_pos2;
1244         int delta_pos = position - m_allClips[clipId]->getPosition();
1245         return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo, revertMove);
1246     }
1247     std::function<bool(void)> undo = []() { return true; };
1248     std::function<bool(void)> redo = []() { return true; };
1249     bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo, revertMove);
1250     if (res && logUndo) {
1251         PUSH_UNDO(undo, redo, i18n("Move clip"));
1252     }
1253     TRACE_RES(res);
1254     return res;
1255 }
1256 
1257 std::shared_ptr<SubtitleModel> TimelineModel::getSubtitleModel()
1258 {
1259     return m_subtitleModel;
1260 }
1261 
1262 int TimelineModel::cutSubtitle(int position, Fun &undo, Fun &redo)
1263 {
1264     if (m_subtitleModel) {
1265         return m_subtitleModel->cutSubtitle(position, undo, redo);
1266     }
1267     return -1;
1268 }
1269 
1270 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool logUndo, bool finalMove)
1271 {
1272     QWriteLocker locker(&m_lock);
1273     Q_ASSERT(m_allSubtitles.count(clipId) > 0);
1274     GenTime oldPos = m_allSubtitles.at(clipId);
1275     GenTime newPos(position, pCore->getCurrentFps());
1276     if (oldPos == newPos) {
1277         return true;
1278     }
1279     if (m_groups->isInGroup(clipId)) {
1280         // element is in a group.
1281         int groupId = m_groups->getRootId(clipId);
1282         int delta_pos = position - oldPos.frames(pCore->getCurrentFps());
1283         return requestGroupMove(clipId, groupId, 0, delta_pos, false, updateView, logUndo);
1284     }
1285     std::function<bool(void)> undo = []() { return true; };
1286     std::function<bool(void)> redo = []() { return true; };
1287     bool res = requestSubtitleMove(clipId, position, updateView, logUndo, logUndo, finalMove, undo, redo);
1288     if (res && logUndo) {
1289         PUSH_UNDO(undo, redo, i18n("Move subtitle"));
1290     }
1291     return res;
1292 }
1293 
1294 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool first, bool last, bool finalMove, Fun &undo, Fun &redo)
1295 {
1296     QWriteLocker locker(&m_lock);
1297     GenTime oldPos = m_allSubtitles.at(clipId);
1298     GenTime newPos(position, pCore->getCurrentFps());
1299     Fun local_redo = [this, clipId, newPos, reloadSubFile = last && finalMove, updateView]() {
1300         return m_subtitleModel->moveSubtitle(clipId, newPos, reloadSubFile, updateView);
1301     };
1302     Fun local_undo = [this, oldPos, clipId, reloadSubFile = first && finalMove, updateView]() {
1303         return m_subtitleModel->moveSubtitle(clipId, oldPos, reloadSubFile, updateView);
1304     };
1305     bool res = local_redo();
1306     if (res) {
1307         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1308     } else {
1309         local_undo();
1310     }
1311     return res;
1312 }
1313 
1314 bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
1315 {
1316     QWriteLocker locker(&m_lock);
1317     Q_ASSERT(m_allClips.count(clipId) > 0);
1318     if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
1319         return true;
1320     }
1321     std::function<bool(void)> undo = []() { return true; };
1322     std::function<bool(void)> redo = []() { return true; };
1323     bool res = true;
1324     if (m_groups->isInGroup(clipId)) {
1325         // element is in a group.
1326         int groupId = m_groups->getRootId(clipId);
1327         int current_trackId = getClipTrackId(clipId);
1328         int track_pos1 = getTrackPosition(trackId);
1329         int track_pos2 = getTrackPosition(current_trackId);
1330         int delta_track = track_pos1 - track_pos2;
1331         int delta_pos = position - m_allClips[clipId]->getPosition();
1332         res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false, false);
1333     } else {
1334         res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
1335     }
1336     if (res) {
1337         undo();
1338     }
1339     return res;
1340 }
1341 
1342 QVariantList TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
1343 {
1344     if (isClip(itemId)) {
1345         return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
1346     }
1347     if (isComposition(itemId)) {
1348         return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
1349     }
1350     if (isSubTitle(itemId)) {
1351         return {suggestSubtitleMove(itemId, position, cursorPosition, snapDistance), -1};
1352     }
1353     return QVariantList();
1354 }
1355 
1356 int TimelineModel::adjustFrame(int frame, int trackId)
1357 {
1358     if (m_editMode == TimelineMode::InsertEdit && isTrack(trackId)) {
1359         frame = qMin(frame, getTrackById_const(trackId)->trackDuration());
1360     }
1361     return frame;
1362 }
1363 
1364 int TimelineModel::suggestSubtitleMove(int subId, int position, int cursorPosition, int snapDistance)
1365 {
1366     QWriteLocker locker(&m_lock);
1367     Q_ASSERT(isSubTitle(subId));
1368     int currentPos = getSubtitlePosition(subId);
1369     if (currentPos == position || m_subtitleModel->isLocked()) {
1370         return position;
1371     }
1372     int newPos = position;
1373     if (snapDistance > 0) {
1374         int offset = 0;
1375         std::vector<int> ignored_pts;
1376         // For snapping, we must ignore all in/outs of the clips of the group being moved
1377         std::unordered_set<int> all_items = {subId};
1378         if (m_groups->isInGroup(subId)) {
1379             int groupId = m_groups->getRootId(subId);
1380             all_items = m_groups->getLeaves(groupId);
1381         }
1382         for (int current_clipId : all_items) {
1383             if (isClip(current_clipId)) {
1384                 m_allClips[current_clipId]->allSnaps(ignored_pts, offset);
1385             } else if (isComposition(current_clipId) || isSubTitle(current_clipId)) {
1386                 // Composition or subtitle
1387                 int in = getItemPosition(current_clipId) - offset;
1388                 ignored_pts.push_back(in);
1389                 ignored_pts.push_back(in + getItemPlaytime(current_clipId));
1390             }
1391         }
1392         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1393         if (snapped >= 0) {
1394             newPos = snapped;
1395         }
1396     }
1397     // m_subtitleModel->moveSubtitle(GenTime(currentPos, pCore->getCurrentFps()), GenTime(position, pCore->getCurrentFps()));
1398     if (requestSubtitleMove(subId, newPos, true, false)) {
1399         return newPos;
1400     }
1401     return currentPos;
1402 }
1403 
1404 QVariantList TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
1405 {
1406     QWriteLocker locker(&m_lock);
1407     TRACE(clipId, trackId, position, cursorPosition, snapDistance);
1408     Q_ASSERT(isClip(clipId));
1409     Q_ASSERT(isTrack(trackId));
1410     int currentPos = m_editMode == TimelineMode::NormalEdit ? getClipPosition(clipId) : m_allClips[clipId]->getFakePosition();
1411     int offset = m_editMode == TimelineMode::NormalEdit ? 0 : getClipPosition(clipId) - currentPos;
1412     int sourceTrackId = (m_editMode != TimelineMode::NormalEdit) ? m_allClips[clipId]->getFakeTrackId() : getClipTrackId(clipId);
1413     if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
1414         // Trying move on incompatible track type, stay on same track
1415         trackId = sourceTrackId;
1416     }
1417     if (currentPos == position && sourceTrackId == trackId) {
1418         TRACE_RES(position);
1419         return {position, trackId};
1420     }
1421     if (m_editMode == TimelineMode::InsertEdit) {
1422         int maxPos = getTrackById_const(trackId)->trackDuration();
1423         if (m_allClips[clipId]->getCurrentTrackId() == trackId) {
1424             maxPos -= m_allClips[clipId]->getPlaytime();
1425         }
1426         position = qMin(position, maxPos);
1427     }
1428     bool after = position > currentPos;
1429     if (snapDistance > 0) {
1430         std::vector<int> ignored_pts;
1431         // For snapping, we must ignore all in/outs of the clips of the group being moved
1432         std::unordered_set<int> all_items = {clipId};
1433         if (m_groups->isInGroup(clipId)) {
1434             int groupId = m_groups->getRootId(clipId);
1435             all_items = m_groups->getLeaves(groupId);
1436         }
1437         for (int current_clipId : all_items) {
1438             if (isClip(current_clipId)) {
1439                 m_allClips[current_clipId]->allSnaps(ignored_pts, offset);
1440             } else if (isComposition(current_clipId) || isSubTitle(current_clipId)) {
1441                 // Composition
1442                 int in = getItemPosition(current_clipId) - offset;
1443                 ignored_pts.push_back(in);
1444                 ignored_pts.push_back(in + getItemPlaytime(current_clipId));
1445             }
1446         }
1447         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1448         if (snapped >= 0) {
1449             position = snapped;
1450         }
1451     }
1452     position = qMax(0, position);
1453     if (currentPos == position && sourceTrackId == trackId) {
1454         TRACE_RES(position);
1455         return {position, trackId};
1456     }
1457     bool isInGroup = m_groups->isInGroup(clipId);
1458     if (sourceTrackId == trackId) {
1459         // Same track move, check if there is a mix and limit move
1460         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(clipId);
1461         if (mixData.first.firstClipId > -1) {
1462             // Clip has start mix
1463             int clipDuration = m_allClips[clipId]->getPlaytime();
1464             // ensure we don't move into clip
1465             bool allowMove = false;
1466             if (isInGroup) {
1467                 // Check if in same group as clip mix
1468                 int groupId = m_groups->getRootId(clipId);
1469                 if (groupId == m_groups->getRootId(mixData.first.firstClipId)) {
1470                     allowMove = true;
1471                 }
1472             }
1473             if (!allowMove && position + clipDuration > mixData.first.firstClipInOut.first && position < mixData.first.firstClipInOut.second) {
1474                 // Abort move
1475                 return {currentPos, sourceTrackId};
1476             }
1477         }
1478         if (mixData.second.firstClipId > -1) {
1479             // Clip has end mix
1480             int clipDuration = m_allClips[clipId]->getPlaytime();
1481             bool allowMove = false;
1482             if (isInGroup) {
1483                 // Check if in same group as clip mix
1484                 int groupId = m_groups->getRootId(clipId);
1485                 if (groupId == m_groups->getRootId(mixData.second.secondClipId)) {
1486                     allowMove = true;
1487                 }
1488             }
1489             if (!allowMove && position + clipDuration > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) {
1490                 // Abort move
1491                 return {currentPos, sourceTrackId};
1492             }
1493         }
1494     }
1495     if (moveMirrorTracks && m_singleSelectionMode) {
1496         // If we are in single selection mode, only move active clip
1497         moveMirrorTracks = false;
1498     }
1499     // we check if move is possible
1500     bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false)
1501                                                              : requestFakeClipMove(clipId, trackId, position, true, false, false);
1502 
1503     if (possible) {
1504         TRACE_RES(position);
1505         if (m_editMode != TimelineMode::NormalEdit) {
1506             trackId = m_allClips[clipId]->getFakeTrackId();
1507         }
1508         return {position, trackId};
1509     }
1510     if (sourceTrackId == -1) {
1511         // not clear what to do here, if the current move doesn't work. We could try to find empty space, but it might end up being far away...
1512         TRACE_RES(currentPos);
1513         return {currentPos, -1};
1514     }
1515     // Find best possible move
1516     if (!isInGroup) {
1517         // Try same track move
1518         if (trackId != sourceTrackId && sourceTrackId != -1) {
1519             trackId = sourceTrackId;
1520             possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
1521             if (!possible) {
1522                 qWarning() << "can't move clip" << clipId << "on track" << trackId << "at" << position;
1523             } else {
1524                 TRACE_RES(position);
1525                 return {position, trackId};
1526             }
1527         }
1528 
1529         int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
1530         if (blank_length < INT_MAX) {
1531             if (after) {
1532                 position = currentPos + blank_length;
1533             } else {
1534                 position = currentPos - blank_length;
1535             }
1536         } else {
1537             TRACE_RES(currentPos);
1538             return {currentPos, sourceTrackId};
1539         }
1540         possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
1541         TRACE_RES(possible ? position : currentPos);
1542         if (possible) {
1543             return {position, trackId};
1544         }
1545         return {currentPos, sourceTrackId};
1546     }
1547     if (trackId != sourceTrackId) {
1548         // Try same track move
1549         possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false);
1550         if (possible) {
1551             return {position, sourceTrackId};
1552         }
1553         return {currentPos, sourceTrackId};
1554     }
1555     // find best pos for groups
1556     int groupId = m_groups->getRootId(clipId);
1557     std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
1558     QMap<int, int> trackPosition;
1559 
1560     // First pass, sort clips by track and keep only the first / last depending on move direction
1561     for (int current_clipId : all_items) {
1562         int clipTrack = getItemTrackId(current_clipId);
1563         if (clipTrack == -1) {
1564             continue;
1565         }
1566         int in = getItemPosition(current_clipId);
1567         if (trackPosition.contains(clipTrack)) {
1568             if (after) {
1569                 // keep only last clip position for track
1570                 int out = in + getItemPlaytime(current_clipId);
1571                 if (trackPosition.value(clipTrack) < out) {
1572                     trackPosition.insert(clipTrack, out);
1573                 }
1574             } else {
1575                 // keep only first clip position for track
1576                 if (trackPosition.value(clipTrack) > in) {
1577                     trackPosition.insert(clipTrack, in);
1578                 }
1579             }
1580         } else {
1581             trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
1582         }
1583     }
1584 
1585     // Now check space on each track
1586     QMapIterator<int, int> i(trackPosition);
1587     int blank_length = 0;
1588     while (i.hasNext()) {
1589         i.next();
1590         int track_space;
1591         if (!after) {
1592             // Check space before the position
1593             if (isSubtitleTrack(i.key())) {
1594                 track_space = i.value();
1595             } else {
1596                 track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1);
1597             }
1598             if (blank_length == 0 || blank_length > track_space) {
1599                 blank_length = track_space;
1600             }
1601         } else {
1602             // Check space after the position
1603             if (isSubtitleTrack(i.key())) {
1604                 continue;
1605             }
1606             track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value();
1607             if (blank_length == 0 || blank_length > track_space) {
1608                 blank_length = track_space;
1609             }
1610         }
1611     }
1612     if (snapDistance > 0) {
1613         if (blank_length > 10 * snapDistance) {
1614             blank_length = 0;
1615         }
1616     } else if (blank_length / pCore->getProjectProfile().fps() > 5) {
1617         blank_length = 0;
1618     }
1619     if (blank_length != 0) {
1620         int updatedPos = currentPos + (after ? blank_length : -blank_length);
1621         possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false);
1622         if (possible) {
1623             TRACE_RES(updatedPos);
1624             return {updatedPos, trackId};
1625         }
1626     }
1627     TRACE_RES(currentPos);
1628     return {currentPos, sourceTrackId};
1629 }
1630 
1631 QVariantList TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
1632 {
1633     QWriteLocker locker(&m_lock);
1634     TRACE(compoId, trackId, position, cursorPosition, snapDistance);
1635     Q_ASSERT(isComposition(compoId));
1636     Q_ASSERT(isTrack(trackId));
1637     int currentPos = getCompositionPosition(compoId);
1638     int currentTrack = getCompositionTrackId(compoId);
1639     if (getTrackById_const(trackId)->isAudioTrack()) {
1640         // Trying move on incompatible track type, stay on same track
1641         trackId = currentTrack;
1642     }
1643     if (currentPos == position && currentTrack == trackId) {
1644         TRACE_RES(position);
1645         return {position, trackId};
1646     }
1647 
1648     if (snapDistance > 0) {
1649         // For snapping, we must ignore all in/outs of the clips of the group being moved
1650         std::vector<int> ignored_pts;
1651         if (m_groups->isInGroup(compoId)) {
1652             int groupId = m_groups->getRootId(compoId);
1653             auto all_items = m_groups->getLeaves(groupId);
1654             for (int current_compoId : all_items) {
1655                 // TODO: fix for composition
1656                 int in = getItemPosition(current_compoId);
1657                 ignored_pts.push_back(in);
1658                 ignored_pts.push_back(in + getItemPlaytime(current_compoId));
1659             }
1660         } else {
1661             int in = currentPos;
1662             int out = in + getCompositionPlaytime(compoId);
1663             ignored_pts.push_back(in);
1664             ignored_pts.push_back(out);
1665         }
1666         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1667         if (snapped >= 0) {
1668             position = snapped;
1669         }
1670     }
1671     position = qMax(0, position);
1672     if (currentPos == position && currentTrack == trackId) {
1673         TRACE_RES(position);
1674         return {position, trackId};
1675     }
1676     // we check if move is possible
1677     bool possible = requestCompositionMove(compoId, trackId, position, true, false);
1678     if (possible) {
1679         TRACE_RES(position);
1680         return {position, trackId};
1681     }
1682     TRACE_RES(currentPos);
1683     return {currentPos, currentTrack};
1684 }
1685 
1686 bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, int audioStream, double speed, bool warp_pitch,
1687                                         Fun &undo, Fun &redo)
1688 {
1689     QString bid = binClipId;
1690     if (binClipId.contains(QLatin1Char('/'))) {
1691         bid = binClipId.section(QLatin1Char('/'), 0, 0);
1692     }
1693     if (!pCore->projectItemModel()->hasClip(bid)) {
1694         qWarning() << "master clip not found";
1695         return false;
1696     }
1697     std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
1698     if (!master->statusReady() || !master->isCompatible(state)) {
1699         qWarning() << "clip not ready or not compatible" << state << master->statusReady();
1700         return false;
1701     }
1702     int clipId = TimelineModel::getNextId();
1703     id = clipId;
1704     qDebug() << "======\nCREATING TIMELINE OBJECT: " << clipId << "\n========================";
1705     Fun local_undo = deregisterClip_lambda(clipId);
1706     ClipModel::construct(shared_from_this(), bid, clipId, state, audioStream, speed, warp_pitch);
1707     auto clip = m_allClips[clipId];
1708     Fun local_redo = [clip, this, state, audioStream, speed, warp_pitch]() {
1709         // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
1710         // sufficient to register it.
1711         registerClip(clip, true);
1712         clip->refreshProducerFromBin(-1, state, audioStream, speed, warp_pitch);
1713         return true;
1714     };
1715 
1716     if (binClipId.contains(QLatin1Char('/'))) {
1717         int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt();
1718         int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt();
1719         int initLength = m_allClips[clipId]->getPlaytime();
1720         bool res = true;
1721         if (in != 0) {
1722             initLength -= in;
1723             res = requestItemResize(clipId, initLength, false, true, local_undo, local_redo);
1724         }
1725         int updatedDuration = out - in + 1; // +1: e.g. in=100, out=101 is 2 frames long
1726         res = res && requestItemResize(clipId, updatedDuration, true, true, local_undo, local_redo);
1727         if (!res) {
1728             bool undone = local_undo();
1729             Q_ASSERT(undone);
1730             return false;
1731         }
1732     }
1733     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1734     return true;
1735 }
1736 
1737 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets)
1738 {
1739     QWriteLocker locker(&m_lock);
1740     TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
1741     Fun undo = []() { return true; };
1742     Fun redo = []() { return true; };
1743     QVector<int> allowedTracks;
1744     if (useTargets) {
1745         auto it = m_allTracks.cbegin();
1746         while (it != m_allTracks.cend()) {
1747             int target_track = (*it)->getId();
1748             if (getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
1749                 allowedTracks << target_track;
1750             }
1751             ++it;
1752         }
1753     }
1754     if (useTargets && allowedTracks.isEmpty()) {
1755         pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage, 500);
1756         return false;
1757     }
1758     bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo, allowedTracks);
1759     if (result && logUndo) {
1760         PUSH_UNDO(undo, redo, i18n("Insert Clip"));
1761     }
1762     TRACE_RES(result);
1763     return result;
1764 }
1765 
1766 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
1767                                          Fun &undo, Fun &redo, const QVector<int> &allowedTracks)
1768 {
1769     Fun local_undo = []() { return true; };
1770     Fun local_redo = []() { return true; };
1771     bool res = false;
1772     ClipType::ProducerType type = ClipType::Unknown;
1773     // binClipId id is in the form: A2/10/50
1774     // A2 means audio only insertion for bin clip with id 2
1775     // 10 is in point
1776     // 50 is out point
1777     QString binIdWithInOut = binClipId;
1778     // bid is the A2 part
1779     QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
1780     // dropType indicates if we want a normal drop (disabled), audio only or video only drop
1781     PlaylistState::ClipState dropType = PlaylistState::Disabled;
1782     if (bid.startsWith(QLatin1Char('A'))) {
1783         dropType = PlaylistState::AudioOnly;
1784         bid.remove(0, 1);
1785         binIdWithInOut.remove(0, 1);
1786     } else if (bid.startsWith(QLatin1Char('V'))) {
1787         dropType = PlaylistState::VideoOnly;
1788         bid.remove(0, 1);
1789         binIdWithInOut.remove(0, 1);
1790     }
1791     if (!pCore->projectItemModel()->hasClip(bid)) {
1792         qWarning() << "no clip found in bin for" << bid;
1793         return false;
1794     }
1795 
1796     bool audioDrop = false;
1797     if (!useTargets) {
1798         audioDrop = getTrackById_const(trackId)->isAudioTrack();
1799         if (audioDrop) {
1800             if (dropType == PlaylistState::VideoOnly) {
1801                 return false;
1802             }
1803         } else if (dropType == PlaylistState::AudioOnly) {
1804             return false;
1805         }
1806     }
1807 
1808     std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
1809     type = master->clipType();
1810     // Ensure we don't insert a timeline clip onto itself
1811     if (type == ClipType::Timeline && !master->canBeDropped(m_uuid)) {
1812         // Abort insert
1813         pCore->displayMessage(i18n("You cannot insert a sequence containing itself"), ErrorMessage);
1814         return false;
1815     }
1816     if (useTargets && m_audioTarget.isEmpty() && m_videoTarget == -1) {
1817         useTargets = false;
1818     }
1819     if ((dropType == PlaylistState::Disabled || dropType == PlaylistState::AudioOnly) &&
1820         (type == ClipType::AV || type == ClipType::Playlist || type == ClipType::Timeline)) {
1821         bool useAudioTarget = false;
1822         if (useTargets && !m_audioTarget.isEmpty() && m_videoTarget == -1) {
1823             // If audio target is set but no video target, only insert audio
1824             useAudioTarget = true;
1825         } else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
1826             // Video target set but locked
1827             useAudioTarget = true;
1828         }
1829         if (useAudioTarget) {
1830             // Find first possible audio target
1831             QList<int> audioTargetTracks = m_audioTarget.keys();
1832             trackId = -1;
1833             for (int tid : qAsConst(audioTargetTracks)) {
1834                 if (tid > -1 && !getTrackById_const(tid)->isLocked() && allowedTracks.contains(tid)) {
1835                     trackId = tid;
1836                     break;
1837                 }
1838             }
1839         }
1840         if (trackId == -1) {
1841             if (!allowedTracks.isEmpty()) {
1842                 // No active tracks, aborting
1843                 return true;
1844             }
1845             pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1846             return false;
1847         }
1848         int audioStream = -1;
1849         QList<int> keys = m_binAudioTargets.keys();
1850         if (!useTargets) {
1851             // Drag and drop, calculate target tracks
1852             if (audioDrop) {
1853                 if (keys.count() > 1) {
1854                     // Dropping a clip with several audio streams
1855                     int tracksBelow = getLowerTracksId(trackId, TrackType::AudioTrack).count();
1856                     if (tracksBelow < keys.count() - 1) {
1857                         // We don't have enough audio tracks below, check above
1858                         QList<int> audioTrackIds = getTracksIds(true);
1859                         if (audioTrackIds.count() < keys.count()) {
1860                             // Not enough audio tracks
1861                             pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage);
1862                             return false;
1863                         }
1864                         trackId = audioTrackIds.at(audioTrackIds.count() - keys.count());
1865                     }
1866                 }
1867                 if (keys.isEmpty()) {
1868                     pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1869                     return false;
1870                 }
1871                 audioStream = keys.first();
1872             } else {
1873                 // Dropping video, ensure we have enough audio tracks for its streams
1874                 int mirror = getMirrorTrackId(trackId);
1875                 QList<int> audioTids = {};
1876                 if (mirror > -1) {
1877                     audioTids = getLowerTracksId(mirror, TrackType::AudioTrack);
1878                 }
1879                 if (audioTids.count() < keys.count() - 1 || (mirror == -1 && !keys.isEmpty())) {
1880                     // Check if project has enough audio tracks
1881                     if (keys.count() > getTracksIds(true).count()) {
1882                         // Not enough audio tracks in the project
1883                         pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage);
1884                         return false;
1885                     } else {
1886                         // Check if all audio tracks are locked. In that case allow inserting video only
1887                         QList<int> audioTracks = getTracksIds(true);
1888                         auto is_unlocked = [&](int tid) { return !getTrackById_const(tid)->isLocked(); };
1889                         bool hasUnlockedAudio = std::any_of(audioTracks.begin(), audioTracks.end(), is_unlocked);
1890                         if (hasUnlockedAudio) {
1891                             pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1892                             return false;
1893                         } else {
1894                             keys.clear();
1895                         }
1896                     }
1897                 }
1898             }
1899         } else if (audioDrop) {
1900             // Drag & drop, use our first audio target
1901             audioStream = m_audioTarget.first();
1902         } else {
1903             // Using target tracks
1904             if (m_audioTarget.contains(trackId)) {
1905                 audioStream = m_audioTarget.value(trackId);
1906             }
1907         }
1908 
1909         res = requestClipCreation(binIdWithInOut, id, getTrackById_const(trackId)->trackType(), audioStream, 1.0, false, local_undo, local_redo);
1910         res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
1911         // Get mirror track
1912         int mirror = dropType == PlaylistState::Disabled ? getMirrorTrackId(trackId) : -1;
1913         if (mirror > -1 && getTrackById_const(mirror)->isLocked() && !useTargets) {
1914             mirror = -1;
1915         }
1916         QList<int> target_track;
1917         if (audioDrop) {
1918             if (m_videoTarget > -1 && !getTrackById_const(m_videoTarget)->isLocked() && dropType != PlaylistState::AudioOnly) {
1919                 target_track << m_videoTarget;
1920             }
1921         } else if (useTargets) {
1922             QList<int> targetIds = m_audioTarget.keys();
1923             targetIds.removeAll(trackId);
1924             for (int &ix : targetIds) {
1925                 if (!getTrackById_const(ix)->isLocked() && allowedTracks.contains(ix)) {
1926                     target_track << ix;
1927                 }
1928             }
1929         }
1930 
1931         bool canMirrorDrop = !useTargets && ((mirror > -1 && (audioDrop || !keys.isEmpty())) || keys.count() > 1);
1932         QMap<int, int> dropTargets;
1933         if (res && (canMirrorDrop || !target_track.isEmpty()) && master->hasAudioAndVideo()) {
1934             if (!useTargets) {
1935                 int streamsCount = 0;
1936                 target_track.clear();
1937                 QList<int> audioTids;
1938                 if (!audioDrop) {
1939                     // insert audio mirror track
1940                     if (mirror > -1) {
1941                         target_track << mirror;
1942                         audioTids = getLowerTracksId(mirror, TrackType::AudioTrack);
1943                     }
1944                 } else {
1945                     audioTids = getLowerTracksId(trackId, TrackType::AudioTrack);
1946                 }
1947                 // First audio stream already inserted in target_track or in timeline
1948                 streamsCount = m_binAudioTargets.count() - 1;
1949                 while (streamsCount > 0 && !audioTids.isEmpty()) {
1950                     target_track << audioTids.takeFirst();
1951                     streamsCount--;
1952                 }
1953                 QList<int> aTargets = m_binAudioTargets.keys();
1954                 if (audioDrop) {
1955                     aTargets.removeAll(audioStream);
1956                 }
1957                 std::sort(aTargets.begin(), aTargets.end());
1958                 for (int i = 0; i < target_track.count() && i < aTargets.count(); ++i) {
1959                     dropTargets.insert(target_track.at(i), aTargets.at(i));
1960                 }
1961                 if (audioDrop && mirror > -1) {
1962                     target_track << mirror;
1963                 }
1964             }
1965             if (target_track.isEmpty() && useTargets) {
1966                 // No available track for splitting, abort
1967                 pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
1968                 res = false;
1969             }
1970             if (!res) {
1971                 bool undone = local_undo();
1972                 Q_ASSERT(undone);
1973                 id = -1;
1974                 return false;
1975             }
1976             // Process all mirror insertions
1977             std::function<bool(void)> audio_undo = []() { return true; };
1978             std::function<bool(void)> audio_redo = []() { return true; };
1979             std::unordered_set<int> createdMirrors = {id};
1980             int mirrorAudioStream = -1;
1981             for (int &target_ix : target_track) {
1982                 bool currentDropIsAudio = !audioDrop;
1983                 if (!useTargets && m_binAudioTargets.count() > 1 && dropTargets.contains(target_ix)) {
1984                     // Audio clip dropped first but has other streams
1985                     currentDropIsAudio = true;
1986                     mirrorAudioStream = dropTargets.value(target_ix);
1987                     if (mirrorAudioStream == audioStream) {
1988                         continue;
1989                     }
1990                 } else if (currentDropIsAudio) {
1991                     if (!useTargets) {
1992                         mirrorAudioStream = dropTargets.value(target_ix);
1993                     } else {
1994                         mirrorAudioStream = m_audioTarget.value(target_ix);
1995                     }
1996                 }
1997                 int newId;
1998                 res = requestClipCreation(binIdWithInOut, newId, currentDropIsAudio ? PlaylistState::AudioOnly : PlaylistState::VideoOnly,
1999                                           currentDropIsAudio ? mirrorAudioStream : -1, 1.0, false, audio_undo, audio_redo);
2000                 if (res) {
2001                     res = requestClipMove(newId, target_ix, position, true, true, true, true, audio_undo, audio_redo);
2002                     // use lazy evaluation to group only if move was successful
2003                     if (!res) {
2004                         pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
2005                         bool undone = audio_undo();
2006                         Q_ASSERT(undone);
2007                         break;
2008                     } else {
2009                         createdMirrors.insert(newId);
2010                     }
2011                 } else {
2012                     pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
2013                     bool undone = audio_undo();
2014                     Q_ASSERT(undone);
2015                     break;
2016                 }
2017             }
2018             if (res) {
2019                 requestClipsGroup(createdMirrors, audio_undo, audio_redo, GroupType::AVSplit);
2020                 UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
2021             }
2022         }
2023     } else {
2024         std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(bid);
2025         if (dropType == PlaylistState::Disabled) {
2026             dropType = getTrackById_const(trackId)->trackType();
2027         } else if (dropType != getTrackById_const(trackId)->trackType()) {
2028             return false;
2029         }
2030         QString normalisedBinId = binClipId;
2031         if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
2032             normalisedBinId.remove(0, 1);
2033         }
2034         res = requestClipCreation(normalisedBinId, id, dropType, binClip->getProducerIntProperty(QStringLiteral("audio_index")), 1.0, false, local_undo,
2035                                   local_redo);
2036         res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
2037     }
2038     if (!res) {
2039         bool undone = local_undo();
2040         Q_ASSERT(undone);
2041         id = -1;
2042         return false;
2043     }
2044     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
2045     return true;
2046 }
2047 
2048 bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo, bool logUndo)
2049 {
2050     QWriteLocker locker(&m_lock);
2051     if (m_groups->isInGroup(itemId)) {
2052         return requestGroupDeletion(itemId, undo, redo);
2053     }
2054     if (isClip(itemId)) {
2055         return requestClipDeletion(itemId, undo, redo, logUndo);
2056     }
2057     if (isComposition(itemId)) {
2058         return requestCompositionDeletion(itemId, undo, redo);
2059     }
2060     if (isSubTitle(itemId)) {
2061         return requestSubtitleDeletion(itemId, undo, redo, true, true);
2062     }
2063     Q_ASSERT(false);
2064     return false;
2065 }
2066 
2067 bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
2068 {
2069     QWriteLocker locker(&m_lock);
2070     TRACE(itemId, logUndo);
2071     Q_ASSERT(isItem(itemId));
2072     QString actionLabel;
2073     if (m_groups->isInGroup(itemId) && !m_singleSelectionMode) {
2074         actionLabel = i18n("Remove group");
2075     } else {
2076         if (isClip(itemId)) {
2077             actionLabel = i18n("Delete Clip");
2078         } else if (isComposition(itemId)) {
2079             actionLabel = i18n("Delete Composition");
2080         } else if (isSubTitle(itemId)) {
2081             actionLabel = i18n("Delete Subtitle");
2082         }
2083     }
2084     Fun undo = []() { return true; };
2085     Fun redo = []() { return true; };
2086 
2087     bool res = true;
2088     if (m_singleSelectionMode) {
2089         auto selection = getCurrentSelection();
2090         // Ungroup all items first
2091         for (int id : selection) {
2092             // Ungroup item before deletion
2093             requestRemoveFromGroup(id, undo, redo);
2094         }
2095         // loop deletion
2096         for (int id : selection) {
2097             res = res && requestItemDeletion(id, undo, redo, logUndo);
2098             ;
2099         }
2100     } else {
2101         res = requestItemDeletion(itemId, undo, redo, logUndo);
2102     }
2103     if (res && logUndo) {
2104         PUSH_UNDO(undo, redo, actionLabel);
2105     }
2106     TRACE_RES(res);
2107     return res;
2108 }
2109 
2110 bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo, bool logUndo)
2111 {
2112     int trackId = getClipTrackId(clipId);
2113     if (trackId != -1) {
2114         bool res = true;
2115         if (getTrackById_const(trackId)->hasStartMix(clipId)) {
2116             MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).first;
2117             if (isClip(mixData.firstClipId)) {
2118                 res = getTrackById(trackId)->requestRemoveMix({mixData.firstClipId, clipId}, undo, redo);
2119             }
2120         }
2121         if (getTrackById_const(trackId)->hasEndMix(clipId)) {
2122             MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).second;
2123             if (isClip(mixData.secondClipId)) {
2124                 res = getTrackById(trackId)->requestRemoveMix({clipId, mixData.secondClipId}, undo, redo);
2125             }
2126         }
2127         res = res && getTrackById(trackId)->requestClipDeletion(clipId, true, logUndo && !m_closing, undo, redo, false, true);
2128         if (!res) {
2129             undo();
2130             return false;
2131         }
2132     }
2133     auto operation = deregisterClip_lambda(clipId);
2134     auto clip = m_allClips[clipId];
2135     Fun reverse = [this, clip]() {
2136         // We capture a shared_ptr to the clip, which means that as long as this undo object lives,
2137         // the clip object is not deleted. To insert it back it is sufficient to register it.
2138         registerClip(clip, true);
2139         return true;
2140     };
2141     if (operation()) {
2142         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2143         return true;
2144     }
2145     undo();
2146     return false;
2147 }
2148 
2149 bool TimelineModel::requestSubtitleDeletion(int clipId, Fun &undo, Fun &redo, bool first, bool last)
2150 {
2151     GenTime startTime = m_allSubtitles.at(clipId);
2152     SubtitledTime sub = m_subtitleModel->getSubtitle(startTime);
2153     Fun operation = [this, clipId, last]() { return m_subtitleModel->removeSubtitle(clipId, false, last); };
2154     GenTime start = sub.start();
2155     GenTime end = sub.end();
2156     QString text = sub.subtitle();
2157     Fun reverse = [this, clipId, start, end, text, first]() { return m_subtitleModel->addSubtitle(clipId, start, end, text, false, first); };
2158     if (operation()) {
2159         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2160         return true;
2161     }
2162     return false;
2163 }
2164 
2165 bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo)
2166 {
2167     int trackId = getCompositionTrackId(compositionId);
2168     if (trackId != -1) {
2169         bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true);
2170         if (!res) {
2171             undo();
2172             return false;
2173         } else {
2174             Fun unplant_op = [this, compositionId]() {
2175                 unplantComposition(compositionId);
2176                 return true;
2177             };
2178             unplant_op();
2179             PUSH_LAMBDA(unplant_op, redo);
2180         }
2181     }
2182     Fun operation = deregisterComposition_lambda(compositionId);
2183     auto composition = m_allCompositions[compositionId];
2184     int new_in = composition->getPosition();
2185     int new_out = new_in + composition->getPlaytime();
2186     Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() {
2187         // We capture a shared_ptr to the composition, which means that as long as this undo object lives,
2188         // the composition object is not deleted. To insert it back it is sufficient to register it.
2189         registerComposition(composition);
2190         composition->setCurrentTrackId(trackId, true);
2191         replantCompositions(compositionId, false);
2192         checkRefresh(new_in, new_out);
2193         return true;
2194     };
2195     if (operation()) {
2196         Fun update_monitor = [this, new_in, new_out]() {
2197             checkRefresh(new_in, new_out);
2198             return true;
2199         };
2200         update_monitor();
2201         PUSH_LAMBDA(update_monitor, operation);
2202         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2203         return true;
2204     }
2205     undo();
2206     return false;
2207 }
2208 
2209 std::unordered_set<int> TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions)
2210 {
2211     Q_UNUSED(listCompositions)
2212 
2213     std::unordered_set<int> allClips;
2214     if (isSubtitleTrack(trackId) || trackId == -1) {
2215         // Subtitles
2216         if (m_subtitleModel) {
2217             std::unordered_set<int> subs = m_subtitleModel->getItemsInRange(start, end);
2218             allClips.insert(subs.begin(), subs.end());
2219         }
2220     }
2221     if (trackId == -1) {
2222         for (const auto &track : m_allTracks) {
2223             if (track->isLocked()) {
2224                 continue;
2225             }
2226             std::unordered_set<int> clipTracks = getItemsInRange(track->getId(), start, end, listCompositions);
2227             allClips.insert(clipTracks.begin(), clipTracks.end());
2228         }
2229     } else if (trackId >= 0) {
2230         std::unordered_set<int> clipTracks = getTrackById(trackId)->getClipsInRange(start, end);
2231         allClips.insert(clipTracks.begin(), clipTracks.end());
2232         if (listCompositions) {
2233             std::unordered_set<int> compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end);
2234             allClips.insert(compoTracks.begin(), compoTracks.end());
2235         }
2236     }
2237     return allClips;
2238 }
2239 
2240 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
2241 {
2242     TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
2243     std::function<bool(void)> undo = []() { return true; };
2244     std::function<bool(void)> redo = []() { return true; };
2245     bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
2246     if (res && logUndo) {
2247         PUSH_UNDO(undo, redo, i18n("Move group"));
2248     }
2249     TRACE_RES(res);
2250     return res;
2251 }
2252 
2253 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
2254                                          bool allowViewRefresh)
2255 {
2256     Q_UNUSED(updateView);
2257     Q_UNUSED(finalMove);
2258     Q_UNUSED(undo);
2259     Q_UNUSED(redo);
2260     Q_UNUSED(allowViewRefresh);
2261     QWriteLocker locker(&m_lock);
2262     Q_ASSERT(m_allGroups.count(groupId) > 0);
2263     bool ok = true;
2264     auto all_items = m_groups->getLeaves(groupId);
2265     Q_ASSERT(all_items.size() > 1);
2266     Fun local_undo = []() { return true; };
2267     Fun local_redo = []() { return true; };
2268 
2269     // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
2270     // This way, we ensure that no conflict will arise with clips inside the group being moved
2271 
2272     // Check if there is a track move
2273 
2274     // First, remove clips
2275     bool hasAudio = false;
2276     bool hasVideo = false;
2277     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track;
2278     for (int item : all_items) {
2279         int old_trackId = getItemTrackId(item);
2280         old_track_ids[item] = old_trackId;
2281         if (old_trackId != -1) {
2282             if (isClip(item)) {
2283                 old_position[item] = m_allClips[item]->getPosition();
2284                 if (!hasAudio && getTrackById_const(old_trackId)->isAudioTrack()) {
2285                     hasAudio = true;
2286                 } else if (!hasVideo && !getTrackById_const(old_trackId)->isAudioTrack()) {
2287                     hasVideo = true;
2288                 }
2289             } else {
2290                 hasVideo = true;
2291                 old_position[item] = m_allCompositions[item]->getPosition();
2292                 old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
2293             }
2294         }
2295     }
2296 
2297     // Second step, calculate delta
2298     int audio_delta, video_delta;
2299     audio_delta = video_delta = delta_track;
2300 
2301     if (getTrackById(old_track_ids[clipId])->isAudioTrack()) {
2302         // Master clip is audio, so reverse delta for video clips
2303         if (hasAudio) {
2304             video_delta = -delta_track;
2305         } else {
2306             video_delta = 0;
2307         }
2308     } else {
2309         if (hasVideo) {
2310             audio_delta = -delta_track;
2311         } else {
2312             audio_delta = 0;
2313         }
2314     }
2315     bool trackChanged = false;
2316     if (delta_track != 0) {
2317         // Ensure the track move is possible (not outside our current tracks)
2318         for (int item : all_items) {
2319             int current_track_id = old_track_ids[item];
2320             int current_track_position = getTrackPosition(current_track_id);
2321             bool audioTrack = getTrackById_const(current_track_id)->isAudioTrack();
2322             int d = audioTrack ? audio_delta : video_delta;
2323             int target_track_position = current_track_position + d;
2324             bool brokenMove = target_track_position < 0 || target_track_position >= getTracksCount();
2325             if (!brokenMove) {
2326                 int target_id = getTrackIndexFromPosition(target_track_position);
2327                 brokenMove = audioTrack != getTrackById_const(target_id)->isAudioTrack();
2328             }
2329             if (brokenMove) {
2330                 if (isClip(item)) {
2331                     int lastTid = m_allClips[item]->getFakeTrackId();
2332                     int originalTid = m_allClips[item]->getCurrentTrackId();
2333                     int last_position = getTrackPosition(lastTid);
2334                     int original_position = getTrackPosition(originalTid);
2335                     int lastDelta = last_position - original_position;
2336                     if (audioTrack) {
2337                         if (qAbs(audio_delta) > qAbs(lastDelta)) {
2338                             audio_delta = lastDelta;
2339                         }
2340                         if (video_delta != 0) {
2341                             video_delta = -lastDelta;
2342                         }
2343                     } else {
2344                         if (qAbs(video_delta) > qAbs(lastDelta)) {
2345                             video_delta = lastDelta;
2346                         }
2347                         if (audio_delta != 0) {
2348                             audio_delta = -lastDelta;
2349                         }
2350                     }
2351                 };
2352             }
2353         }
2354     }
2355 
2356     // Reverse sort. We need to insert from left to right to avoid confusing the view
2357     for (int item : all_items) {
2358         int current_track_id = old_track_ids[item];
2359         int current_track_position = getTrackPosition(current_track_id);
2360         int d = getTrackById_const(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2361         int target_track_position = current_track_position + d;
2362         if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2363             auto it = m_allTracks.cbegin();
2364             std::advance(it, target_track_position);
2365             int target_track = (*it)->getId();
2366             int target_position = old_position[item] + delta_pos;
2367             if (isClip(item)) {
2368                 m_allClips[item]->setFakePosition(target_position);
2369                 if (m_allClips[item]->getFakeTrackId() != target_track) {
2370                     trackChanged = true;
2371                 }
2372                 m_allClips[item]->setFakeTrackId(target_track);
2373             } else {
2374             }
2375         } else {
2376             ok = false;
2377         }
2378         if (!ok) {
2379             bool undone = local_undo();
2380             Q_ASSERT(undone);
2381             return false;
2382         }
2383     }
2384     QModelIndex modelIndex;
2385     QVector<int> roles{FakePositionRole};
2386     if (trackChanged) {
2387         roles << FakeTrackIdRole;
2388     }
2389     for (int item : all_items) {
2390         if (isClip(item)) {
2391             modelIndex = makeClipIndexFromID(item);
2392         } else {
2393             modelIndex = makeCompositionIndexFromID(item);
2394         }
2395         notifyChange(modelIndex, modelIndex, roles);
2396     }
2397     return true;
2398 }
2399 
2400 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo,
2401                                      bool revertMove)
2402 {
2403     QWriteLocker locker(&m_lock);
2404     TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo);
2405     std::function<bool(void)> undo = []() { return true; };
2406     std::function<bool(void)> redo = []() { return true; };
2407     bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, revertMove, moveMirrorTracks);
2408     if (res && logUndo) {
2409         PUSH_UNDO(undo, redo, i18n("Move group"));
2410     }
2411     TRACE_RES(res);
2412     return res;
2413 }
2414 
2415 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
2416                                      bool revertMove, bool moveMirrorTracks, bool allowViewRefresh, const QVector<int> &allowedTracks)
2417 {
2418     QWriteLocker locker(&m_lock);
2419     Q_ASSERT(m_allGroups.count(groupId) > 0);
2420     Q_ASSERT(isItem(itemId));
2421     if (getGroupElements(groupId).count(itemId) == 0) {
2422         // this group doesn't contain the clip, abort
2423         return false;
2424     }
2425     bool ok = true;
2426     auto all_items = m_groups->getLeaves(groupId);
2427     Q_ASSERT(all_items.size() > 1);
2428     Fun local_undo = []() { return true; };
2429     Fun local_redo = []() { return true; };
2430     std::vector<std::pair<int, int>> sorted_clips;
2431     QVector<int> sorted_clips_ids;
2432     std::vector<std::pair<int, std::pair<int, int>>> sorted_compositions;
2433     std::vector<std::pair<int, GenTime>> sorted_subtitles;
2434     int lowerTrack = -1;
2435     int upperTrack = -1;
2436     QVector<int> tracksWithMix;
2437 
2438     // Separate clips from compositions to sort and check source tracks
2439     QMap<std::pair<int, int>, int> mixesToDelete;
2440     // Mixes might be deleted while moving clips to another track, so store them before attempting a move
2441     QMap<int, std::pair<MixInfo, MixInfo>> mixDataArray;
2442     for (int affectedItemId : all_items) {
2443         if (delta_track != 0 && !isSubTitle(affectedItemId)) {
2444             // Check if an upper / lower move is possible
2445             const int trackPos = getTrackPosition(getItemTrackId(affectedItemId));
2446             if (lowerTrack == -1 || lowerTrack > trackPos) {
2447                 lowerTrack = trackPos;
2448             }
2449             if (upperTrack == -1 || upperTrack < trackPos) {
2450                 upperTrack = trackPos;
2451             }
2452         }
2453         if (isClip(affectedItemId)) {
2454             sorted_clips.emplace_back(affectedItemId, m_allClips[affectedItemId]->getPosition());
2455             sorted_clips_ids.push_back(affectedItemId);
2456             int current_track_id = getClipTrackId(affectedItemId);
2457             // Check if we have a mix in the group
2458             if (getTrackById_const(current_track_id)->hasMix(affectedItemId)) {
2459                 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(current_track_id)->getMixInfo(affectedItemId);
2460                 mixDataArray.insert(affectedItemId, mixData);
2461                 if (delta_track != 0) {
2462                     if (mixData.first.firstClipId > -1 && all_items.find(mixData.first.firstClipId) == all_items.end()) {
2463                         // First part of the mix is not moving, delete start mix
2464                         mixesToDelete.insert({mixData.first.firstClipId, affectedItemId}, current_track_id);
2465                     }
2466                     if (mixData.second.firstClipId > -1 && all_items.find(mixData.second.secondClipId) == all_items.end()) {
2467                         // First part of the mix is not moving, delete start mix
2468                         mixesToDelete.insert({affectedItemId, mixData.second.secondClipId}, current_track_id);
2469                     }
2470                 } else if (!tracksWithMix.contains(current_track_id)) {
2471                     // There is a mix, prepare for update
2472                     tracksWithMix << current_track_id;
2473                 }
2474             }
2475         } else if (isComposition(affectedItemId)) {
2476             sorted_compositions.push_back(
2477                 {affectedItemId, {m_allCompositions[affectedItemId]->getPosition(), getTrackMltIndex(m_allCompositions[affectedItemId]->getCurrentTrackId())}});
2478         } else if (isSubTitle(affectedItemId)) {
2479             sorted_subtitles.emplace_back(affectedItemId, m_allSubtitles.at(affectedItemId));
2480         }
2481     }
2482 
2483     if (!sorted_subtitles.empty() && m_subtitleModel->isLocked()) {
2484         // Group with a locked subtitle, abort
2485         return false;
2486     }
2487 
2488     // Sort clips first
2489     std::sort(sorted_clips.begin(), sorted_clips.end(), [delta_pos](const std::pair<int, int> &clipId1, const std::pair<int, int> &clipId2) {
2490         return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second;
2491     });
2492 
2493     // Sort subtitles
2494     std::sort(sorted_subtitles.begin(), sorted_subtitles.end(), [delta_pos](const std::pair<int, GenTime> &clipId1, const std::pair<int, GenTime> &clipId2) {
2495         return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second;
2496     });
2497 
2498     // Sort compositions. We need to delete in the move direction from top to bottom
2499     std::sort(sorted_compositions.begin(), sorted_compositions.end(),
2500               [delta_track, delta_pos](const std::pair<int, std::pair<int, int>> &clipId1, const std::pair<int, std::pair<int, int>> &clipId2) {
2501                   const int p1 = delta_track < 0 ? clipId1.second.second : delta_track > 0 ? -clipId1.second.second : clipId1.second.first;
2502                   const int p2 = delta_track < 0 ? clipId2.second.second : delta_track > 0 ? -clipId2.second.second : clipId2.second.first;
2503                   return delta_track == 0 ? (delta_pos > 0 ? p2 < p1 : p1 < p2) : p1 < p2;
2504               });
2505 
2506     // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions.
2507     // This way, we ensure that no conflict will arise with clips inside the group being moved
2508 
2509     Fun update_model = [this, finalMove]() {
2510         if (finalMove) {
2511             updateDuration();
2512         }
2513         return true;
2514     };
2515     // Check that we don't move subtitles before 0
2516     if (!sorted_subtitles.empty() && sorted_subtitles.front().second.frames(pCore->getCurrentFps()) + delta_pos < 0) {
2517         delta_pos = -sorted_subtitles.front().second.frames(pCore->getCurrentFps());
2518         if (delta_pos == 0) {
2519             return false;
2520         }
2521     }
2522 
2523     // Check if there is a track move
2524     // Second step, reinsert clips at correct positions
2525     int audio_delta, video_delta;
2526     audio_delta = video_delta = delta_track;
2527     bool masterIsAudio = delta_track != 0 ? getTrackById_const(getItemTrackId(itemId))->isAudioTrack() : false;
2528     if (delta_track < 0) {
2529         if (!masterIsAudio) {
2530             // Case 1, dragging a video clip down
2531             bool lowerTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(lowerTrack))->isAudioTrack();
2532             int lowerPos = lowerTrackIsAudio ? lowerTrack - delta_track : lowerTrack + delta_track;
2533             if (lowerPos < 0) {
2534                 // No space below
2535                 delta_track = 0;
2536             } else if (!lowerTrackIsAudio) {
2537                 // Moving a group of video clips
2538                 if (getTrackById_const(getTrackIndexFromPosition(lowerPos))->isAudioTrack()) {
2539                     // Moving to a non matching track (video on audio track)
2540                     delta_track = 0;
2541                 }
2542             }
2543         } else if (lowerTrack + delta_track < 0) {
2544             // Case 2, dragging an audio clip down
2545             delta_track = 0;
2546         }
2547     } else if (delta_track > 0) {
2548         if (!masterIsAudio) {
2549             // Case 1, dragging a video clip up
2550             int upperPos = upperTrack + delta_track;
2551             if (upperPos >= getTracksCount()) {
2552                 // Moving above top track, not allowed
2553                 delta_track = 0;
2554             } else if (getTrackById_const(getTrackIndexFromPosition(upperPos))->isAudioTrack()) {
2555                 // Trying to move to a non matching track (video clip on audio track)
2556                 delta_track = 0;
2557             }
2558         } else {
2559             bool upperTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(upperTrack))->isAudioTrack();
2560             if (!upperTrackIsAudio) {
2561                 // Dragging an audio clip up, check that upper video clip has an available video track
2562                 int targetPos = upperTrack - delta_track;
2563                 if (moveMirrorTracks && (targetPos < 0 || getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack())) {
2564                     delta_track = 0;
2565                 }
2566             } else {
2567                 int targetPos = upperTrack + delta_track;
2568                 if (targetPos >= getTracksCount() || !getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack()) {
2569                     // Trying to drag audio above topmost track or on video track
2570                     delta_track = 0;
2571                 }
2572             }
2573         }
2574     }
2575     if (delta_track != 0 && !revertMove) {
2576         // Ensure destination tracks are empty
2577         for (const std::pair<int, int> &item : sorted_clips) {
2578             int currentTrack = getClipTrackId(item.first);
2579             int trackOffset = delta_track;
2580             // Adjust delta_track depending on master
2581             if (getTrackById_const(currentTrack)->isAudioTrack()) {
2582                 if (!masterIsAudio) {
2583                     trackOffset = -delta_track;
2584                 }
2585             } else if (masterIsAudio) {
2586                 trackOffset = -delta_track;
2587             }
2588             int newTrackPosition = getTrackPosition(currentTrack) + trackOffset;
2589             if (newTrackPosition < 0 || newTrackPosition >= int(m_allTracks.size())) {
2590                 if (!moveMirrorTracks && item.first != itemId) {
2591                     continue;
2592                 }
2593                 delta_track = 0;
2594                 break;
2595             }
2596             int newItemTrackId = getTrackIndexFromPosition(newTrackPosition);
2597             int newIn = item.second + delta_pos;
2598             if (!getTrackById_const(newItemTrackId)->isAvailableWithExceptions(newIn, getClipPlaytime(item.first) - 1, sorted_clips_ids)) {
2599                 if (!moveMirrorTracks && item.first != itemId) {
2600                     continue;
2601                 }
2602                 delta_track = 0;
2603                 break;
2604             }
2605         }
2606     }
2607     if (delta_track == 0 && delta_pos == 0) {
2608         return false;
2609     }
2610     bool updateSubtitles = updateView;
2611     if (delta_track == 0 && updateView) {
2612         updateView = false;
2613         allowViewRefresh = false;
2614         update_model = [sorted_clips, sorted_compositions, finalMove, this]() {
2615             QModelIndex modelIndex;
2616             QVector<int> roles{StartRole};
2617             for (const std::pair<int, int> &item : sorted_clips) {
2618                 modelIndex = makeClipIndexFromID(item.first);
2619                 notifyChange(modelIndex, modelIndex, roles);
2620             }
2621             for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2622                 modelIndex = makeCompositionIndexFromID(item.first);
2623                 notifyChange(modelIndex, modelIndex, roles);
2624             }
2625             if (finalMove) {
2626                 updateDuration();
2627             }
2628             return true;
2629         };
2630     }
2631 
2632     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track;
2633     QMap<int, int> oldTrackIds;
2634     // Check for mixes
2635     for (const std::pair<int, int> &item : sorted_clips) {
2636         // Keep track of old track for mixes
2637         oldTrackIds.insert(item.first, getClipTrackId(item.first));
2638     }
2639     // First delete mixes that have to
2640     if (finalMove && !mixesToDelete.isEmpty()) {
2641         QMapIterator<std::pair<int, int>, int> i(mixesToDelete);
2642         while (i.hasNext()) {
2643             i.next();
2644             // Delete mix
2645             getTrackById(i.value())->requestRemoveMix(i.key(), local_undo, local_redo);
2646         }
2647     }
2648 
2649     // First, remove clips
2650     if (delta_track != 0) {
2651         // We delete our clips only if changing track
2652         for (const std::pair<int, int> &item : sorted_clips) {
2653             int old_trackId = getClipTrackId(item.first);
2654             old_track_ids[item.first] = old_trackId;
2655             if (old_trackId != -1) {
2656                 bool updateThisView = allowViewRefresh;
2657                 ok = ok && getTrackById(old_trackId)->requestClipDeletion(item.first, updateThisView, finalMove, local_undo, local_redo, true, false);
2658                 old_position[item.first] = item.second;
2659                 if (!ok) {
2660                     bool undone = local_undo();
2661                     Q_ASSERT(undone);
2662                     return false;
2663                 }
2664             }
2665         }
2666         for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2667             int old_trackId = getCompositionTrackId(item.first);
2668             if (old_trackId != -1) {
2669                 old_track_ids[item.first] = old_trackId;
2670                 old_position[item.first] = item.second.first;
2671                 old_forced_track[item.first] = m_allCompositions[item.first]->getForcedTrack();
2672             }
2673         }
2674         if (masterIsAudio) {
2675             // Master clip is audio, so reverse delta for video clips
2676             video_delta = -delta_track;
2677         } else {
2678             audio_delta = -delta_track;
2679         }
2680     }
2681 
2682     Fun sync_mix = [this, tracksWithMix, finalMove]() {
2683         if (!finalMove) {
2684             return true;
2685         }
2686         for (int tid : tracksWithMix) {
2687             getTrackById_const(tid)->syncronizeMixes(finalMove);
2688         }
2689         return true;
2690     };
2691     // We need to insert depending on the move direction to avoid confusing the view
2692     // std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
2693     bool updateThisView = allowViewRefresh;
2694     if (delta_track == 0) {
2695         // Special case, we are moving on same track, avoid too many calculations
2696         // First pass, check for collisions and suggest better delta
2697         QVector<int> processedTracks;
2698         for (const std::pair<int, int> &item : sorted_clips) {
2699             int current_track_id = getClipTrackId(item.first);
2700             if (processedTracks.contains(current_track_id)) {
2701                 // We only check the first clip for each track since they are sorted depending on the move direction
2702                 continue;
2703             }
2704             processedTracks << current_track_id;
2705             if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2706                 continue;
2707             }
2708             int current_in = item.second;
2709             int playtime = getClipPlaytime(item.first);
2710             int target_position = current_in + delta_pos;
2711             int subPlaylist = -1;
2712             if (delta_pos < 0) {
2713                 if (getTrackById_const(current_track_id)->hasStartMix(item.first)) {
2714                     subPlaylist = m_allClips[item.first]->getSubPlaylistIndex();
2715                 }
2716                 if (!getTrackById_const(current_track_id)->isAvailable(target_position, qMin(qAbs(delta_pos), playtime), subPlaylist)) {
2717                     if (!getTrackById_const(current_track_id)->isBlankAt(current_in - 1)) {
2718                         // No move possible, abort
2719                         bool undone = local_undo();
2720                         Q_ASSERT(undone);
2721                         return false;
2722                     }
2723                     int newStart = getTrackById_const(current_track_id)->getBlankStart(current_in - 1, subPlaylist);
2724                     delta_pos = qMax(delta_pos, newStart - current_in);
2725                 }
2726             } else {
2727                 int moveEnd = target_position + playtime;
2728                 int moveStart = qMax(current_in + playtime, target_position);
2729                 if (getTrackById_const(current_track_id)->hasEndMix(item.first)) {
2730                     subPlaylist = m_allClips[item.first]->getSubPlaylistIndex();
2731                 }
2732                 if (!getTrackById_const(current_track_id)->isAvailable(moveStart, moveEnd - moveStart, subPlaylist)) {
2733                     int newStart = getTrackById_const(current_track_id)->getBlankEnd(current_in + playtime, subPlaylist);
2734                     if (newStart == current_in + playtime) {
2735                         // No move possible, abort
2736                         bool undone = local_undo();
2737                         Q_ASSERT(undone);
2738                         return false;
2739                     }
2740                     delta_pos = qMin(delta_pos, newStart - (current_in + playtime));
2741                 }
2742             }
2743         }
2744         PUSH_LAMBDA(sync_mix, local_undo);
2745         for (const std::pair<int, int> &item : sorted_clips) {
2746             int current_track_id = getClipTrackId(item.first);
2747             if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2748                 continue;
2749             }
2750             int current_in = item.second;
2751             int target_position = current_in + delta_pos;
2752             ok = requestClipMove(item.first, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo,
2753                                  revertMove, true, oldTrackIds,
2754                                  mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo, MixInfo>());
2755             if (!ok) {
2756                 qWarning() << "failed moving clip on track " << current_track_id;
2757                 break;
2758             }
2759         }
2760         if (ok) {
2761             sync_mix();
2762             PUSH_LAMBDA(sync_mix, local_redo);
2763             // Move compositions
2764             for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2765                 int current_track_id = getItemTrackId(item.first);
2766                 if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2767                     continue;
2768                 }
2769                 int current_in = item.second.first;
2770                 int target_position = current_in + delta_pos;
2771                 ok = requestCompositionMove(item.first, current_track_id, m_allCompositions[item.first]->getForcedTrack(), target_position, updateThisView,
2772                                             finalMove, local_undo, local_redo);
2773                 if (!ok) {
2774                     break;
2775                 }
2776             }
2777         }
2778         if (!ok) {
2779             bool undone = local_undo();
2780             Q_ASSERT(undone);
2781             return false;
2782         }
2783     } else {
2784         // Track changed
2785         PUSH_LAMBDA(sync_mix, local_undo);
2786         for (const std::pair<int, int> &item : sorted_clips) {
2787             int current_track_id = old_track_ids[item.first];
2788             int current_track_position = getTrackPosition(current_track_id);
2789             int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2790             if (!moveMirrorTracks && item.first != itemId) {
2791                 d = 0;
2792             }
2793             int target_track_position = current_track_position + d;
2794             if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2795                 auto it = m_allTracks.cbegin();
2796                 std::advance(it, target_track_position);
2797                 int target_track = (*it)->getId();
2798                 int target_position = old_position[item.first] + delta_pos;
2799                 ok = ok && requestClipMove(item.first, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo,
2800                                            local_redo, revertMove, true, oldTrackIds,
2801                                            mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo, MixInfo>());
2802             } else {
2803                 ok = false;
2804             }
2805             if (!ok) {
2806                 bool undone = local_undo();
2807                 Q_ASSERT(undone);
2808                 return false;
2809             }
2810         }
2811         sync_mix();
2812         PUSH_LAMBDA(sync_mix, local_redo);
2813         for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2814             int current_track_id = old_track_ids[item.first];
2815             int current_track_position = getTrackPosition(current_track_id);
2816             int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2817             int target_track_position = current_track_position + d;
2818 
2819             if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2820                 auto it = m_allTracks.cbegin();
2821                 std::advance(it, target_track_position);
2822                 int target_track = (*it)->getId();
2823                 int target_position = old_position[item.first] + delta_pos;
2824                 ok = ok && requestCompositionMove(item.first, target_track, old_forced_track[item.first], target_position, updateThisView, finalMove,
2825                                                   local_undo, local_redo);
2826             } else {
2827                 qWarning() << "aborting move tried on track" << target_track_position;
2828                 ok = false;
2829             }
2830             if (!ok) {
2831                 bool undone = local_undo();
2832                 Q_ASSERT(undone);
2833                 return false;
2834             }
2835         }
2836     }
2837     // Move subtitles
2838     if (!sorted_subtitles.empty()) {
2839         std::vector<std::pair<int, GenTime>>::iterator ptr;
2840         auto last = std::prev(sorted_subtitles.end());
2841 
2842         for (ptr = sorted_subtitles.begin(); ptr < sorted_subtitles.end(); ptr++) {
2843             ok = requestSubtitleMove((*ptr).first, (*ptr).second.frames(pCore->getCurrentFps()) + delta_pos, updateSubtitles, ptr == sorted_subtitles.begin(),
2844                                      ptr == last, finalMove, local_undo, local_redo);
2845             if (!ok) {
2846                 bool undone = local_undo();
2847                 Q_ASSERT(undone);
2848                 return false;
2849             }
2850         }
2851     }
2852     update_model();
2853     PUSH_LAMBDA(update_model, local_redo);
2854     PUSH_LAMBDA(update_model, local_undo);
2855     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
2856     return true;
2857 }
2858 
2859 bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
2860 {
2861     QWriteLocker locker(&m_lock);
2862     TRACE(clipId, logUndo);
2863     if (!m_groups->isInGroup(clipId)) {
2864         TRACE_RES(false);
2865         return false;
2866     }
2867     bool res = requestItemDeletion(clipId, logUndo);
2868     TRACE_RES(res);
2869     return res;
2870 }
2871 
2872 bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo)
2873 {
2874     // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves.
2875     std::queue<int> group_queue;
2876     group_queue.push(m_groups->getRootId(clipId));
2877     std::unordered_set<int> all_items;
2878     std::unordered_set<int> all_compositions;
2879     // Subtitles MUST BE SORTED BY REVERSE timeline id to preserve the qml model index on undo!!
2880     std::set<int> all_subtitles;
2881     while (!group_queue.empty()) {
2882         int current_group = group_queue.front();
2883         bool isSelection = m_currentSelection.count(current_group);
2884 
2885         group_queue.pop();
2886         Q_ASSERT(isGroup(current_group));
2887         auto children = m_groups->getDirectChildren(current_group);
2888         int one_child = -1; // we need the id on any of the indices of the elements of the group
2889         for (int c : children) {
2890             if (isClip(c)) {
2891                 all_items.insert(c);
2892                 one_child = c;
2893             } else if (isComposition(c)) {
2894                 all_compositions.insert(c);
2895                 one_child = c;
2896             } else if (isSubTitle(c)) {
2897                 all_subtitles.insert(c);
2898                 one_child = c;
2899             } else {
2900                 Q_ASSERT(isGroup(c));
2901                 one_child = c;
2902                 group_queue.push(c);
2903             }
2904         }
2905         if (one_child != -1) {
2906             if (m_groups->getType(current_group) == GroupType::Selection) {
2907                 Q_ASSERT(isSelection);
2908                 // in the case of a selection group, we delete the group but don't log it in the undo object
2909                 Fun tmp_undo = []() { return true; };
2910                 Fun tmp_redo = []() { return true; };
2911                 m_groups->ungroupItem(one_child, tmp_undo, tmp_redo);
2912             } else {
2913                 bool res = m_groups->ungroupItem(one_child, undo, redo);
2914                 if (!res) {
2915                     undo();
2916                     return false;
2917                 }
2918             }
2919         }
2920         if (isSelection) {
2921             requestClearSelection(true);
2922         }
2923     }
2924     for (int clip : all_items) {
2925         bool res = requestClipDeletion(clip, undo, redo);
2926         if (!res) {
2927             // Undo is processed in requestClipDeletion
2928             return false;
2929         }
2930     }
2931     for (int compo : all_compositions) {
2932         bool res = requestCompositionDeletion(compo, undo, redo);
2933         if (!res) {
2934             undo();
2935             return false;
2936         }
2937     }
2938     std::set<int>::reverse_iterator rit;
2939     for (rit = all_subtitles.rbegin(); rit != all_subtitles.rend(); ++rit) {
2940         bool res = requestSubtitleDeletion(*rit, undo, redo, rit == all_subtitles.rbegin(), rit == std::prev(all_subtitles.rend()));
2941         if (!res) {
2942             undo();
2943             return false;
2944         }
2945     }
2946     return true;
2947 }
2948 
2949 const QVariantList TimelineModel::getGroupData(int itemId)
2950 {
2951     QWriteLocker locker(&m_lock);
2952     if (!m_groups->isInGroup(itemId)) {
2953         return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)};
2954     }
2955     int groupId = m_groups->getRootId(itemId);
2956     QVariantList result;
2957     std::unordered_set<int> items = m_groups->getLeaves(groupId);
2958     for (int id : items) {
2959         result << id << getItemPosition(id) << getItemPlaytime(id);
2960     }
2961     return result;
2962 }
2963 
2964 void TimelineModel::processGroupResize(QVariantList startPosList, QVariantList endPosList, bool right)
2965 {
2966     Q_ASSERT(startPosList.size() == endPosList.size());
2967     QMap<int, QPair<int, int>> startData;
2968     QMap<int, QPair<int, int>> endData;
2969     while (!startPosList.isEmpty()) {
2970         int id = startPosList.takeFirst().toInt();
2971         int in = startPosList.takeFirst().toInt();
2972         int duration = startPosList.takeFirst().toInt();
2973         startData.insert(id, {in, duration});
2974         id = endPosList.takeFirst().toInt();
2975         in = endPosList.takeFirst().toInt();
2976         duration = endPosList.takeFirst().toInt();
2977         endData.insert(id, {in, duration});
2978     }
2979     QMapIterator<int, QPair<int, int>> i(startData);
2980     QList<int> changedItems;
2981     Fun undo = []() { return true; };
2982     Fun redo = []() { return true; };
2983     bool result = true;
2984     QVector<int> mixTracks;
2985     QList<int> ids = startData.keys();
2986     for (auto &id : ids) {
2987         if (isClip(id)) {
2988             int tid = m_allClips[id]->getCurrentTrackId();
2989             if (tid > -1 && getTrackById(tid)->hasMix(id)) {
2990                 if (!mixTracks.contains(tid)) {
2991                     mixTracks << tid;
2992                 }
2993                 if (right) {
2994                     if (getTrackById_const(tid)->hasEndMix(id)) {
2995                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
2996                         QPair<int, int> endPos = endData.value(id);
2997                         if (endPos.first + endPos.second <= mixData.second.secondClipInOut.first) {
2998                             Fun sync_mix_undo = [this, tid, mixData]() {
2999                                 getTrackById_const(tid)->createMix(mixData.second, getTrackById_const(tid)->isAudioTrack());
3000                                 return true;
3001                             };
3002                             PUSH_LAMBDA(sync_mix_undo, undo);
3003                         }
3004                     }
3005                 } else if (getTrackById_const(tid)->hasStartMix(id)) {
3006                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
3007                     QPair<int, int> endPos = endData.value(id);
3008                     if (endPos.first >= mixData.first.firstClipInOut.second) {
3009                         Fun sync_mix_undo = [this, tid, mixData]() {
3010                             getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3011                             return true;
3012                         };
3013                         PUSH_LAMBDA(sync_mix_undo, undo);
3014                     }
3015                 }
3016             }
3017         }
3018     }
3019     Fun sync_mix = []() { return true; };
3020     if (!mixTracks.isEmpty()) {
3021         sync_mix = [this, mixTracks]() {
3022             for (auto &tid : mixTracks) {
3023                 getTrackById_const(tid)->syncronizeMixes(true);
3024             }
3025             return true;
3026         };
3027         PUSH_LAMBDA(sync_mix, undo);
3028     }
3029     while (i.hasNext()) {
3030         i.next();
3031         QPair<int, int> startItemPos = i.value();
3032         QPair<int, int> endItemPos = endData.value(i.key());
3033         if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) {
3034             // Revert individual items to original position
3035             requestItemResize(i.key(), startItemPos.second, right, false, 0, true);
3036             changedItems << i.key();
3037         }
3038     }
3039     for (int id : qAsConst(changedItems)) {
3040         QPair<int, int> endItemPos = endData.value(id);
3041         int duration = endItemPos.second;
3042         result = result & requestItemResize(id, duration, right, true, undo, redo, false);
3043         if (!result) {
3044             break;
3045         }
3046     }
3047     if (result) {
3048         sync_mix();
3049         PUSH_LAMBDA(sync_mix, redo);
3050         PUSH_UNDO(undo, redo, i18n("Resize group"));
3051     } else {
3052         undo();
3053     }
3054 }
3055 
3056 const std::vector<int> TimelineModel::getBoundaries(int itemId)
3057 {
3058     std::vector<int> boundaries;
3059     std::unordered_set<int> items;
3060     if (m_groups->isInGroup(itemId)) {
3061         int groupId = m_groups->getRootId(itemId);
3062         items = m_groups->getLeaves(groupId);
3063     } else {
3064         items.insert(itemId);
3065     }
3066     for (int id : items) {
3067         if (isItem(id)) {
3068             int pos = getItemPosition(id);
3069             boundaries.push_back(pos);
3070             pos += getItemPlaytime(id);
3071             boundaries.push_back(pos);
3072         }
3073     }
3074     return boundaries;
3075 }
3076 
3077 int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed)
3078 {
3079     Q_UNUSED(snapDistance)
3080     QWriteLocker locker(&m_lock);
3081     TRACE(itemId, size, right, true, snapDistance, allowSingleResize);
3082     Q_ASSERT(isClip(itemId));
3083     if (size <= 0) {
3084         TRACE_RES(-1);
3085         return -1;
3086     }
3087     int in = getItemPosition(itemId);
3088     int out = in + getItemPlaytime(itemId);
3089     // size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
3090     Fun undo = []() { return true; };
3091     Fun redo = []() { return true; };
3092     std::unordered_set<int> all_items;
3093     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3094         int groupId = m_groups->getRootId(itemId);
3095         std::unordered_set<int> items;
3096         if (m_groups->getType(groupId) == GroupType::AVSplit) {
3097             // Only resize group elements if it is an avsplit
3098             items = m_groups->getLeaves(groupId);
3099         } else {
3100             all_items.insert(itemId);
3101         }
3102         for (int id : items) {
3103             if (id == itemId) {
3104                 all_items.insert(id);
3105                 continue;
3106             }
3107             int start = getItemPosition(id);
3108             int end = in + getItemPlaytime(id);
3109             if (right) {
3110                 if (out == end) {
3111                     all_items.insert(id);
3112                 }
3113             } else if (start == in) {
3114                 all_items.insert(id);
3115             }
3116         }
3117     } else {
3118         all_items.insert(itemId);
3119     }
3120     bool result = true;
3121     for (int id : all_items) {
3122         int tid = getItemTrackId(id);
3123         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
3124             continue;
3125         }
3126         // First delete clip, then timewarp, resize and reinsert
3127         int pos = getItemPosition(id);
3128         int invalidateIn = pos;
3129         int invalidateOut = invalidateIn + getClipPlaytime(id);
3130         if (!right) {
3131             pos += getItemPlaytime(id) - size;
3132         }
3133         bool hasVideo = false;
3134         if (tid != -1 && !getTrackById_const(tid)->isAudioTrack()) {
3135             hasVideo = true;
3136         }
3137         int trackDuration = getTrackById_const(tid)->trackDuration();
3138         result = getTrackById(tid)->requestClipDeletion(id, true, false, undo, redo, false, false);
3139         bool pitchCompensate = m_allClips[id]->getIntProperty(QStringLiteral("warp_pitch"));
3140         result = result && requestClipTimeWarp(id, speed, pitchCompensate, true, undo, redo);
3141         result = result && requestItemResize(id, size, true, true, undo, redo);
3142         result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, false, undo, redo, false, false);
3143         if (!result) {
3144             break;
3145         }
3146         bool durationChanged = false;
3147         if (trackDuration != getTrackById_const(tid)->trackDuration()) {
3148             durationChanged = true;
3149             getTrackById(tid)->adjustStackLength(trackDuration, getTrackById_const(tid)->trackDuration(), undo, redo);
3150         }
3151         if (right) {
3152             invalidateOut = qMax(invalidateOut, invalidateIn + getClipPlaytime(id));
3153         } else {
3154             invalidateIn = qMin(invalidateIn, invalidateOut - getClipPlaytime(id));
3155         }
3156         Fun view_redo = [this, invalidateIn, invalidateOut, hasVideo, durationChanged]() {
3157             if (hasVideo) {
3158                 Q_EMIT invalidateZone(invalidateIn, invalidateOut);
3159             }
3160             if (durationChanged) {
3161                 // last clip in playlist updated
3162                 updateDuration();
3163             }
3164             return true;
3165         };
3166         view_redo();
3167         PUSH_LAMBDA(view_redo, redo);
3168         PUSH_LAMBDA(view_redo, undo);
3169     }
3170     if (!result) {
3171         bool undone = undo();
3172         Q_ASSERT(undone);
3173     } else {
3174         PUSH_UNDO(undo, redo, i18n("Resize clip speed"));
3175     }
3176     int res = result ? size : -1;
3177     TRACE_RES(res);
3178     return res;
3179 }
3180 
3181 int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance)
3182 {
3183     int trackId = getItemTrackId(itemId);
3184     bool checkMix = trackId != -1;
3185     Fun temp_undo = []() { return true; };
3186     Fun temp_redo = []() { return true; };
3187     bool skipSnaps = snapDistance <= 0;
3188     bool sizeUpdated = false;
3189     if (checkMix && right && size > out - in && isClip(itemId)) {
3190         int playlist = -1;
3191         if (getTrackById_const(trackId)->hasEndMix(itemId)) {
3192             playlist = m_allClips[itemId]->getSubPlaylistIndex();
3193         }
3194         int targetPos = in + size - 1;
3195         if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) {
3196             int updatedSize = getTrackById_const(trackId)->getBlankEnd(out, playlist) - in + 1;
3197             if (!skipSnaps && size - updatedSize > snapDistance) {
3198                 skipSnaps = true;
3199             }
3200             size = updatedSize;
3201             sizeUpdated = true;
3202         }
3203     } else if (checkMix && !right && size > (out - in) && isClip(itemId)) {
3204         int targetPos = out - size;
3205         int playlist = -1;
3206         if (getTrackById_const(trackId)->hasStartMix(itemId)) {
3207             playlist = m_allClips[itemId]->getSubPlaylistIndex();
3208         }
3209         if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) {
3210             int updatedSize = out - getTrackById_const(trackId)->getBlankStart(in - 1, playlist);
3211             if (!skipSnaps && size - updatedSize > snapDistance) {
3212                 skipSnaps = true;
3213             }
3214             size = updatedSize;
3215             sizeUpdated = true;
3216         }
3217     }
3218     int proposed_size = size;
3219     if (!skipSnaps) {
3220         int timelinePos = pCore->getMonitorPosition();
3221         m_snaps->addPoint(timelinePos);
3222         proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance);
3223         m_snaps->removePoint(timelinePos);
3224     }
3225     if (proposed_size > 0 && (!skipSnaps || sizeUpdated)) {
3226         // only test move if proposed_size is valid
3227         bool success = false;
3228         if (isClip(itemId)) {
3229             bool hasMix = getTrackById_const(trackId)->hasMix(itemId);
3230             success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false, hasMix);
3231         } else if (isComposition(itemId)) {
3232             success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false);
3233         } else if (isSubTitle(itemId)) {
3234             // TODO: don't allow subtitle overlap?
3235             success = true;
3236         }
3237         // undo temp move
3238         temp_undo();
3239         if (success) {
3240             size = proposed_size;
3241         }
3242     }
3243     return size;
3244 }
3245 
3246 bool TimelineModel::trackIsBlankAt(int tid, int pos, int playlist) const
3247 {
3248     if (pos > getTrackById_const(tid)->trackDuration() - 1) {
3249         return true;
3250     }
3251     return getTrackById_const(tid)->isBlankAt(pos, playlist);
3252 }
3253 
3254 bool TimelineModel::trackIsAvailable(int tid, int pos, int duration, int playlist) const
3255 {
3256     return getTrackById_const(tid)->isAvailable(pos, duration, playlist);
3257 }
3258 
3259 int TimelineModel::getClipStartAt(int tid, int pos, int playlist) const
3260 {
3261     return getTrackById_const(tid)->getClipStart(pos, playlist);
3262 }
3263 
3264 int TimelineModel::getClipEndAt(int tid, int pos, int playlist) const
3265 {
3266     return getTrackById_const(tid)->getClipEnd(pos, playlist);
3267 }
3268 
3269 int TimelineModel::requestItemSpeedChange(int itemId, int size, bool right, int snapDistance)
3270 {
3271     Q_ASSERT(isClip(itemId));
3272     QWriteLocker locker(&m_lock);
3273     TRACE(itemId, size, right, snapDistance);
3274     Q_ASSERT(isItem(itemId));
3275     if (size <= 0) {
3276         TRACE_RES(-1);
3277         return -1;
3278     }
3279     int in = getItemPosition(itemId);
3280     int out = in + getItemPlaytime(itemId);
3281 
3282     if (right && size > out - in) {
3283         int targetPos = in + size - 1;
3284         int trackId = getItemTrackId(itemId);
3285         if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, out + 1, targetPos, false).empty()) {
3286             size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in;
3287         }
3288     } else if (!right && size > (out - in)) {
3289         int targetPos = out - size;
3290         int trackId = getItemTrackId(itemId);
3291         if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, targetPos, in - 1, false).empty()) {
3292             size = out - getTrackById_const(trackId)->getBlankStart(in - 1);
3293         }
3294     }
3295     int timelinePos = pCore->getMonitorPosition();
3296     m_snaps->addPoint(timelinePos);
3297     int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance);
3298     m_snaps->removePoint(timelinePos);
3299     return proposed_size > 0 ? proposed_size : size;
3300 }
3301 
3302 bool TimelineModel::removeMixWithUndo(int cid, Fun &undo, Fun &redo)
3303 {
3304     int tid = getItemTrackId(cid);
3305     if (isTrack(tid)) {
3306         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
3307         if (mixData.firstClipId > -1 && mixData.secondClipId > -1) {
3308             bool res = getTrackById(tid)->requestRemoveMix({mixData.firstClipId, mixData.secondClipId}, undo, redo);
3309             return res;
3310         }
3311     }
3312     return true;
3313 }
3314 
3315 bool TimelineModel::removeMix(int cid)
3316 {
3317     Fun undo = []() { return true; };
3318     Fun redo = []() { return true; };
3319     bool res = removeMixWithUndo(cid, undo, redo);
3320     if (res) {
3321         PUSH_UNDO(undo, redo, i18n("Remove mix"));
3322     } else {
3323         pCore->displayMessage(i18n("Removing mix failed"), ErrorMessage, 500);
3324     }
3325     return res;
3326 }
3327 
3328 int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize)
3329 {
3330     QWriteLocker locker(&m_lock);
3331     TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize)
3332     Q_ASSERT(isItem(itemId));
3333     if (size <= 0) {
3334         TRACE_RES(-1)
3335         return -1;
3336     }
3337     int in = 0;
3338     int offset = getItemPlaytime(itemId);
3339     int tid = getItemTrackId(itemId);
3340     int out = offset;
3341     qDebug() << "======= REQUESTING NEW CLIP SIZE: " << size << ", ON TID: " << tid;
3342     if (tid != -1 || !isClip(itemId)) {
3343         in = qMax(0, getItemPosition(itemId));
3344         out += in;
3345         size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
3346     }
3347     qDebug() << "======= ADJUSTED NEW CLIP SIZE: " << size << " FROM " << offset;
3348     offset -= size;
3349     Fun undo = []() { return true; };
3350     Fun redo = []() { return true; };
3351     Fun adjust_mix = []() { return true; };
3352     Fun sync_end_mix = []() { return true; };
3353     Fun sync_end_mix_undo = []() { return true; };
3354     std::unordered_set<int> all_items;
3355     QList<int> tracksWithMixes;
3356     all_items.insert(itemId);
3357     int resizedCount = 0;
3358     if (logUndo && isClip(itemId)) {
3359         if (tid > -1) {
3360             if (right) {
3361                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3362                     tracksWithMixes << tid;
3363                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3364                     int resizeOut = in + size;
3365                     int clipMixCut = mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() -
3366                                      m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3367                     int clipMixStart = mixData.second.secondClipInOut.first;
3368                     if (resizeOut < clipMixCut || resizeOut <= clipMixStart) {
3369                         // Clip resized outside of mix zone, mix will be deleted
3370                         bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3371                         if (res) {
3372                             size = m_allClips[itemId]->getPlaytime();
3373                             resizedCount++;
3374                         } else {
3375                             return -1;
3376                         }
3377                     } else {
3378                         // Mix was resized, update cut position
3379                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3380                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3381                         adjust_mix = [this, tid, mixData, currentMixCut, itemId]() {
3382                             MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second;
3383                             int mixOffset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3384                             getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId,
3385                                                                     secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first,
3386                                                                     currentMixCut - mixOffset);
3387                             QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3388                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3389                             return true;
3390                         };
3391                         Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3392                             getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3393                             QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3394                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3395                             return true;
3396                         };
3397                         PUSH_LAMBDA(adjust_mix_undo, undo);
3398                     }
3399                 }
3400                 if (getTrackById_const(tid)->hasStartMix(itemId)) {
3401                     // Resize mix if necessary
3402                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3403                     if (in + size <= mixData.first.firstClipInOut.second) {
3404                         // Resized smaller than mix, adjust
3405                         int updatedSize = in + size - mixData.first.firstClipInOut.first;
3406                         // Mix was resized, update cut position
3407                         int currentMixDuration = m_allClips[itemId]->getMixDuration();
3408                         int currentMixCut = m_allClips[itemId]->getMixCutPosition();
3409                         Fun adjust_mix1 = [this, tid, currentMixDuration, currentMixCut, itemId,
3410                                            mixOffset = mixData.first.firstClipInOut.second - (in + size)]() {
3411                             getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration - mixOffset, currentMixCut - mixOffset);
3412                             QModelIndex ix = makeClipIndexFromID(itemId);
3413                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3414                             return true;
3415                         };
3416                         Fun adjust_mix_undo = [this, tid, itemId, currentMixCut, currentMixDuration]() {
3417                             getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, currentMixCut);
3418                             QModelIndex ix = makeClipIndexFromID(itemId);
3419                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3420                             return true;
3421                         };
3422                         PUSH_LAMBDA(adjust_mix1, adjust_mix);
3423                         PUSH_LAMBDA(adjust_mix_undo, undo);
3424                         requestItemResize(mixData.first.firstClipId, updatedSize, true, logUndo, undo, redo);
3425                         resizedCount++;
3426                     }
3427                 }
3428             } else {
3429                 // Resized left side
3430                 if (getTrackById_const(tid)->hasStartMix(itemId)) {
3431                     tracksWithMixes << tid;
3432                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3433                     if (out - size >= mixData.first.firstClipInOut.second) {
3434                         // Moved outside mix, delete
3435                         bool res = removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3436                         if (res) {
3437                             size = m_allClips[itemId]->getPlaytime();
3438                             resizedCount++;
3439                         } else {
3440                             return -1;
3441                         }
3442                     } else {
3443                         // Mix was resized, update cut position
3444                         int currentMixDuration = m_allClips[mixData.first.secondClipId]->getMixDuration();
3445                         int currentMixCut = m_allClips[mixData.first.secondClipId]->getMixCutPosition();
3446                         adjust_mix = [this, tid, currentMixCut, itemId]() {
3447                             MixInfo firstMixData = getTrackById_const(tid)->getMixInfo(itemId).first;
3448                             if (firstMixData.firstClipId > -1 && firstMixData.secondClipId > -1) {
3449                                 getTrackById_const(tid)->setMixDuration(firstMixData.secondClipId,
3450                                                                         firstMixData.firstClipInOut.second - firstMixData.secondClipInOut.first, currentMixCut);
3451                                 QModelIndex ix = makeClipIndexFromID(firstMixData.secondClipId);
3452                                 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3453                             }
3454                             return true;
3455                         };
3456                         Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3457                             getTrackById_const(tid)->setMixDuration(mixData.first.secondClipId, currentMixDuration, currentMixCut);
3458                             QModelIndex ix = makeClipIndexFromID(mixData.first.secondClipId);
3459                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3460                             return true;
3461                         };
3462                         PUSH_LAMBDA(adjust_mix_undo, undo);
3463                     }
3464                 }
3465                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3466                     // Resize mix if necessary
3467                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3468                     if (out - size >= mixData.second.secondClipInOut.first) {
3469                         // Resized smaller than mix, adjust
3470                         int updatedClipSize = mixData.second.secondClipInOut.second - (out - size);
3471                         int updatedMixDuration = mixData.second.firstClipInOut.second - (out - size);
3472                         // Mix was resized, update cut position
3473                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3474                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3475                         Fun adjust_mix1 = [this, tid, currentMixCut, secondId = mixData.second.secondClipId, updatedMixDuration]() {
3476                             getTrackById_const(tid)->setMixDuration(secondId, updatedMixDuration, currentMixCut);
3477                             QModelIndex ix = makeClipIndexFromID(secondId);
3478                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3479                             return true;
3480                         };
3481                         Fun adjust_mix_undo = [this, tid, secondId = mixData.second.secondClipId, currentMixCut, currentMixDuration]() {
3482                             getTrackById_const(tid)->setMixDuration(secondId, currentMixDuration, currentMixCut);
3483                             QModelIndex ix = makeClipIndexFromID(secondId);
3484                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3485                             return true;
3486                         };
3487                         PUSH_LAMBDA(adjust_mix1, adjust_mix);
3488                         PUSH_LAMBDA(adjust_mix_undo, undo);
3489                         requestItemResize(mixData.second.secondClipId, updatedClipSize, false, logUndo, undo, redo);
3490                         resizedCount++;
3491                     }
3492                 }
3493             }
3494         }
3495     }
3496     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3497         int groupId = m_groups->getRootId(itemId);
3498         std::unordered_set<int> items = m_groups->getLeaves(groupId);
3499         /*if (m_groups->getType(groupId) == GroupType::AVSplit) {
3500             // Only resize group elements if it is an avsplit
3501             items = m_groups->getLeaves(groupId);
3502         }*/
3503         for (int id : items) {
3504             if (id == itemId) {
3505                 continue;
3506             }
3507             int start = getItemPosition(id);
3508             int end = start + getItemPlaytime(id);
3509             bool resizeMix = false;
3510             if (right) {
3511                 if (out == end) {
3512                     all_items.insert(id);
3513                     resizeMix = true;
3514                 }
3515             } else if (start == in) {
3516                 all_items.insert(id);
3517                 resizeMix = true;
3518             }
3519             if (logUndo && resizeMix && isClip(id)) {
3520                 int trackId = getItemTrackId(id);
3521                 if (trackId > -1) {
3522                     if (right) {
3523                         if (getTrackById_const(trackId)->hasEndMix(id)) {
3524                             if (!tracksWithMixes.contains(trackId)) {
3525                                 tracksWithMixes << trackId;
3526                             }
3527                             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id);
3528                             if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() -
3529                                                     m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3530                                 // Resized outside mix
3531                                 removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3532                                 resizedCount++;
3533                             } else {
3534                                 // Mix was resized, update cut position
3535                                 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3536                                 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3537                                 Fun adjust_mix2 = [this, trackId, mixData, currentMixCut, id]() {
3538                                     MixInfo secondMixData = getTrackById_const(trackId)->getMixInfo(id).second;
3539                                     int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3540                                     getTrackById_const(trackId)->setMixDuration(secondMixData.secondClipId,
3541                                                                                 secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first,
3542                                                                                 currentMixCut - offset);
3543                                     QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3544                                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3545                                     return true;
3546                                 };
3547                                 Fun adjust_mix_undo = [this, trackId, mixData, currentMixCut, currentMixDuration]() {
3548                                     getTrackById_const(trackId)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3549                                     QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3550                                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3551                                     return true;
3552                                 };
3553                                 PUSH_LAMBDA(adjust_mix2, adjust_mix);
3554                                 PUSH_LAMBDA(adjust_mix_undo, undo);
3555                             }
3556                         }
3557                     } else if (getTrackById_const(trackId)->hasStartMix(id)) {
3558                         if (!tracksWithMixes.contains(trackId)) {
3559                             tracksWithMixes << trackId;
3560                         }
3561                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id);
3562                         if (start + offset >= mixData.first.firstClipInOut.second) {
3563                             // Moved outside mix, remove
3564                             removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3565                             resizedCount++;
3566                         }
3567                     }
3568                 }
3569             }
3570         }
3571     }
3572     bool result = true;
3573     int finalPos = right ? in + size : out - size;
3574     int finalSize;
3575     for (int id : all_items) {
3576         int trackId = getItemTrackId(id);
3577         if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
3578             continue;
3579         }
3580         if (isSubtitleTrack(trackId) && m_subtitleModel && m_subtitleModel->isLocked()) {
3581             continue;
3582         }
3583         if (right) {
3584             finalSize = finalPos - qMax(0, getItemPosition(id));
3585         } else {
3586             int currentDuration = getItemPlaytime(id);
3587             if (trackId == -1) {
3588                 finalSize = qMax(0, getItemPosition(id)) + currentDuration - finalPos;
3589             } else {
3590                 finalSize = qMax(0, getItemPosition(id)) + currentDuration - qMax(0, finalPos);
3591                 if (finalSize == currentDuration) {
3592                     continue;
3593                 }
3594             }
3595         }
3596         if (finalSize < 1) {
3597             // Abort resize
3598             result = false;
3599         }
3600         result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo);
3601         if (!result) {
3602             break;
3603         }
3604         if (id == itemId) {
3605             size = finalSize;
3606         }
3607         resizedCount++;
3608     }
3609     result = result && resizedCount != 0;
3610     if (!result) {
3611         qDebug() << "resize aborted" << result;
3612         bool undone = undo();
3613         Q_ASSERT(undone);
3614     } else if (logUndo) {
3615         if (isClip(itemId)) {
3616             adjust_mix();
3617             PUSH_LAMBDA(adjust_mix, redo);
3618             PUSH_UNDO(undo, redo, i18n("Resize clip"))
3619         } else if (isComposition(itemId)) {
3620             PUSH_UNDO(undo, redo, i18n("Resize composition"))
3621         } else if (isSubTitle(itemId)) {
3622             PUSH_UNDO(undo, redo, i18n("Resize subtitle"))
3623         }
3624     }
3625     int res = result ? size : -1;
3626     TRACE_RES(res)
3627     return res;
3628 }
3629 
3630 bool TimelineModel::requestItemResize(int itemId, int &size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
3631 {
3632     Q_UNUSED(blockUndo)
3633     Fun local_undo = []() { return true; };
3634     Fun local_redo = []() { return true; };
3635     bool result = false;
3636     if (isClip(itemId)) {
3637         bool hasMix = false;
3638         int tid = m_allClips[itemId]->getCurrentTrackId();
3639         if (tid > -1) {
3640             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3641             if (right && mixData.second.firstClipId > -1) {
3642                 hasMix = true;
3643                 if (mixData.second.firstClipInOut.first + size < mixData.second.secondClipInOut.first) {
3644                     // Resize is outside mix zone, remove mix
3645                     removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3646                 } else {
3647                     size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first);
3648                 }
3649             } else if (!right && mixData.first.firstClipId > -1) {
3650                 hasMix = true;
3651                 // We have a mix at clip start, limit size to previous clip start
3652                 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first);
3653                 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first;
3654                 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size);
3655                 Fun local_update = [this, itemId, tid, mixData, mixDuration] {
3656                     getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset);
3657                     QModelIndex ix = makeClipIndexFromID(itemId);
3658                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3659                     return true;
3660                 };
3661                 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] {
3662                     if (getTrackById_const(tid)->hasStartMix(itemId)) {
3663                         getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset);
3664                         QModelIndex ix = makeClipIndexFromID(itemId);
3665                         Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3666                     }
3667                     return true;
3668                 };
3669                 local_update();
3670                 if (logUndo) {
3671                     UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo);
3672                 }
3673             } else {
3674                 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1;
3675             }
3676         }
3677         result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix);
3678     } else if (isComposition(itemId)) {
3679         result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
3680     } else if (isSubTitle(itemId)) {
3681         result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo);
3682     }
3683     if (result) {
3684         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
3685     }
3686     return result;
3687 }
3688 
3689 int TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, bool moveGuides,
3690                                            int snapDistance, bool allowSingleResize)
3691 {
3692     QWriteLocker locker(&m_lock);
3693     TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize)
3694     Q_ASSERT(isItem(itemId));
3695     if (size <= 0) {
3696         TRACE_RES(-1)
3697         return -1;
3698     }
3699     int in = 0;
3700     int offset = getItemPlaytime(itemId);
3701     int tid = getItemTrackId(itemId);
3702     int out = offset;
3703     qDebug() << "======= REQUESTING NEW CLIP SIZE (RIPPLE): " << size;
3704     if (tid != -1 || !isClip(itemId)) {
3705         in = qMax(0, getItemPosition(itemId));
3706         out += in;
3707         // m_snaps->addPoint(cursorPos);
3708         int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, true, snapDistance);
3709         // m_snaps->removePoint(cursorPos);
3710         if (proposed_size > -1) {
3711             size = proposed_size;
3712         }
3713     }
3714     qDebug() << "======= ADJUSTED NEW CLIP SIZE (RIPPLE): " << size;
3715     offset -= size;
3716     Fun undo = []() { return true; };
3717     Fun redo = []() { return true; };
3718     Fun sync_mix = []() { return true; };
3719     Fun adjust_mix = []() { return true; };
3720     Fun sync_end_mix = []() { return true; };
3721     Fun sync_end_mix_undo = []() { return true; };
3722     PUSH_LAMBDA(sync_mix, undo);
3723     std::unordered_set<int> all_items;
3724     QList<int> tracksWithMixes;
3725     all_items.insert(itemId);
3726     if (logUndo && isClip(itemId)) {
3727         /*if (tid > -1) {
3728             if (right) {
3729                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3730                     tracksWithMixes << tid;
3731                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3732                     if (in + size <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() -
3733         m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3734                         // Clip resized outside of mix zone, mix will be deleted
3735                         bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3736                         if (res) {
3737                             size = m_allClips[itemId]->getPlaytime();
3738                         } else {
3739                             return -1;
3740                         }
3741                     } else {
3742                         // Mix was resized, update cut position
3743                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3744                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3745                         adjust_mix = [this, tid, mixData, currentMixCut, itemId]() {
3746                             MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second;
3747                             int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3748                             getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second -
3749         secondMixData.secondClipInOut.first, currentMixCut - offset); QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId); Q_EMIT dataChanged(ix,
3750         ix, {TimelineModel::MixRole,TimelineModel::MixCutRole}); return true;
3751                         };
3752                         Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3753                             getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3754                             QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3755                             Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3756                             return true;
3757                         };
3758                         PUSH_LAMBDA(adjust_mix_undo, undo);
3759                     }
3760                 }
3761             } else if (getTrackById_const(tid)->hasStartMix(itemId)) {
3762                 tracksWithMixes << tid;
3763                 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3764                 if (out - size >= mixData.first.firstClipInOut.second) {
3765                     // Moved outside mix, delete
3766                     Fun sync_mix_undo = [this, tid, mixData]() {
3767                         getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3768                         getTrackById_const(tid)->syncronizeMixes(true);
3769                         return true;
3770                     };
3771                     bool switchPlaylist = getTrackById_const(tid)->hasEndMix(itemId) == false && m_allClips[itemId]->getSubPlaylistIndex() == 1;
3772                     if (switchPlaylist) {
3773                         sync_end_mix = [this, tid, mixData]() {
3774                             return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1,
3775         0);
3776                         };
3777                         sync_end_mix_undo = [this, tid, mixData]() {
3778                             return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0,
3779         1);
3780                         };
3781                     }
3782                     PUSH_LAMBDA(sync_mix_undo, undo);
3783 
3784                 }
3785             }
3786         }*/
3787     }
3788     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3789         int groupId = m_groups->getRootId(itemId);
3790         std::unordered_set<int> items = m_groups->getLeaves(groupId);
3791         /*if (m_groups->getType(groupId) == GroupType::AVSplit) {
3792             // Only resize group elements if it is an avsplit
3793             items = m_groups->getLeaves(groupId);
3794         }*/
3795         for (int id : items) {
3796             if (id == itemId) {
3797                 continue;
3798             }
3799             int start = getItemPosition(id);
3800             int end = start + getItemPlaytime(id);
3801             bool resizeMix = false;
3802             if (right) {
3803                 if (out == end) {
3804                     all_items.insert(id);
3805                     resizeMix = true;
3806                 }
3807             } else if (start == in) {
3808                 all_items.insert(id);
3809                 resizeMix = true;
3810             }
3811             if (logUndo && resizeMix && isClip(id)) {
3812                 int trackId = getItemTrackId(id);
3813                 if (trackId > -1) {
3814                     if (right) {
3815                         if (getTrackById_const(trackId)->hasEndMix(id)) {
3816                             if (!tracksWithMixes.contains(trackId)) {
3817                                 tracksWithMixes << trackId;
3818                             }
3819                             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id);
3820                             if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() -
3821                                                     m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3822                                 // Resized outside mix
3823                                 removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3824                                 Fun sync_mix_undo = [this, trackId, mixData]() {
3825                                     getTrackById_const(trackId)->createMix(mixData.second, getTrackById_const(trackId)->isAudioTrack());
3826                                     getTrackById_const(trackId)->syncronizeMixes(true);
3827                                     return true;
3828                                 };
3829                                 bool switchPlaylist = getTrackById_const(trackId)->hasEndMix(mixData.second.secondClipId) == false &&
3830                                                       m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == 1;
3831                                 if (switchPlaylist) {
3832                                     Fun sync_end_mix2 = [this, trackId, mixData]() {
3833                                         return getTrackById_const(trackId)->switchPlaylist(mixData.second.secondClipId, mixData.second.secondClipInOut.first, 1,
3834                                                                                            0);
3835                                     };
3836                                     Fun sync_end_mix_undo2 = [this, trackId, mixData]() {
3837                                         return getTrackById_const(trackId)->switchPlaylist(mixData.second.secondClipId,
3838                                                                                            m_allClips[mixData.second.secondClipId]->getPosition(), 0, 1);
3839                                     };
3840                                     PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3841                                     PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3842                                 }
3843                                 PUSH_LAMBDA(sync_mix_undo, undo);
3844                             } else {
3845                                 // Mix was resized, update cut position
3846                                 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3847                                 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3848                                 Fun adjust_mix2 = [this, trackId, mixData, currentMixCut, id]() {
3849                                     MixInfo secondMixData = getTrackById_const(trackId)->getMixInfo(id).second;
3850                                     int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3851                                     getTrackById_const(trackId)->setMixDuration(secondMixData.secondClipId,
3852                                                                                 secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first,
3853                                                                                 currentMixCut - offset);
3854                                     QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3855                                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3856                                     return true;
3857                                 };
3858                                 Fun adjust_mix_undo = [this, trackId, mixData, currentMixCut, currentMixDuration]() {
3859                                     getTrackById_const(trackId)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3860                                     QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3861                                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
3862                                     return true;
3863                                 };
3864                                 PUSH_LAMBDA(adjust_mix2, adjust_mix);
3865                                 PUSH_LAMBDA(adjust_mix_undo, undo);
3866                             }
3867                         }
3868                     } else if (getTrackById_const(trackId)->hasStartMix(id)) {
3869                         if (!tracksWithMixes.contains(trackId)) {
3870                             tracksWithMixes << trackId;
3871                         }
3872                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id);
3873                         if (start + offset >= mixData.first.firstClipInOut.second) {
3874                             // Moved outside mix, remove
3875                             Fun sync_mix_undo = [this, trackId, mixData]() {
3876                                 getTrackById_const(trackId)->createMix(mixData.first, getTrackById_const(trackId)->isAudioTrack());
3877                                 getTrackById_const(trackId)->syncronizeMixes(true);
3878                                 return true;
3879                             };
3880                             bool switchPlaylist = getTrackById_const(trackId)->hasEndMix(id) == false && m_allClips[id]->getSubPlaylistIndex() == 1;
3881                             if (switchPlaylist) {
3882                                 Fun sync_end_mix2 = [this, trackId, mixData]() {
3883                                     return getTrackById_const(trackId)->switchPlaylist(mixData.first.secondClipId,
3884                                                                                        m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0);
3885                                 };
3886                                 Fun sync_end_mix_undo2 = [this, trackId, mixData]() {
3887                                     return getTrackById_const(trackId)->switchPlaylist(mixData.first.secondClipId,
3888                                                                                        m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1);
3889                                 };
3890                                 PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3891                                 PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3892                             }
3893                             PUSH_LAMBDA(sync_mix_undo, undo);
3894                         }
3895                     }
3896                 }
3897             }
3898         }
3899     }
3900     if (logUndo && !tracksWithMixes.isEmpty()) {
3901         sync_mix = [this, tracksWithMixes]() {
3902             for (auto &t : tracksWithMixes) {
3903                 getTrackById_const(t)->syncronizeMixes(true);
3904             }
3905             return true;
3906         };
3907     }
3908     bool result = true;
3909     int finalPos = right ? in + size : out - size;
3910     int finalSize;
3911     int resizedCount = 0;
3912     for (int id : all_items) {
3913         int trackId = getItemTrackId(id);
3914         if (trackId > -1 && getTrackById_const(trackId)->isLocked()) {
3915             continue;
3916         }
3917         if (isSubtitleTrack(trackId) && m_subtitleModel && m_subtitleModel->isLocked()) {
3918             continue;
3919         }
3920         if (right) {
3921             finalSize = finalPos - qMax(0, getItemPosition(id));
3922         } else {
3923             finalSize = qMax(0, getItemPosition(id)) + getItemPlaytime(id) - finalPos;
3924         }
3925         result = result && requestItemRippleResize(timeline, id, finalSize, right, logUndo, moveGuides, undo, redo);
3926         resizedCount++;
3927     }
3928     result = result && resizedCount != 0;
3929     if (!result) {
3930         qDebug() << "resize aborted" << result;
3931         bool undone = undo();
3932         Q_ASSERT(undone);
3933     } else if (logUndo) {
3934         if (isClip(itemId)) {
3935             sync_end_mix();
3936             sync_mix();
3937             adjust_mix();
3938             PUSH_LAMBDA(sync_end_mix, redo);
3939             PUSH_LAMBDA(adjust_mix, redo);
3940             PUSH_LAMBDA(sync_mix, redo);
3941             PUSH_LAMBDA(undo, sync_end_mix_undo);
3942             PUSH_UNDO(sync_end_mix_undo, redo, i18n("Ripple resize clip"))
3943         } else if (isComposition(itemId)) {
3944             PUSH_UNDO(undo, redo, i18n("Ripple resize composition"))
3945         } else if (isSubTitle(itemId)) {
3946             PUSH_UNDO(undo, redo, i18n("Ripple resize subtitle"))
3947         }
3948     }
3949     int res = result ? size : -1;
3950     TRACE_RES(res)
3951     return res;
3952 }
3953 
3954 bool TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, bool moveGuides,
3955                                             Fun &undo, Fun &redo, bool blockUndo)
3956 {
3957     Q_UNUSED(blockUndo)
3958     Q_UNUSED(moveGuides)
3959     Fun local_undo = []() { return true; };
3960     Fun local_redo = []() { return true; };
3961     bool result = false;
3962     if (isClip(itemId)) {
3963         bool hasMix = false;
3964         int tid = m_allClips[itemId]->getCurrentTrackId();
3965         if (tid > -1) {
3966             /*std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3967             if (right && mixData.second.firstClipId > -1) {
3968                 hasMix = true;
3969                 size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first);
3970             } else if (!right && mixData.first.firstClipId > -1) {
3971                 hasMix = true;
3972                 // We have a mix at clip start, limit size to previous clip start
3973                 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first);
3974                 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first;
3975                 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size);
3976                 Fun local_update = [this, itemId, tid, mixData, mixDuration] {
3977                     getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset);
3978                     QModelIndex ix = makeClipIndexFromID(itemId);
3979                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3980                     return true;
3981                 };
3982                 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] {
3983                     getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset);
3984                     QModelIndex ix = makeClipIndexFromID(itemId);
3985                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3986                     return true;
3987                 };
3988                 local_update();
3989                 if (logUndo) {
3990                     UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo);
3991                 }
3992             } else {
3993                 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1;
3994             }*/
3995         }
3996         bool affectAllTracks = false;
3997         size = m_allClips[itemId]->getMaxDuration() > 0 ? qBound(1, size, m_allClips[itemId]->getMaxDuration()) : qMax(1, size);
3998         int delta = size - m_allClips[itemId]->getPlaytime();
3999         qDebug() << "requestItemRippleResize logUndo: " << logUndo << " size: " << size << " playtime: " << m_allClips[itemId]->getPlaytime()
4000                  << " delta: " << delta;
4001         auto spacerOperation = [this, itemId, affectAllTracks, &local_undo, &local_redo, delta, right, timeline](int position) {
4002             int trackId = getItemTrackId(itemId);
4003             if (right && getTrackById_const(trackId)->isLastClip(getItemPosition(itemId))) {
4004                 return true;
4005             }
4006             std::pair<int, int> spacerOp = TimelineFunctions::requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position + 1, true, true);
4007             int cid = spacerOp.first;
4008             if (cid == -1) {
4009                 return false;
4010             }
4011             int endPos = getItemPosition(cid) + delta;
4012             // Start undoable command
4013             TimelineFunctions::requestSpacerEndOperation(timeline, cid, getItemPosition(cid), endPos, affectAllTracks ? -1 : trackId,
4014                                                          KdenliveSettings::lockedGuides() ? -1 : position, local_undo, local_redo, false);
4015             return true;
4016         };
4017         if (delta > 0) {
4018             if (right) {
4019                 int position = getItemPosition(itemId) + getItemPlaytime(itemId);
4020                 if (!spacerOperation(position)) {
4021                     return false;
4022                 }
4023             } else {
4024                 int position = getItemPosition(itemId);
4025                 if (!spacerOperation(position)) {
4026                     return false;
4027                 }
4028             }
4029         }
4030 
4031         result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix);
4032         if (!result && delta > 0) {
4033             local_undo();
4034         }
4035         if (result && delta < 0) {
4036             if (right) {
4037                 int position = getItemPosition(itemId) + getItemPlaytime(itemId) - delta;
4038                 if (!spacerOperation(position)) {
4039                     return false;
4040                 }
4041             } else {
4042                 int position = getItemPosition(itemId) + delta;
4043                 if (!spacerOperation(position)) {
4044                     return false;
4045                 }
4046             }
4047         }
4048 
4049     } else if (isComposition(itemId)) {
4050         return false;
4051         // TODO? Does it make sense?
4052         // result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
4053     } else if (isSubTitle(itemId)) {
4054         return false;
4055         // TODO?
4056         // result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo);
4057     }
4058     if (result) {
4059         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4060     }
4061     return result;
4062 }
4063 
4064 int TimelineModel::requestSlipSelection(int offset, bool logUndo)
4065 {
4066     QWriteLocker locker(&m_lock);
4067     TRACE(offset, logUndo)
4068 
4069     Fun undo = []() { return true; };
4070     Fun redo = []() { return true; };
4071     bool result = true;
4072     int slipCount = 0;
4073     for (auto id : getCurrentSelection()) {
4074         int tid = getItemTrackId(id);
4075         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
4076             continue;
4077         }
4078         if (!isClip(id)) {
4079             continue;
4080         }
4081         result = result && requestClipSlip(id, offset, logUndo, undo, redo);
4082         slipCount++;
4083     }
4084     result = result && slipCount != 0;
4085     if (!result) {
4086         bool undone = undo();
4087         Q_ASSERT(undone);
4088     } else if (logUndo) {
4089         PUSH_UNDO(undo, redo, i18ncp("Undo/Redo menu text", "Slip clip", "Slip clips", slipCount));
4090     }
4091     int res = result ? offset : 0;
4092     TRACE_RES(res)
4093     return res;
4094 }
4095 
4096 int TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, bool allowSingleResize)
4097 {
4098     QWriteLocker locker(&m_lock);
4099     TRACE(itemId, offset, logUndo, allowSingleResize)
4100     Q_ASSERT(isClip(itemId));
4101     Fun undo = []() { return true; };
4102     Fun redo = []() { return true; };
4103     std::unordered_set<int> all_items;
4104     all_items.insert(itemId);
4105     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
4106         int groupId = m_groups->getRootId(itemId);
4107         all_items = m_groups->getLeaves(groupId);
4108     }
4109     bool result = true;
4110     int slipCount = 0;
4111     for (int id : all_items) {
4112         int tid = getItemTrackId(id);
4113         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
4114             continue;
4115         }
4116         result = result && requestClipSlip(id, offset, logUndo, undo, redo);
4117         slipCount++;
4118     }
4119     result = result && slipCount != 0;
4120     if (!result) {
4121         bool undone = undo();
4122         Q_ASSERT(undone);
4123     } else if (logUndo) {
4124         PUSH_UNDO(undo, redo, i18n("Slip clip"))
4125     }
4126     int res = result ? offset : 0;
4127     TRACE_RES(res)
4128     return res;
4129 }
4130 
4131 bool TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
4132 {
4133     Q_UNUSED(blockUndo)
4134     Fun local_undo = []() { return true; };
4135     Fun local_redo = []() { return true; };
4136     bool result = false;
4137     if (isClip(itemId)) {
4138         result = m_allClips[itemId]->requestSlip(offset, local_undo, local_redo, logUndo);
4139     }
4140     if (result) {
4141         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4142     }
4143     return result;
4144 }
4145 
4146 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, bool logUndo, GroupType type)
4147 {
4148     QWriteLocker locker(&m_lock);
4149     TRACE(ids, logUndo, type);
4150     if (type == GroupType::Selection || type == GroupType::Leaf) {
4151         // Selections shouldn't be done here. Call requestSetSelection instead
4152         TRACE_RES(-1);
4153         return -1;
4154     }
4155     Fun undo = []() { return true; };
4156     Fun redo = []() { return true; };
4157     int result = requestClipsGroup(ids, undo, redo, type);
4158     if (result > -1 && logUndo) {
4159         PUSH_UNDO(undo, redo, i18n("Group clips"));
4160     }
4161     TRACE_RES(result);
4162     return result;
4163 }
4164 
4165 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, Fun &undo, Fun &redo, GroupType type)
4166 {
4167     QWriteLocker locker(&m_lock);
4168     if (type != GroupType::Selection) {
4169         requestClearSelection();
4170     }
4171     int clipsCount = 0;
4172     QList<int> tracks;
4173     for (int id : ids) {
4174         if (isClip(id)) {
4175             int trackId = getClipTrackId(id);
4176             if (trackId == -1) {
4177                 return -1;
4178             }
4179             tracks << trackId;
4180             clipsCount++;
4181         } else if (isComposition(id)) {
4182             if (getCompositionTrackId(id) == -1) {
4183                 return -1;
4184             }
4185         } else if (isSubTitle(id)) {
4186         } else if (!isGroup(id)) {
4187             return -1;
4188         }
4189     }
4190     if (type == GroupType::Selection && ids.size() == 1) {
4191         // only one element selected, no group created
4192         return -1;
4193     }
4194     if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) {
4195         // Check if we are grouping an AVSplit
4196         auto it = ids.begin();
4197         int firstId = *it;
4198         std::advance(it, 1);
4199         int secondId = *it;
4200         bool isAVGroup = false;
4201         if (getClipBinId(firstId) == getClipBinId(secondId)) {
4202             if (getClipState(firstId) == PlaylistState::AudioOnly) {
4203                 if (getClipState(secondId) == PlaylistState::VideoOnly) {
4204                     isAVGroup = true;
4205                 }
4206             } else if (getClipState(secondId) == PlaylistState::AudioOnly) {
4207                 isAVGroup = true;
4208             }
4209         }
4210         if (isAVGroup) {
4211             type = GroupType::AVSplit;
4212         }
4213     }
4214     int groupId = m_groups->groupItems(ids, undo, redo, type);
4215     if (type != GroupType::Selection) {
4216         // we make sure that the undo and the redo are going to unselect before doing anything else
4217         Fun unselect = [this]() { return requestClearSelection(); };
4218         PUSH_FRONT_LAMBDA(unselect, undo);
4219         PUSH_FRONT_LAMBDA(unselect, redo);
4220     }
4221     return groupId;
4222 }
4223 
4224 bool TimelineModel::requestClipsUngroup(const std::unordered_set<int> &itemIds, bool logUndo)
4225 {
4226     QWriteLocker locker(&m_lock);
4227     TRACE(itemIds, logUndo);
4228     Fun undo = []() { return true; };
4229     Fun redo = []() { return true; };
4230     bool result = true;
4231     requestClearSelection();
4232     std::unordered_set<int> roots;
4233     std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
4234     for (int root : roots) {
4235         if (isGroup(root)) {
4236             result = result && requestClipUngroup(root, undo, redo);
4237         }
4238     }
4239     if (!result) {
4240         bool undone = undo();
4241         Q_ASSERT(undone);
4242     }
4243     if (result && logUndo) {
4244         PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
4245     }
4246     TRACE_RES(result);
4247     return result;
4248 }
4249 
4250 bool TimelineModel::requestClipUngroup(int itemId, bool logUndo)
4251 {
4252     QWriteLocker locker(&m_lock);
4253     TRACE(itemId, logUndo);
4254     requestClearSelection();
4255     Fun undo = []() { return true; };
4256     Fun redo = []() { return true; };
4257     bool result = true;
4258     result = requestClipUngroup(itemId, undo, redo);
4259     if (result && logUndo) {
4260         PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
4261     }
4262     TRACE_RES(result);
4263     return result;
4264 }
4265 
4266 bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo)
4267 {
4268     QWriteLocker locker(&m_lock);
4269     bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection;
4270     if (!isSelection) {
4271         requestClearSelection();
4272     }
4273     bool res = m_groups->ungroupItem(itemId, undo, redo);
4274     if (res && !isSelection) {
4275         // we make sure that the undo and the redo are going to unselect before doing anything else
4276         Fun unselect = [this]() { return requestClearSelection(); };
4277         PUSH_FRONT_LAMBDA(unselect, undo);
4278         PUSH_FRONT_LAMBDA(unselect, redo);
4279     }
4280     return res;
4281 }
4282 
4283 bool TimelineModel::requestRemoveFromGroup(int itemId, Fun &undo, Fun &redo)
4284 {
4285     QWriteLocker locker(&m_lock);
4286     GroupType type = m_groups->getType(m_groups->getRootId(itemId));
4287     bool isSelection = type == GroupType::Selection;
4288     if (!isSelection) {
4289         requestClearSelection();
4290     }
4291     std::unordered_set<int> items = getGroupElements(itemId);
4292     bool res = m_groups->ungroupItem(itemId, undo, redo);
4293     if (res && items.size() > 1) {
4294         items.erase(itemId);
4295         res = m_groups->groupItems(items, undo, redo, type);
4296     }
4297 
4298     if (res && !isSelection) {
4299         // we make sure that the undo and the redo are going to unselect before doing anything else
4300         Fun unselect = [this]() { return requestClearSelection(); };
4301         PUSH_FRONT_LAMBDA(unselect, undo);
4302         PUSH_FRONT_LAMBDA(unselect, redo);
4303     }
4304     return res;
4305 }
4306 
4307 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack)
4308 {
4309     QWriteLocker locker(&m_lock);
4310     TRACE(position, id, trackName, audioTrack);
4311     Fun undo = []() { return true; };
4312     Fun redo = []() { return true; };
4313     bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo);
4314     if (result) {
4315         PUSH_UNDO(undo, redo, i18nc("@action", "Insert Track"));
4316     }
4317     TRACE_RES(result);
4318     return result;
4319 }
4320 
4321 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool addCompositing)
4322 {
4323     // TODO: make sure we disable overlayTrack before inserting a track
4324     if (position == -1) {
4325         position = int(m_allTracks.size());
4326     }
4327     if (position < 0 || position > int(m_allTracks.size())) {
4328         return false;
4329     }
4330     int previousId = -1;
4331     if (position < int(m_allTracks.size())) {
4332         previousId = getTrackIndexFromPosition(position);
4333     }
4334     int trackId = TimelineModel::getNextId();
4335     id = trackId;
4336     Fun local_undo = deregisterTrack_lambda(trackId);
4337     TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack, addCompositing);
4338     // Adjust compositions that were affecting track at previous pos
4339     QList<std::shared_ptr<CompositionModel>> updatedCompositions;
4340     if (previousId > -1) {
4341         for (auto &compo : m_allCompositions) {
4342             if (position > 0 && compo.second->getATrack() == position && compo.second->getForcedTrack() == -1) {
4343                 updatedCompositions << compo.second;
4344             }
4345         }
4346     }
4347     Fun local_update = [position, updatedCompositions]() {
4348         for (auto &compo : updatedCompositions) {
4349             compo->setATrack(position + 1, -1);
4350         }
4351         return true;
4352     };
4353     Fun local_update_undo = [position, updatedCompositions]() {
4354         for (auto &compo : updatedCompositions) {
4355             compo->setATrack(position, -1);
4356         }
4357         return true;
4358     };
4359 
4360     Fun local_name_update = [position, audioTrack, this]() {
4361         if (KdenliveSettings::audiotracksbelow() == 0) {
4362             _resetView();
4363         } else {
4364             if (audioTrack) {
4365                 for (int i = 0; i <= position && i < int(m_allTracks.size()); i++) {
4366                     QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4367                     Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4368                 }
4369             } else {
4370                 for (int i = position; i < int(m_allTracks.size()); i++) {
4371                     QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4372                     Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4373                 }
4374             }
4375         }
4376         return true;
4377     };
4378 
4379     local_update();
4380     local_name_update();
4381     Fun rebuild_compositing = [this]() {
4382         buildTrackCompositing(true);
4383         return true;
4384     };
4385     if (addCompositing) {
4386         buildTrackCompositing(true);
4387     }
4388     auto track = getTrackById(trackId);
4389     Fun local_redo = [track, position, local_update, addCompositing, this]() {
4390         // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
4391         // sufficient to register it.
4392         registerTrack(track, position, true);
4393         local_update();
4394         if (addCompositing) {
4395             buildTrackCompositing(true);
4396         }
4397         return true;
4398     };
4399     if (addCompositing) {
4400         PUSH_LAMBDA(local_update_undo, local_undo);
4401         PUSH_LAMBDA(rebuild_compositing, local_undo);
4402     }
4403     PUSH_LAMBDA(local_name_update, local_undo);
4404     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4405     PUSH_LAMBDA(local_name_update, redo);
4406     return true;
4407 }
4408 
4409 bool TimelineModel::requestTrackDeletion(int trackId)
4410 {
4411     // TODO: make sure we disable overlayTrack before deleting a track
4412     QWriteLocker locker(&m_lock);
4413     TRACE(trackId);
4414     Fun undo = []() { return true; };
4415     Fun redo = []() { return true; };
4416     bool result = requestTrackDeletion(trackId, undo, redo);
4417     if (result) {
4418         if (m_videoTarget == trackId) {
4419             m_videoTarget = -1;
4420         }
4421         if (m_audioTarget.contains(trackId)) {
4422             m_audioTarget.remove(trackId);
4423         }
4424         PUSH_UNDO(undo, redo, i18n("Delete Track"));
4425     }
4426     TRACE_RES(result);
4427     return result;
4428 }
4429 
4430 bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo)
4431 {
4432     Q_ASSERT(isTrack(trackId));
4433     if (m_allTracks.size() < 2) {
4434         pCore->displayMessage(i18n("Cannot delete last track in timeline"), ErrorMessage, 500);
4435         return false;
4436     }
4437     // Discard running jobs
4438     pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::TimelineTrack, trackId, m_uuid));
4439 
4440     std::vector<int> clips_to_delete;
4441     for (const auto &it : getTrackById(trackId)->m_allClips) {
4442         clips_to_delete.push_back(it.first);
4443     }
4444     Fun local_undo = []() { return true; };
4445     Fun local_redo = []() { return true; };
4446     for (int clip : clips_to_delete) {
4447         bool res = true;
4448         while (res && m_groups->isInGroup(clip)) {
4449             res = requestClipUngroup(clip, local_undo, local_redo);
4450         }
4451         if (res) {
4452             res = requestClipDeletion(clip, local_undo, local_redo);
4453         }
4454         if (!res) {
4455             bool u = local_undo();
4456             Q_ASSERT(u);
4457             return false;
4458         }
4459     }
4460     std::vector<int> compositions_to_delete;
4461     for (const auto &it : getTrackById(trackId)->m_allCompositions) {
4462         compositions_to_delete.push_back(it.first);
4463     }
4464     for (int compo : compositions_to_delete) {
4465         bool res = true;
4466         while (res && m_groups->isInGroup(compo)) {
4467             res = requestClipUngroup(compo, local_undo, local_redo);
4468         }
4469         if (res) {
4470             res = requestCompositionDeletion(compo, local_undo, local_redo);
4471         }
4472         if (!res) {
4473             bool u = local_undo();
4474             Q_ASSERT(u);
4475             return false;
4476         }
4477     }
4478     int old_position = getTrackPosition(trackId);
4479     int previousTrack = getPreviousVideoTrackPos(trackId);
4480     auto operation = deregisterTrack_lambda(trackId);
4481     std::shared_ptr<TrackModel> track = getTrackById(trackId);
4482     bool audioTrack = track->isAudioTrack();
4483     QList<std::shared_ptr<CompositionModel>> updatedCompositions;
4484     for (auto &compo : m_allCompositions) {
4485         if (compo.second->getATrack() == old_position + 1 && compo.second->getForcedTrack() == -1) {
4486             updatedCompositions << compo.second;
4487         }
4488     }
4489     Fun reverse = [this, track, old_position, updatedCompositions]() {
4490         // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
4491         // sufficient to register it.
4492         registerTrack(track, old_position);
4493         for (auto &compo : updatedCompositions) {
4494             compo->setATrack(old_position + 1, -1);
4495         }
4496         return true;
4497     };
4498     Fun local_update = [previousTrack, updatedCompositions]() {
4499         for (auto &compo : updatedCompositions) {
4500             compo->setATrack(previousTrack, -1);
4501         }
4502         return true;
4503     };
4504     Fun rebuild_compositing = [this]() {
4505         buildTrackCompositing(true);
4506         return true;
4507     };
4508     Fun local_name_update = [old_position, audioTrack, this]() {
4509         if (audioTrack) {
4510             for (int i = 0; i < qMin(old_position + 1, getTracksCount()); i++) {
4511                 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4512                 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4513             }
4514         } else {
4515             for (int i = old_position; i < getTracksCount(); i++) {
4516                 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4517                 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4518             }
4519         }
4520         return true;
4521     };
4522     if (operation()) {
4523         local_update();
4524         rebuild_compositing();
4525         local_name_update();
4526         PUSH_LAMBDA(rebuild_compositing, local_undo);
4527         PUSH_LAMBDA(local_name_update, local_undo);
4528         UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
4529         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4530         PUSH_LAMBDA(local_update, redo);
4531         PUSH_LAMBDA(rebuild_compositing, redo);
4532         PUSH_LAMBDA(local_name_update, redo);
4533         return true;
4534     }
4535     local_undo();
4536     return false;
4537 }
4538 
4539 void TimelineModel::registerTrack(std::shared_ptr<TrackModel> track, int pos, bool doInsert, bool singleOperation)
4540 {
4541     int id = track->getId();
4542     if (pos == -1) {
4543         pos = static_cast<int>(m_allTracks.size());
4544     }
4545     Q_ASSERT(pos >= 0);
4546     Q_ASSERT(pos <= static_cast<int>(m_allTracks.size()));
4547 
4548     // effective insertion (MLT operation), add 1 to account for black background track
4549     if (doInsert) {
4550         if (!singleOperation) {
4551             m_tractor->block();
4552         }
4553         int error = m_tractor->insert_track(*track, pos + 1);
4554         if (!singleOperation) {
4555             m_tractor->unblock();
4556         }
4557         Q_ASSERT(error == 0); // we might need better error handling...
4558     }
4559 
4560     // we now insert in the list
4561     auto posIt = m_allTracks.begin();
4562     std::advance(posIt, pos);
4563     beginInsertRows(QModelIndex(), pos, pos);
4564     auto it = m_allTracks.insert(posIt, std::move(track));
4565     // it now contains the iterator to the inserted element, we store it
4566     Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen)
4567     m_iteratorTable[id] = it;
4568     endInsertRows();
4569     int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2;
4570     mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache));
4571 }
4572 
4573 void TimelineModel::registerClip(const std::shared_ptr<ClipModel> &clip, bool registerProducer)
4574 {
4575     int id = clip->getId();
4576     Q_ASSERT(m_allClips.count(id) == 0);
4577     m_allClips[id] = clip;
4578     clip->registerClipToBin(clip->getProducer(), registerProducer);
4579     m_groups->createGroupItem(id);
4580     clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled);
4581 }
4582 
4583 void TimelineModel::registerSubtitle(int id, GenTime startTime, bool temporary)
4584 {
4585     Q_ASSERT(m_allSubtitles.count(id) == 0);
4586     m_allSubtitles.emplace(id, startTime);
4587     if (!temporary) {
4588         m_groups->createGroupItem(id);
4589     }
4590 }
4591 
4592 int TimelineModel::positionForIndex(int id)
4593 {
4594     return int(std::distance(m_allSubtitles.begin(), m_allSubtitles.find(id)));
4595 }
4596 
4597 void TimelineModel::deregisterSubtitle(int id, bool temporary)
4598 {
4599     Q_ASSERT(m_allSubtitles.count(id) > 0);
4600     if (!temporary && m_subtitleModel->isSelected(id)) {
4601         requestClearSelection(true);
4602     }
4603     m_allSubtitles.erase(id);
4604     if (!temporary) {
4605         m_groups->destructGroupItem(id);
4606     }
4607 }
4608 
4609 void TimelineModel::registerGroup(int groupId)
4610 {
4611     Q_ASSERT(m_allGroups.count(groupId) == 0);
4612     m_allGroups.insert(groupId);
4613 }
4614 
4615 Fun TimelineModel::deregisterTrack_lambda(int id)
4616 {
4617     return [this, id]() {
4618         if (!m_closing) {
4619             Q_EMIT checkTrackDeletion(id);
4620         }
4621         auto it = m_iteratorTable[id];    // iterator to the element
4622         int index = getTrackPosition(id); // compute index in list
4623         if (!m_closing) {
4624             // send update to the model
4625             beginRemoveRows(QModelIndex(), index, index);
4626         }
4627         // melt operation, add 1 to account for black background track
4628         m_tractor->remove_track(static_cast<int>(index + 1));
4629         // actual deletion of object
4630         m_allTracks.erase(it);
4631         // clean table
4632         m_iteratorTable.erase(id);
4633         if (!m_closing) {
4634             // Finish operation
4635             endRemoveRows();
4636             int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2;
4637             mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache));
4638         }
4639         return true;
4640     };
4641 }
4642 
4643 Fun TimelineModel::deregisterClip_lambda(int clipId)
4644 {
4645     return [this, clipId]() {
4646         // Clear effect stack
4647         Q_EMIT requestClearAssetView(clipId);
4648         if (!m_closing) {
4649             Q_EMIT checkItemDeletion(clipId);
4650         }
4651         Q_ASSERT(m_allClips.count(clipId) > 0);
4652         Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point
4653         Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point
4654         auto clip = m_allClips[clipId];
4655         m_allClips.erase(clipId);
4656         clip->deregisterClipToBin(m_uuid);
4657         m_groups->destructGroupItem(clipId);
4658         return true;
4659     };
4660 }
4661 
4662 void TimelineModel::deregisterGroup(int id)
4663 {
4664     Q_ASSERT(m_allGroups.count(id) > 0);
4665     m_allGroups.erase(id);
4666 }
4667 
4668 std::shared_ptr<TrackModel> TimelineModel::getTrackById(int trackId)
4669 {
4670     Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4671     return *m_iteratorTable[trackId];
4672 }
4673 
4674 const std::shared_ptr<TrackModel> TimelineModel::getTrackById_const(int trackId) const
4675 {
4676     Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4677     return *m_iteratorTable.at(trackId);
4678 }
4679 
4680 bool TimelineModel::addTrackEffect(int trackId, const QString &effectId)
4681 {
4682     if (trackId == -1) {
4683         if (m_masterStack == nullptr || m_masterStack->appendEffect(effectId) == false) {
4684             QString effectName = EffectsRepository::get()->getName(effectId);
4685             pCore->displayMessage(i18n("Cannot add effect %1 to master track", effectName), InformationMessage, 500);
4686             return false;
4687         }
4688     } else {
4689         Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4690         if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) {
4691             QString effectName = EffectsRepository::get()->getName(effectId);
4692             pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500);
4693             return false;
4694         }
4695     }
4696     return true;
4697 }
4698 
4699 bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId)
4700 {
4701     QStringList source = sourceId.split(QLatin1Char(','));
4702     Q_ASSERT(source.count() == 4);
4703     int itemType = source.at(0).toInt();
4704     int itemId = source.at(1).toInt();
4705     int itemRow = source.at(2).toInt();
4706     const QUuid uuid(source.at(3));
4707     std::shared_ptr<EffectStackModel> effectStack = pCore->getItemEffectStack(uuid, itemType, itemId);
4708 
4709     if (trackId == -1) {
4710         QWriteLocker locker(&m_lock);
4711         if (m_masterStack == nullptr || m_masterStack->copyEffect(effectStack->getEffectStackRow(itemRow), PlaylistState::Disabled) ==
4712                                             false) { // We use "disabled" in a hacky way to accept video and audio on master
4713             pCore->displayMessage(i18n("Cannot paste effect to master track"), InformationMessage, 500);
4714             return false;
4715         }
4716     } else {
4717         Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4718         if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) {
4719             pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500);
4720             return false;
4721         }
4722     }
4723     return true;
4724 }
4725 
4726 std::shared_ptr<ClipModel> TimelineModel::getClipPtr(int clipId) const
4727 {
4728     Q_ASSERT(m_allClips.count(clipId) > 0);
4729     return m_allClips.at(clipId);
4730 }
4731 
4732 QVariantList TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify)
4733 {
4734     Q_ASSERT(m_allClips.count(clipId) > 0);
4735     bool result = false;
4736     QVariantList affectedClips;
4737     std::unordered_set<int> items;
4738     if (m_singleSelectionMode && m_currentSelection.count(clipId)) {
4739         // only operate on the selected item(s)
4740         items = m_currentSelection;
4741     } else if (m_groups->isInGroup(clipId)) {
4742         int parentGroup = m_groups->getRootId(clipId);
4743         if (parentGroup > -1) {
4744             items = m_groups->getLeaves(parentGroup);
4745         }
4746     } else {
4747         items = {clipId};
4748     }
4749     Fun undo = []() { return true; };
4750     Fun redo = []() { return true; };
4751     for (auto &s : items) {
4752         if (isClip(s)) {
4753             if (m_allClips.at(s)->addEffectWithUndo(effectId, undo, redo)) {
4754                 result = true;
4755                 affectedClips << s;
4756             }
4757         }
4758     }
4759     if (result) {
4760         pCore->pushUndo(undo, redo, i18n("Add effect %1", EffectsRepository::get()->getName(effectId)));
4761     } else if (notify) {
4762         QString effectName = EffectsRepository::get()->getName(effectId);
4763         pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500);
4764     }
4765     return affectedClips;
4766 }
4767 
4768 bool TimelineModel::removeFade(int clipId, bool fromStart)
4769 {
4770     Q_ASSERT(m_allClips.count(clipId) > 0);
4771     return m_allClips.at(clipId)->removeFade(fromStart);
4772 }
4773 
4774 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStack(int itemId)
4775 {
4776     Q_ASSERT(m_allClips.count(itemId));
4777     return m_allClips.at(itemId)->m_effectStack;
4778 }
4779 
4780 bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration)
4781 {
4782     Q_ASSERT(m_allClips.count(clipId));
4783     Fun undo = []() { return true; };
4784     Fun redo = []() { return true; };
4785     bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo);
4786     if (res && initialDuration > 0) {
4787         PUSH_UNDO(undo, redo, i18n("Adjust Fade"));
4788     }
4789     return res;
4790 }
4791 
4792 std::shared_ptr<CompositionModel> TimelineModel::getCompositionPtr(int compoId) const
4793 {
4794     Q_ASSERT(m_allCompositions.count(compoId) > 0);
4795     return m_allCompositions.at(compoId);
4796 }
4797 
4798 int TimelineModel::getNextId()
4799 {
4800     return KdenliveDoc::next_id++;
4801 }
4802 
4803 bool TimelineModel::isClip(int id) const
4804 {
4805     return m_allClips.count(id) > 0;
4806 }
4807 
4808 bool TimelineModel::isComposition(int id) const
4809 {
4810     return m_allCompositions.count(id) > 0;
4811 }
4812 
4813 bool TimelineModel::isSubTitle(int id) const
4814 {
4815     return m_allSubtitles.count(id) > 0;
4816 }
4817 
4818 bool TimelineModel::isItem(int id) const
4819 {
4820     return isClip(id) || isComposition(id) || isSubTitle(id);
4821 }
4822 
4823 bool TimelineModel::isTrack(int id) const
4824 {
4825     return m_iteratorTable.count(id) > 0;
4826 }
4827 
4828 bool TimelineModel::isGroup(int id) const
4829 {
4830     return m_allGroups.count(id) > 0;
4831 }
4832 
4833 bool TimelineModel::isInGroup(int id) const
4834 {
4835     return m_groups->isInGroup(id);
4836 }
4837 
4838 void TimelineModel::updateDuration()
4839 {
4840     if (m_closing) {
4841         return;
4842     }
4843     int current = m_blackClip->get_playtime() - TimelineModel::seekDuration - 1;
4844     int duration = 0;
4845     for (const auto &tck : m_iteratorTable) {
4846         auto track = (*tck.second);
4847         if (track->isAudioTrack()) {
4848             if (track->isMute()) {
4849                 continue;
4850             }
4851         } else if (track->isHidden()) {
4852             continue;
4853         }
4854         duration = qMax(duration, track->trackDuration());
4855     }
4856     if (m_subtitleModel) {
4857         duration = qMax(duration, m_subtitleModel->trackDuration());
4858     }
4859     if (duration != current) {
4860         // update black track length
4861         std::unique_ptr<Mlt::Field> field(m_tractor->field());
4862         field->lock();
4863         m_blackClip->set("out", duration + TimelineModel::seekDuration);
4864         field->unlock();
4865         Q_EMIT durationUpdated(m_uuid);
4866         if (m_masterStack) {
4867             Q_EMIT m_masterStack->dataChanged(QModelIndex(), QModelIndex(), {});
4868         }
4869     }
4870 }
4871 
4872 int TimelineModel::duration() const
4873 {
4874     int duration = 0;
4875     auto it = m_allTracks.cbegin();
4876     while (it != m_allTracks.cend()) {
4877         if ((*it)->isAudioTrack()) {
4878             if ((*it)->isMute()) {
4879                 // Muted audio track
4880                 ++it;
4881                 continue;
4882             }
4883         } else if ((*it)->isHidden()) {
4884             // Hidden video track
4885             ++it;
4886             continue;
4887         }
4888         int trackDuration = (*it)->getTrackService()->get_playtime();
4889         duration = qMax(duration, trackDuration);
4890         ++it;
4891     }
4892     if (m_subtitleModel && !m_subtitleModel->isDisabled()) {
4893         duration = qMax(duration, m_subtitleModel->trackDuration());
4894     }
4895     return duration;
4896 }
4897 
4898 std::unordered_set<int> TimelineModel::getGroupElements(int clipId)
4899 {
4900     int groupId = m_groups->getRootId(clipId);
4901     return m_groups->getLeaves(groupId);
4902 }
4903 
4904 bool TimelineModel::requestReset(Fun &undo, Fun &redo)
4905 {
4906     std::vector<int> all_ids;
4907     for (const auto &track : m_iteratorTable) {
4908         all_ids.push_back(track.first);
4909     }
4910     bool ok = true;
4911     for (int trackId : all_ids) {
4912         ok = ok && requestTrackDeletion(trackId, undo, redo);
4913     }
4914     return ok;
4915 }
4916 
4917 void TimelineModel::setUndoStack(std::weak_ptr<DocUndoStack> undo_stack)
4918 {
4919     m_undoStack = std::move(undo_stack);
4920 }
4921 
4922 int TimelineModel::suggestSnapPoint(int pos, int snapDistance)
4923 {
4924     int cursorPosition = pCore->getMonitorPosition();
4925     m_snaps->addPoint(cursorPosition);
4926     int snapped = m_snaps->getClosestPoint(pos);
4927     m_snaps->removePoint(cursorPosition);
4928     return (qAbs(snapped - pos) < snapDistance ? snapped : pos);
4929 }
4930 
4931 int TimelineModel::getBestSnapPos(int referencePos, int diff, std::vector<int> pts, int cursorPosition, int snapDistance)
4932 {
4933     if (!pts.empty()) {
4934         if (m_editMode == TimelineMode::NormalEdit) {
4935             m_snaps->ignore(pts);
4936         }
4937     } else {
4938         return -1;
4939     }
4940     // Sort and remove duplicates
4941     std::sort(pts.begin(), pts.end());
4942     pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
4943     m_snaps->addPoint(cursorPosition);
4944     int closest = -1;
4945     int lowestDiff = snapDistance + 1;
4946     for (int point : pts) {
4947         int snapped = m_snaps->getClosestPoint(point + diff);
4948         int currentDiff = qAbs(point + diff - snapped);
4949         if (currentDiff < lowestDiff) {
4950             lowestDiff = currentDiff;
4951             closest = snapped - (point - referencePos);
4952             if (lowestDiff < 2) {
4953                 break;
4954             }
4955         }
4956     }
4957     if (m_editMode == TimelineMode::NormalEdit) {
4958         m_snaps->unIgnore();
4959     }
4960     m_snaps->removePoint(cursorPosition);
4961     return closest;
4962 }
4963 
4964 int TimelineModel::getNextSnapPos(int pos, std::vector<int> &snaps, std::vector<int> &ignored)
4965 {
4966     QVector<int> tracks;
4967     // Get active tracks
4968     auto it = m_allTracks.cbegin();
4969     while (it != m_allTracks.cend()) {
4970         if ((*it)->shouldReceiveTimelineOp()) {
4971             tracks << (*it)->getId();
4972         }
4973         ++it;
4974     }
4975     bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty();
4976     bool filterOutSubtitles = false;
4977     if (hasSubtitles) {
4978         // If subtitle track is locked or hidden, don't snap to it
4979         if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) {
4980             filterOutSubtitles = true;
4981         }
4982     }
4983     if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) {
4984         // No active track, use all possible snap points
4985         m_snaps->ignore(ignored);
4986         int next = m_snaps->getNextPoint(pos);
4987         m_snaps->unIgnore();
4988         return next;
4989     }
4990     for (auto num : ignored) {
4991         snaps.erase(std::remove(snaps.begin(), snaps.end(), num), snaps.end());
4992     }
4993     // Build snap points for selected tracks
4994     for (const auto &cp : m_allClips) {
4995         // Check if clip is on a target track
4996         if (tracks.contains(cp.second->getCurrentTrackId())) {
4997             auto clip = (cp.second);
4998             clip->allSnaps(snaps);
4999         }
5000     }
5001     // Subtitle snaps
5002     if (hasSubtitles && !filterOutSubtitles) {
5003         // Add subtitle snaps
5004         m_subtitleModel->allSnaps(snaps);
5005     }
5006     // sort snaps
5007     std::sort(snaps.begin(), snaps.end());
5008     for (auto i : snaps) {
5009         if (int(i) > pos) {
5010             return int(i);
5011         }
5012     }
5013     return pos;
5014 }
5015 
5016 int TimelineModel::getPreviousSnapPos(int pos, std::vector<int> &snaps, std::vector<int> &ignored)
5017 {
5018     QVector<int> tracks;
5019     // Get active tracks
5020     auto it = m_allTracks.cbegin();
5021     while (it != m_allTracks.cend()) {
5022         if ((*it)->shouldReceiveTimelineOp()) {
5023             tracks << (*it)->getId();
5024         }
5025         ++it;
5026     }
5027     bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty();
5028     bool filterOutSubtitles = false;
5029     if (hasSubtitles) {
5030         // If subtitle track is locked or hidden, don't snap to it
5031         if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) {
5032             filterOutSubtitles = true;
5033         }
5034     }
5035     if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) {
5036         // No active track, use all possible snap points
5037         m_snaps->ignore(ignored);
5038         int previous = m_snaps->getPreviousPoint(pos);
5039         m_snaps->unIgnore();
5040         return previous;
5041     }
5042     // Build snap points for selected tracks
5043     for (auto num : ignored) {
5044         snaps.erase(std::remove(snaps.begin(), snaps.end(), num), snaps.end());
5045     }
5046     for (const auto &cp : m_allClips) {
5047         // Check if clip is on a target track
5048         if (tracks.contains(cp.second->getCurrentTrackId())) {
5049             auto clip = (cp.second);
5050             clip->allSnaps(snaps);
5051         }
5052     }
5053     // Subtitle snaps
5054     if (hasSubtitles && !filterOutSubtitles) {
5055         // Add subtitle snaps
5056         m_subtitleModel->allSnaps(snaps);
5057     }
5058     // sort snaps
5059     std::sort(snaps.begin(), snaps.end());
5060     // sort descending
5061     std::reverse(snaps.begin(), snaps.end());
5062     for (auto i : snaps) {
5063         if (int(i) < pos) {
5064             return int(i);
5065         }
5066     }
5067     return 0;
5068 }
5069 
5070 void TimelineModel::addSnap(int pos)
5071 {
5072     TRACE(pos);
5073     return m_snaps->addPoint(pos);
5074 }
5075 
5076 void TimelineModel::removeSnap(int pos)
5077 {
5078     TRACE(pos);
5079     return m_snaps->removePoint(pos);
5080 }
5081 
5082 void TimelineModel::registerComposition(const std::shared_ptr<CompositionModel> &composition)
5083 {
5084     int id = composition->getId();
5085     Q_ASSERT(m_allCompositions.count(id) == 0);
5086     m_allCompositions[id] = composition;
5087     m_groups->createGroupItem(id);
5088 }
5089 
5090 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr<Mlt::Properties> transProps,
5091                                                 int &id, bool logUndo)
5092 {
5093     QWriteLocker locker(&m_lock);
5094     // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo);
5095     Fun undo = []() { return true; };
5096     Fun redo = []() { return true; };
5097     bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo);
5098     if (result && logUndo) {
5099         PUSH_UNDO(undo, redo, i18n("Insert Composition"));
5100     }
5101     // TRACE_RES(result);
5102     return result;
5103 }
5104 
5105 bool TimelineModel::requestCompositionCreation(const QString &transitionId, int length, std::unique_ptr<Mlt::Properties> transProps, int &id, Fun &undo,
5106                                                Fun &redo, bool finalMove, const QString &originalDecimalPoint)
5107 {
5108     Q_UNUSED(finalMove)
5109     int compositionId = TimelineModel::getNextId();
5110     id = compositionId;
5111     Fun local_undo = deregisterComposition_lambda(compositionId);
5112     CompositionModel::construct(shared_from_this(), transitionId, originalDecimalPoint, length, compositionId, std::move(transProps));
5113     auto composition = m_allCompositions[compositionId];
5114     Fun local_redo = [composition, this]() {
5115         // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
5116         // back it is sufficient to register it.
5117         registerComposition(composition);
5118         return true;
5119     };
5120     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5121     return true;
5122 }
5123 
5124 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length,
5125                                                 std::unique_ptr<Mlt::Properties> transProps, int &id, Fun &undo, Fun &redo, bool finalMove,
5126                                                 const QString &originalDecimalPoint)
5127 {
5128     int compositionId = TimelineModel::getNextId();
5129     id = compositionId;
5130     Fun local_undo = deregisterComposition_lambda(compositionId);
5131     CompositionModel::construct(shared_from_this(), transitionId, originalDecimalPoint, length, compositionId, std::move(transProps));
5132     auto composition = m_allCompositions[compositionId];
5133     Fun local_redo = [composition, this]() {
5134         // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
5135         // back it is sufficient to register it.
5136         registerComposition(composition);
5137         return true;
5138     };
5139     bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo);
5140     if (res) {
5141         res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true);
5142     }
5143     if (!res) {
5144         bool undone = local_undo();
5145         Q_ASSERT(undone);
5146         id = -1;
5147         return false;
5148     }
5149     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5150     return true;
5151 }
5152 
5153 Fun TimelineModel::deregisterComposition_lambda(int compoId)
5154 {
5155     return [this, compoId]() {
5156         Q_ASSERT(m_allCompositions.count(compoId) > 0);
5157         Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point
5158         requestClearSelection(true);
5159         Q_EMIT requestClearAssetView(compoId);
5160         m_allCompositions.erase(compoId);
5161         m_groups->destructGroupItem(compoId);
5162         return true;
5163     };
5164 }
5165 
5166 int TimelineModel::getSubtitlePosition(int subId) const
5167 {
5168     Q_ASSERT(m_allSubtitles.count(subId) > 0);
5169     return m_allSubtitles.at(subId).frames(pCore->getCurrentFps());
5170 }
5171 
5172 int TimelineModel::getCompositionPosition(int compoId) const
5173 {
5174     Q_ASSERT(m_allCompositions.count(compoId) > 0);
5175     const auto trans = m_allCompositions.at(compoId);
5176     return trans->getPosition();
5177 }
5178 
5179 int TimelineModel::getCompositionEnd(int compoId) const
5180 {
5181     Q_ASSERT(m_allCompositions.count(compoId) > 0);
5182     const auto trans = m_allCompositions.at(compoId);
5183     return trans->getPosition() + trans->getPlaytime();
5184 }
5185 
5186 int TimelineModel::getCompositionPlaytime(int compoId) const
5187 {
5188     READ_LOCK();
5189     Q_ASSERT(m_allCompositions.count(compoId) > 0);
5190     const auto trans = m_allCompositions.at(compoId);
5191     int playtime = trans->getPlaytime();
5192     return playtime;
5193 }
5194 
5195 int TimelineModel::getItemPosition(int itemId) const
5196 {
5197     if (isClip(itemId)) {
5198         return getClipPosition(itemId);
5199     }
5200     if (isComposition(itemId)) {
5201         return getCompositionPosition(itemId);
5202     }
5203     if (isSubTitle(itemId)) {
5204         return getSubtitlePosition(itemId);
5205     }
5206     return -1;
5207 }
5208 
5209 int TimelineModel::getClipSubPlaylistIndex(int cid) const
5210 {
5211     Q_ASSERT(isClip(cid));
5212     return m_allClips.at(cid)->getSubPlaylistIndex();
5213 }
5214 
5215 const QString TimelineModel::getClipName(int cid) const
5216 {
5217     Q_ASSERT(isClip(cid));
5218     return m_allClips.at(cid)->clipName();
5219 }
5220 
5221 int TimelineModel::getItemEnd(int itemId) const
5222 {
5223     if (isClip(itemId)) {
5224         return getClipEnd(itemId);
5225     }
5226     if (isComposition(itemId)) {
5227         return getCompositionEnd(itemId);
5228     }
5229     if (isSubTitle(itemId)) {
5230         return m_subtitleModel->getSubtitleEnd(itemId);
5231     }
5232     return -1;
5233 }
5234 
5235 int TimelineModel::getItemPlaytime(int itemId) const
5236 {
5237     if (isClip(itemId)) {
5238         return getClipPlaytime(itemId);
5239     }
5240     if (isComposition(itemId)) {
5241         return getCompositionPlaytime(itemId);
5242     }
5243     if (isSubTitle(itemId)) {
5244         return m_subtitleModel->getSubtitlePlaytime(itemId);
5245     }
5246     return -1;
5247 }
5248 
5249 int TimelineModel::getTrackCompositionsCount(int trackId) const
5250 {
5251     Q_ASSERT(isTrack(trackId));
5252     return getTrackById_const(trackId)->getCompositionsCount();
5253 }
5254 
5255 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo)
5256 {
5257     QWriteLocker locker(&m_lock);
5258     Q_ASSERT(isComposition(compoId));
5259     if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) {
5260         return true;
5261     }
5262     if (m_groups->isInGroup(compoId)) {
5263         // element is in a group.
5264         int groupId = m_groups->getRootId(compoId);
5265         int current_trackId = getCompositionTrackId(compoId);
5266         int track_pos1 = getTrackPosition(trackId);
5267         int track_pos2 = getTrackPosition(current_trackId);
5268         int delta_track = track_pos1 - track_pos2;
5269         int delta_pos = position - m_allCompositions[compoId]->getPosition();
5270         return requestGroupMove(compoId, groupId, delta_track, delta_pos, true, updateView, logUndo);
5271     }
5272     std::function<bool(void)> undo = []() { return true; };
5273     std::function<bool(void)> redo = []() { return true; };
5274     int min = getCompositionPosition(compoId);
5275     int max = min + getCompositionPlaytime(compoId);
5276     int tk = getCompositionTrackId(compoId);
5277     bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo);
5278     if (tk > -1) {
5279         min = qMin(min, getCompositionPosition(compoId));
5280         max = qMax(max, getCompositionPosition(compoId));
5281     } else {
5282         min = getCompositionPosition(compoId);
5283         max = min + getCompositionPlaytime(compoId);
5284     }
5285 
5286     if (res && logUndo) {
5287         PUSH_UNDO(undo, redo, i18n("Move composition"));
5288         checkRefresh(min, max);
5289     }
5290     return res;
5291 }
5292 
5293 bool TimelineModel::isAudioTrack(int trackId) const
5294 {
5295     READ_LOCK();
5296     Q_ASSERT(isTrack(trackId));
5297     auto it = m_iteratorTable.at(trackId);
5298     return (*it)->isAudioTrack();
5299 }
5300 
5301 bool TimelineModel::isSubtitleTrack(int trackId) const
5302 {
5303     return trackId == -2;
5304 }
5305 
5306 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
5307 {
5308     QWriteLocker locker(&m_lock);
5309     Q_ASSERT(isComposition(compoId));
5310     Q_ASSERT(isTrack(trackId));
5311     if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) {
5312         compositionTrack = getPreviousVideoTrackPos(trackId);
5313     }
5314     if (compositionTrack == -1) {
5315         // it doesn't make sense to insert a composition on the last track
5316         qDebug() << "Move failed because of last track";
5317         return false;
5318     }
5319 
5320     Fun local_undo = []() { return true; };
5321     Fun local_redo = []() { return true; };
5322     bool ok = true;
5323     int old_trackId = getCompositionTrackId(compoId);
5324     bool notifyViewOnly = false;
5325     Fun update_model = []() { return true; };
5326     if (updateView && old_trackId == trackId) {
5327         // Move on same track, only send view update
5328         updateView = false;
5329         notifyViewOnly = true;
5330         update_model = [compoId, this]() {
5331             QModelIndex modelIndex = makeCompositionIndexFromID(compoId);
5332             notifyChange(modelIndex, modelIndex, StartRole);
5333             return true;
5334         };
5335     }
5336     if (old_trackId != -1) {
5337         Fun delete_operation = []() { return true; };
5338         Fun delete_reverse = []() { return true; };
5339         if (old_trackId != trackId) {
5340             delete_operation = [this, compoId]() {
5341                 bool res = unplantComposition(compoId);
5342                 if (res) m_allCompositions[compoId]->setATrack(-1, -1);
5343                 return res;
5344             };
5345             int oldAtrack = m_allCompositions[compoId]->getATrack();
5346             delete_reverse = [this, compoId, oldAtrack, updateView]() {
5347                 m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack < 1 ? -1 : getTrackIndexFromPosition(oldAtrack - 1));
5348                 return replantCompositions(compoId, updateView);
5349             };
5350         }
5351         ok = delete_operation();
5352         if (!ok) qDebug() << "Move failed because of first delete operation";
5353 
5354         if (ok) {
5355             if (notifyViewOnly) {
5356                 PUSH_LAMBDA(update_model, local_undo);
5357             }
5358             UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo);
5359             ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo, false);
5360         }
5361         if (!ok) {
5362             qDebug() << "Move failed because of first deletion request";
5363             bool undone = local_undo();
5364             Q_ASSERT(undone);
5365             return false;
5366         }
5367     }
5368     ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo);
5369     if (!ok) qDebug() << "Move failed because of second insertion request";
5370     if (ok) {
5371         Fun insert_operation = []() { return true; };
5372         Fun insert_reverse = []() { return true; };
5373         if (old_trackId != trackId) {
5374             insert_operation = [this, compoId, compositionTrack, updateView]() {
5375                 m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack < 1 ? -1 : getTrackIndexFromPosition(compositionTrack - 1));
5376                 return replantCompositions(compoId, updateView);
5377             };
5378             insert_reverse = [this, compoId]() {
5379                 bool res = unplantComposition(compoId);
5380                 if (res) m_allCompositions[compoId]->setATrack(-1, -1);
5381                 return res;
5382             };
5383         }
5384         ok = insert_operation();
5385         if (!ok) qDebug() << "Move failed because of second insert operation";
5386         if (ok) {
5387             if (notifyViewOnly) {
5388                 PUSH_LAMBDA(update_model, local_redo);
5389             }
5390             UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo);
5391         }
5392     }
5393     if (!ok) {
5394         bool undone = local_undo();
5395         Q_ASSERT(undone);
5396         return false;
5397     }
5398     update_model();
5399     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5400     return true;
5401 }
5402 
5403 bool TimelineModel::replantCompositions(int currentCompo, bool updateView)
5404 {
5405     // We ensure that the compositions are planted in a decreasing order of a_track, and increasing order of b_track.
5406     // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order.
5407     std::vector<std::pair<int, int>> compos;
5408     for (const auto &compo : m_allCompositions) {
5409         int trackId = compo.second->getCurrentTrackId();
5410         if (trackId == -1 || compo.second->getATrack() == -1) {
5411             continue;
5412         }
5413         // Note: we need to retrieve the position of the track, that is its melt index.
5414         int trackPos = getTrackMltIndex(trackId);
5415         compos.emplace_back(trackPos, compo.first);
5416         if (compo.first != currentCompo) {
5417             unplantComposition(compo.first);
5418         }
5419     }
5420     // sort by decreasing b_track
5421     std::sort(compos.begin(), compos.end(), [&](const std::pair<int, int> &a, const std::pair<int, int> &b) {
5422         if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) {
5423             return a.first < b.first;
5424         }
5425         return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack();
5426     });
5427     // replant
5428     QScopedPointer<Mlt::Field> field(m_tractor->field());
5429     field->lock();
5430 
5431     // Unplant track compositing
5432     mlt_service nextservice = mlt_service_get_producer(field->get_service());
5433     mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice);
5434     QString resource = mlt_properties_get(properties, "mlt_service");
5435 
5436     mlt_service_type mlt_type = mlt_service_identify(nextservice);
5437     QList<Mlt::Transition *> trackCompositions;
5438     while (mlt_type == mlt_service_transition_type) {
5439         Mlt::Transition transition(reinterpret_cast<mlt_transition>(nextservice));
5440         nextservice = mlt_service_producer(nextservice);
5441         int internal = transition.get_int("internal_added");
5442         if (internal > 0 && resource != QLatin1String("mix")) {
5443             trackCompositions << new Mlt::Transition(transition);
5444             field->disconnect_service(transition);
5445             transition.disconnect_all_producers();
5446         }
5447         if (nextservice == nullptr) {
5448             break;
5449         }
5450         mlt_type = mlt_service_identify(nextservice);
5451         properties = MLT_SERVICE_PROPERTIES(nextservice);
5452         resource = mlt_properties_get(properties, "mlt_service");
5453     }
5454     // Sort track compositing
5455     std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
5456 
5457     for (const auto &compo : compos) {
5458         int aTrack = m_allCompositions[compo.second]->getATrack();
5459         Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count());
5460 
5461         Mlt::Transition &transition = *m_allCompositions[compo.second].get();
5462         transition.set_tracks(aTrack, compo.first);
5463         int ret = field->plant_transition(transition, aTrack, compo.first);
5464 
5465         mlt_service consumer = mlt_service_consumer(transition.get_service());
5466         Q_ASSERT(consumer != nullptr);
5467         if (ret != 0) {
5468             field->unlock();
5469             return false;
5470         }
5471     }
5472     // Replant last tracks compositing
5473     while (!trackCompositions.isEmpty()) {
5474         Mlt::Transition *firstTr = trackCompositions.takeFirst();
5475         field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track());
5476     }
5477     field->unlock();
5478     if (updateView) {
5479         QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo);
5480         notifyChange(modelIndex, modelIndex, ItemATrack);
5481     }
5482     return true;
5483 }
5484 
5485 bool TimelineModel::unplantComposition(int compoId)
5486 {
5487     Mlt::Transition &transition = *m_allCompositions[compoId].get();
5488     mlt_service consumer = mlt_service_consumer(transition.get_service());
5489     Q_ASSERT(consumer != nullptr);
5490     QScopedPointer<Mlt::Field> field(m_tractor->field());
5491     field->lock();
5492     field->disconnect_service(transition);
5493     int ret = transition.disconnect_all_producers();
5494 
5495     mlt_service nextservice = mlt_service_get_producer(transition.get_service());
5496     // mlt_service consumer = mlt_service_consumer(transition.get_service());
5497     Q_ASSERT(nextservice == nullptr);
5498     // Q_ASSERT(consumer == nullptr);
5499     field->unlock();
5500     return ret != 0;
5501 }
5502 
5503 bool TimelineModel::checkConsistency(const std::vector<int> &guideSnaps)
5504 {
5505     // We store all in/outs of clips to check snap points
5506     std::map<int, int> snaps;
5507     for (const auto &tck : m_iteratorTable) {
5508         auto track = (*tck.second);
5509         // Check parent/children link for tracks
5510         if (auto ptr = track->m_parent.lock()) {
5511             if (ptr.get() != this) {
5512                 qWarning() << "Wrong parent for track" << tck.first;
5513                 return false;
5514             }
5515         } else {
5516             qWarning() << "NULL parent for track" << tck.first;
5517             return false;
5518         }
5519         // check consistency of track
5520         if (!track->checkConsistency()) {
5521             qWarning() << "Consistency check failed for track" << tck.first;
5522             return false;
5523         }
5524     }
5525     // Check parent/children link for clips
5526     for (const auto &cp : m_allClips) {
5527         auto clip = (cp.second);
5528         // Check parent/children link for tracks
5529         if (auto ptr = clip->m_parent.lock()) {
5530             if (ptr.get() != this) {
5531                 qWarning() << "Wrong parent for clip" << cp.first;
5532                 return false;
5533             }
5534         } else {
5535             qWarning() << "NULL parent for clip" << cp.first;
5536             return false;
5537         }
5538         if (getClipTrackId(cp.first) != -1) {
5539             snaps[clip->getPosition()] += 1;
5540             snaps[clip->getPosition() + clip->getPlaytime()] += 1;
5541             if (clip->getMixDuration() > 0) {
5542                 snaps[clip->getPosition() + clip->getMixDuration() - clip->getMixCutPosition()] += 1;
5543             }
5544         }
5545         if (!clip->checkConsistency()) {
5546             qWarning() << "Consistency check failed for clip" << cp.first;
5547             return false;
5548         }
5549     }
5550     for (const auto &cp : m_allCompositions) {
5551         auto clip = (cp.second);
5552         // Check parent/children link for tracks
5553         if (auto ptr = clip->m_parent.lock()) {
5554             if (ptr.get() != this) {
5555                 qWarning() << "Wrong parent for compo" << cp.first;
5556                 return false;
5557             }
5558         } else {
5559             qWarning() << "NULL parent for compo" << cp.first;
5560             return false;
5561         }
5562         if (getCompositionTrackId(cp.first) != -1) {
5563             snaps[clip->getPosition()] += 1;
5564             snaps[clip->getPosition() + clip->getPlaytime()] += 1;
5565         }
5566     }
5567     for (auto p : guideSnaps) {
5568         snaps[p] += 1;
5569     }
5570 
5571     // Check snaps
5572     auto stored_snaps = m_snaps->_snaps();
5573     if (snaps.size() != stored_snaps.size()) {
5574         qWarning() << "Wrong number of snaps" << snaps.size() << stored_snaps.size();
5575         return false;
5576     }
5577     for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) {
5578         if (*i != *j) {
5579             qWarning() << "Wrong snap info at point" << (*i).first;
5580             return false;
5581         }
5582     }
5583 
5584     // We check consistency with bin model
5585     auto binClips = pCore->projectItemModel()->getAllClipIds();
5586     // First step: all clips referenced by the bin model exist and are inserted
5587     for (const auto &binClip : binClips) {
5588         auto projClip = pCore->projectItemModel()->getClipByBinID(binClip);
5589         QList<int> referenced = projClip->m_registeredClipsByUuid.value(uuid());
5590         for (int cid : referenced) {
5591             if (!isClip(cid)) {
5592                 qWarning() << "Bin model registers a bad clip ID" << cid;
5593                 qDebug() << ":::: GOT REF CLIPS FOR UUID: " << referenced;
5594                 for (const auto &clip : m_allClips) {
5595                     qWarning() << "Existing cids:" << clip.first;
5596                 }
5597                 return false;
5598             }
5599         }
5600     }
5601 
5602     // Second step: all clips are referenced
5603     for (const auto &clip : m_allClips) {
5604         auto binId = clip.second->m_binClipId;
5605         auto projClip = pCore->projectItemModel()->getClipByBinID(binId);
5606         if (!projClip->m_registeredClipsByUuid.contains(uuid()) || !projClip->m_registeredClipsByUuid.value(uuid()).contains(clip.first)) {
5607             qWarning() << "Clip " << clip.first << "not registered in bin";
5608             return false;
5609         }
5610     }
5611 
5612     // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our
5613     // m_allCompositions
5614     std::unordered_set<int> remaining_compo;
5615     for (const auto &compo : m_allCompositions) {
5616         if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) {
5617             remaining_compo.insert(compo.first);
5618 
5619             // check validity of the consumer
5620             Mlt::Transition &transition = *m_allCompositions[compo.first].get();
5621             mlt_service consumer = mlt_service_consumer(transition.get_service());
5622             Q_ASSERT(consumer != nullptr);
5623         }
5624     }
5625     QScopedPointer<Mlt::Field> field(m_tractor->field());
5626     field->lock();
5627 
5628     mlt_service nextservice = mlt_service_get_producer(field->get_service());
5629     mlt_service_type mlt_type = mlt_service_identify(nextservice);
5630     while (nextservice != nullptr) {
5631         if (mlt_type == mlt_service_transition_type) {
5632             auto tr = mlt_transition(nextservice);
5633             if (mlt_properties_get_int(MLT_TRANSITION_PROPERTIES(tr), "internal_added") > 0) {
5634                 // Skip track compositing
5635                 nextservice = mlt_service_producer(nextservice);
5636                 continue;
5637             }
5638             int currentTrack = mlt_transition_get_b_track(tr);
5639             int currentATrack = mlt_transition_get_a_track(tr);
5640             if (currentTrack == currentATrack) {
5641                 // Skip invalid transitions created by MLT on track deletion
5642                 nextservice = mlt_service_producer(nextservice);
5643                 continue;
5644             }
5645 
5646             int currentIn = mlt_transition_get_in(tr);
5647             int currentOut = mlt_transition_get_out(tr);
5648 
5649             int foundId = -1;
5650             // we iterate to try to find a matching compo
5651             for (int compoId : remaining_compo) {
5652                 if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack &&
5653                     m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) {
5654                     foundId = compoId;
5655                     break;
5656                 }
5657             }
5658             if (foundId == -1) {
5659                 qWarning() << "No matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack
5660                            << ", SERVICE: " << mlt_properties_get(MLT_TRANSITION_PROPERTIES(tr), "mlt_service")
5661                            << "\nID: " << mlt_properties_get(MLT_TRANSITION_PROPERTIES(tr), "id");
5662                 field->unlock();
5663                 return false;
5664             }
5665             remaining_compo.erase(foundId);
5666         }
5667         nextservice = mlt_service_producer(nextservice);
5668         if (nextservice == nullptr) {
5669             break;
5670         }
5671         mlt_type = mlt_service_identify(nextservice);
5672     }
5673     field->unlock();
5674 
5675     if (!remaining_compo.empty()) {
5676         qWarning() << "Compositions have not been found:";
5677         for (int compoId : remaining_compo) {
5678             qWarning() << compoId;
5679         }
5680         return false;
5681     }
5682 
5683     // We check consistency of groups
5684     if (!m_groups->checkConsistency(true, true)) {
5685         qWarning() << "error in group consistency";
5686         return false;
5687     }
5688 
5689     // Check that the selection is in a valid state:
5690     if (m_currentSelection.size() == 1 && !isClip(*m_currentSelection.begin()) && !isComposition(*m_currentSelection.begin()) &&
5691         !isSubTitle(*m_currentSelection.begin()) && !isGroup(*m_currentSelection.begin())) {
5692         qWarning() << "Selection is in inconsistent state";
5693         return false;
5694     }
5695     return true;
5696 }
5697 
5698 void TimelineModel::setTimelineEffectsEnabled(bool enabled)
5699 {
5700     m_timelineEffectsEnabled = enabled;
5701     // propagate info to clips
5702     for (const auto &clip : m_allClips) {
5703         clip.second->setTimelineEffectsEnabled(enabled);
5704     }
5705 
5706     // TODO if we support track effects, they should be disabled here too
5707 }
5708 
5709 std::shared_ptr<Mlt::Producer> TimelineModel::producer()
5710 {
5711     return std::make_shared<Mlt::Producer>(tractor());
5712 }
5713 
5714 const QString TimelineModel::sceneList(const QString &root, const QString &fullPath, const QString &filterData)
5715 {
5716     LocaleHandling::resetLocale();
5717     QString playlist;
5718     Mlt::Consumer xmlConsumer(pCore->getProjectProfile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData());
5719     if (!root.isEmpty()) {
5720         xmlConsumer.set("root", root.toUtf8().constData());
5721     }
5722     if (!xmlConsumer.is_valid()) {
5723         return QString();
5724     }
5725     xmlConsumer.set("store", "kdenlive");
5726     xmlConsumer.set("time_format", "clock");
5727     // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc)
5728     // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening
5729     // xmlConsumer.set("no_meta", 1);
5730     Mlt::Service s(m_tractor->get_service());
5731     std::unique_ptr<Mlt::Filter> filter = nullptr;
5732     if (!filterData.isEmpty()) {
5733         filter = std::make_unique<Mlt::Filter>(pCore->getProjectProfile().get_profile(), QString("dynamictext:%1").arg(filterData).toUtf8().constData());
5734         filter->set("fgcolour", "#ffffff");
5735         filter->set("bgcolour", "#bb333333");
5736         s.attach(*filter.get());
5737     }
5738     xmlConsumer.connect(s);
5739     xmlConsumer.run();
5740     if (filter) {
5741         s.detach(*filter.get());
5742     }
5743     playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath;
5744     return playlist;
5745 }
5746 
5747 void TimelineModel::checkRefresh(int start, int end)
5748 {
5749     if (m_blockRefresh) {
5750         return;
5751     }
5752     int currentPos = tractor()->position();
5753     if (currentPos >= start && currentPos < end) {
5754         Q_EMIT requestMonitorRefresh();
5755     }
5756 }
5757 
5758 std::shared_ptr<AssetParameterModel> TimelineModel::getCompositionParameterModel(int compoId) const
5759 {
5760     READ_LOCK();
5761     Q_ASSERT(isComposition(compoId));
5762     return std::static_pointer_cast<AssetParameterModel>(m_allCompositions.at(compoId));
5763 }
5764 
5765 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStackModel(int clipId) const
5766 {
5767     READ_LOCK();
5768     Q_ASSERT(isClip(clipId));
5769     return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack);
5770 }
5771 
5772 std::shared_ptr<EffectStackModel> TimelineModel::getClipMixStackModel(int clipId) const
5773 {
5774     READ_LOCK();
5775     Q_ASSERT(isClip(clipId));
5776     return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack);
5777 }
5778 
5779 std::shared_ptr<EffectStackModel> TimelineModel::getTrackEffectStackModel(int trackId)
5780 {
5781     READ_LOCK();
5782     Q_ASSERT(isTrack(trackId));
5783     return getTrackById(trackId)->m_effectStack;
5784 }
5785 
5786 std::shared_ptr<EffectStackModel> TimelineModel::getMasterEffectStackModel()
5787 {
5788     READ_LOCK();
5789     if (m_masterStack == nullptr) {
5790         m_masterService.reset(new Mlt::Service(*m_tractor.get()));
5791         m_masterStack = EffectStackModel::construct(m_masterService, ObjectId(KdenliveObjectType::Master, 0, m_uuid), m_undoStack);
5792         connect(m_masterStack.get(), &EffectStackModel::updateMasterZones, pCore.get(), &Core::updateMasterZones);
5793     }
5794     return m_masterStack;
5795 }
5796 
5797 void TimelineModel::importMasterEffects(std::weak_ptr<Mlt::Service> service)
5798 {
5799     READ_LOCK();
5800     if (m_masterStack == nullptr) {
5801         getMasterEffectStackModel();
5802     }
5803     m_masterStack->importEffects(std::move(service), PlaylistState::Disabled, false, QString(), m_uuid);
5804 }
5805 
5806 QStringList TimelineModel::extractCompositionLumas() const
5807 {
5808     QStringList urls;
5809     for (const auto &compo : m_allCompositions) {
5810         QString luma = compo.second->getProperty(QStringLiteral("resource"));
5811         if (luma.isEmpty()) {
5812             luma = compo.second->getProperty(QStringLiteral("luma"));
5813         }
5814         if (!luma.isEmpty()) {
5815             urls << QUrl::fromLocalFile(luma).toLocalFile();
5816         }
5817     }
5818     urls.removeDuplicates();
5819     return urls;
5820 }
5821 
5822 QStringList TimelineModel::extractExternalEffectFiles() const
5823 {
5824     QStringList urls;
5825     for (const auto &clip : m_allClips) {
5826         urls << clip.second->externalFiles();
5827     }
5828     return urls;
5829 }
5830 
5831 void TimelineModel::adjustAssetRange(int clipId, int in, int out)
5832 {
5833     Q_UNUSED(clipId)
5834     Q_UNUSED(in)
5835     Q_UNUSED(out)
5836     // pCore->adjustAssetRange(clipId, in, out);
5837 }
5838 
5839 bool TimelineModel::requestClipReload(int clipId, int forceDuration, Fun &local_undo, Fun &local_redo)
5840 {
5841     if (m_closing) {
5842         return false;
5843     }
5844     // in order to make the producer change effective, we need to unplant / replant the clip in its track
5845     int old_trackId = getClipTrackId(clipId);
5846     int oldPos = getClipPosition(clipId);
5847     int oldOut = getClipIn(clipId) + getClipPlaytime(clipId);
5848     int currentSubplaylist = m_allClips[clipId]->getSubPlaylistIndex();
5849     int maxDuration = m_allClips[clipId]->getMaxDuration();
5850     bool hasPitch = false;
5851     double speed = m_allClips[clipId]->getSpeed();
5852     PlaylistState::ClipState state = m_allClips[clipId]->clipState();
5853     if (!qFuzzyCompare(speed, 1.)) {
5854         hasPitch = m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
5855     }
5856     int audioStream = m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index"));
5857     bool timeremap = m_allClips[clipId]->hasTimeRemap();
5858     // Check if clip out is longer than actual producer duration (if user forced duration)
5859     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId));
5860     bool clipIsShorter = oldOut > int(binClip->frameDuration());
5861     bool refreshView = clipIsShorter || forceDuration > -1;
5862     if (old_trackId != -1) {
5863         if (clipIsShorter && forceDuration == -1 && binClip->hasLimitedDuration()) {
5864             // replacement clip is shorter, resize first
5865             int resizeDuration = int(binClip->frameDuration());
5866             requestItemResize(clipId, resizeDuration, true, true, local_undo, local_redo);
5867         }
5868         getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo, false, false);
5869         m_allClips[clipId]->refreshProducerFromBin(old_trackId, state, audioStream, 0, hasPitch, currentSubplaylist == 1, timeremap);
5870         if (forceDuration > -1) {
5871             m_allClips[clipId]->requestResize(forceDuration, true, local_undo, local_redo);
5872         }
5873         getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo, false, false);
5874         if (maxDuration != m_allClips[clipId]->getMaxDuration()) {
5875             QModelIndex ix = makeClipIndexFromID(clipId);
5876             Q_EMIT dataChanged(ix, ix, {TimelineModel::MaxDurationRole});
5877         }
5878     }
5879     return clipIsShorter;
5880 }
5881 
5882 void TimelineModel::replugClip(int clipId)
5883 {
5884     int old_trackId = getClipTrackId(clipId);
5885     if (old_trackId != -1) {
5886         getTrackById(old_trackId)->replugClip(clipId);
5887     }
5888 }
5889 
5890 void TimelineModel::requestClipUpdate(int clipId, const QVector<int> &roles)
5891 {
5892     QModelIndex modelIndex = makeClipIndexFromID(clipId);
5893     if (roles.contains(TimelineModel::ReloadAudioThumbRole)) {
5894         m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload;
5895     }
5896     if (roles.contains(TimelineModel::ResourceRole)) {
5897         int in = getClipPosition(clipId);
5898         Q_EMIT invalidateZone(in, in + getClipPlaytime(clipId));
5899     }
5900     notifyChange(modelIndex, modelIndex, roles);
5901 }
5902 
5903 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration, Fun &undo, Fun &redo)
5904 {
5905     QWriteLocker locker(&m_lock);
5906     Fun local_undo = []() { return true; };
5907     Fun local_redo = []() { return true; };
5908     int oldPos = getClipPosition(clipId);
5909     // in order to make the producer change effective, we need to unplant / replant the clip in its track
5910     bool success = true;
5911     int trackId = getClipTrackId(clipId);
5912     if (trackId != -1) {
5913         success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false);
5914     }
5915     if (success) {
5916         success = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, local_undo, local_redo);
5917     }
5918     if (trackId != -1) {
5919         success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, false, false);
5920     }
5921     if (!success) {
5922         local_undo();
5923         return false;
5924     }
5925     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5926     return success;
5927 }
5928 
5929 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable)
5930 {
5931     if (!enable || !m_allClips[clipId]->hasTimeRemap()) {
5932         Fun undo = []() { return true; };
5933         Fun redo = []() { return true; };
5934         int splitId = m_groups->getSplitPartner(clipId);
5935         bool result = true;
5936         if (splitId > -1) {
5937             result = requestClipTimeRemap(splitId, enable, undo, redo);
5938         }
5939         result = result && requestClipTimeRemap(clipId, enable, undo, redo);
5940         if (result) {
5941             PUSH_UNDO(undo, redo, i18n("Enable time remap"));
5942             Q_EMIT refreshClipActions();
5943             return true;
5944         } else {
5945             return false;
5946         }
5947     }
5948     return true;
5949 }
5950 
5951 std::shared_ptr<Mlt::Producer> TimelineModel::getClipProducer(int clipId)
5952 {
5953     Q_ASSERT(m_allClips.count(clipId) > 0);
5954     return m_allClips[clipId]->getProducer();
5955 }
5956 
5957 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable, Fun &undo, Fun &redo)
5958 {
5959     QWriteLocker locker(&m_lock);
5960     std::function<bool(void)> local_undo = []() { return true; };
5961     std::function<bool(void)> local_redo = []() { return true; };
5962     int oldPos = getClipPosition(clipId);
5963     // in order to make the producer change effective, we need to unplant / replant the clip in int track
5964     bool success = true;
5965     int trackId = getClipTrackId(clipId);
5966     int previousDuration = 0;
5967     qDebug() << "=== REQUEST REMAP: " << enable << "\n\nWWWWWWWWWWWWWWWWWWWWWWWWWWWW";
5968     if (!enable && m_allClips[clipId]->hasTimeRemap()) {
5969         previousDuration = m_allClips[clipId]->getRemapInputDuration();
5970         qDebug() << "==== CALCULATED INPIUT DURATION: " << previousDuration << "\n\nHHHHHHHHHHHHHH";
5971     }
5972     if (trackId != -1) {
5973         success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false);
5974     }
5975     if (success) {
5976         success = m_allClips[clipId]->useTimeRemapProducer(enable, local_undo, local_redo);
5977     }
5978     if (trackId != -1) {
5979         success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, false, false);
5980         if (success && !enable && previousDuration > 0) {
5981             // Restore input duration
5982             requestItemResize(clipId, previousDuration, true, true, local_undo, local_redo);
5983         }
5984     }
5985     if (!success) {
5986         local_undo();
5987         return false;
5988     }
5989     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5990     return success;
5991 }
5992 
5993 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration)
5994 {
5995     QWriteLocker locker(&m_lock);
5996     if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed()) && pitchCompensate == bool(m_allClips[clipId]->getIntProperty("warp_pitch"))) {
5997         return true;
5998     }
5999     TRACE(clipId, speed);
6000     Fun undo = []() { return true; };
6001     Fun redo = []() { return true; };
6002     // Get main clip info
6003     int trackId = getClipTrackId(clipId);
6004     bool result = true;
6005     if (trackId != -1) {
6006         // Check if clip has a split partner
6007         int splitId = m_groups->getSplitPartner(clipId);
6008         if (splitId > -1) {
6009             result = requestClipTimeWarp(splitId, speed / 100.0, pitchCompensate, changeDuration, undo, redo);
6010         }
6011         if (result) {
6012             result = requestClipTimeWarp(clipId, speed / 100.0, pitchCompensate, changeDuration, undo, redo);
6013         }
6014         if (!result) {
6015             pCore->displayMessage(i18n("Change speed failed"), ErrorMessage);
6016             undo();
6017             TRACE_RES(false);
6018             return false;
6019         }
6020     } else {
6021         // If clip is not inserted on a track, we just change the producer
6022         result = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, undo, redo);
6023     }
6024     if (result) {
6025         PUSH_UNDO(undo, redo, i18n("Change clip speed"));
6026     }
6027     TRACE_RES(result);
6028     return result;
6029 }
6030 
6031 const QString TimelineModel::getTrackTagById(int trackId) const
6032 {
6033     READ_LOCK();
6034     Q_ASSERT(isTrack(trackId));
6035     bool isAudio = getTrackById_const(trackId)->isAudioTrack();
6036     int count = 1;
6037     int totalAudio = 2;
6038     auto it = m_allTracks.cbegin();
6039     bool found = false;
6040     while ((isAudio || !found) && it != m_allTracks.cend()) {
6041         if ((*it)->isAudioTrack()) {
6042             totalAudio++;
6043             if (isAudio && !found) {
6044                 count++;
6045             }
6046         } else if (!isAudio) {
6047             count++;
6048         }
6049         if ((*it)->getId() == trackId) {
6050             found = true;
6051         }
6052         it++;
6053     }
6054     return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1);
6055 }
6056 
6057 /*void TimelineModel::updateProfile(Mlt::Profile profile)
6058 {
6059     m_profile = profile;
6060     m_tractor->set_profile(m_profile.get_profile());
6061     for (int i = 0; i < m_tractor->count(); i++) {
6062         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(i));
6063         tk->set_profile(m_profile.get_profile());
6064         if (tk->type() == mlt_service_tractor_type) {
6065             Mlt::Tractor sub(*tk.get());
6066             for (int j = 0; j < sub.count(); j++) {
6067                 std::shared_ptr<Mlt::Producer> subtk(sub.track(j));
6068                 subtk->set_profile(m_profile.get_profile());
6069             }
6070         }
6071     }
6072     m_blackClip->set_profile(m_profile.get_profile());
6073     // Rebuild compositions since profile has changed
6074     buildTrackCompositing(true);
6075 }*/
6076 
6077 void TimelineModel::updateFieldOrderFilter(std::unique_ptr<ProfileModel> &ptr)
6078 {
6079     std::shared_ptr<Mlt::Filter> foFilter = nullptr;
6080     for (int i = 0; i < m_tractor->filter_count(); i++) {
6081         std::shared_ptr<Mlt::Filter> fl(m_tractor->filter(i));
6082         if (!fl->is_valid()) {
6083             continue;
6084         }
6085         const QString filterService = fl->get("mlt_service");
6086         int foundCount = 0;
6087         if (filterService == QLatin1String("avfilter.fieldorder")) {
6088             foundCount++;
6089             if ((ptr->progressive() || foundCount > 1) && fl->get_int("internal_added") == 237) {
6090                 // If the profile is progressive, field order is redundant: remove
6091                 // Also we only need one field order filter
6092                 m_tractor->detach(*fl.get());
6093                 pCore->currentDoc()->setModified(true);
6094             } else {
6095                 foFilter = fl;
6096                 foFilter->set("internal_added", 237);
6097                 QString value = ptr->bottom_field_first() ? "bff" : "tff";
6098                 if (foFilter->get("av.order") != value) {
6099                     pCore->currentDoc()->setModified(true);
6100                 }
6101                 foFilter->set("av.order", value.toUtf8().constData());
6102             }
6103         }
6104     }
6105     // Build default filter if not found
6106     if (!ptr->progressive() && foFilter == nullptr) {
6107         foFilter.reset(new Mlt::Filter(m_tractor->get_profile(), "avfilter.fieldorder"));
6108         if (foFilter->is_valid()) {
6109             foFilter->set("internal_added", 237);
6110             foFilter->set("av.order", ptr->bottom_field_first() ? "bff" : "tff");
6111             m_tractor->attach(*foFilter.get());
6112             pCore->currentDoc()->setModified(true);
6113         }
6114     }
6115 }
6116 
6117 int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const
6118 {
6119     READ_LOCK();
6120     Q_ASSERT(m_allClips.count(clipId) > 0);
6121     int trackId = getClipTrackId(clipId);
6122     if (trackId != -1) {
6123         return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
6124     }
6125     return 0;
6126 }
6127 
6128 int TimelineModel::getPreviousTrackId(int trackId)
6129 {
6130     READ_LOCK();
6131     Q_ASSERT(isTrack(trackId));
6132     auto it = m_iteratorTable.at(trackId);
6133     bool audioWanted = (*it)->isAudioTrack();
6134     while (it != m_allTracks.cbegin()) {
6135         --it;
6136         if ((*it)->isAudioTrack() == audioWanted) {
6137             return (*it)->getId();
6138         }
6139     }
6140     return trackId;
6141 }
6142 
6143 int TimelineModel::getNextTrackId(int trackId)
6144 {
6145     READ_LOCK();
6146     Q_ASSERT(isTrack(trackId));
6147     auto it = m_iteratorTable.at(trackId);
6148     bool audioWanted = (*it)->isAudioTrack();
6149     while (it != m_allTracks.cend()) {
6150         ++it;
6151         if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) {
6152             break;
6153         }
6154     }
6155     return it == m_allTracks.cend() ? trackId : (*it)->getId();
6156 }
6157 
6158 bool TimelineModel::requestClearSelection(bool onDeletion)
6159 {
6160     QWriteLocker locker(&m_lock);
6161     qDebug() << "::: REQUESTING SELECTION CLEAR!!!!!!";
6162     TRACE();
6163     if (m_singleSelectionMode) {
6164         m_singleSelectionMode = false;
6165         Q_EMIT selectionModeChanged();
6166     }
6167     if (m_selectedMix > -1) {
6168         m_selectedMix = -1;
6169         Q_EMIT selectedMixChanged(-1, nullptr);
6170     }
6171     if (m_currentSelection.size() == 0) {
6172         TRACE_RES(true);
6173         return true;
6174     }
6175     if (isGroup(*m_currentSelection.begin())) {
6176         // Reset offset display on clips
6177         std::unordered_set<int> items = m_groups->getLeaves(*m_currentSelection.begin());
6178         for (auto &id : items) {
6179             if (isGroup(id)) {
6180                 std::unordered_set<int> children = m_groups->getLeaves(id);
6181                 items.insert(children.begin(), children.end());
6182             } else if (isClip(id)) {
6183                 m_allClips[id]->clearOffset();
6184                 m_allClips[id]->setGrab(false);
6185                 m_allClips[id]->setSelected(false);
6186             } else if (isComposition(id)) {
6187                 m_allCompositions[id]->setGrab(false);
6188                 m_allCompositions[id]->setSelected(false);
6189             } else if (isSubTitle(id)) {
6190                 m_subtitleModel->setSelected(id, false);
6191             }
6192             if (m_groups->getType(*m_currentSelection.begin()) == GroupType::Selection) {
6193                 m_groups->destructGroupItem(*m_currentSelection.begin());
6194             }
6195         }
6196     } else {
6197         for (auto s : m_currentSelection) {
6198             if (isClip(s)) {
6199                 m_allClips[s]->setGrab(false);
6200                 m_allClips[s]->setSelected(false);
6201             } else if (isComposition(s)) {
6202                 m_allCompositions[s]->setGrab(false);
6203                 m_allCompositions[s]->setSelected(false);
6204             } else if (isSubTitle(s)) {
6205                 m_subtitleModel->setSelected(s, false);
6206             }
6207             Q_ASSERT(onDeletion || isClip(s) || isComposition(s) || isSubTitle(s));
6208         }
6209     }
6210     m_currentSelection.clear();
6211     if (m_subtitleModel) {
6212         m_subtitleModel->clearGrab();
6213     }
6214     Q_EMIT selectionChanged();
6215     TRACE_RES(true);
6216     return true;
6217 }
6218 
6219 bool TimelineModel::hasMultipleSelection() const
6220 {
6221     READ_LOCK();
6222     if (m_currentSelection.size() == 0) {
6223         return false;
6224     }
6225     if (isGroup(*m_currentSelection.begin())) {
6226         // Reset offset display on clips
6227         std::unordered_set<int> items = m_groups->getLeaves(*m_currentSelection.begin());
6228         return items.size() > 1;
6229     }
6230     return m_currentSelection.size() > 1;
6231 }
6232 
6233 void TimelineModel::requestMixSelection(int cid)
6234 {
6235     requestClearSelection();
6236     int tid = getItemTrackId(cid);
6237     if (tid > -1) {
6238         m_selectedMix = cid;
6239         Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid));
6240     }
6241 }
6242 
6243 void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo)
6244 {
6245     Fun operation = [this, onDeletion]() {
6246         requestClearSelection(onDeletion);
6247         return true;
6248     };
6249     Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); };
6250     if (operation()) {
6251         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
6252     }
6253 }
6254 
6255 void TimelineModel::clearGroupSelectionOnDelete(std::vector<int> groups)
6256 {
6257     READ_LOCK();
6258     if (m_currentSelection.size() == 0) {
6259         return;
6260     }
6261     if (std::find(groups.begin(), groups.end(), *m_currentSelection.begin()) != groups.end()) {
6262         requestClearSelection(true);
6263     }
6264 }
6265 
6266 std::unordered_set<int> TimelineModel::getCurrentSelection() const
6267 {
6268     READ_LOCK();
6269     if (m_currentSelection.size() == 0) {
6270         return {};
6271     }
6272     if (isGroup(*m_currentSelection.begin())) {
6273         return m_groups->getLeaves(*m_currentSelection.begin());
6274     } else {
6275         for (auto &s : m_currentSelection) {
6276             Q_ASSERT(isClip(s) || isComposition(s) || isSubTitle(s));
6277         }
6278         return m_currentSelection;
6279     }
6280 }
6281 
6282 void TimelineModel::requestAddToSelection(int itemId, bool clear, bool singleSelect)
6283 {
6284     QWriteLocker locker(&m_lock);
6285     TRACE(itemId, clear);
6286     std::unordered_set<int> selection;
6287     if (clear) {
6288         requestClearSelection();
6289     } else {
6290         selection = getCurrentSelection();
6291     }
6292     if (singleSelect) {
6293         QWriteLocker locker(&m_lock);
6294         selection.insert(itemId);
6295         m_currentSelection = selection;
6296         setSelected(itemId, true);
6297         Q_EMIT selectionChanged();
6298         if (!m_singleSelectionMode) {
6299             m_singleSelectionMode = true;
6300             Q_EMIT selectionModeChanged();
6301         }
6302         return;
6303     }
6304     if (m_singleSelectionMode) {
6305         m_singleSelectionMode = false;
6306         Q_EMIT selectionModeChanged();
6307     }
6308     if (selection.insert(itemId).second) {
6309         requestSetSelection(selection);
6310     }
6311 }
6312 
6313 void TimelineModel::requestRemoveFromSelection(int itemId)
6314 {
6315     QWriteLocker locker(&m_lock);
6316     TRACE(itemId);
6317     std::unordered_set<int> all_items = {itemId};
6318     int parentGroup = m_groups->getDirectAncestor(itemId);
6319     if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) {
6320         all_items = m_groups->getLeaves(parentGroup);
6321     }
6322     std::unordered_set<int> selection = getCurrentSelection();
6323     for (int current_itemId : all_items) {
6324         if (selection.count(current_itemId) > 0) {
6325             selection.erase(current_itemId);
6326         }
6327     }
6328     requestSetSelection(selection);
6329 }
6330 
6331 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids)
6332 {
6333     QWriteLocker locker(&m_lock);
6334     TRACE(ids);
6335     requestClearSelection();
6336     // if the items are in groups, we must retrieve their topmost containing groups
6337     std::unordered_set<int> roots;
6338     std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
6339 
6340     bool result = true;
6341     if (roots.size() == 0) {
6342         m_currentSelection.clear();
6343     } else if (roots.size() == 1) {
6344         m_currentSelection = {*(roots.begin())};
6345         setSelected(*m_currentSelection.begin(), true);
6346     } else {
6347         Fun undo = []() { return true; };
6348         Fun redo = []() { return true; };
6349         if (ids.size() == 2) {
6350             // Check if we selected 2 clips from the same master
6351             QList<int> pairIds;
6352             for (auto &id : roots) {
6353                 if (isClip(id)) {
6354                     pairIds << id;
6355                 }
6356             }
6357             if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) {
6358                 // Check if they have same bin id
6359                 ClipType::ProducerType type = m_allClips[pairIds.at(0)]->clipType();
6360                 if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Video) {
6361                     // Both clips have same bin ID, display offset
6362                     int pos1 = getClipPosition(pairIds.at(0));
6363                     int pos2 = getClipPosition(pairIds.at(1));
6364                     if (pos2 > pos1) {
6365                         int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0)));
6366                         if (offset != 0) {
6367                             m_allClips[pairIds.at(1)]->setOffset(offset);
6368                             m_allClips[pairIds.at(0)]->setOffset(-offset);
6369                         }
6370                     } else {
6371                         int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1)));
6372                         if (offset != 0) {
6373                             m_allClips[pairIds.at(0)]->setOffset(offset);
6374                             m_allClips[pairIds.at(1)]->setOffset(-offset);
6375                         }
6376                     }
6377                 }
6378             }
6379         }
6380         int groupId = m_groups->groupItems(ids, undo, redo, GroupType::Selection);
6381         if (groupId > -1) {
6382             m_currentSelection = {groupId};
6383             result = true;
6384         } else {
6385             result = false;
6386         }
6387         Q_ASSERT(m_currentSelection.size() > 0);
6388     }
6389     if (m_subtitleModel) {
6390         m_subtitleModel->clearGrab();
6391     }
6392     Q_EMIT selectionChanged();
6393     return result;
6394 }
6395 
6396 void TimelineModel::setSelected(int itemId, bool sel)
6397 {
6398     if (isClip(itemId)) {
6399         m_allClips[itemId]->setSelected(sel);
6400     } else if (isComposition(itemId)) {
6401         m_allCompositions[itemId]->setSelected(sel);
6402     } else if (isSubTitle(itemId)) {
6403         m_subtitleModel->setSelected(itemId, sel);
6404     } else if (isGroup(itemId)) {
6405         auto leaves = m_groups->getLeaves(itemId);
6406         for (auto &id : leaves) {
6407             setSelected(id, true);
6408         }
6409     }
6410 }
6411 
6412 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids, Fun &undo, Fun &redo)
6413 {
6414     QWriteLocker locker(&m_lock);
6415     Fun reverse = [this]() {
6416         requestClearSelection(false);
6417         return true;
6418     };
6419     Fun operation = [this, ids]() { return requestSetSelection(ids); };
6420     if (operation()) {
6421         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
6422         return true;
6423     }
6424     return false;
6425 }
6426 
6427 void TimelineModel::lockTrack(int trackId, bool lock)
6428 {
6429     if (lock) {
6430         getTrackById(trackId)->lock();
6431     } else {
6432         getTrackById(trackId)->unlock();
6433     }
6434 }
6435 
6436 void TimelineModel::setTrackLockedState(int trackId, bool lock)
6437 {
6438     QWriteLocker locker(&m_lock);
6439     TRACE(trackId, lock);
6440     Fun undo = []() { return true; };
6441     Fun redo = []() { return true; };
6442 
6443     Fun lock_lambda = [this, trackId]() {
6444         lockTrack(trackId, true);
6445         return true;
6446     };
6447     Fun unlock_lambda = [this, trackId]() {
6448         lockTrack(trackId, false);
6449         return true;
6450     };
6451     if (lock) {
6452         if (lock_lambda()) {
6453             UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo);
6454             PUSH_UNDO(undo, redo, i18n("Lock track"));
6455         }
6456     } else {
6457         if (unlock_lambda()) {
6458             UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo);
6459             PUSH_UNDO(undo, redo, i18n("Unlock track"));
6460         }
6461     }
6462 }
6463 
6464 std::unordered_set<int> TimelineModel::getAllTracksIds() const
6465 {
6466     READ_LOCK();
6467     std::unordered_set<int> result;
6468     std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; });
6469     return result;
6470 }
6471 
6472 void TimelineModel::switchComposition(int cid, const QString &compoId)
6473 {
6474     Fun undo = []() { return true; };
6475     Fun redo = []() { return true; };
6476     if (isClip(cid)) {
6477         // We are working on a mix
6478         requestClearSelection(true);
6479         int tid = getClipTrackId(cid);
6480         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
6481         getTrackById(tid)->switchMix(cid, compoId, undo, redo);
6482         Fun local_update = [cid, mixData, this]() {
6483             requestMixSelection(cid);
6484             int in = mixData.secondClipInOut.first;
6485             int out = mixData.firstClipInOut.second;
6486             Q_EMIT invalidateZone(in, out);
6487             checkRefresh(in, out);
6488             return true;
6489         };
6490         PUSH_LAMBDA(local_update, redo);
6491         PUSH_FRONT_LAMBDA(local_update, undo);
6492         if (redo()) {
6493             pCore->pushUndo(undo, redo, i18n("Change composition"));
6494         }
6495         return;
6496     }
6497     Q_ASSERT(isComposition(cid));
6498     std::shared_ptr<CompositionModel> compo = m_allCompositions.at(cid);
6499     int currentPos = compo->getPosition();
6500     int duration = compo->getPlaytime();
6501     int currentTrack = compo->getCurrentTrackId();
6502     int a_track = compo->getATrack();
6503     int forcedTrack = compo->getForcedTrack();
6504     // Clear selection
6505     requestClearSelection(true);
6506     if (m_groups->isInGroup(cid)) {
6507         pCore->displayMessage(i18n("Cannot operate on grouped composition, please ungroup"), ErrorMessage);
6508         return;
6509     }
6510 
6511     bool res = requestCompositionDeletion(cid, undo, redo);
6512     int newId = -1;
6513     // Check if composition should be reversed (top clip at beginning, bottom at end)
6514     int topClip = getTrackById_const(currentTrack)->getClipByPosition(currentPos);
6515     int bottomTid = a_track < 1 ? -1 : getTrackIndexFromPosition(a_track - 1);
6516     int bottomClip = -1;
6517     if (bottomTid > -1) {
6518         bottomClip = getTrackById_const(bottomTid)->getClipByPosition(currentPos);
6519     }
6520     bool reverse = false;
6521     if (topClip > -1 && bottomClip > -1) {
6522         if (getClipPosition(topClip) + getClipPlaytime(topClip) < getClipPosition(bottomClip) + getClipPlaytime(bottomClip)) {
6523             reverse = true;
6524         }
6525     }
6526     std::unique_ptr<Mlt::Properties> props(nullptr);
6527     if (reverse) {
6528         props = std::make_unique<Mlt::Properties>();
6529         if (compoId == QLatin1String("dissolve")) {
6530             props->set("reverse", 1);
6531         } else if (compoId == QLatin1String("composite")) {
6532             props->set("invert", 1);
6533         } else if (compoId == QLatin1String("wipe")) {
6534             props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
6535         } else if (compoId == QLatin1String("slide")) {
6536             props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%");
6537         }
6538     }
6539     res = res && requestCompositionInsertion(compoId, currentTrack, a_track, currentPos, duration, std::move(props), newId, undo, redo);
6540     if (res) {
6541         if (forcedTrack > -1 && isComposition(newId)) {
6542             m_allCompositions[newId]->setForceTrack(true);
6543         }
6544         Fun local_redo = [newId, this]() {
6545             requestSetSelection({newId});
6546             return true;
6547         };
6548         Fun local_undo = [cid, this]() {
6549             requestSetSelection({cid});
6550             return true;
6551         };
6552         local_redo();
6553         PUSH_LAMBDA(local_redo, redo);
6554         PUSH_LAMBDA(local_undo, undo);
6555         PUSH_UNDO(undo, redo, i18n("Change composition"));
6556     } else {
6557         undo();
6558     }
6559 }
6560 
6561 bool TimelineModel::plantMix(int tid, Mlt::Transition *t)
6562 {
6563     if (getTrackById_const(tid)->hasClipStart(t->get_in())) {
6564         int a_track = t->get_a_track();
6565         int b_track = t->get_b_track();
6566         getTrackById_const(tid)->getTrackService()->plant_transition(*t, a_track, b_track);
6567         return getTrackById_const(tid)->loadMix(t);
6568     } else {
6569         qDebug() << "=== INVALID MIX FOUND AT: " << t->get_in() << " - " << t->get("mlt_service");
6570         return false;
6571     }
6572 }
6573 
6574 bool TimelineModel::resizeStartMix(int cid, int duration, bool singleResize)
6575 {
6576     Q_ASSERT(isClip(cid));
6577     int tid = m_allClips.at(cid)->getCurrentTrackId();
6578     if (tid > -1) {
6579         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(cid);
6580         if (mixData.first.firstClipId > -1) {
6581             int clipToResize = mixData.first.firstClipId;
6582             Q_ASSERT(isClip(clipToResize));
6583             duration = qMin(duration, m_allClips.at(cid)->getPlaytime());
6584             int updatedDuration = m_allClips.at(cid)->getPosition() + duration - m_allClips[clipToResize]->getPosition();
6585             int result = requestItemResize(clipToResize, updatedDuration, true, true, 0, singleResize);
6586             return result > -1;
6587         }
6588     }
6589     return false;
6590 }
6591 
6592 int TimelineModel::getMixDuration(int cid) const
6593 {
6594     Q_ASSERT(isClip(cid));
6595     int tid = m_allClips.at(cid)->getCurrentTrackId();
6596     if (tid > -1) {
6597         if (getTrackById_const(tid)->hasStartMix(cid)) {
6598             return getTrackById_const(tid)->getMixDuration(cid);
6599         } else {
6600             // Mix is not yet inserted in timeline
6601             std::pair<int, int> mixInOut = getMixInOut(cid);
6602             return mixInOut.second - mixInOut.first;
6603         }
6604     }
6605     return 0;
6606 }
6607 
6608 std::pair<int, int> TimelineModel::getMixInOut(int cid) const
6609 {
6610     Q_ASSERT(isClip(cid));
6611     int tid = m_allClips.at(cid)->getCurrentTrackId();
6612     if (tid > -1) {
6613         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
6614         if (mixData.firstClipId > -1) {
6615             return {mixData.secondClipInOut.first, mixData.firstClipInOut.second};
6616         }
6617     }
6618     return {-1, -1};
6619 }
6620 
6621 int TimelineModel::getMixCutPos(int cid) const
6622 {
6623     Q_ASSERT(isClip(cid));
6624     return m_allClips.at(cid)->getMixCutPosition();
6625 }
6626 
6627 MixAlignment TimelineModel::getMixAlign(int cid) const
6628 {
6629     Q_ASSERT(isClip(cid));
6630     int tid = m_allClips.at(cid)->getCurrentTrackId();
6631     if (tid > -1) {
6632         int mixDuration = m_allClips.at(cid)->getMixDuration();
6633         int mixCutPos = m_allClips.at(cid)->getMixCutPosition();
6634         if (mixCutPos == 0) {
6635             return MixAlignment::AlignRight;
6636         } else if (mixCutPos == mixDuration) {
6637             return MixAlignment::AlignLeft;
6638         } else if (mixCutPos == mixDuration - mixDuration / 2) {
6639             return MixAlignment::AlignCenter;
6640         }
6641     }
6642     return MixAlignment::AlignNone;
6643 }
6644 
6645 void TimelineModel::requestResizeMix(int cid, int duration, MixAlignment align, int leftFrames)
6646 {
6647     Q_ASSERT(isClip(cid));
6648     int tid = m_allClips.at(cid)->getCurrentTrackId();
6649     if (tid > -1) {
6650         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
6651         int clipToResize = mixData.firstClipId;
6652         if (clipToResize > -1) {
6653             Fun undo = []() { return true; };
6654             Fun redo = []() { return true; };
6655             // The mix cut position shoud never change through a resize operation
6656             int cutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getMixCutPosition();
6657             int maxLengthLeft = m_allClips.at(clipToResize)->getMaxDuration();
6658             // Maximum space for expanding the right clip part
6659             int leftMax = maxLengthLeft > -1 ? (maxLengthLeft - 1 - m_allClips.at(clipToResize)->getOut()) : -1;
6660             // Maximum space available on the right clip
6661             int availableLeft = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() -
6662                                 (m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime());
6663             if (leftMax == -1) {
6664                 leftMax = availableLeft;
6665             } else {
6666                 leftMax = qMin(leftMax, availableLeft);
6667             }
6668 
6669             int maxLengthRight = m_allClips.at(cid)->getMaxDuration();
6670             // maximum space to resize clip on the left
6671             int availableRight = m_allClips.at(cid)->getPosition() - m_allClips.at(clipToResize)->getPosition();
6672             int rightMax = maxLengthRight > -1 ? (m_allClips.at(cid)->getIn()) : -1;
6673             if (rightMax == -1) {
6674                 rightMax = availableRight;
6675             } else {
6676                 rightMax = qMin(rightMax, availableRight);
6677             }
6678             Fun adjust_mix_undo = [this, tid, cid, prevCut = m_allClips.at(cid)->getMixCutPosition(), prevDuration = m_allClips.at(cid)->getMixDuration()]() {
6679                 getTrackById_const(tid)->setMixDuration(cid, prevDuration, prevCut);
6680                 QModelIndex ix = makeClipIndexFromID(cid);
6681                 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
6682                 return true;
6683             };
6684             if (align == MixAlignment::AlignLeft) {
6685                 // Adjust left clip
6686                 int updatedDurationLeft = cutPos + duration - m_allClips.at(clipToResize)->getPosition();
6687                 if (leftMax > -1) {
6688                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6689                 }
6690                 // Adjust right clip
6691                 int updatedDurationRight = m_allClips.at(cid)->getPlaytime();
6692                 if (cutPos != m_allClips.at(cid)->getPosition()) {
6693                     updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos;
6694                     if (rightMax > -1) {
6695                         updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6696                     }
6697                 }
6698                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft -
6699                                       (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6700                 if (updatedDuration < 1) {
6701                     //
6702                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6703                     // update mix widget
6704                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6705                     return;
6706                 }
6707                 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo);
6708                 if (m_allClips.at(cid)->getPlaytime() != updatedDurationRight) {
6709                     requestItemResize(cid, updatedDurationRight, false, true, undo, redo);
6710                 }
6711                 int updatedCutPosition = m_allClips.at(cid)->getPosition();
6712                 if (updatedCutPosition != cutPos) {
6713                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6714                     undo();
6715                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6716                     return;
6717                 }
6718                 Fun adjust_mix = [this, tid, cid, updatedDuration]() {
6719                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, updatedDuration);
6720                     QModelIndex ix = makeClipIndexFromID(cid);
6721                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
6722                     return true;
6723                 };
6724                 adjust_mix();
6725                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6726             } else if (align == MixAlignment::AlignRight) {
6727                 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration;
6728                 if (rightMax > -1) {
6729                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6730                 }
6731                 int updatedDurationLeft = cutPos - m_allClips.at(clipToResize)->getPosition();
6732                 if (leftMax > -1) {
6733                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6734                 }
6735                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft -
6736                                       (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6737                 if (updatedDuration < 1) {
6738                     //
6739                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6740                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6741                     return;
6742                 }
6743                 requestItemResize(cid, updatedDurationRight, false, true, undo, redo);
6744                 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo);
6745                 int updatedCutPosition = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime();
6746                 if (updatedCutPosition != cutPos) {
6747                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6748                     undo();
6749                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6750                     return;
6751                 }
6752                 Fun adjust_mix = [this, tid, cid, updatedDuration]() {
6753                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, 0);
6754                     QModelIndex ix = makeClipIndexFromID(cid);
6755                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
6756                     return true;
6757                 };
6758                 adjust_mix();
6759                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6760             } else if (align == MixAlignment::AlignCenter) {
6761                 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration / 2;
6762                 if (rightMax > -1) {
6763                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6764                 }
6765                 int updatedDurationLeft = cutPos + (duration - duration / 2) - m_allClips.at(clipToResize)->getPosition();
6766                 if (leftMax > -1) {
6767                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6768                 }
6769                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft -
6770                                       (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6771                 if (updatedDuration < 1) {
6772                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6773                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6774                     return;
6775                 }
6776                 int deltaLeft = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - cutPos;
6777                 int deltaRight = cutPos - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6778 
6779                 if (deltaRight) {
6780                     if (!requestItemResize(cid, updatedDurationRight, false, true, undo, redo)) {
6781                         qDebug() << ":::: ERROR RESIZING CID1\n\nAAAAAAAAAAAAAAAAAAAA";
6782                     }
6783                 }
6784                 if (deltaLeft > 0) {
6785                     if (!requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo)) {
6786                         qDebug() << ":::: ERROR RESIZING clipToResize\n\nAAAAAAAAAAAAAAAAAAAA";
6787                     }
6788                 }
6789                 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos;
6790                 if (mixCutPos > updatedDuration) {
6791                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6792                     undo();
6793                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6794                     return;
6795                 }
6796                 if (qAbs(deltaLeft - deltaRight) > 2) {
6797                     // Mix not exactly centered
6798                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6799                     return;
6800                 }
6801                 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() {
6802                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos);
6803                     QModelIndex ix = makeClipIndexFromID(cid);
6804                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
6805                     return true;
6806                 };
6807                 adjust_mix();
6808                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6809             } else {
6810                 // No alignment specified
6811                 int updatedDurationRight;
6812                 int updatedDurationLeft;
6813                 if (leftFrames > -1) {
6814                     // A right frame offset was specified
6815                     updatedDurationLeft = qBound(0, leftFrames, duration);
6816                     updatedDurationRight = duration - updatedDurationLeft;
6817                 } else {
6818                     updatedDurationRight = m_allClips.at(cid)->getMixCutPosition();
6819                     updatedDurationLeft = m_allClips.at(cid)->getMixDuration() - updatedDurationRight;
6820                     int currentDuration = m_allClips.at(cid)->getMixDuration();
6821                     if (qAbs(duration - currentDuration) == 1) {
6822                         if (duration < currentDuration) {
6823                             // We are reducing the duration
6824                             if (currentDuration % 2 == 0) {
6825                                 updatedDurationRight--;
6826                                 if (updatedDurationRight < 0) {
6827                                     updatedDurationRight = 0;
6828                                     updatedDurationLeft--;
6829                                 }
6830                             } else {
6831                                 updatedDurationLeft--;
6832                                 if (updatedDurationLeft < 0) {
6833                                     updatedDurationLeft = 0;
6834                                     updatedDurationRight--;
6835                                 }
6836                             }
6837                         } else {
6838                             // Increasing duration
6839                             if (currentDuration % 2 == 0) {
6840                                 updatedDurationRight++;
6841                             } else {
6842                                 updatedDurationLeft++;
6843                             }
6844                         }
6845                     } else {
6846                         double ratio = double(duration) / currentDuration;
6847                         updatedDurationRight *= ratio;
6848                         updatedDurationLeft = duration - updatedDurationRight;
6849                     }
6850                 }
6851                 if (updatedDurationLeft + updatedDurationRight < 1) {
6852                     //
6853                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6854                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6855                     return;
6856                 }
6857                 updatedDurationLeft -= (m_allClips.at(cid)->getMixDuration() - m_allClips.at(cid)->getMixCutPosition());
6858                 updatedDurationRight -= m_allClips.at(cid)->getMixCutPosition();
6859                 if (leftMax > -1) {
6860                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6861                 }
6862                 if (rightMax > -1) {
6863                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6864                 }
6865                 if (updatedDurationLeft != 0) {
6866                     int updatedDurL = m_allClips.at(cid)->getPlaytime() + updatedDurationLeft;
6867                     requestItemResize(cid, updatedDurL, false, true, undo, redo);
6868                 }
6869                 if (updatedDurationRight != 0) {
6870                     int updatedDurR = m_allClips.at(clipToResize)->getPlaytime() + updatedDurationRight;
6871                     requestItemResize(clipToResize, updatedDurR, true, true, undo, redo);
6872                 }
6873                 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos;
6874                 int updatedDuration =
6875                     m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getPosition();
6876                 if (mixCutPos > updatedDuration) {
6877                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6878                     undo();
6879                     Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6880                     return;
6881                 }
6882                 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() {
6883                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos);
6884                     QModelIndex ix = makeClipIndexFromID(cid);
6885                     Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole});
6886                     return true;
6887                 };
6888                 adjust_mix();
6889                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6890             }
6891             pCore->pushUndo(undo, redo, i18n("Resize mix"));
6892         }
6893     }
6894 }
6895 
6896 int TimelineModel::getSubtitleIndex(int subId) const
6897 {
6898     if (m_allSubtitles.count(subId) == 0) {
6899         return -1;
6900     }
6901     auto it = m_allSubtitles.find(subId);
6902     return int(std::distance(m_allSubtitles.begin(), it));
6903 }
6904 
6905 std::pair<int, GenTime> TimelineModel::getSubtitleIdFromIndex(int index) const
6906 {
6907     if (index >= static_cast<int>(m_allSubtitles.size())) {
6908         return {-1, GenTime()};
6909     }
6910     auto it = m_allSubtitles.begin();
6911     std::advance(it, index);
6912     return {it->first, it->second};
6913 }
6914 
6915 QVariantList TimelineModel::getMasterEffectZones() const
6916 {
6917     if (m_masterStack) {
6918         return m_masterStack->getEffectZones();
6919     }
6920     return {};
6921 }
6922 
6923 const QSize TimelineModel::getCompositionSizeOnTrack(const ObjectId &id)
6924 {
6925     int pos = getCompositionPosition(id.itemId);
6926     int tid = getCompositionTrackId(id.itemId);
6927     int cid = getTrackById_const(tid)->getClipByPosition(pos);
6928     if (cid > -1) {
6929         return getClipFrameSize(cid);
6930     }
6931     return QSize();
6932 }
6933 
6934 QStringList TimelineModel::getProxiesAt(int position)
6935 {
6936     QStringList done;
6937     QStringList proxied;
6938     auto it = m_allTracks.begin();
6939     while (it != m_allTracks.end()) {
6940         if ((*it)->isAudioTrack()) {
6941             ++it;
6942             continue;
6943         }
6944         int clip1 = (*it)->getClipByPosition(position, 0);
6945         int clip2 = (*it)->getClipByPosition(position, 1);
6946         if (clip1 > -1) {
6947             // Check if proxied
6948             const QString binId = m_allClips[clip1]->binId();
6949             if (!done.contains(binId)) {
6950                 done << binId;
6951                 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
6952                 if (binClip->hasProxy()) {
6953                     proxied << binId;
6954                 }
6955             }
6956         }
6957         if (clip2 > -1) {
6958             // Check if proxied
6959             const QString binId = m_allClips[clip2]->binId();
6960             if (!done.contains(binId)) {
6961                 done << binId;
6962                 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
6963                 if (binClip->hasProxy()) {
6964                     proxied << binId;
6965                 }
6966             }
6967         }
6968         ++it;
6969     }
6970     return proxied;
6971 }
6972 
6973 QByteArray TimelineModel::timelineHash()
6974 {
6975     QByteArray fileData;
6976     // Get track hashes
6977     auto it = m_allTracks.begin();
6978     while (it != m_allTracks.end()) {
6979         fileData.append((*it)->trackHash());
6980         ++it;
6981     }
6982     // Compositions hash
6983     for (auto &compo : m_allCompositions) {
6984         int track = getTrackPosition(compo.second->getCurrentTrackId());
6985         QString compoData = QString("%1 %2 %3 %4")
6986                                 .arg(QString::number(compo.second->getATrack()), QString::number(track), QString::number(compo.second->getPosition()),
6987                                      QString::number(compo.second->getPlaytime()));
6988         compoData.append(compo.second->getAssetId());
6989         fileData.append(compoData.toLatin1());
6990     }
6991     // Guides
6992     if (m_guidesModel) {
6993         QString guidesData = m_guidesModel->toJson();
6994         fileData.append(guidesData.toUtf8().constData());
6995     }
6996     QByteArray fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
6997     return fileHash;
6998 }
6999 
7000 std::shared_ptr<MarkerSortModel> TimelineModel::getFilteredGuideModel()
7001 {
7002     return m_guidesFilterModel;
7003 }
7004 
7005 std::shared_ptr<MarkerListModel> TimelineModel::getGuideModel()
7006 {
7007     return m_guidesModel;
7008 }
7009 
7010 bool TimelineModel::trackIsLocked(int trackId) const
7011 {
7012     if (isSubtitleTrack(trackId)) {
7013         return m_subtitleModel->isLocked();
7014     }
7015     Q_ASSERT(isTrack(trackId));
7016     return getTrackById_const(trackId)->isLocked();
7017 }
7018 
7019 const QUuid TimelineModel::uuid() const
7020 {
7021     return m_uuid;
7022 }
7023 
7024 void TimelineModel::initializePreviewManager()
7025 {
7026     if (m_timelinePreview == nullptr) {
7027         m_timelinePreview = std::shared_ptr<PreviewManager>(new PreviewManager(m_tractor.get(), m_uuid, this));
7028         bool initialized = m_timelinePreview->initialize();
7029         if (!initialized) {
7030             pCore->displayMessage(i18n("Error initializing timeline preview"), ErrorMessage);
7031             m_timelinePreview.reset();
7032             return;
7033         }
7034         Q_EMIT connectPreviewManager();
7035         connect(this, &TimelineModel::invalidateZone, m_timelinePreview.get(), &PreviewManager::invalidatePreview, Qt::DirectConnection);
7036     }
7037 }
7038 
7039 std::shared_ptr<PreviewManager> TimelineModel::previewManager()
7040 {
7041     return m_timelinePreview;
7042 }
7043 
7044 void TimelineModel::resetPreviewManager()
7045 {
7046     if (m_timelinePreview) {
7047         disconnect(this, &TimelineModel::invalidateZone, m_timelinePreview.get(), &PreviewManager::invalidatePreview);
7048         m_timelinePreview.reset();
7049     }
7050 }
7051 
7052 bool TimelineModel::hasTimelinePreview() const
7053 {
7054     return m_timelinePreview != nullptr;
7055 }
7056 
7057 void TimelineModel::updatePreviewConnection(bool enable)
7058 {
7059     if (hasTimelinePreview()) {
7060         if (enable) {
7061             m_timelinePreview->enable();
7062         } else {
7063             m_timelinePreview->disable();
7064         }
7065     }
7066 }
7067 
7068 bool TimelineModel::buildPreviewTrack()
7069 {
7070     bool res = false;
7071     if (m_timelinePreview) {
7072         res = m_timelinePreview->buildPreviewTrack();
7073         m_overlayTrackCount = m_timelinePreview->addedTracks();
7074     }
7075     return res;
7076 }
7077 
7078 void TimelineModel::setOverlayTrack(Mlt::Playlist *overlay)
7079 {
7080     if (m_timelinePreview) {
7081         m_timelinePreview->setOverlayTrack(overlay);
7082         m_overlayTrackCount = m_timelinePreview->addedTracks();
7083     }
7084 }
7085 
7086 void TimelineModel::removeOverlayTrack()
7087 {
7088     if (m_timelinePreview) {
7089         m_timelinePreview->removeOverlayTrack();
7090         m_overlayTrackCount = m_timelinePreview->addedTracks();
7091     }
7092 }
7093 
7094 void TimelineModel::deletePreviewTrack()
7095 {
7096     if (m_timelinePreview) {
7097         m_timelinePreview->deletePreviewTrack();
7098         m_overlayTrackCount = m_timelinePreview->addedTracks();
7099     }
7100 }
7101 
7102 bool TimelineModel::hasSubtitleModel()
7103 {
7104     return m_subtitleModel != nullptr;
7105 }
7106 
7107 void TimelineModel::makeTransparentBg(bool transparent)
7108 {
7109     m_blackClip->lock();
7110     if (transparent) {
7111         m_blackClip->set("resource", 0);
7112     } else {
7113         m_blackClip->set("resource", "black");
7114     }
7115     m_blackClip->unlock();
7116 }
7117 
7118 void TimelineModel::prepareShutDown()
7119 {
7120     m_softDelete = true;
7121 }
7122 
7123 void TimelineModel::updateVisibleSequenceName(const QString displayName)
7124 {
7125     m_visibleSequenceName = displayName;
7126     Q_EMIT visibleSequenceNameChanged();
7127 }
7128 
7129 void TimelineModel::registerTimeline()
7130 {
7131     qDebug() << "::: CLIPS IN THIS MODDEL: " << m_allClips.size();
7132     for (auto clip : m_allClips) {
7133         clip.second->registerClipToBin(clip.second->getProducer(), false);
7134     }
7135 }
7136 
7137 void TimelineModel::loadPreview(const QString &chunks, const QString &dirty, bool enable, Mlt::Playlist &playlist)
7138 {
7139     if (chunks.isEmpty() && dirty.isEmpty()) {
7140         return;
7141     }
7142     if (!hasTimelinePreview()) {
7143         initializePreviewManager();
7144     }
7145     QVariantList renderedChunks;
7146     QVariantList dirtyChunks;
7147     QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts);
7148     QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts);
7149     for (const QString &frame : qAsConst(chunksList)) {
7150         if (frame.contains(QLatin1Char('-'))) {
7151             // Range, process
7152             int start = frame.section(QLatin1Char('-'), 0, 0).toInt();
7153             int end = frame.section(QLatin1Char('-'), 1, 1).toInt();
7154             for (int i = start; i <= end; i += 25) {
7155                 renderedChunks << i;
7156             }
7157         } else {
7158             renderedChunks << frame.toInt();
7159         }
7160     }
7161     for (const QString &frame : qAsConst(dirtyList)) {
7162         if (frame.contains(QLatin1Char('-'))) {
7163             // Range, process
7164             int start = frame.section(QLatin1Char('-'), 0, 0).toInt();
7165             int end = frame.section(QLatin1Char('-'), 1, 1).toInt();
7166             for (int i = start; i <= end; i += 25) {
7167                 dirtyChunks << i;
7168             }
7169         } else {
7170             dirtyChunks << frame.toInt();
7171         }
7172     }
7173 
7174     if (hasTimelinePreview()) {
7175         if (!enable) {
7176             buildPreviewTrack();
7177         }
7178         previewManager()->loadChunks(renderedChunks, dirtyChunks, playlist);
7179     }
7180 }
7181 
7182 bool TimelineModel::clipIsAudio(int cid) const
7183 {
7184     if (isClip(cid)) {
7185         int tid = getClipTrackId(cid);
7186         if (tid > -1) {
7187             return getTrackById_const(tid)->isAudioTrack();
7188         }
7189     }
7190     return false;
7191 }
7192 
7193 bool TimelineModel::singleSelectionMode() const
7194 {
7195     return m_singleSelectionMode;
7196 }