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

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