File indexing completed on 2024-05-05 04:54:12

0001 /*
0002     SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "timelinecontroller.h"
0007 #include "../model/timelinefunctions.hpp"
0008 #include "assets/keyframes/model/keyframemodellist.hpp"
0009 #include "audiomixer/mixermanager.hpp"
0010 #include "bin/bin.h"
0011 #include "bin/clipcreator.hpp"
0012 #include "bin/model/markerlistmodel.hpp"
0013 #include "bin/model/markersortmodel.h"
0014 #include "bin/model/subtitlemodel.hpp"
0015 #include "bin/projectclip.h"
0016 #include "bin/projectfolder.h"
0017 #include "bin/projectitemmodel.h"
0018 #include "core.h"
0019 #include "dialogs/importsubtitle.h"
0020 #include "dialogs/managesubtitles.h"
0021 #include "dialogs/spacerdialog.h"
0022 #include "dialogs/speechdialog.h"
0023 #include "dialogs/speeddialog.h"
0024 #include "dialogs/timeremap.h"
0025 #include "doc/kdenlivedoc.h"
0026 #include "effects/effectsrepository.hpp"
0027 #include "effects/effectstack/model/effectstackmodel.hpp"
0028 #include "glaxnimatelauncher.h"
0029 #include "kdenlivesettings.h"
0030 #include "lib/audio/audioEnvelope.h"
0031 #include "mainwindow.h"
0032 #include "monitor/monitormanager.h"
0033 #include "previewmanager.h"
0034 #include "project/projectmanager.h"
0035 #include "timeline2/model/clipmodel.hpp"
0036 #include "timeline2/model/compositionmodel.hpp"
0037 #include "timeline2/model/groupsmodel.hpp"
0038 #include "timeline2/model/snapmodel.hpp"
0039 #include "timeline2/model/trackmodel.hpp"
0040 #include "timeline2/view/dialogs/clipdurationdialog.h"
0041 #include "timeline2/view/dialogs/trackdialog.h"
0042 #include "timeline2/view/timelinewidget.h"
0043 #include "transitions/transitionsrepository.hpp"
0044 
0045 #include <KColorScheme>
0046 #include <KMessageBox>
0047 #include <KRecentDirs>
0048 #include <KUrlRequesterDialog>
0049 #include <QClipboard>
0050 #include <QFontDatabase>
0051 #include <QQuickItem>
0052 #include <kio_version.h>
0053 
0054 #include <QtMath>
0055 
0056 #include <memory>
0057 #include <unistd.h>
0058 
0059 TimelineController::TimelineController(QObject *parent)
0060     : QObject(parent)
0061     , multicamIn(-1)
0062     , m_duration(0)
0063     , m_root(nullptr)
0064     , m_usePreview(false)
0065     , m_audioRef(-1)
0066     , m_zone(-1, -1)
0067     , m_activeTrack(-1)
0068     , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
0069     , m_ready(false)
0070     , m_snapStackIndex(-1)
0071     , m_effectZone({0, 0})
0072     , m_autotrackHeight(KdenliveSettings::autotrackheight())
0073 {
0074     m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
0075     connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
0076     m_disablePreview->setEnabled(false);
0077     connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
0078     connect(pCore.get(), &Core::refreshActiveGuides, this, [this]() { m_activeSnaps.clear(); });
0079     connect(pCore.get(), &Core::autoTrackHeight, this, [this](bool enable) {
0080         m_autotrackHeight = enable;
0081         Q_EMIT autotrackHeightChanged();
0082     });
0083 }
0084 
0085 TimelineController::~TimelineController() {}
0086 
0087 void TimelineController::prepareClose()
0088 {
0089     // Clear root so we don't call its methods anymore
0090     QObject::disconnect(m_deleteConnection);
0091     disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
0092     disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
0093     disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
0094     disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
0095     disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
0096     disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
0097     m_ready = false;
0098     m_root = nullptr;
0099     // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
0100     m_model->resetPreviewManager();
0101     m_model.reset();
0102 }
0103 
0104 void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model)
0105 {
0106     m_zone = QPoint(-1, -1);
0107     m_hasAudioTarget = 0;
0108     m_lastVideoTarget = -1;
0109     m_lastAudioTarget.clear();
0110     m_usePreview = false;
0111     m_model = model;
0112     m_activeSnaps.clear();
0113     connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel);
0114     m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this](int id) {
0115         if (m_ready) {
0116             QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id));
0117         }
0118     });
0119     connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) {
0120         if (tid > -1) {
0121             showTrackAsset(tid);
0122         } else {
0123             showMasterEffects();
0124         }
0125     });
0126     if (m_model->hasTimelinePreview()) {
0127         // this timeline model already contains a timeline preview, connect it
0128         connectPreviewManager();
0129     }
0130     connect(m_model.get(), &TimelineModel::connectPreviewManager, this, &TimelineController::connectPreviewManager);
0131     connect(m_model.get(), &TimelineModel::selectionModeChanged, this, &TimelineController::colorsChanged);
0132     connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
0133     connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateTrimmingMode);
0134     connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
0135     connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
0136     connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->refreshProjectMonitorOnce(); });
0137     connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
0138     connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
0139     connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
0140     connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
0141     connect(m_model.get(), &TimelineModel::dataChanged, this, &TimelineController::checkClipPosition);
0142     connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
0143     connect(m_model.get(), &TimelineModel::flashLock, this, &TimelineController::slotFlashLock);
0144     connect(m_model.get(), &TimelineModel::refreshClipActions, this, &TimelineController::updateClipActions);
0145     connect(m_model.get(), &TimelineModel::highlightSub, this,
0146             [this](int index) { QMetaObject::invokeMethod(m_root, "highlightSub", Qt::QueuedConnection, Q_ARG(QVariant, index)); });
0147     if (m_model->hasSubtitleModel()) {
0148         loadSubtitleIndex();
0149     }
0150     connect(m_model.get(), &TimelineItemModel::subtitleModelInitialized, this, &TimelineController::loadSubtitleIndex);
0151 }
0152 
0153 void TimelineController::loadSubtitleIndex()
0154 {
0155     int currentIx = pCore->currentDoc()->getSequenceProperty(m_model->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt();
0156     auto subtitleModel = m_model->getSubtitleModel();
0157     QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList();
0158     QMapIterator<std::pair<int, QString>, QString> i(currentSubs);
0159     int counter = 0;
0160     while (i.hasNext()) {
0161         i.next();
0162         if (i.key().first == currentIx) {
0163             m_activeSubPosition = counter;
0164             break;
0165         }
0166         counter++;
0167     }
0168     Q_EMIT activeSubtitlePositionChanged();
0169 }
0170 
0171 void TimelineController::restoreTargetTracks()
0172 {
0173     setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets);
0174 }
0175 
0176 void TimelineController::setTargetTracks(bool hasVideo, const QMap<int, QString> &audioTargets)
0177 {
0178     if (m_model->isLoading) {
0179         // Timeline is still being build
0180         return;
0181     }
0182     int videoTrack = -1;
0183     m_model->m_binAudioTargets = audioTargets;
0184     QMap<int, int> audioTracks;
0185     m_hasVideoTarget = hasVideo;
0186     m_hasAudioTarget = audioTargets.size();
0187     if (m_hasVideoTarget) {
0188         videoTrack = m_model->getFirstVideoTrackIndex();
0189     }
0190     if (m_hasAudioTarget > 0) {
0191         if (m_lastAudioTarget.count() == audioTargets.count()) {
0192             // Use existing track targets
0193             QList<int> audioStreams = audioTargets.keys();
0194             QMapIterator<int, int> st(m_lastAudioTarget);
0195             while (st.hasNext()) {
0196                 st.next();
0197                 audioTracks.insert(st.key(), audioStreams.takeLast());
0198             }
0199         } else {
0200             // Use audio tracks from the first
0201             QVector<int> tracks;
0202             auto it = m_model->m_allTracks.cbegin();
0203             while (it != m_model->m_allTracks.cend()) {
0204                 if ((*it)->isAudioTrack()) {
0205                     tracks << (*it)->getId();
0206                 }
0207                 ++it;
0208             }
0209             if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) {
0210                 pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count());
0211             }
0212             QMapIterator<int, QString> st(audioTargets);
0213             while (st.hasNext()) {
0214                 st.next();
0215                 if (tracks.isEmpty()) {
0216                     break;
0217                 }
0218                 audioTracks.insert(tracks.takeLast(), st.key());
0219             }
0220         }
0221     }
0222     Q_EMIT hasAudioTargetChanged();
0223     Q_EMIT hasVideoTargetChanged();
0224     setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
0225     setAudioTarget(audioTracks);
0226 }
0227 
0228 std::shared_ptr<TimelineItemModel> TimelineController::getModel() const
0229 {
0230     return m_model;
0231 }
0232 
0233 void TimelineController::setRoot(QQuickItem *root)
0234 {
0235     m_root = root;
0236     m_ready = true;
0237 }
0238 
0239 Mlt::Tractor *TimelineController::tractor()
0240 {
0241     return m_model->tractor();
0242 }
0243 
0244 Mlt::Producer TimelineController::trackProducer(int tid)
0245 {
0246     return *(m_model->getTrackById(tid).get());
0247 }
0248 
0249 double TimelineController::scaleFactor() const
0250 {
0251     return m_scale;
0252 }
0253 
0254 const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
0255 {
0256     if (trackPos == -1) {
0257         return i18n("unknown");
0258     }
0259     if (trackPos == 0) {
0260         return i18n("Black");
0261     }
0262     return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
0263 }
0264 
0265 const QString TimelineController::getTrackNameFromIndex(int trackIndex)
0266 {
0267     QString trackName = m_model->getTrackFullName(trackIndex);
0268     return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
0269 }
0270 
0271 QMap<int, QString> TimelineController::getTrackNames(bool videoOnly)
0272 {
0273     QMap<int, QString> names;
0274     for (const auto &track : m_model->m_iteratorTable) {
0275         if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
0276             continue;
0277         }
0278         QString trackName = m_model->getTrackFullName(track.first);
0279         names[m_model->getTrackMltIndex(track.first)] = trackName;
0280     }
0281     return names;
0282 }
0283 
0284 void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
0285 {
0286     if (m_root) {
0287         m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMax(0, getMousePos()) : -1);
0288         m_scale = scale;
0289         Q_EMIT scaleFactorChanged();
0290     } else {
0291         qWarning() << "Timeline root not created, impossible to zoom in";
0292     }
0293 }
0294 
0295 void TimelineController::setScaleFactor(double scale)
0296 {
0297     m_scale = scale;
0298     // Update mainwindow's zoom slider
0299     Q_EMIT updateZoom(scale);
0300     // inform qml
0301     Q_EMIT scaleFactorChanged();
0302 }
0303 
0304 int TimelineController::duration() const
0305 {
0306     return m_duration;
0307 }
0308 
0309 int TimelineController::fullDuration() const
0310 {
0311     return m_duration + TimelineModel::seekDuration;
0312 }
0313 
0314 void TimelineController::checkDuration()
0315 {
0316     int currentLength = m_model->duration();
0317     if (currentLength != m_duration) {
0318         m_duration = currentLength;
0319         Q_EMIT durationChanged(m_duration);
0320     }
0321 }
0322 
0323 void TimelineController::hideTrack(int trackId, bool hide)
0324 {
0325     bool isAudio = m_model->isAudioTrack(trackId);
0326     QString state = hide ? (isAudio ? "1" : "2") : "3";
0327     QString previousState = m_model->getTrackProperty(trackId, QStringLiteral("hide")).toString();
0328     Fun undo_lambda = [this, trackId, previousState]() {
0329         m_model->setTrackProperty(trackId, QStringLiteral("hide"), previousState);
0330         m_model->updateDuration();
0331         return true;
0332     };
0333     Fun redo_lambda = [this, trackId, state]() {
0334         m_model->setTrackProperty(trackId, QStringLiteral("hide"), state);
0335         m_model->updateDuration();
0336         return true;
0337     };
0338     redo_lambda();
0339     pCore->pushUndo(undo_lambda, redo_lambda, state == QLatin1String("3") ? i18n("Hide Track") : i18n("Enable Track"));
0340 }
0341 
0342 int TimelineController::selectedTrack() const
0343 {
0344     std::unordered_set<int> sel = m_model->getCurrentSelection();
0345     if (sel.empty()) return -1;
0346     std::vector<std::pair<int, int>> selected_tracks; // contains pairs of (track position, track id) for each selected item
0347     for (int s : sel) {
0348         int tid = m_model->getItemTrackId(s);
0349         selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid);
0350     }
0351     // sort by track position
0352     std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
0353     return selected_tracks.front().second;
0354 }
0355 
0356 bool TimelineController::selectCurrentItem(KdenliveObjectType type, bool select, bool addToCurrent, bool showErrorMsg)
0357 {
0358     int currentClip = -1;
0359     if (m_activeTrack == -1 || (m_model->isSubtitleTrack(m_activeTrack) && type != KdenliveObjectType::TimelineClip)) {
0360         // Cannot select item
0361     } else if (type == KdenliveObjectType::TimelineClip) {
0362         currentClip = m_model->isSubtitleTrack(m_activeTrack) ? m_model->getSubtitleByPosition(pCore->getMonitorPosition())
0363                                                               : m_model->getClipByPosition(m_activeTrack, pCore->getMonitorPosition());
0364     } else if (type == KdenliveObjectType::TimelineComposition) {
0365         currentClip = m_model->getCompositionByPosition(m_activeTrack, pCore->getMonitorPosition());
0366     } else if (type == KdenliveObjectType::TimelineMix) {
0367         if (m_activeTrack >= 0) {
0368             currentClip = m_model->getClipByPosition(m_activeTrack, pCore->getMonitorPosition());
0369         }
0370         if (currentClip > -1) {
0371             if (m_model->hasClipEndMix(currentClip)) {
0372                 int mixPartner = m_model->getTrackById_const(m_activeTrack)->getSecondMixPartner(currentClip);
0373                 int clipEnd = m_model->getClipPosition(currentClip) + m_model->getClipPlaytime(currentClip);
0374                 int mixStart = clipEnd - m_model->getMixDuration(mixPartner);
0375                 if (mixStart < pCore->getMonitorPosition() && pCore->getMonitorPosition() < clipEnd) {
0376                     if (select) {
0377                         m_model->requestMixSelection(mixPartner);
0378                         return true;
0379                     } else if (selectedMix() == mixPartner) {
0380                         m_model->requestClearSelection();
0381                         return true;
0382                     }
0383                 }
0384             }
0385             int delta = pCore->getMonitorPosition() - m_model->getClipPosition(currentClip);
0386             if (m_model->getMixDuration(currentClip) >= delta) {
0387                 if (select) {
0388                     m_model->requestMixSelection(currentClip);
0389                     return true;
0390                 } else if (selectedMix() == currentClip) {
0391                     m_model->requestClearSelection();
0392                     return true;
0393                 }
0394                 return true;
0395             } else {
0396                 currentClip = -1;
0397             }
0398         }
0399     }
0400 
0401     if (currentClip == -1) {
0402         if (showErrorMsg) {
0403             pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500);
0404         }
0405         return false;
0406     }
0407     if (!select) {
0408         m_model->requestRemoveFromSelection(currentClip);
0409     } else {
0410         bool grouped = m_model->m_groups->isInGroup(currentClip);
0411         m_model->requestAddToSelection(currentClip, !addToCurrent);
0412         if (grouped) {
0413             // If part of a group, ensure the effect/composition stack displays the selected item's properties
0414             showAsset(currentClip);
0415         }
0416     }
0417     return true;
0418 }
0419 
0420 QList<int> TimelineController::selection() const
0421 {
0422     if (!m_root) return QList<int>();
0423     std::unordered_set<int> sel = m_model->getCurrentSelection();
0424     QList<int> items;
0425     for (int id : sel) {
0426         items << id;
0427     }
0428     return items;
0429 }
0430 
0431 int TimelineController::selectedMix() const
0432 {
0433     return m_model->m_selectedMix;
0434 }
0435 
0436 void TimelineController::selectItems(const QList<int> &ids)
0437 {
0438     std::unordered_set<int> ids_s(ids.begin(), ids.end());
0439     m_model->requestSetSelection(ids_s);
0440 }
0441 
0442 void TimelineController::setScrollPos(int pos)
0443 {
0444     if (pos > 0 && m_root) {
0445         QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
0446     }
0447 }
0448 
0449 void TimelineController::resetView()
0450 {
0451     m_model->_resetView();
0452     if (m_root) {
0453         QMetaObject::invokeMethod(m_root, "updatePalette");
0454     }
0455     Q_EMIT colorsChanged();
0456 }
0457 
0458 bool TimelineController::snap()
0459 {
0460     return KdenliveSettings::snaptopoints();
0461 }
0462 
0463 bool TimelineController::ripple()
0464 {
0465     return false;
0466 }
0467 
0468 bool TimelineController::scrub()
0469 {
0470     return false;
0471 }
0472 
0473 int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
0474 {
0475     int id;
0476     if (tid == -1) {
0477         tid = m_activeTrack;
0478     }
0479     if (position == -1) {
0480         position = pCore->getMonitorPosition();
0481     }
0482     if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
0483         id = -1;
0484     }
0485     return id;
0486 }
0487 
0488 QList<int> TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
0489 {
0490     QList<int> clipIds;
0491     if (tid == -1) {
0492         tid = m_activeTrack;
0493     }
0494     if (position == -1) {
0495         position = pCore->getMonitorPosition();
0496     }
0497     TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
0498     // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids.
0499     return clipIds;
0500 }
0501 
0502 void TimelineController::insertNewMix(int tid, int position, const QString &transitionId)
0503 {
0504     int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
0505     if (clipId > 0) {
0506         m_model->mixClip(clipId, transitionId, -1);
0507     }
0508 }
0509 
0510 int TimelineController::insertNewCompositionAtPos(int tid, int position, const QString &transitionId)
0511 {
0512     // TODO: adjust position and duration to existing clips ?
0513     return insertComposition(tid, position, transitionId, true);
0514 }
0515 
0516 int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
0517 {
0518     int id;
0519     int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset;
0520     int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration());
0521     int endPos = minimumPos + clip_duration;
0522     int position = minimumPos;
0523     int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration()));
0524     int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
0525     bool revert = offset > clip_duration / 2;
0526     int bottomId = 0;
0527     if (lowerVideoTrackId > 0) {
0528         bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset);
0529     }
0530     if (bottomId <= 0) {
0531         // No video track underneath
0532         if (offset < duration && duration < 2 * clip_duration) {
0533             // Composition dropped close to start, keep default composition duration
0534         } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) {
0535             // Composition dropped close to end, keep default composition duration
0536             position = endPos - duration;
0537         } else {
0538             // Use full clip length for duration
0539             duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
0540         }
0541     } else {
0542         duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position));
0543         QPair<int, int> bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
0544         if (bottom.first > minimumPos) {
0545             // Lower clip is after top clip
0546             if (position + offset > bottom.first) {
0547                 int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
0548                 if (test_duration > 0) {
0549                     offset -= (bottom.first - position);
0550                     position = bottom.first;
0551                     duration = test_duration;
0552                     revert = position > minimumPos;
0553                 }
0554             }
0555         } else if (position >= bottom.first) {
0556             // Lower clip is before or at same pos as top clip
0557             int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
0558             if (test_duration > 0) {
0559                 duration = qMin(test_duration, clip_duration);
0560             }
0561         }
0562     }
0563     int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration());
0564     bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition;
0565     if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) {
0566         duration = defaultLength;
0567     } else if (duration <= 1) {
0568         // if suggested composition duration is lower than 4 frames, use default
0569         duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
0570         if (minimumPos + clip_duration - position < 3) {
0571             position = minimumPos + clip_duration - duration;
0572         }
0573     }
0574     QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
0575     position = finalPos.first;
0576     duration = finalPos.second;
0577 
0578     std::unique_ptr<Mlt::Properties> props(nullptr);
0579     if (revert) {
0580         props = std::make_unique<Mlt::Properties>();
0581         if (transitionId == QLatin1String("dissolve")) {
0582             props->set("reverse", 1);
0583         } else if (transitionId == QLatin1String("composite")) {
0584             props->set("invert", 1);
0585         } else if (transitionId == QLatin1String("wipe")) {
0586             props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
0587         } else if (transitionId == QLatin1String("slide")) {
0588             props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%");
0589         }
0590     }
0591     if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
0592         id = -1;
0593         pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
0594     }
0595     return id;
0596 }
0597 
0598 int TimelineController::isOnCut(int cid) const
0599 {
0600     Q_ASSERT(m_model->isComposition(cid));
0601     int tid = m_model->getItemTrackId(cid);
0602     return m_model->getTrackById_const(tid)->isOnCut(cid);
0603 }
0604 
0605 int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
0606 {
0607     int id;
0608     int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
0609     // Check if composition should be reversed (top clip at beginning, bottom at end)
0610     int a_track = m_model->getPreviousVideoTrackPos(tid);
0611     int topClip = m_model->getTrackById_const(tid)->getClipByPosition(position);
0612     int bottomClip = -1;
0613     if (a_track > 0) {
0614         // There is a video track below, check its clip
0615         int bottomTid = m_model->getTrackIndexFromPosition(a_track - 1);
0616         if (bottomTid > -1) {
0617             bottomClip = m_model->getTrackById_const(bottomTid)->getClipByPosition(position);
0618         }
0619     }
0620     bool reverse = false;
0621     if (topClip > -1 && bottomClip > -1) {
0622         if (m_model->getClipPosition(topClip) + m_model->getClipPlaytime(topClip) <
0623             m_model->getClipPosition(bottomClip) + m_model->getClipPlaytime(bottomClip)) {
0624             reverse = true;
0625         }
0626     }
0627     std::unique_ptr<Mlt::Properties> props(nullptr);
0628     if (reverse) {
0629         props = std::make_unique<Mlt::Properties>();
0630         if (transitionId == QLatin1String("dissolve")) {
0631             props->set("reverse", 1);
0632         } else if (transitionId == QLatin1String("composite")) {
0633             props->set("invert", 1);
0634         } else if (transitionId == QLatin1String("wipe")) {
0635             props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
0636         } else if (transitionId == QLatin1String("slide")) {
0637             props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%");
0638         }
0639     }
0640     if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
0641         id = -1;
0642     }
0643     return id;
0644 }
0645 
0646 void TimelineController::slotFlashLock(int trackId)
0647 {
0648     QMetaObject::invokeMethod(m_root, "animateLockButton", Qt::QueuedConnection, Q_ARG(QVariant, trackId));
0649 }
0650 
0651 void TimelineController::deleteSelectedClips()
0652 {
0653     if (dragOperationRunning()) {
0654         // Don't allow timeline operation while drag in progress
0655         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
0656         return;
0657     }
0658     auto sel = m_model->getCurrentSelection();
0659     // Check if we are operating on a locked track
0660     std::unordered_set<int> trackIds;
0661     for (auto &id : sel) {
0662         if (m_model->isItem(id)) {
0663             trackIds.insert(m_model->getItemTrackId(id));
0664         }
0665     }
0666     for (auto &tid : trackIds) {
0667         if (m_model->trackIsLocked(tid)) {
0668             m_model->flashLock(tid);
0669             return;
0670         }
0671     }
0672     if (sel.empty()) {
0673         // Check if a mix is selected
0674         if (m_model->m_selectedMix > -1 && m_model->isClip(m_model->m_selectedMix)) {
0675             m_model->removeMix(m_model->m_selectedMix);
0676             m_model->requestClearAssetView(m_model->m_selectedMix);
0677             m_model->requestClearSelection(true);
0678         }
0679         return;
0680     }
0681     // only need to delete the first item, the others will be deleted in cascade
0682     if (m_model->m_editMode == TimelineMode::InsertEdit) {
0683         // In insert mode, perform an extract operation (don't leave gaps)
0684         if (m_model->singleSelectionMode()) {
0685             // TODO only create 1 undo operation
0686             m_model->requestClearSelection();
0687             std::function<bool(void)> undo = []() { return true; };
0688             std::function<bool(void)> redo = []() { return true; };
0689             for (auto &s : sel) {
0690                 // Remove item from group
0691                 int clipToUngroup = s;
0692                 std::unordered_set<int> clipsToRegroup = m_model->m_groups->getLeaves(m_model->m_groups->getRootId(s));
0693                 clipsToRegroup.erase(clipToUngroup);
0694                 int in = m_model->getClipPosition(s);
0695                 int out = in + m_model->getClipPlaytime(s);
0696                 int tid = m_model->getClipTrackId(s);
0697                 std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(s);
0698                 if (mixData.first.firstClipId > -1) {
0699                     // Clip has a start mix, adjust in point
0700                     in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
0701                 }
0702                 if (mixData.second.firstClipId > -1) {
0703                     // Clip has end mix, adjust out point
0704                     out -= mixData.second.mixOffset;
0705                 }
0706                 QVector<int> tracks = {tid};
0707                 TimelineFunctions::extractZoneWithUndo(m_model, tracks, QPoint(in, out), false, clipToUngroup, clipsToRegroup, undo, redo);
0708             }
0709             pCore->pushUndo(undo, redo, i18n("Extract zone"));
0710         } else {
0711             extract(*sel.begin());
0712         }
0713     } else {
0714         m_model->requestItemDeletion(*sel.begin());
0715     }
0716 }
0717 
0718 int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
0719 {
0720     auto sel = m_model->getCurrentSelection();
0721     if (sel.empty() || sel.size() > 2) {
0722         return -1;
0723     }
0724     int itemId = *(sel.begin());
0725     if (sel.size() == 2) {
0726         int parentGroup = m_model->m_groups->getRootId(itemId);
0727         if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
0728             return -1;
0729         }
0730     }
0731     if (!restrictToCurrentPos) {
0732         if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
0733             return itemId;
0734         }
0735     }
0736     if (m_model->isClip(itemId)) {
0737         int position = pCore->getMonitorPosition();
0738         int start = m_model->getClipPosition(itemId);
0739         int end = start + m_model->getClipPlaytime(itemId);
0740         if (position >= start && position <= end) {
0741             return itemId;
0742         }
0743     }
0744     return -1;
0745 }
0746 
0747 std::pair<int, int> TimelineController::selectionPosition(int *aTracks, int *vTracks)
0748 {
0749     std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
0750     if (selectedIds.empty()) {
0751         return {-1, -1};
0752     }
0753     int position = -1;
0754     int targetTrackId = -1;
0755     std::pair<int, int> audioTracks = {-1, -1};
0756     std::pair<int, int> videoTracks = {-1, -1};
0757     int topVideoWithSplit = -1;
0758     for (auto &id : selectedIds) {
0759         int tid = m_model->getItemTrackId(id);
0760         if (m_model->isSubtitleTrack(tid)) {
0761             // Subtitle track not supported
0762             continue;
0763         }
0764         if (position == -1 || position > m_model->getItemPosition(id)) {
0765             position = m_model->getItemPosition(id);
0766         }
0767         int trackPos = m_model->getTrackPosition(tid);
0768         if (m_model->isAudioTrack(tid)) {
0769             // Find audio track range
0770             if (audioTracks.first < 0 || trackPos < audioTracks.first) {
0771                 audioTracks.first = trackPos;
0772             }
0773             if (audioTracks.second < 0 || trackPos > audioTracks.second) {
0774                 audioTracks.second = trackPos;
0775             }
0776         } else {
0777             // Find video track range
0778             int splitId = m_model->m_groups->getSplitPartner(id);
0779             if (splitId > -1 && (topVideoWithSplit == -1 || trackPos > topVideoWithSplit)) {
0780                 topVideoWithSplit = trackPos;
0781             }
0782             if (videoTracks.first < 0 || trackPos < videoTracks.first) {
0783                 videoTracks.first = trackPos;
0784             }
0785             if (videoTracks.second < 0 || trackPos > videoTracks.second) {
0786                 videoTracks.second = trackPos;
0787             }
0788         }
0789     }
0790     int minimumMirrorTracks = 0;
0791     if (topVideoWithSplit > -1) {
0792         // Ensure we have enough audio tracks for audio partners
0793         minimumMirrorTracks = topVideoWithSplit - videoTracks.first + 1;
0794     }
0795 
0796     if (videoTracks.first > -1) {
0797         *vTracks = videoTracks.second - videoTracks.first + 1;
0798         targetTrackId = m_model->getTrackIndexFromPosition(videoTracks.first);
0799     } else {
0800         *vTracks = 0;
0801     }
0802     if (audioTracks.first > -1) {
0803         *aTracks = qMax(audioTracks.second - audioTracks.first + 1, minimumMirrorTracks);
0804         if (targetTrackId == -1) {
0805             targetTrackId = m_model->getTrackIndexFromPosition(audioTracks.second);
0806         }
0807     } else {
0808         *aTracks = qMax(0, minimumMirrorTracks);
0809     }
0810     return {position, targetTrackId};
0811 }
0812 
0813 int TimelineController::copyItem()
0814 {
0815     std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
0816     if (selectedIds.empty()) {
0817         return -1;
0818     }
0819     int clipId = *(selectedIds.begin());
0820     QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
0821     QClipboard *clipboard = QApplication::clipboard();
0822     clipboard->setText(copyString);
0823     m_root->setProperty("copiedClip", clipId);
0824     return clipId;
0825 }
0826 
0827 std::pair<int, QString> TimelineController::getCopyItemData()
0828 {
0829     std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
0830     if (selectedIds.empty()) {
0831         return {-1, QString()};
0832     }
0833     int clipId = *(selectedIds.begin());
0834     QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
0835     return {clipId, copyString};
0836 }
0837 
0838 bool TimelineController::pasteItem(int position, int tid)
0839 {
0840     QClipboard *clipboard = QApplication::clipboard();
0841     QString txt = clipboard->text();
0842     if (tid == -1) {
0843         tid = m_activeTrack;
0844     }
0845     if (position == -1) {
0846         position = getMenuOrTimelinePos();
0847     }
0848     return TimelineFunctions::pasteClips(m_model, txt, tid, position);
0849 }
0850 
0851 void TimelineController::triggerAction(const QString &name)
0852 {
0853     pCore->triggerAction(name);
0854 }
0855 
0856 const QString TimelineController::actionText(const QString &name)
0857 {
0858     return pCore->actionText(name);
0859 }
0860 
0861 QString TimelineController::timecode(int frames) const
0862 {
0863     return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
0864 }
0865 
0866 QString TimelineController::framesToClock(int frames) const
0867 {
0868     return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
0869 }
0870 
0871 QString TimelineController::simplifiedTC(int frames) const
0872 {
0873     if (KdenliveSettings::frametimecode()) {
0874         return QString::number(frames);
0875     }
0876     QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
0877     return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s;
0878 }
0879 
0880 bool TimelineController::showThumbnails() const
0881 {
0882     return KdenliveSettings::videothumbnails();
0883 }
0884 
0885 bool TimelineController::showAudioThumbnails() const
0886 {
0887     return KdenliveSettings::audiothumbnails();
0888 }
0889 
0890 bool TimelineController::showMarkers() const
0891 {
0892     return KdenliveSettings::showmarkers();
0893 }
0894 
0895 bool TimelineController::audioThumbFormat() const
0896 {
0897     return KdenliveSettings::displayallchannels();
0898 }
0899 
0900 bool TimelineController::audioThumbNormalize() const
0901 {
0902     return KdenliveSettings::normalizechannels();
0903 }
0904 
0905 bool TimelineController::showWaveforms() const
0906 {
0907     return KdenliveSettings::audiothumbnails();
0908 }
0909 
0910 void TimelineController::beginAddTrack(int tid)
0911 {
0912     if (tid == -1) {
0913         tid = m_activeTrack;
0914     }
0915     QScopedPointer<TrackDialog> d(new TrackDialog(m_model, tid, qApp->activeWindow()));
0916     if (d->exec() == QDialog::Accepted) {
0917         auto trackName = d->trackName();
0918         bool result =
0919             m_model->addTracksAtPosition(d->selectedTrackPosition(), d->tracksCount(), trackName, d->addAudioTrack(), d->addAVTrack(), d->addRecTrack());
0920         if (!result) {
0921             pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
0922         }
0923     }
0924 }
0925 
0926 void TimelineController::deleteMultipleTracks(int tid)
0927 {
0928     Fun undo = []() { return true; };
0929     Fun redo = []() { return true; };
0930     QScopedPointer<TrackDialog> d(new TrackDialog(m_model, tid, qApp->activeWindow(), true, m_activeTrack));
0931     if (d->exec() == QDialog::Accepted) {
0932         bool result = true;
0933         QList<int> allIds = d->toDeleteTrackIds();
0934         for (int selectedTrackIx : qAsConst(allIds)) {
0935             result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo);
0936             if (!result) {
0937                 break;
0938             }
0939             if (m_activeTrack == -1) {
0940                 setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
0941             }
0942         }
0943         if (result) {
0944             pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track"));
0945         } else {
0946             undo();
0947         }
0948     }
0949 }
0950 
0951 void TimelineController::switchTrackRecord(int tid, bool monitor)
0952 {
0953     if (tid == -1) {
0954         tid = m_activeTrack;
0955     }
0956     if (!m_model->getTrackById_const(tid)->isAudioTrack()) {
0957         pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500);
0958     }
0959     int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt();
0960     if (monitor == false) {
0961         // Disable rec controls
0962         if (recDisplayed == 0) {
0963             // Already hidden
0964             return;
0965         }
0966         m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0"));
0967     } else {
0968         // Enable rec controls
0969         if (recDisplayed == 1) {
0970             // Already displayed
0971             return;
0972         }
0973         m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1"));
0974     }
0975     QModelIndex ix = m_model->makeTrackIndexFromID(tid);
0976     if (ix.isValid()) {
0977         Q_EMIT m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole});
0978     }
0979 }
0980 
0981 void TimelineController::checkTrackDeletion(int selectedTrackIx)
0982 {
0983     if (m_activeTrack == selectedTrackIx) {
0984         // Make sure we don't keep an index on a deleted track
0985         m_activeTrack = -1;
0986         Q_EMIT activeTrackChanged();
0987     }
0988     if (m_model->m_audioTarget.contains(selectedTrackIx)) {
0989         QMap<int, int> selection = m_model->m_audioTarget;
0990         selection.remove(selectedTrackIx);
0991         setAudioTarget(selection);
0992     }
0993     if (m_model->m_videoTarget == selectedTrackIx) {
0994         setVideoTarget(-1);
0995     }
0996     if (m_lastAudioTarget.contains(selectedTrackIx)) {
0997         m_lastAudioTarget.remove(selectedTrackIx);
0998         Q_EMIT lastAudioTargetChanged();
0999     }
1000     if (m_lastVideoTarget == selectedTrackIx) {
1001         m_lastVideoTarget = -1;
1002         Q_EMIT lastVideoTargetChanged();
1003     }
1004 }
1005 
1006 void TimelineController::showConfig(int page, int tab)
1007 {
1008     Q_EMIT pCore->showConfigDialog((Kdenlive::ConfigPage)page, tab);
1009 }
1010 
1011 void TimelineController::gotoNextSnap()
1012 {
1013     if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
1014         m_snapStackIndex = pCore->undoIndex();
1015         m_activeSnaps.clear();
1016         m_activeSnaps = m_model->getGuideModel()->getSnapPoints();
1017         m_activeSnaps.push_back(m_zone.x());
1018         m_activeSnaps.push_back(m_zone.y() - 1);
1019     }
1020     std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints();
1021     int nextSnap = m_model->getNextSnapPos(pCore->getMonitorPosition(), m_activeSnaps, canceled);
1022     if (nextSnap > pCore->getMonitorPosition()) {
1023         setPosition(nextSnap);
1024     }
1025 }
1026 
1027 void TimelineController::gotoPreviousSnap()
1028 {
1029     if (pCore->getMonitorPosition() > 0) {
1030         if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
1031             m_snapStackIndex = pCore->undoIndex();
1032             m_activeSnaps.clear();
1033             m_activeSnaps = m_model->getGuideModel()->getSnapPoints();
1034             m_activeSnaps.push_back(m_zone.x());
1035             m_activeSnaps.push_back(m_zone.y() - 1);
1036         }
1037         std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints();
1038         setPosition(m_model->getPreviousSnapPos(pCore->getMonitorPosition(), m_activeSnaps, canceled));
1039     }
1040 }
1041 
1042 void TimelineController::gotoNextGuide()
1043 {
1044     QList<CommentedTime> guides = m_model->getGuideModel()->getAllMarkers();
1045     std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints();
1046     int pos = pCore->getMonitorPosition();
1047     double fps = pCore->getCurrentFps();
1048     int guidePos = 0;
1049     for (auto &guide : guides) {
1050         guidePos = guide.time().frames(fps);
1051         if (std::find(canceled.begin(), canceled.end(), guidePos) != canceled.end()) {
1052             continue;
1053         }
1054         if (guidePos > pos) {
1055             setPosition(guidePos);
1056             return;
1057         }
1058     }
1059     setPosition(m_duration - 1);
1060 }
1061 
1062 void TimelineController::gotoPreviousGuide()
1063 {
1064     if (pCore->getMonitorPosition() > 0) {
1065         QList<CommentedTime> guides = m_model->getGuideModel()->getAllMarkers();
1066         std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints();
1067         int pos = pCore->getMonitorPosition();
1068         double fps = pCore->getCurrentFps();
1069         int lastGuidePos = 0;
1070         int guidePos = 0;
1071         for (auto &guide : guides) {
1072             guidePos = guide.time().frames(fps);
1073             if (std::find(canceled.begin(), canceled.end(), guidePos) != canceled.end()) {
1074                 continue;
1075             }
1076             if (guidePos >= pos) {
1077                 setPosition(lastGuidePos);
1078                 return;
1079             }
1080             lastGuidePos = guidePos;
1081         }
1082         setPosition(lastGuidePos);
1083     }
1084 }
1085 
1086 void TimelineController::groupSelection()
1087 {
1088     if (dragOperationRunning()) {
1089         // Don't allow timeline operation while drag in progress
1090         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
1091         return;
1092     }
1093     const auto selection = m_model->getCurrentSelection();
1094     if (selection.size() < 2) {
1095         pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500);
1096         return;
1097     }
1098     m_model->requestClearSelection();
1099     m_model->requestClipsGroup(selection);
1100     m_model->requestSetSelection(selection);
1101 }
1102 
1103 void TimelineController::unGroupSelection(int cid)
1104 {
1105     if (dragOperationRunning()) {
1106         // Don't allow timeline operation while drag in progress
1107         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
1108         return;
1109     }
1110     auto ids = m_model->getCurrentSelection();
1111     // ask to unselect if needed
1112     m_model->requestClearSelection();
1113     if (cid > -1) {
1114         ids.insert(cid);
1115     }
1116     if (!ids.empty()) {
1117         m_model->requestClipsUngroup(ids);
1118     }
1119 }
1120 
1121 bool TimelineController::dragOperationRunning()
1122 {
1123     QVariant returnedValue;
1124     QMetaObject::invokeMethod(m_root, "isDragging", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue));
1125     return returnedValue.toBool();
1126 }
1127 
1128 bool TimelineController::trimmingActive()
1129 {
1130     ToolType::ProjectTool tool = pCore->window()->getCurrentTimeline()->activeTool();
1131     return tool == ToolType::SlideTool || tool == ToolType::SlipTool || tool == ToolType::RippleTool || tool == ToolType::RollTool;
1132 }
1133 
1134 void TimelineController::setInPoint(bool ripple)
1135 {
1136     if (dragOperationRunning()) {
1137         // Don't allow timeline operation while drag in progress
1138         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
1139         qDebug() << "Cannot operate while dragging";
1140         return;
1141     }
1142     auto requestResize = [this, ripple](int id, int size) {
1143         if (ripple) {
1144             m_model->requestItemRippleResize(m_model, id, size, false, true, !KdenliveSettings::lockedGuides(), 0, false);
1145             setPosition(m_model->getItemPosition(id));
1146         } else {
1147             m_model->requestItemResize(id, size, false, true, 0, false);
1148         }
1149     };
1150     int cursorPos = pCore->getMonitorPosition();
1151     const auto selection = m_model->getCurrentSelection();
1152     bool selectionFound = false;
1153     if (!selection.empty()) {
1154         for (int id : selection) {
1155             int start = m_model->getItemPosition(id);
1156             if (start == cursorPos) {
1157                 continue;
1158             }
1159             int size = start + m_model->getItemPlaytime(id) - cursorPos;
1160             requestResize(id, size);
1161             selectionFound = true;
1162         }
1163     }
1164     if (!selectionFound) {
1165         if (m_activeTrack >= 0) {
1166             int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1167             if (cid < 0) {
1168                 // Check first item after timeline position
1169                 int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos);
1170                 if (maximumSpace < INT_MAX) {
1171                     cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1);
1172                 }
1173             }
1174             if (cid >= 0) {
1175                 int start = m_model->getItemPosition(cid);
1176                 if (start != cursorPos) {
1177                     int size = start + m_model->getItemPlaytime(cid) - cursorPos;
1178                     requestResize(cid, size);
1179                     selectionFound = true;
1180                 }
1181             }
1182         } else if (m_model->isSubtitleTrack(m_activeTrack)) {
1183             // Subtitle track
1184             auto subtitleModel = m_model->getSubtitleModel();
1185             if (subtitleModel) {
1186                 int sid = -1;
1187                 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
1188                 if (sids.empty()) {
1189                     sids = subtitleModel->getItemsInRange(cursorPos, -1);
1190                     for (int s : sids) {
1191                         if (sid == -1 || subtitleModel->getStartPosForId(s) < subtitleModel->getStartPosForId(sid)) {
1192                             sid = s;
1193                         }
1194                     }
1195                 } else {
1196                     sid = *sids.begin();
1197                 }
1198                 if (sid > -1) {
1199                     int start = m_model->getItemPosition(sid);
1200                     if (start != cursorPos) {
1201                         int size = start + m_model->getItemPlaytime(sid) - cursorPos;
1202                         requestResize(sid, size);
1203                         selectionFound = true;
1204                     }
1205                 }
1206             }
1207         }
1208         if (!selectionFound) {
1209             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1210         }
1211     }
1212 }
1213 
1214 void TimelineController::setOutPoint(bool ripple)
1215 {
1216     if (dragOperationRunning()) {
1217         // Don't allow timeline operation while drag in progress
1218         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
1219         qDebug() << "Cannot operate while dragging";
1220         return;
1221     }
1222     auto requestResize = [this, ripple](int id, int size) {
1223         if (ripple) {
1224             m_model->requestItemRippleResize(m_model, id, size, true, true, !KdenliveSettings::lockedGuides(), 0, false);
1225         } else {
1226             m_model->requestItemResize(id, size, true, true, 0, false);
1227         }
1228     };
1229     int cursorPos = pCore->getMonitorPosition();
1230     const auto selection = m_model->getCurrentSelection();
1231     bool selectionFound = false;
1232     if (!selection.empty()) {
1233         for (int id : selection) {
1234             int start = m_model->getItemPosition(id);
1235             if (start + m_model->getItemPlaytime(id) == cursorPos) {
1236                 continue;
1237             }
1238             int size = cursorPos - start;
1239             requestResize(id, size);
1240             selectionFound = true;
1241         }
1242     }
1243     if (!selectionFound) {
1244         if (m_activeTrack >= 0) {
1245             int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1246             if (cid < 0 || cursorPos == m_model->getItemPosition(cid)) {
1247                 // If no clip found at cursor pos or we are at the first frame of a clip, try to find previous clip
1248                 // Check first item before timeline position
1249                 // If we are at a clip start, check space before this clip
1250                 int offset = cid >= 0 ? 1 : 0;
1251                 int previousPos = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos - offset);
1252                 cid = m_model->getClipByPosition(m_activeTrack, qMax(0, previousPos - 1));
1253             }
1254             if (cid >= 0) {
1255                 int start = m_model->getItemPosition(cid);
1256                 if (start + m_model->getItemPlaytime(cid) != cursorPos) {
1257                     int size = cursorPos - start;
1258                     requestResize(cid, size);
1259                     selectionFound = true;
1260                 }
1261             }
1262         } else if (m_model->isSubtitleTrack(m_activeTrack)) {
1263             // Subtitle track
1264             auto subtitleModel = m_model->getSubtitleModel();
1265             if (subtitleModel) {
1266                 int sid = -1;
1267                 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
1268                 if (sids.empty()) {
1269                     sids = subtitleModel->getItemsInRange(0, cursorPos);
1270                     for (int s : sids) {
1271                         if (sid == -1 || subtitleModel->getSubtitleEnd(s) > subtitleModel->getSubtitleEnd(sid)) {
1272                             sid = s;
1273                         }
1274                     }
1275                 } else {
1276                     sid = *sids.begin();
1277                 }
1278                 if (sid > -1) {
1279                     int start = m_model->getItemPosition(sid);
1280                     if (start + m_model->getItemPlaytime(sid) != cursorPos) {
1281                         int size = cursorPos - start;
1282                         requestResize(sid, size);
1283                         selectionFound = true;
1284                     }
1285                 }
1286             }
1287         }
1288         if (!selectionFound) {
1289             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1290         }
1291     }
1292 }
1293 
1294 void TimelineController::editMarker(int cid, int position)
1295 {
1296     if (cid == -1) {
1297         cid = getMainSelectedClip();
1298         if (cid == -1) {
1299             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1300             return;
1301         }
1302     }
1303     Q_ASSERT(m_model->isClip(cid));
1304     double speed = m_model->getClipSpeed(cid);
1305     if (position == -1) {
1306         // Calculate marker position relative to timeline cursor
1307         position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1308         position = int(position * speed);
1309     }
1310     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1311         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1312         return;
1313     }
1314     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1315     if (clip->getMarkerModel()->hasMarker(position)) {
1316         GenTime pos(position, pCore->getCurrentFps());
1317         clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
1318     } else {
1319         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1320     }
1321 }
1322 
1323 void TimelineController::addMarker(int cid, int position)
1324 {
1325     if (cid == -1) {
1326         cid = getMainSelectedClip();
1327         if (cid == -1) {
1328             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1329             return;
1330         }
1331     }
1332     Q_ASSERT(m_model->isClip(cid));
1333     double speed = m_model->getClipSpeed(cid);
1334     if (position == -1) {
1335         // Calculate marker position relative to timeline cursor
1336         position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1337         position = int(position * speed);
1338     }
1339     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1340         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1341         return;
1342     }
1343     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1344     GenTime pos(position, pCore->getCurrentFps());
1345     clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get());
1346 }
1347 
1348 int TimelineController::getMainSelectedClip()
1349 {
1350     int clipId = m_root->property("mainItemId").toInt();
1351     if (clipId == -1 || !isInSelection(clipId)) {
1352         std::unordered_set<int> sel = m_model->getCurrentSelection();
1353         for (int i : sel) {
1354             if (m_model->isClip(i)) {
1355                 clipId = i;
1356                 break;
1357             }
1358         }
1359     }
1360     return m_model->isClip(clipId) ? clipId : -1;
1361 }
1362 
1363 void TimelineController::addQuickMarker(int cid, int position)
1364 {
1365     if (cid == -1) {
1366         cid = getMainSelectedClip();
1367         if (cid == -1) {
1368             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1369             return;
1370         }
1371     }
1372     Q_ASSERT(m_model->isClip(cid));
1373     double speed = m_model->getClipSpeed(cid);
1374     if (position == -1) {
1375         // Calculate marker position relative to timeline cursor
1376         position = pCore->getMonitorPosition() - m_model->getClipPosition(cid);
1377         position = int(position * speed);
1378     }
1379     if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) {
1380         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1381         return;
1382     }
1383     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1384     GenTime pos(position, pCore->getCurrentFps());
1385     CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
1386     clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
1387 }
1388 
1389 void TimelineController::deleteMarker(int cid, int position)
1390 {
1391     if (cid == -1) {
1392         cid = getMainSelectedClip();
1393         if (cid == -1) {
1394             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1395             return;
1396         }
1397     }
1398     Q_ASSERT(m_model->isClip(cid));
1399     double speed = m_model->getClipSpeed(cid);
1400     if (position == -1) {
1401         // Calculate marker position relative to timeline cursor
1402         position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1403         position = int(position * speed);
1404     }
1405     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1406         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1407         return;
1408     }
1409     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1410     GenTime pos(position, pCore->getCurrentFps());
1411     clip->getMarkerModel()->removeMarker(pos);
1412 }
1413 
1414 void TimelineController::deleteAllMarkers(int cid)
1415 {
1416     if (cid == -1) {
1417         cid = getMainSelectedClip();
1418         if (cid == -1) {
1419             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1420             return;
1421         }
1422     }
1423     Q_ASSERT(m_model->isClip(cid));
1424     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1425     clip->getMarkerModel()->removeAllMarkers();
1426 }
1427 
1428 void TimelineController::editGuide(int frame)
1429 {
1430     if (frame == -1) {
1431         frame = pCore->getMonitorPosition();
1432     }
1433     auto guideModel = m_model->getGuideModel();
1434     GenTime pos(frame, pCore->getCurrentFps());
1435     guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
1436 }
1437 
1438 void TimelineController::moveGuideById(int id, int newFrame)
1439 {
1440     if (newFrame < 0) {
1441         return;
1442     }
1443     auto guideModel = m_model->getGuideModel();
1444     GenTime newPos(newFrame, pCore->getCurrentFps());
1445     GenTime oldPos = guideModel->markerById(id).time();
1446     guideModel->editMarker(oldPos, newPos);
1447 }
1448 
1449 int TimelineController::moveGuideWithoutUndo(int mid, int newFrame)
1450 {
1451     if (newFrame < 0) {
1452         return -1;
1453     }
1454     auto guideModel = m_model->getGuideModel();
1455     GenTime newPos(newFrame, pCore->getCurrentFps());
1456     if (guideModel->moveMarker(mid, newPos)) {
1457         return newFrame;
1458     }
1459     return -1;
1460 }
1461 
1462 bool TimelineController::moveGuidesInRange(int start, int end, int offset)
1463 {
1464     std::function<bool(void)> undo = []() { return true; };
1465     std::function<bool(void)> redo = []() { return true; };
1466     bool final = false;
1467     final = moveGuidesInRange(start, end, offset, undo, redo);
1468     if (final) {
1469         if (offset > 0) {
1470             pCore->pushUndo(undo, redo, i18n("Insert space"));
1471         } else {
1472             pCore->pushUndo(undo, redo, i18n("Remove space"));
1473         }
1474         return true;
1475     } else {
1476         undo();
1477     }
1478     return false;
1479 }
1480 
1481 bool TimelineController::moveGuidesInRange(int start, int end, int offset, Fun &undo, Fun &redo)
1482 {
1483     GenTime fromPos(start, pCore->getCurrentFps());
1484     GenTime toPos(start + offset, pCore->getCurrentFps());
1485     QList<CommentedTime> guides = m_model->getGuideModel()->getMarkersInRange(start, end);
1486     return m_model->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo);
1487 }
1488 
1489 void TimelineController::switchGuide(int frame, bool deleteOnly, bool showGui)
1490 {
1491     bool markerFound = false;
1492     if (frame == -1) {
1493         frame = pCore->getMonitorPosition();
1494     }
1495     qDebug() << "::: ADDING GUIDE TO MODEL: " << m_model->uuid();
1496     CommentedTime marker = m_model->getGuideModel()->getMarker(frame, &markerFound);
1497     if (!markerFound) {
1498         if (deleteOnly) {
1499             pCore->displayMessage(i18n("No guide found at current position"), ErrorMessage, 500);
1500             return;
1501         }
1502         GenTime pos(frame, pCore->getCurrentFps());
1503 
1504         if (showGui) {
1505             m_model->getGuideModel()->editMarkerGui(pos, qApp->activeWindow(), true);
1506         } else {
1507             m_model->getGuideModel()->addMarker(pos, i18n("guide"));
1508         }
1509     } else {
1510         m_model->getGuideModel()->removeMarker(marker.time());
1511     }
1512 }
1513 
1514 void TimelineController::addAsset(const QVariantMap &data)
1515 {
1516     const auto selection = m_model->getCurrentSelection();
1517     if (!selection.empty()) {
1518         QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
1519         QVariantList effectSelection = m_model->addClipEffect(*selection.begin(), effect, false);
1520         if (effectSelection.isEmpty()) {
1521             QString effectName = EffectsRepository::get()->getName(effect);
1522             pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500);
1523         } else if (KdenliveSettings::seekonaddeffect() && effectSelection.count() == 1) {
1524             // Move timeline cursor inside clip if it is not
1525             int cid = effectSelection.first().toInt();
1526             int in = m_model->getClipPosition(cid);
1527             int out = in + m_model->getClipPlaytime(cid);
1528             int position = pCore->getMonitorPosition();
1529             if (position < in || position > out) {
1530                 Q_EMIT seeked(in);
1531             }
1532         }
1533     } else {
1534         pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
1535     }
1536 }
1537 
1538 void TimelineController::requestRefresh()
1539 {
1540     pCore->refreshProjectMonitorOnce();
1541 }
1542 
1543 void TimelineController::showAsset(int id)
1544 {
1545     if (m_model->isComposition(id)) {
1546         Q_EMIT showTransitionModel(id, m_model->getCompositionParameterModel(id));
1547     } else if (m_model->isClip(id)) {
1548         QModelIndex clipIx = m_model->makeClipIndexFromID(id);
1549         QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
1550         bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
1551         qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
1552         Q_EMIT showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
1553     } else if (m_model->isSubTitle(id)) {
1554         qDebug() << "::: SHOWING SUBTITLE: " << id;
1555         Q_EMIT showSubtitle(id);
1556     }
1557 }
1558 
1559 void TimelineController::showTrackAsset(int trackId)
1560 {
1561     Q_EMIT showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
1562 }
1563 
1564 void TimelineController::adjustTrackHeight(int trackId, int height)
1565 {
1566     if (trackId > -1) {
1567         m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
1568         m_model->setTrackProperty(trackId, "kdenlive:collapsed", QStringLiteral("0"));
1569         QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId);
1570         Q_EMIT m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole});
1571         return;
1572     }
1573 }
1574 
1575 void TimelineController::adjustAllTrackHeight(int trackId, int height)
1576 {
1577     bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1578     auto it = m_model->m_allTracks.cbegin();
1579     while (it != m_model->m_allTracks.cend()) {
1580         int target_track = (*it)->getId();
1581         if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1582             m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
1583         }
1584         ++it;
1585     }
1586     int tracksCount = m_model->getTracksCount();
1587     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1588     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1589     Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1590 }
1591 
1592 void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight)
1593 {
1594     bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1595     auto it = m_model->m_allTracks.cbegin();
1596     while (it != m_model->m_allTracks.cend()) {
1597         int target_track = (*it)->getId();
1598         if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1599             if (collapse) {
1600                 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight));
1601             } else {
1602                 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0"));
1603             }
1604         }
1605         ++it;
1606     }
1607     int tracksCount = m_model->getTracksCount();
1608     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1609     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1610     Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1611 }
1612 
1613 void TimelineController::defaultTrackHeight(int trackId)
1614 {
1615     if (trackId > -1) {
1616         m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1617         QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId);
1618         Q_EMIT m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole});
1619         return;
1620     }
1621     auto it = m_model->m_allTracks.cbegin();
1622     while (it != m_model->m_allTracks.cend()) {
1623         int target_track = (*it)->getId();
1624         m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1625         ++it;
1626     }
1627     int tracksCount = m_model->getTracksCount();
1628     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1629     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1630     Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1631 }
1632 
1633 void TimelineController::setPosition(int position)
1634 {
1635     // Process seek request
1636     Q_EMIT seeked(position);
1637 }
1638 
1639 void TimelineController::setAudioTarget(const QMap<int, int> &tracks)
1640 {
1641     // Clear targets before re-adding to trigger qml refresh
1642     m_model->m_audioTarget.clear();
1643     Q_EMIT audioTargetChanged();
1644 
1645     if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) {
1646         return;
1647     }
1648 
1649     m_model->m_audioTarget = tracks;
1650     Q_EMIT audioTargetChanged();
1651 }
1652 
1653 void TimelineController::switchAudioTarget(int trackId)
1654 {
1655     if (m_model->m_audioTarget.contains(trackId)) {
1656         m_model->m_audioTarget.remove(trackId);
1657     } else {
1658         // TODO: use track description
1659         if (m_model->m_binAudioTargets.count() == 1) {
1660             // Only one audio stream, remove previous and switch
1661             m_model->m_audioTarget.clear();
1662         }
1663         int ix = getFirstUnassignedStream();
1664         if (ix > -1) {
1665             m_model->m_audioTarget.insert(trackId, ix);
1666         }
1667     }
1668     Q_EMIT audioTargetChanged();
1669 }
1670 
1671 void TimelineController::assignCurrentTarget(int index)
1672 {
1673     if (m_activeTrack == -1 || !m_model->isTrack(m_activeTrack)) {
1674         pCore->displayMessage(i18n("No active track"), ErrorMessage, 500);
1675         return;
1676     }
1677     bool isAudio = m_model->isAudioTrack(m_activeTrack);
1678     if (isAudio) {
1679         // Select audio target stream
1680         if (index >= 0 && index < m_model->m_binAudioTargets.size()) {
1681             // activate requested stream
1682             int stream = m_model->m_binAudioTargets.keys().at(index);
1683             assignAudioTarget(m_activeTrack, stream);
1684         } else {
1685             // Remove audio target
1686             m_model->m_audioTarget.remove(m_activeTrack);
1687             Q_EMIT audioTargetChanged();
1688         }
1689     } else {
1690         // Select video target stream
1691         setVideoTarget(m_activeTrack);
1692     }
1693 }
1694 
1695 void TimelineController::assignAudioTarget(int trackId, int stream)
1696 {
1697     QList<int> assignedStreams = m_model->m_audioTarget.values();
1698     if (assignedStreams.contains(stream)) {
1699         // This stream was assigned to another track, remove
1700         m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream));
1701     }
1702     // Remove and re-add target track to trigger a refresh in qml track headers
1703     m_model->m_audioTarget.remove(trackId);
1704     Q_EMIT audioTargetChanged();
1705 
1706     m_model->m_audioTarget.insert(trackId, stream);
1707     Q_EMIT audioTargetChanged();
1708 }
1709 
1710 int TimelineController::getFirstUnassignedStream() const
1711 {
1712     QList<int> keys = m_model->m_binAudioTargets.keys();
1713     QList<int> assigned = m_model->m_audioTarget.values();
1714     for (int k : qAsConst(keys)) {
1715         if (!assigned.contains(k)) {
1716             return k;
1717         }
1718     }
1719     return -1;
1720 }
1721 
1722 void TimelineController::setVideoTarget(int track)
1723 {
1724     if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) {
1725         m_model->m_videoTarget = -1;
1726         return;
1727     }
1728     m_model->m_videoTarget = track;
1729     Q_EMIT videoTargetChanged();
1730 }
1731 
1732 void TimelineController::setActiveTrack(int track)
1733 {
1734     if (track > -1 && !m_model->isTrack(track)) {
1735         return;
1736     }
1737     m_activeTrack = track;
1738     Q_EMIT activeTrackChanged();
1739 }
1740 
1741 void TimelineController::setZoneToSelection()
1742 {
1743     std::unordered_set<int> selection = m_model->getCurrentSelection();
1744     QPoint zone(-1, -1);
1745     for (int cid : selection) {
1746         int inPos = m_model->getItemPosition(cid);
1747         int outPos = inPos + m_model->getItemPlaytime(cid);
1748         if (zone.x() == -1 || inPos < zone.x()) {
1749             zone.setX(inPos);
1750         }
1751         if (outPos > zone.y()) {
1752             zone.setY(outPos);
1753         }
1754     }
1755     if (zone.x() > -1 && zone.y() > -1) {
1756         updateZone(m_zone, zone, true);
1757     } else {
1758         pCore->displayMessage(i18n("No item selected in timeline"), ErrorMessage, 500);
1759     }
1760 }
1761 
1762 void TimelineController::setZone(const QPoint &zone, bool withUndo)
1763 {
1764     if (m_zone.x() > 0) {
1765         m_model->removeSnap(m_zone.x());
1766     }
1767     if (m_zone.y() > 0) {
1768         m_model->removeSnap(m_zone.y() - 1);
1769     }
1770     if (zone.x() > 0) {
1771         m_model->addSnap(zone.x());
1772     }
1773     if (zone.y() > 0) {
1774         m_model->addSnap(zone.y() - 1);
1775     }
1776     updateZone(m_zone, zone, withUndo);
1777 }
1778 
1779 void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1780 {
1781     if (!withUndo) {
1782         m_zone = newZone;
1783         Q_EMIT zoneChanged();
1784         // Update monitor zone
1785         Q_EMIT zoneMoved(m_zone);
1786         return;
1787     }
1788     Fun undo_zone = [this, oldZone]() {
1789         setZone(oldZone, false);
1790         return true;
1791     };
1792     Fun redo_zone = [this, newZone]() {
1793         setZone(newZone, false);
1794         return true;
1795     };
1796     redo_zone();
1797     pCore->pushUndo(undo_zone, redo_zone, i18n("Set Zone"));
1798 }
1799 
1800 void TimelineController::updateEffectZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1801 {
1802     Q_UNUSED(oldZone)
1803     Q_EMIT pCore->updateEffectZone(newZone, withUndo);
1804 }
1805 
1806 void TimelineController::setZoneIn(int inPoint)
1807 {
1808     if (m_zone.x() > 0) {
1809         m_model->removeSnap(m_zone.x());
1810     }
1811     if (inPoint > 0) {
1812         m_model->addSnap(inPoint);
1813     }
1814     m_zone.setX(inPoint);
1815     Q_EMIT zoneChanged();
1816     // Update monitor zone
1817     Q_EMIT zoneMoved(m_zone);
1818 }
1819 
1820 void TimelineController::setZoneOut(int outPoint)
1821 {
1822     if (m_zone.y() > 0) {
1823         m_model->removeSnap(m_zone.y() - 1);
1824     }
1825     if (outPoint > 0) {
1826         m_model->addSnap(outPoint - 1);
1827     }
1828     m_zone.setY(outPoint);
1829     Q_EMIT zoneChanged();
1830     Q_EMIT zoneMoved(m_zone);
1831 }
1832 
1833 void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions,
1834                                      bool selectSubTitles)
1835 {
1836     std::unordered_set<int> itemsToSelect;
1837     if (addToSelect) {
1838         itemsToSelect = m_model->getCurrentSelection();
1839     }
1840     for (int i = 0; i < tracks.count(); i++) {
1841         if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
1842             continue;
1843         }
1844         auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions);
1845         itemsToSelect.insert(currentClips.begin(), currentClips.end());
1846     }
1847     if (selectSubTitles && m_model->hasSubtitleModel()) {
1848         auto currentSubs = m_model->getSubtitleModel()->getItemsInRange(startFrame, endFrame);
1849         itemsToSelect.insert(currentSubs.begin(), currentSubs.end());
1850     }
1851     m_model->requestSetSelection(itemsToSelect);
1852 }
1853 
1854 void TimelineController::requestClipCut(int clipId, int position)
1855 {
1856     if (position == -1) {
1857         position = pCore->getMonitorPosition();
1858     }
1859     TimelineFunctions::requestClipCut(m_model, clipId, position);
1860 }
1861 
1862 void TimelineController::cutClipUnderCursor(int position, int track)
1863 {
1864     if (position == -1) {
1865         position = pCore->getMonitorPosition();
1866     }
1867     QMutexLocker lk(&m_metaMutex);
1868     bool foundClip = false;
1869     const auto selection = m_model->getCurrentSelection();
1870     if (track == -1) {
1871         for (int cid : selection) {
1872             if ((m_model->isClip(cid) || m_model->isSubTitle(cid)) && positionIsInItem(cid)) {
1873                 if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
1874                     foundClip = true;
1875                     // Cutting clips in the selection group is handled in TimelineFunctions
1876                 }
1877                 break;
1878             }
1879         }
1880     }
1881     if (!foundClip) {
1882         if (track == -1) {
1883             track = m_activeTrack;
1884         }
1885         if (track != -1) {
1886             int cid = m_model->getClipByPosition(track, position);
1887             if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
1888                 foundClip = true;
1889             }
1890         }
1891     }
1892     if (!foundClip) {
1893         pCore->displayMessage(i18n("No clip to cut"), ErrorMessage, 500);
1894     }
1895 }
1896 
1897 void TimelineController::cutAllClipsUnderCursor(int position)
1898 {
1899     if (position == -1) {
1900         position = pCore->getMonitorPosition();
1901     }
1902     QMutexLocker lk(&m_metaMutex);
1903     TimelineFunctions::requestClipCutAll(m_model, position);
1904 }
1905 
1906 int TimelineController::requestSpacerStartOperation(int trackId, int position)
1907 {
1908     QMutexLocker lk(&m_metaMutex);
1909     std::pair<int, int> spacerOp = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
1910     int itemId = spacerOp.first;
1911     return itemId;
1912 }
1913 
1914 int TimelineController::spacerMinPos() const
1915 {
1916     return TimelineFunctions::spacerMinPos();
1917 }
1918 
1919 void TimelineController::spacerMoveGuides(const QVector<int> &ids, int offset)
1920 {
1921     m_model->getGuideModel()->moveMarkersWithoutUndo(ids, offset);
1922 }
1923 
1924 QVector<int> TimelineController::spacerSelection(int startFrame)
1925 {
1926     return m_model->getGuideModel()->getMarkersIdInRange(startFrame, -1);
1927 }
1928 
1929 int TimelineController::getGuidePosition(int id)
1930 {
1931     return m_model->getGuideModel()->getMarkerPos(id);
1932 }
1933 
1934 bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition, int affectedTrack, const QVector<int> &selectedGuides,
1935                                                    int guideStart)
1936 {
1937     QMutexLocker lk(&m_metaMutex);
1938     // Start undoable command
1939     std::function<bool(void)> undo = []() { return true; };
1940     std::function<bool(void)> redo = []() { return true; };
1941     if (guideStart > -1) {
1942         // Move guides back to original position
1943         m_model->getGuideModel()->moveMarkersWithoutUndo(selectedGuides, startPosition - endPosition, false);
1944     }
1945     bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition, affectedTrack, guideStart, undo, redo);
1946     return result;
1947 }
1948 
1949 void TimelineController::seekCurrentClip(bool seekToEnd)
1950 {
1951     const auto selection = m_model->getCurrentSelection();
1952     int cid = -1;
1953     if (!selection.empty()) {
1954         cid = *selection.begin();
1955     } else {
1956         int cursorPos = pCore->getMonitorPosition();
1957         cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1958         if (cid < 0) {
1959             /* If the cursor is at the clip end it is one frame after the clip,
1960              * make it possible to jump to the clip start in that situation too
1961              */
1962             cid = m_model->getClipByPosition(m_activeTrack, cursorPos - 1);
1963         }
1964     }
1965     if (cid > -1) {
1966         seekToClip(cid, seekToEnd);
1967     }
1968 }
1969 
1970 void TimelineController::seekToClip(int cid, bool seekToEnd)
1971 {
1972     int start = m_model->getItemPosition(cid);
1973     if (seekToEnd) {
1974         // -1 because to go to the end of a 10-frame clip,
1975         // need to go from frame 0 to frame 9 (10th frame)
1976         start += m_model->getItemPlaytime(cid) - 1;
1977     }
1978     setPosition(start);
1979 }
1980 
1981 void TimelineController::seekToMouse()
1982 {
1983     int mousePos = getMousePos();
1984     if (mousePos > -1) {
1985         setPosition(mousePos);
1986     }
1987 }
1988 
1989 int TimelineController::getMousePos()
1990 {
1991     QVariant returnedValue;
1992     QMetaObject::invokeMethod(m_root, "getMousePos", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue));
1993     return returnedValue.toInt();
1994 }
1995 
1996 int TimelineController::getMouseTrack()
1997 {
1998     QVariant returnedValue;
1999     QMetaObject::invokeMethod(m_root, "getMouseTrack", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue));
2000     return returnedValue.toInt();
2001 }
2002 
2003 bool TimelineController::positionIsInItem(int id)
2004 {
2005     int in = m_model->getItemPosition(id);
2006     int position = pCore->getMonitorPosition();
2007     if (in > position) {
2008         return false;
2009     }
2010     if (position <= in + m_model->getItemPlaytime(id)) {
2011         return true;
2012     }
2013     return false;
2014 }
2015 
2016 void TimelineController::refreshItem(int id)
2017 {
2018     if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) {
2019         return;
2020     }
2021     if (positionIsInItem(id)) {
2022         pCore->refreshProjectMonitorOnce();
2023     }
2024 }
2025 
2026 QPair<int, int> TimelineController::getAvTracksCount() const
2027 {
2028     return m_model->getAVtracksCount();
2029 }
2030 
2031 QStringList TimelineController::extractCompositionLumas() const
2032 {
2033     return m_model->extractCompositionLumas();
2034 }
2035 
2036 QStringList TimelineController::extractExternalEffectFiles() const
2037 {
2038     return m_model->extractExternalEffectFiles();
2039 }
2040 
2041 void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
2042 {
2043     QList<int> activeClips;
2044     for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
2045         int trackIx = m_model->getTrackIndexFromPosition(track);
2046         int cid = m_model->getClipByPosition(trackIx, pCore->getMonitorPosition());
2047         if (cid > -1) {
2048             activeClips << cid;
2049         }
2050     }
2051     if (!activeClips.isEmpty()) {
2052         if (effectData.count() == 4) {
2053             QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
2054             m_model->copyClipEffect(activeClips.first(), effectString);
2055         } else {
2056             m_model->addClipEffect(activeClips.first(), effectData.constFirst());
2057         }
2058     }
2059 }
2060 
2061 void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
2062 {
2063     if (initialDuration == -2) {
2064         // Add default fade
2065         duration = pCore->getDurationFromString(KdenliveSettings::fade_duration());
2066         initialDuration = 0;
2067     }
2068     if (duration <= 0) {
2069         // remove fade
2070         if (initialDuration > 0) {
2071             // Restore original fade duration
2072             m_model->adjustEffectLength(cid, effectId, initialDuration, -1);
2073         }
2074         m_model->removeFade(cid, effectId == QLatin1String("fadein"));
2075     } else {
2076         m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
2077     }
2078 }
2079 
2080 QPair<int, int> TimelineController::getCompositionATrack(int cid) const
2081 {
2082     QPair<int, int> result;
2083     std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
2084     if (compo) {
2085         result = QPair<int, int>(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
2086     }
2087     return result;
2088 }
2089 
2090 void TimelineController::setCompositionATrack(int cid, int aTrack)
2091 {
2092     TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
2093 }
2094 
2095 bool TimelineController::compositionAutoTrack(int cid) const
2096 {
2097     std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
2098     return compo && compo->getForcedTrack() == -1;
2099 }
2100 
2101 const QString TimelineController::getClipBinId(int clipId) const
2102 {
2103     return m_model->getClipBinId(clipId);
2104 }
2105 
2106 void TimelineController::focusItem(int itemId)
2107 {
2108     int start = m_model->getItemPosition(itemId);
2109     int tid = m_model->getItemTrackId(itemId);
2110     setPosition(start);
2111     setActiveTrack(tid);
2112     Q_EMIT centerView();
2113 }
2114 
2115 int TimelineController::headerWidth() const
2116 {
2117     return qMax(10, KdenliveSettings::headerwidth());
2118 }
2119 
2120 void TimelineController::setHeaderWidth(int width)
2121 {
2122     KdenliveSettings::setHeaderwidth(width);
2123 }
2124 
2125 bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr<Mlt::Filter> filter)
2126 {
2127     if (m_model->hasTimelinePreview() && m_model->previewManager()->hasOverlayTrack()) {
2128         return true;
2129     }
2130     if (clipId == -1) {
2131         pCore->displayMessage(i18n("Select a clip to compare effect"), ErrorMessage, 500);
2132         return false;
2133     }
2134     std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId);
2135     const QString binId = clip->binId();
2136 
2137     // Get clean bin copy of the clip
2138     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
2139     std::shared_ptr<Mlt::Producer> binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
2140 
2141     // Get copy of timeline producer
2142     std::shared_ptr<Mlt::Producer> clipProducer(new Mlt::Producer(*clip));
2143 
2144     // Built tractor and compositing
2145     Mlt::Tractor trac(pCore->getProjectProfile());
2146     Mlt::Playlist play(pCore->getProjectProfile());
2147     Mlt::Playlist play2(pCore->getProjectProfile());
2148     play.append(*clipProducer.get());
2149     play2.append(*binProd);
2150     trac.set_track(play, 0);
2151     trac.set_track(play2, 1);
2152     play2.attach(*filter.get());
2153     QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
2154     Mlt::Transition t(pCore->getProjectProfile(), splitTransition.toUtf8().constData());
2155     t.set("always_active", 1);
2156     trac.plant_transition(t, 0, 1);
2157     int startPos = m_model->getClipPosition(clipId);
2158 
2159     // plug in overlay playlist
2160     auto *overlay = new Mlt::Playlist(pCore->getProjectProfile());
2161     overlay->insert_blank(0, startPos);
2162     Mlt::Producer split(trac.get_producer());
2163     overlay->insert_at(startPos, &split, 1);
2164 
2165     // insert in tractor
2166     if (!m_model->hasTimelinePreview()) {
2167         initializePreview();
2168     }
2169     m_model->setOverlayTrack(overlay);
2170     return true;
2171 }
2172 
2173 void TimelineController::removeSplitOverlay()
2174 {
2175     if (!m_model->hasTimelinePreview() || !m_model->previewManager()->hasOverlayTrack()) {
2176         return;
2177     }
2178     // disconnect
2179     m_model->removeOverlayTrack();
2180 }
2181 
2182 int TimelineController::requestItemRippleResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize)
2183 {
2184     return m_model->requestItemRippleResize(m_model, itemId, size, right, logUndo, !KdenliveSettings::lockedGuides(), snapDistance, allowSingleResize);
2185 }
2186 
2187 void TimelineController::updateTrimmingMode()
2188 {
2189     if (trimmingActive()) {
2190         requestStartTrimmingMode();
2191     } else {
2192         requestEndTrimmingMode();
2193     }
2194 }
2195 
2196 int TimelineController::trimmingBoundOffset(int offset)
2197 {
2198     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2199     return qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
2200 }
2201 
2202 void TimelineController::slipPosChanged(int offset)
2203 {
2204     if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
2205         return;
2206     }
2207     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2208     offset = qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
2209     int outPoint = mainClip->getOut() - offset;
2210     int inPoint = mainClip->getIn() - offset;
2211 
2212     pCore->monitorManager()->projectMonitor()->slotTrimmingPos(inPoint, offset, inPoint, outPoint);
2213     showToolTip(i18n("In:%1, Out:%2 (%3%4)", simplifiedTC(inPoint), simplifiedTC(outPoint), (offset < 0 ? "-" : "+"), simplifiedTC(qFabs(offset))));
2214 }
2215 
2216 void TimelineController::ripplePosChanged(int size, bool right)
2217 {
2218     if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
2219         return;
2220     }
2221     if (size < 0) {
2222         return;
2223     }
2224     qDebug() << "ripplePosChanged" << size << right;
2225     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2226     int delta = size - mainClip->getPlaytime();
2227     if (!right) {
2228         delta *= -1;
2229     }
2230     int pos = right ? mainClip->getOut() : mainClip->getIn();
2231     pos += delta;
2232     if (mainClip->getMaxDuration() > -1) {
2233         pos = qBound(0, pos, mainClip->getMaxDuration());
2234     } else {
2235         pos = qMax(0, pos);
2236     }
2237     pCore->monitorManager()->projectMonitor()->slotTrimmingPos(pos + 1, delta, right ? mainClip->getIn() : pos, right ? pos : mainClip->getOut());
2238 }
2239 
2240 bool TimelineController::slipProcessSelection(int mainClipId, bool addToSelection)
2241 {
2242     std::unordered_set<int> sel = m_model->getCurrentSelection();
2243     std::unordered_set<int> newSel;
2244 
2245     for (int i : sel) {
2246         if (m_model->isClip(i) && m_model->getClipPtr(i)->getMaxDuration() != -1) {
2247             newSel.insert(i);
2248         }
2249     }
2250 
2251     if (mainClipId != -1 && !m_model->isClip(mainClipId) && m_model->getClipPtr(mainClipId)->getMaxDuration() == -1) {
2252         mainClipId = -1;
2253     }
2254 
2255     if ((newSel.empty() || !isInSelection(mainClipId)) && mainClipId != -1) {
2256         m_trimmingMainClip = mainClipId;
2257         Q_EMIT trimmingMainClipChanged();
2258         if (!addToSelection) {
2259             newSel.clear();
2260         }
2261         newSel.insert(mainClipId);
2262     }
2263 
2264     if (newSel != sel) {
2265         m_model->requestSetSelection(newSel);
2266         return false;
2267     }
2268 
2269     if (sel.empty()) {
2270         return false;
2271     }
2272 
2273     Q_ASSERT(!sel.empty());
2274 
2275     if (mainClipId == -1) {
2276         mainClipId = getMainSelectedClip();
2277     }
2278 
2279     if (m_model->getTrackById(m_model->getClipTrackId(mainClipId))->isLocked()) {
2280         int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2281         if (partnerId == -1 || m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2282             mainClipId = -1;
2283             for (int i : sel) {
2284                 if (i != mainClipId && !m_model->getTrackById(m_model->getClipTrackId(i))->isLocked()) {
2285                     mainClipId = i;
2286                     break;
2287                 }
2288             }
2289         } else {
2290             mainClipId = partnerId;
2291         }
2292     }
2293 
2294     if (mainClipId == -1) {
2295         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2296         return false;
2297     }
2298 
2299     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(mainClipId);
2300 
2301     if (mainClip->getMaxDuration() == -1) {
2302         return false;
2303     }
2304 
2305     int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2306 
2307     if (mainClip->isAudioOnly() && partnerId != -1 && !m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2308         mainClip = m_model->getClipPtr(partnerId);
2309     }
2310 
2311     m_trimmingMainClip = mainClip->getId();
2312     Q_EMIT trimmingMainClipChanged();
2313     return true;
2314 }
2315 
2316 bool TimelineController::requestStartTrimmingMode(int mainClipId, bool addToSelection, bool right)
2317 {
2318 
2319     if (pCore->monitorManager()->isTrimming() && m_trimmingMainClip == mainClipId) {
2320         return true;
2321     }
2322 
2323     if (pCore->activeTool() == ToolType::SlipTool && !slipProcessSelection(mainClipId, addToSelection)) {
2324         return false;
2325     }
2326 
2327     if (pCore->activeTool() == ToolType::RippleTool) {
2328         if (m_model.get()->isClip(mainClipId)) {
2329             m_trimmingMainClip = mainClipId;
2330             Q_EMIT trimmingMainClipChanged();
2331         } else {
2332             return false;
2333         }
2334     }
2335 
2336     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2337 
2338     const int previousClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() - 1);
2339     std::shared_ptr<Mlt::Producer> previousFrame;
2340     if (pCore->activeTool() == ToolType::SlipTool && previousClipId > -1) {
2341         std::shared_ptr<ClipModel> previousClip = m_model->getClipPtr(previousClipId);
2342         previousFrame = std::shared_ptr<Mlt::Producer>(previousClip->getProducer()->cut(0));
2343         Mlt::Filter filter(pCore->getProjectProfile(), "freeze");
2344         filter.set("mlt_service", "freeze");
2345         filter.set("frame", previousClip->getOut());
2346         previousFrame->attach(filter);
2347     } else {
2348         previousFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(pCore->getProjectProfile(), "color:black"));
2349     }
2350 
2351     const int nextClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() + mainClip->getPlaytime());
2352     std::shared_ptr<Mlt::Producer> nextFrame;
2353     if (pCore->activeTool() == ToolType::SlipTool && nextClipId > -1) {
2354         std::shared_ptr<ClipModel> nextClip = m_model->getClipPtr(nextClipId);
2355         nextFrame = std::shared_ptr<Mlt::Producer>(nextClip->getProducer()->cut(0));
2356         Mlt::Filter filter(pCore->getProjectProfile(), "freeze");
2357         filter.set("mlt_service", "freeze");
2358         filter.set("frame", nextClip->getIn());
2359         nextFrame->attach(filter);
2360     } else {
2361         nextFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(pCore->getProjectProfile(), "color:black"));
2362     }
2363 
2364     std::shared_ptr<Mlt::Producer> inOutFrame;
2365     if (pCore->activeTool() == ToolType::RippleTool) {
2366         inOutFrame = std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0));
2367         Mlt::Filter filter(pCore->getProjectProfile(), "freeze");
2368         filter.set("mlt_service", "freeze");
2369         filter.set("frame", right ? mainClip->getIn() : mainClip->getOut());
2370         inOutFrame->attach(filter);
2371     }
2372 
2373     std::vector<std::shared_ptr<Mlt::Producer>> producers;
2374     int previewLength = 0;
2375     switch (pCore->activeTool()) {
2376     case ToolType::SlipTool:
2377         producers.push_back(std::shared_ptr<Mlt::Producer>(previousFrame));
2378         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2379         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(mainClip->getOut() - mainClip->getIn())));
2380         producers.push_back(nextFrame);
2381         previewLength = producers[1]->get_length();
2382         break;
2383     case ToolType::SlideTool:
2384         break;
2385     case ToolType::RollTool:
2386         break;
2387     case ToolType::RippleTool:
2388         if (right) {
2389             producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2390         }
2391         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2392         if (!right) {
2393             producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2394             previewLength = producers[0]->get_length();
2395         } else {
2396             previewLength = producers[1]->get_length();
2397         }
2398         break;
2399     default:
2400         return false;
2401     }
2402 
2403     // Built tractor
2404     Mlt::Tractor trac(pCore->getProjectProfile());
2405 
2406     // Now that we know the length of the preview create and add black background producer
2407     std::shared_ptr<Mlt::Producer> black(new Mlt::Producer(pCore->getProjectProfile(), "color:black"));
2408     black->set("length", previewLength);
2409     black->set_in_and_out(0, previewLength);
2410     black->set("mlt_image_format", "rgba");
2411     trac.set_track(*black.get(), 0);
2412     // trac.set_track( 1);
2413 
2414     if (!mainClip->isAudioOnly()) {
2415         int count = 1; // 0 is background track so we start at 1
2416         for (auto const &producer : producers) {
2417             trac.set_track(*producer.get(), count);
2418             count++;
2419         }
2420 
2421         // Add "composite" transitions for multi clip view
2422         for (int i = 0; i < int(producers.size()); i++) {
2423             // Construct transition
2424             Mlt::Transition transition(pCore->getProjectProfile(), "composite");
2425             transition.set("mlt_service", "composite");
2426             transition.set("a_track", 0);
2427             transition.set("b_track", i + 1);
2428             transition.set("distort", 0);
2429             transition.set("aligned", 0);
2430             // 200 is an arbitrary number so we can easily remove these transition later
2431             // transition.set("internal_added", 200);
2432 
2433             QString geometry;
2434             switch (pCore->activeTool()) {
2435             case ToolType::RollTool:
2436             case ToolType::RippleTool:
2437                 switch (i) {
2438                 case 0:
2439                     geometry = QStringLiteral("0 0 50% 100%");
2440                     break;
2441                 case 1:
2442                     geometry = QStringLiteral("50% 0 50% 100%");
2443                     break;
2444                 }
2445                 break;
2446             case ToolType::SlipTool:
2447                 switch (i) {
2448                 case 0:
2449                     geometry = QStringLiteral("0 0 25% 25%");
2450                     break;
2451                 case 1:
2452                     geometry = QStringLiteral("0 25% 50% 50%");
2453                     break;
2454                 case 2:
2455                     geometry = QStringLiteral("50% 25% 50% 50%");
2456                     break;
2457                 case 3:
2458                     geometry = QStringLiteral("75% 75% 25% 25%");
2459                     break;
2460                 }
2461                 break;
2462             case ToolType::SlideTool:
2463                 switch (i) {
2464                 case 0:
2465                     geometry = QStringLiteral("0 0 25% 25%");
2466                     break;
2467                 case 1:
2468                     geometry = QStringLiteral("50% 25% 50% 50%");
2469                     break;
2470                 case 2:
2471                     geometry = QStringLiteral("0 25% 50% 50%");
2472                     break;
2473                 case 3:
2474                     geometry = QStringLiteral("50% 75% 25% 25%");
2475                     break;
2476                 }
2477                 break;
2478             default:
2479                 break;
2480             }
2481 
2482             // Add transition to track:
2483             transition.set("geometry", geometry.toUtf8().constData());
2484             transition.set("always_active", 1);
2485             trac.plant_transition(transition, 0, i + 1);
2486         }
2487     }
2488 
2489     pCore->monitorManager()->projectMonitor()->setProducer(std::make_shared<Mlt::Producer>(trac), -2);
2490     pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(true);
2491 
2492     switch (pCore->activeTool()) {
2493     case ToolType::RollTool:
2494     case ToolType::RippleTool:
2495         ripplePosChanged(mainClip->getPlaytime(), right);
2496         break;
2497     case ToolType::SlipTool:
2498         slipPosChanged(0);
2499         break;
2500     case ToolType::SlideTool:
2501         break;
2502     default:
2503         break;
2504     }
2505 
2506     return true;
2507 }
2508 
2509 void TimelineController::requestEndTrimmingMode()
2510 {
2511     if (pCore->monitorManager()->isTrimming()) {
2512         pCore->monitorManager()->projectMonitor()->setProducer(pCore->window()->getCurrentTimeline()->model()->producer(), 0);
2513         pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(false);
2514     }
2515 }
2516 
2517 void TimelineController::addPreviewRange(bool add)
2518 {
2519     if (m_zone.isNull()) {
2520         return;
2521     }
2522     if (!m_model->hasTimelinePreview()) {
2523         initializePreview();
2524     }
2525     if (m_model->hasTimelinePreview()) {
2526         m_model->previewManager()->addPreviewRange(m_zone, add);
2527     }
2528 }
2529 
2530 void TimelineController::clearPreviewRange(bool resetZones)
2531 {
2532     if (m_model->hasTimelinePreview()) {
2533         m_model->previewManager()->clearPreviewRange(resetZones);
2534     }
2535 }
2536 
2537 void TimelineController::startPreviewRender()
2538 {
2539     // Timeline preview stuff
2540     if (!m_model->hasTimelinePreview()) {
2541         initializePreview();
2542     } else if (m_disablePreview->isChecked()) {
2543         m_disablePreview->setChecked(false);
2544         disablePreview(false);
2545     }
2546     if (m_model->hasTimelinePreview()) {
2547         if (!m_usePreview) {
2548             m_model->buildPreviewTrack();
2549             m_usePreview = true;
2550         }
2551         if (!m_model->previewManager()->hasDefinedRange()) {
2552             addPreviewRange(true);
2553         }
2554         m_model->previewManager()->startPreviewRender();
2555     }
2556 }
2557 
2558 void TimelineController::stopPreviewRender()
2559 {
2560     if (m_model->hasTimelinePreview()) {
2561         m_model->previewManager()->abortRendering();
2562     }
2563 }
2564 
2565 void TimelineController::initializePreview()
2566 {
2567     if (m_model->hasTimelinePreview()) {
2568         // Update parameters
2569         if (!m_model->previewManager()->loadParams()) {
2570             if (m_usePreview) {
2571                 // Disconnect preview track
2572                 m_model->previewManager()->disconnectTrack();
2573                 m_usePreview = false;
2574             }
2575             m_model->resetPreviewManager();
2576         }
2577     } else {
2578         m_model->initializePreviewManager();
2579     }
2580 }
2581 
2582 void TimelineController::connectPreviewManager()
2583 {
2584     if (m_model->hasTimelinePreview()) {
2585         connect(m_model->previewManager().get(), &PreviewManager::dirtyChunksChanged, this, &TimelineController::dirtyChunksChanged,
2586                 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection));
2587         connect(m_model->previewManager().get(), &PreviewManager::renderedChunksChanged, this, &TimelineController::renderedChunksChanged,
2588                 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection));
2589         connect(m_model->previewManager().get(), &PreviewManager::workingPreviewChanged, this, &TimelineController::workingPreviewChanged,
2590                 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection));
2591     }
2592 }
2593 
2594 bool TimelineController::hasPreviewTrack() const
2595 {
2596     return (m_model->hasTimelinePreview() && (m_model->previewManager()->hasOverlayTrack() || m_model->previewManager()->hasPreviewTrack()));
2597 }
2598 
2599 void TimelineController::disablePreview(bool disable)
2600 {
2601     if (disable) {
2602         m_model->deletePreviewTrack();
2603         m_usePreview = false;
2604     } else {
2605         if (!m_usePreview) {
2606             if (!m_model->buildPreviewTrack()) {
2607                 // preview track already exists, reconnect
2608                 m_model->m_tractor->lock();
2609                 m_model->previewManager()->reconnectTrack();
2610                 m_model->m_tractor->unlock();
2611             }
2612             Mlt::Playlist playlist;
2613             m_model->previewManager()->loadChunks(QVariantList(), QVariantList(), playlist);
2614             m_usePreview = true;
2615         }
2616     }
2617 }
2618 
2619 QVariantList TimelineController::dirtyChunks() const
2620 {
2621     return m_model->hasTimelinePreview() ? m_model->previewManager()->m_dirtyChunks : QVariantList();
2622 }
2623 
2624 QVariantList TimelineController::renderedChunks() const
2625 {
2626     return m_model->hasTimelinePreview() ? m_model->previewManager()->m_renderedChunks : QVariantList();
2627 }
2628 
2629 int TimelineController::workingPreview() const
2630 {
2631     return m_model->hasTimelinePreview() ? m_model->previewManager()->workingPreview : -1;
2632 }
2633 
2634 bool TimelineController::useRuler() const
2635 {
2636     return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
2637 }
2638 
2639 bool TimelineController::scrollVertically() const
2640 {
2641     return KdenliveSettings::scrollvertically() == 1;
2642 }
2643 
2644 void TimelineController::resetPreview()
2645 {
2646     if (m_model->hasTimelinePreview()) {
2647         m_model->previewManager()->clearPreviewRange(true);
2648         initializePreview();
2649     }
2650 }
2651 
2652 void TimelineController::saveSequenceProperties()
2653 {
2654     int activeTrack = m_activeTrack < 0 ? m_activeTrack : m_model->getTrackPosition(m_activeTrack);
2655     m_model->tractor()->set("kdenlive:sequenceproperties.activeTrack", activeTrack);
2656     QVariant returnedValue;
2657     QMetaObject::invokeMethod(m_root, "getScrollPos", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue));
2658     int scrollPos = returnedValue.toInt();
2659     m_model->tractor()->set("kdenlive:sequenceproperties.scrollPos", scrollPos);
2660     m_model->tractor()->set("kdenlive:sequenceproperties.zonein", m_zone.x());
2661     m_model->tractor()->set("kdenlive:sequenceproperties.zoneout", m_zone.y());
2662     tractor()->set("kdenlive:sequenceproperties.disablepreview", m_disablePreview->isChecked());
2663     if (m_model->hasSubtitleModel()) {
2664         const QString subtitlesData = m_model->getSubtitleModel()->subtitlesFilesToJson();
2665         m_model->tractor()->set("kdenlive:sequenceproperties.subtitlesList", subtitlesData.toUtf8().constData());
2666     }
2667 }
2668 
2669 QMap<QString, QString> TimelineController::documentProperties()
2670 {
2671     QMap<QString, QString> props = pCore->currentDoc()->documentProperties();
2672     // Ensure current timeline properties are saved (groups, guides, etc)
2673     saveSequenceProperties();
2674     return props;
2675 }
2676 
2677 int TimelineController::getMenuOrTimelinePos() const
2678 {
2679     int frame = m_root->property("clickFrame").toInt();
2680     if (frame == -1) {
2681         frame = pCore->getMonitorPosition();
2682     }
2683     return frame;
2684 }
2685 
2686 void TimelineController::insertSpace(int trackId, int frame)
2687 {
2688     if (frame == -1) {
2689         frame = getMenuOrTimelinePos();
2690     }
2691     if (trackId == -1) {
2692         trackId = m_activeTrack;
2693     }
2694     QPointer<SpacerDialog> d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
2695     if (d->exec() != QDialog::Accepted) {
2696         delete d;
2697         return;
2698     }
2699     bool affectAllTracks = d->affectAllTracks();
2700     int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame);
2701     int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
2702     delete d;
2703     if (cid == -1) {
2704         pCore->displayMessage(i18n("No clips found to insert space"), ErrorMessage, 500);
2705         return;
2706     }
2707     int start = m_model->getItemPosition(cid);
2708     requestSpacerEndOperation(cid, start, start + spaceDuration, affectAllTracks ? -1 : trackId);
2709 }
2710 
2711 void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
2712 {
2713     if (frame == -1) {
2714         frame = getMenuOrTimelinePos();
2715     }
2716     if (trackId == -1) {
2717         trackId = m_activeTrack;
2718     }
2719     bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
2720     if (!res) {
2721         pCore->displayMessage(i18n("Cannot remove space at given position"), ErrorMessage, 500);
2722     }
2723 }
2724 
2725 void TimelineController::removeTrackSpaces(int trackId, int frame)
2726 {
2727     if (frame == -1) {
2728         frame = getMenuOrTimelinePos();
2729     }
2730     if (trackId == -1) {
2731         trackId = m_activeTrack;
2732     }
2733     bool res = TimelineFunctions::requestDeleteAllBlanksFrom(m_model, trackId, frame);
2734     if (!res) {
2735         pCore->displayMessage(i18n("Cannot remove all spaces"), ErrorMessage, 500);
2736     }
2737 }
2738 
2739 void TimelineController::removeTrackClips(int trackId, int frame)
2740 {
2741     if (frame == -1) {
2742         frame = getMenuOrTimelinePos();
2743     }
2744     if (trackId == -1) {
2745         trackId = m_activeTrack;
2746     }
2747     bool res = TimelineFunctions::requestDeleteAllClipsFrom(m_model, trackId, frame);
2748     if (!res) {
2749         pCore->displayMessage(i18n("Cannot remove all clips"), ErrorMessage, 500);
2750     }
2751 }
2752 
2753 void TimelineController::invalidateItem(int cid)
2754 {
2755     if (!m_model->hasTimelinePreview() || !m_model->isItem(cid)) {
2756         return;
2757     }
2758     const int tid = m_model->getItemTrackId(cid);
2759     if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) {
2760         return;
2761     }
2762     int start = m_model->getItemPosition(cid);
2763     int end = start + m_model->getItemPlaytime(cid);
2764     m_model->previewManager()->invalidatePreview(start, end);
2765 }
2766 
2767 void TimelineController::invalidateTrack(int tid)
2768 {
2769     if (!m_model->hasTimelinePreview() || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) {
2770         return;
2771     }
2772     for (const auto &clp : m_model->getTrackById_const(tid)->m_allClips) {
2773         invalidateItem(clp.first);
2774     }
2775 }
2776 
2777 void TimelineController::remapItemTime(int clipId)
2778 {
2779     if (clipId == -1) {
2780         clipId = getMainSelectedClip();
2781     }
2782     // Don't allow remaping a clip with speed effect
2783     if (clipId == -1 || !m_model->isClip(clipId) || !qFuzzyCompare(1., m_model->m_allClips[clipId]->getSpeed())) {
2784         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2785         return;
2786     }
2787     ClipType::ProducerType type = m_model->m_allClips[clipId]->clipType();
2788     if (type == ClipType::Color || type == ClipType::Image) {
2789         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2790         return;
2791     }
2792     if (m_model->m_allClips[clipId]->hasTimeRemap()) {
2793         // Remove remap effect
2794         m_model->requestClipTimeRemap(clipId, false);
2795         Q_EMIT pCore->remapClip(-1);
2796     } else {
2797         // Add remap effect
2798         Q_EMIT pCore->remapClip(clipId);
2799     }
2800 }
2801 
2802 void TimelineController::changeItemSpeed(int clipId, double speed)
2803 {
2804     /*if (clipId == -1) {
2805         clipId = getMainSelectedItem(false, true);
2806     }*/
2807     if (clipId == -1) {
2808         clipId = getMainSelectedClip();
2809     }
2810     if (clipId == -1) {
2811         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2812         return;
2813     }
2814     bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
2815     if (qFuzzyCompare(speed, -1)) {
2816         speed = 100 * m_model->getClipSpeed(clipId);
2817         int duration = m_model->getItemPlaytime(clipId);
2818         // this is the max speed so that the clip is at least one frame long
2819         double maxSpeed = duration * qAbs(speed);
2820         // this is the min speed so that the clip doesn't bump into the next one on track
2821         double minSpeed = duration * qAbs(speed) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)));
2822 
2823         // if there is a split partner, we must also take it into account
2824         int partner = m_model->getClipSplitPartner(clipId);
2825         if (partner != -1) {
2826             double duration2 = m_model->getItemPlaytime(partner);
2827             double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
2828             double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)));
2829             minSpeed = std::max(minSpeed, minSpeed2);
2830             maxSpeed = std::min(maxSpeed, maxSpeed2);
2831         }
2832         std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId));
2833         QScopedPointer<SpeedDialog> d(
2834             new SpeedDialog(QApplication::activeWindow(), std::abs(speed), duration, minSpeed, maxSpeed, speed < 0, pitchCompensate, binClip->clipType()));
2835         if (d->exec() != QDialog::Accepted) {
2836             Q_EMIT regainFocus();
2837             return;
2838         }
2839         Q_EMIT regainFocus();
2840         speed = d->getValue();
2841         pitchCompensate = d->getPitchCompensate();
2842         qDebug() << "requesting speed " << speed;
2843     }
2844     bool res = m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true);
2845     if (res) {
2846         updateClipActions();
2847     }
2848 }
2849 
2850 void TimelineController::switchCompositing(bool enable)
2851 {
2852     // m_model->m_tractor->lock();
2853     pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(enable));
2854     QScopedPointer<Mlt::Service> service(m_model->m_tractor->field());
2855     QScopedPointer<Mlt::Field> field(m_model->m_tractor->field());
2856     field->lock();
2857     while ((service != nullptr) && service->is_valid()) {
2858         if (service->type() == mlt_service_transition_type) {
2859             Mlt::Transition t(mlt_transition(service->get_service()));
2860             service.reset(service->producer());
2861             QString serviceName = t.get("mlt_service");
2862             if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
2863                 // remove all compositing transitions
2864                 field->disconnect_service(t);
2865                 t.disconnect_all_producers();
2866             }
2867         } else {
2868             service.reset(service->producer());
2869         }
2870     }
2871     if (enable) {
2872         // Loop through tracks
2873         for (int track = 0; track < m_model->getTracksCount(); track++) {
2874             if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
2875                 // This is a video track
2876                 Mlt::Transition t(pCore->getProjectProfile(), TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
2877                 t.set("always_active", 1);
2878                 t.set_tracks(0, track + 1);
2879                 t.set("internal_added", 237);
2880                 field->plant_transition(t, 0, track + 1);
2881             }
2882         }
2883     }
2884     field->unlock();
2885     pCore->refreshProjectMonitorOnce();
2886 }
2887 
2888 void TimelineController::extractZone(QPoint zone, bool liftOnly)
2889 {
2890     QVector<int> tracks;
2891     auto it = m_model->m_allTracks.cbegin();
2892     while (it != m_model->m_allTracks.cend()) {
2893         int target_track = (*it)->getId();
2894         if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
2895             tracks << target_track;
2896         }
2897         ++it;
2898     }
2899     if (tracks.isEmpty()) {
2900         pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
2901     }
2902     if (m_zone.isNull()) {
2903         // Use current timeline position and clip zone length
2904         zone.setY(pCore->getMonitorPosition() + zone.y() - zone.x());
2905         zone.setX(pCore->getMonitorPosition());
2906     }
2907     TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
2908     if (!liftOnly && !m_zone.isNull()) {
2909         setPosition(m_zone.x());
2910     }
2911 }
2912 
2913 void TimelineController::extract(int clipId, bool singleSelectionMode)
2914 {
2915     if (clipId == -1) {
2916         std::unordered_set<int> sel = m_model->getCurrentSelection();
2917         for (int i : sel) {
2918             if (m_model->isClip(i)) {
2919                 clipId = i;
2920                 break;
2921             }
2922         }
2923         if (clipId == -1) {
2924             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2925             return;
2926         }
2927     }
2928     int in = m_model->getClipPosition(clipId);
2929     int out = in + m_model->getClipPlaytime(clipId);
2930     int tid = m_model->getClipTrackId(clipId);
2931     std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(clipId);
2932     if (mixData.first.firstClipId > -1) {
2933         // Clip has a start mix, adjust in point
2934         in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
2935     }
2936     if (mixData.second.firstClipId > -1) {
2937         // Clip has end mix, adjust out point
2938         out -= mixData.second.mixOffset;
2939     }
2940     QVector<int> tracks = {tid};
2941     int clipToUngroup = -1;
2942     std::unordered_set<int> clipsToRegroup;
2943     if (m_model->m_groups->isInGroup(clipId)) {
2944         if (singleSelectionMode) {
2945             // Remove item from group
2946             clipsToRegroup = m_model->m_groups->getLeaves(m_model->m_groups->getRootId(clipId));
2947             clipToUngroup = clipId;
2948             clipsToRegroup.erase(clipToUngroup);
2949             m_model->requestClearSelection();
2950         } else {
2951             int targetRoot = m_model->m_groups->getRootId(clipId);
2952             if (m_model->isGroup(targetRoot)) {
2953                 std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
2954                 for (int current_id : sub) {
2955                     if (current_id == clipId) {
2956                         continue;
2957                     }
2958                     if (m_model->isClip(current_id)) {
2959                         int newIn = m_model->getClipPosition(current_id);
2960                         int newOut = newIn + m_model->getClipPlaytime(current_id);
2961                         int tk = m_model->getClipTrackId(current_id);
2962                         std::pair<MixInfo, MixInfo> cMixData = m_model->getTrackById_const(tk)->getMixInfo(current_id);
2963                         if (cMixData.first.firstClipId > -1) {
2964                             // Clip has a start mix, adjust in point
2965                             newIn += (cMixData.first.firstClipInOut.second - cMixData.first.secondClipInOut.first - cMixData.first.mixOffset);
2966                         }
2967                         if (cMixData.second.firstClipId > -1) {
2968                             // Clip has end mix, adjust out point
2969                             newOut -= cMixData.second.mixOffset;
2970                         }
2971                         in = qMin(in, newIn);
2972                         out = qMax(out, newOut);
2973                         if (!tracks.contains(tk)) {
2974                             tracks << tk;
2975                         }
2976                     }
2977                 }
2978             }
2979         }
2980     }
2981     TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false, clipToUngroup, clipsToRegroup);
2982 }
2983 
2984 void TimelineController::saveZone(int clipId)
2985 {
2986     if (clipId == -1) {
2987         clipId = getMainSelectedClip();
2988         if (clipId == -1) {
2989             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2990             return;
2991         }
2992     }
2993     int in = m_model->getClipIn(clipId);
2994     int out = in + m_model->getClipPlaytime(clipId) - 1;
2995     QString id;
2996     pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId());
2997 }
2998 
2999 bool TimelineController::insertClipZone(const QString &binId, int tid, int position)
3000 {
3001     QStringList binIdData = binId.split(QLatin1Char('/'));
3002     int in = 0;
3003     int out = -1;
3004     if (binIdData.size() >= 3) {
3005         in = binIdData.at(1).toInt();
3006         out = binIdData.at(2).toInt();
3007     }
3008 
3009     QString bid = binIdData.first();
3010     // dropType indicates if we want a normal drop (disabled), audio only or video only drop
3011     PlaylistState::ClipState dropType = PlaylistState::Disabled;
3012     if (bid.startsWith(QLatin1Char('A'))) {
3013         dropType = PlaylistState::AudioOnly;
3014         bid = bid.remove(0, 1);
3015     } else if (bid.startsWith(QLatin1Char('V'))) {
3016         dropType = PlaylistState::VideoOnly;
3017         bid = bid.remove(0, 1);
3018     }
3019     QList<int> audioTracks;
3020     int vTrack = -1;
3021     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(bid);
3022     if (out <= in) {
3023         out = int(clip->frameDuration() - 1);
3024     }
3025     QList<int> audioStreams = m_model->m_binAudioTargets.keys();
3026     if (dropType == PlaylistState::VideoOnly) {
3027         vTrack = tid;
3028     } else if (dropType == PlaylistState::AudioOnly) {
3029         audioTracks << tid;
3030         if (audioStreams.size() > 1) {
3031             // insert the other audio streams
3032             QList<int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
3033             while (audioStreams.size() > 1 && !lower.isEmpty()) {
3034                 audioTracks << lower.takeFirst();
3035                 audioStreams.takeFirst();
3036             }
3037         }
3038     } else {
3039         if (m_model->getTrackById_const(tid)->isAudioTrack()) {
3040             audioTracks << tid;
3041             if (audioStreams.size() > 1) {
3042                 // insert the other audio streams
3043                 QList<int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
3044                 while (audioStreams.size() > 1 && !lower.isEmpty()) {
3045                     audioTracks << lower.takeFirst();
3046                     audioStreams.takeFirst();
3047                 }
3048             }
3049             vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(tid) : -1;
3050         } else {
3051             vTrack = tid;
3052             if (clip->hasAudioAndVideo()) {
3053                 int firstAudio = m_model->getMirrorAudioTrackId(vTrack);
3054                 audioTracks << firstAudio;
3055                 if (audioStreams.size() > 1) {
3056                     // insert the other audio streams
3057                     QList<int> lower = m_model->getLowerTracksId(firstAudio, TrackType::AudioTrack);
3058                     while (audioStreams.size() > 1 && !lower.isEmpty()) {
3059                         audioTracks << lower.takeFirst();
3060                         audioStreams.takeFirst();
3061                     }
3062                 }
3063             }
3064         }
3065     }
3066     QList<int> target_tracks;
3067     if (vTrack > -1) {
3068         target_tracks << vTrack;
3069     }
3070     if (!audioTracks.isEmpty()) {
3071         target_tracks << audioTracks;
3072     }
3073     qDebug() << "=====================\n\nREADY TO INSERT IN TRACKS: " << audioTracks << " / VIDEO: " << vTrack << "\n\n=========";
3074     std::function<bool(void)> undo = []() { return true; };
3075     std::function<bool(void)> redo = []() { return true; };
3076     bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit;
3077     QPoint zone(in, out + 1);
3078     bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo);
3079     if (res) {
3080         int newPos = position + (zone.y() - zone.x());
3081         int currentPos = pCore->getMonitorPosition();
3082         Fun redoPos = [this, newPos]() {
3083             Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
3084             pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
3085             pCore->monitorManager()->refreshProjectMonitor();
3086             setPosition(newPos);
3087             pCore->monitorManager()->activateMonitor(activeMonitor);
3088             return true;
3089         };
3090         Fun undoPos = [this, currentPos]() {
3091             Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
3092             pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
3093             pCore->monitorManager()->refreshProjectMonitor();
3094             setPosition(currentPos);
3095             pCore->monitorManager()->activateMonitor(activeMonitor);
3096             return true;
3097         };
3098         redoPos();
3099         UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
3100         pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
3101     } else {
3102         pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
3103         undo();
3104     }
3105     return res;
3106 }
3107 
3108 int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite, Fun &undo, Fun &redo)
3109 {
3110     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
3111     int aTrack = -1;
3112     int vTrack = -1;
3113     if (clip->hasAudio() && !m_model->m_audioTarget.isEmpty()) {
3114         QList<int> audioTracks = m_model->m_audioTarget.keys();
3115         for (int tid : qAsConst(audioTracks)) {
3116             if (m_model->getTrackById_const(tid)->shouldReceiveTimelineOp()) {
3117                 aTrack = tid;
3118                 break;
3119             }
3120         }
3121     }
3122     if (clip->hasVideo()) {
3123         vTrack = videoTarget();
3124     }
3125 
3126     int insertPoint;
3127     QPoint sourceZone;
3128     if (useRuler() && m_zone != QPoint()) {
3129         // We want to use timeline zone for in/out insert points
3130         insertPoint = m_zone.x();
3131         sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
3132     } else {
3133         // Use current timeline pos and clip zone for in/out
3134         insertPoint = pCore->getMonitorPosition();
3135         sourceZone = zone;
3136     }
3137     QList<int> target_tracks;
3138     if (vTrack > -1) {
3139         target_tracks << vTrack;
3140     }
3141     if (aTrack > -1) {
3142         target_tracks << aTrack;
3143     }
3144     if (target_tracks.isEmpty()) {
3145         pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), ErrorMessage);
3146         return -1;
3147     }
3148     bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo);
3149     if (res) {
3150         int newPos = insertPoint + (sourceZone.y() - sourceZone.x());
3151         int currentPos = pCore->getMonitorPosition();
3152         Fun redoPos = [this, newPos]() {
3153             setPosition(newPos);
3154             pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
3155             return true;
3156         };
3157         Fun undoPos = [this, currentPos]() {
3158             setPosition(currentPos);
3159             pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
3160             return true;
3161         };
3162         redoPos();
3163         UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
3164     }
3165     return res;
3166 }
3167 
3168 void TimelineController::updateClip(int clipId, const QVector<int> &roles)
3169 {
3170     QModelIndex ix = m_model->makeClipIndexFromID(clipId);
3171     if (ix.isValid()) {
3172         Q_EMIT m_model->dataChanged(ix, ix, roles);
3173     }
3174 }
3175 
3176 void TimelineController::showClipKeyframes(int clipId, bool value)
3177 {
3178     TimelineFunctions::showClipKeyframes(m_model, clipId, value);
3179 }
3180 
3181 void TimelineController::showCompositionKeyframes(int clipId, bool value)
3182 {
3183     TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
3184 }
3185 
3186 void TimelineController::switchEnableState(std::unordered_set<int> selection)
3187 {
3188     if (selection.empty()) {
3189         selection = m_model->getCurrentSelection();
3190         // clipId = getMainSelectedItem(false, false);
3191     }
3192     if (selection.empty()) {
3193         return;
3194     }
3195     TimelineFunctions::switchEnableState(m_model, selection);
3196 }
3197 
3198 void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
3199 {
3200     if (clipId == -1) {
3201         clipId = getMainSelectedClip();
3202         if (clipId == -1) {
3203             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3204             return;
3205         }
3206     }
3207     if (offset == -1) {
3208         offset = m_root->property("clickFrame").toInt();
3209     }
3210     int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack;
3211     int compoId = -1;
3212     if (assetId.isEmpty()) {
3213         QStringList compositions = KdenliveSettings::favorite_transitions();
3214         if (compositions.isEmpty()) {
3215             pCore->displayMessage(i18n("Select a favorite composition"), ErrorMessage, 500);
3216             return;
3217         }
3218         compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
3219     } else {
3220         compoId = insertNewComposition(track, clipId, offset, assetId, true);
3221     }
3222     if (compoId > 0) {
3223         m_model->requestSetSelection({compoId});
3224     }
3225 }
3226 
3227 void TimelineController::setEffectsEnabled(int clipId, bool enabled)
3228 {
3229     std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId);
3230     clip->setTimelineEffectsEnabled(enabled);
3231 }
3232 
3233 void TimelineController::addEffectToClip(const QString &assetId, int clipId)
3234 {
3235     if (clipId == -1) {
3236         clipId = getMainSelectedClip();
3237         if (clipId == -1) {
3238             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3239             return;
3240         }
3241     }
3242     if (m_model->addClipEffect(clipId, assetId).size() > 0 && KdenliveSettings::seekonaddeffect()) {
3243         // Move timeline cursor inside clip if it is not
3244         int in = m_model->getClipPosition(clipId);
3245         int out = in + m_model->getClipPlaytime(clipId);
3246         int position = pCore->getMonitorPosition();
3247         if (position < in || position > out) {
3248             Q_EMIT seeked(in);
3249         }
3250     }
3251 }
3252 
3253 bool TimelineController::splitAV()
3254 {
3255     int cid = *m_model->getCurrentSelection().begin();
3256     if (m_model->isClip(cid)) {
3257         std::shared_ptr<ClipModel> clip = m_model->getClipPtr(cid);
3258         if (clip->clipState() == PlaylistState::AudioOnly) {
3259             return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
3260         } else {
3261             QVariantList aTargets = audioTarget();
3262             int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3263             return TimelineFunctions::requestSplitAudio(m_model, cid, targetTrack);
3264         }
3265     }
3266     pCore->displayMessage(i18n("No clip found to perform AV split operation"), ErrorMessage, 500);
3267     return false;
3268 }
3269 
3270 void TimelineController::splitAudio(int clipId)
3271 {
3272     QVariantList aTargets = audioTarget();
3273     int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3274     TimelineFunctions::requestSplitAudio(m_model, clipId, targetTrack);
3275 }
3276 
3277 void TimelineController::splitVideo(int clipId)
3278 {
3279     TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
3280 }
3281 
3282 void TimelineController::setAudioRef(int clipId)
3283 {
3284     if (clipId == -1) {
3285         clipId = getMainSelectedClip();
3286         if (clipId == -1) {
3287             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3288             return;
3289         }
3290     }
3291     m_audioRef = clipId;
3292     std::unique_ptr<AudioEnvelope> envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
3293     m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
3294     connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, this, [&](int cid, int shift) {
3295         // Ensure the clip was not deleted while processing calculations
3296         if (m_model->isClip(cid)) {
3297             int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef);
3298             bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true);
3299             if (!result) {
3300                 pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), ErrorMessage, 500);
3301             }
3302         } else {
3303             // Clip was deleted, discard audio reference
3304             m_audioRef = -1;
3305         }
3306     });
3307     connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
3308 }
3309 
3310 void TimelineController::alignAudio(int clipId)
3311 {
3312     // find other clip
3313     if (clipId == -1) {
3314         clipId = getMainSelectedClip();
3315         if (clipId == -1) {
3316             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3317             return;
3318         }
3319     }
3320     if (m_audioRef == -1 || m_audioRef == clipId || !m_model->isClip(m_audioRef)) {
3321         pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
3322         return;
3323     }
3324     const QString masterBinClipId = getClipBinId(m_audioRef);
3325     std::unordered_set<int> clipsToAnalyse;
3326     if (m_model->m_groups->isInGroup(clipId)) {
3327         clipsToAnalyse = m_model->getGroupElements(clipId);
3328         m_model->requestClearSelection();
3329     } else {
3330         clipsToAnalyse.insert(clipId);
3331     }
3332     QList<int> processedGroups;
3333     int processed = 0;
3334     for (int cid : clipsToAnalyse) {
3335         if (!m_model->isClip(cid) || cid == m_audioRef) {
3336             continue;
3337         }
3338         const QString otherBinId = getClipBinId(cid);
3339         if (m_model->m_groups->isInGroup(cid)) {
3340             int parentGroup = m_model->m_groups->getRootId(cid);
3341             if (processedGroups.contains(parentGroup)) {
3342                 continue;
3343             }
3344             // Only process one clip from the group
3345             std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(otherBinId);
3346             if (clip->hasAudio()) {
3347                 processedGroups << parentGroup;
3348             } else {
3349                 continue;
3350             }
3351         }
3352         if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) {
3353             // Cannot process non audi clips
3354             continue;
3355         }
3356         if (otherBinId == masterBinClipId) {
3357             // easy, same clip.
3358             int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid);
3359             if (newPos) {
3360                 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true);
3361                 processed++;
3362                 if (!result) {
3363                     pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), ErrorMessage, 500);
3364                 }
3365                 continue;
3366             }
3367         }
3368         processed++;
3369         // Perform audio calculation
3370         auto *envelope =
3371             new AudioEnvelope(otherBinId, cid, size_t(m_model->getClipIn(cid)), size_t(m_model->getClipPlaytime(cid)), size_t(m_model->getClipPosition(cid)));
3372         m_audioCorrelator->addChild(envelope);
3373     }
3374     if (processed == 0) {
3375         // TODO: improve feedback message after freeze
3376         pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
3377     }
3378 }
3379 
3380 void TimelineController::switchTrackActive(int trackId)
3381 {
3382     if (trackId == -1) {
3383         trackId = m_activeTrack;
3384     }
3385     if (trackId < 0) {
3386         return;
3387     }
3388     bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
3389     m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3390     m_activeSnaps.clear();
3391 }
3392 
3393 void TimelineController::switchAllTrackActive()
3394 {
3395     auto it = m_model->m_allTracks.cbegin();
3396     while (it != m_model->m_allTracks.cend()) {
3397         bool active = (*it)->isTimelineActive();
3398         int target_track = (*it)->getId();
3399         m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3400         ++it;
3401     }
3402     m_activeSnaps.clear();
3403 }
3404 
3405 void TimelineController::makeAllTrackActive()
3406 {
3407     // Check current status
3408     auto it = m_model->m_allTracks.cbegin();
3409     bool makeActive = false;
3410     while (it != m_model->m_allTracks.cend()) {
3411         if (!(*it)->isTimelineActive()) {
3412             // There is an inactive track, activate all
3413             makeActive = true;
3414             break;
3415         }
3416         ++it;
3417     }
3418     it = m_model->m_allTracks.cbegin();
3419     while (it != m_model->m_allTracks.cend()) {
3420         int target_track = (*it)->getId();
3421         m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0"));
3422         ++it;
3423     }
3424     m_activeSnaps.clear();
3425 }
3426 
3427 void TimelineController::switchTrackDisabled()
3428 {
3429 
3430     if (m_model->isSubtitleTrack(m_activeTrack)) {
3431         // Subtitle track
3432         switchSubtitleDisable();
3433     } else {
3434         bool isAudio = m_model->getTrackById_const(m_activeTrack)->isAudioTrack();
3435         bool enabled = isAudio ? m_model->getTrackById_const(m_activeTrack)->isMute() : m_model->getTrackById_const(m_activeTrack)->isHidden();
3436         hideTrack(m_activeTrack, enabled);
3437     }
3438 }
3439 
3440 void TimelineController::switchTrackLock(bool applyToAll)
3441 {
3442     if (!applyToAll) {
3443         // apply to active track only
3444         if (m_model->isSubtitleTrack(m_activeTrack)) {
3445             // Subtitle track
3446             switchSubtitleLock();
3447         } else {
3448             bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
3449             m_model->setTrackLockedState(m_activeTrack, !locked);
3450         }
3451     } else {
3452         // Invert track lock
3453         const auto ids = m_model->getAllTracksIds();
3454         // count the number of tracks to be locked
3455         for (const int id : ids) {
3456             bool isLocked = m_model->getTrackById_const(id)->isLocked();
3457             m_model->setTrackLockedState(id, !isLocked);
3458         }
3459         if (m_model->hasSubtitleModel()) {
3460             switchSubtitleLock();
3461         }
3462     }
3463 }
3464 
3465 void TimelineController::switchTargetTrack()
3466 {
3467     if (m_activeTrack < 0) {
3468         return;
3469     }
3470     bool isAudio = m_model->isAudioTrack(m_activeTrack);
3471     if (isAudio) {
3472         QMap<int, int> current = m_model->m_audioTarget;
3473         if (current.contains(m_activeTrack)) {
3474             current.remove(m_activeTrack);
3475         } else {
3476             int ix = getFirstUnassignedStream();
3477             if (ix > -1) {
3478                 current.insert(m_activeTrack, ix);
3479             } else if (current.size() == 1) {
3480                 // If we only have one video stream, directly reassign it
3481                 int stream = current.first();
3482                 current.clear();
3483                 current.insert(m_activeTrack, stream);
3484             } else {
3485                 pCore->displayMessage(i18n("All streams already assigned, deselect another audio target first"), InformationMessage, 500);
3486                 return;
3487             }
3488         }
3489         setAudioTarget(current);
3490     } else {
3491         setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
3492     }
3493 }
3494 
3495 QVariantList TimelineController::audioTarget() const
3496 {
3497     QVariantList audioTracks;
3498     QMapIterator<int, int> i(m_model->m_audioTarget);
3499     while (i.hasNext()) {
3500         i.next();
3501         audioTracks << i.key();
3502     }
3503     return audioTracks;
3504 }
3505 
3506 QVariantList TimelineController::lastAudioTarget() const
3507 {
3508     QVariantList audioTracks;
3509     QMapIterator<int, int> i(m_lastAudioTarget);
3510     while (i.hasNext()) {
3511         i.next();
3512         audioTracks << i.key();
3513     }
3514     return audioTracks;
3515 }
3516 
3517 const QString TimelineController::audioTargetName(int tid) const
3518 {
3519     if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) {
3520         int streamIndex = m_model->m_audioTarget.value(tid);
3521         if (m_model->m_binAudioTargets.contains(streamIndex)) {
3522             QString targetName = m_model->m_binAudioTargets.value(streamIndex);
3523             return targetName.isEmpty() ? QChar('x') : targetName.section(QLatin1Char('|'), 0, 0);
3524         } else {
3525             qDebug() << "STREAM INDEX NOT IN TARGET : " << streamIndex << " = " << m_model->m_binAudioTargets;
3526         }
3527     } else {
3528         qDebug() << "TRACK NOT IN TARGET : " << tid << " = " << m_model->m_audioTarget.keys();
3529     }
3530     return QString();
3531 }
3532 
3533 int TimelineController::videoTarget() const
3534 {
3535     return m_model->m_videoTarget;
3536 }
3537 
3538 int TimelineController::hasAudioTarget() const
3539 {
3540     return m_hasAudioTarget;
3541 }
3542 
3543 int TimelineController::clipTargets() const
3544 {
3545     return m_model->m_binAudioTargets.size();
3546 }
3547 
3548 bool TimelineController::hasVideoTarget() const
3549 {
3550     return m_hasVideoTarget;
3551 }
3552 
3553 bool TimelineController::autoScroll() const
3554 {
3555     return !pCore->monitorManager()->projectMonitor()->isPlaying() || KdenliveSettings::autoscroll();
3556 }
3557 
3558 void TimelineController::resetTrackHeight()
3559 {
3560     int tracksCount = m_model->getTracksCount();
3561     for (int track = tracksCount - 1; track >= 0; track--) {
3562         int trackIx = m_model->getTrackIndexFromPosition(track);
3563         m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
3564     }
3565     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
3566     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
3567     Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
3568 }
3569 
3570 void TimelineController::selectAll()
3571 {
3572     std::unordered_set<int> ids;
3573     for (const auto &clp : m_model->m_allClips) {
3574         ids.insert(clp.first);
3575     }
3576     for (const auto &clp : m_model->m_allCompositions) {
3577         ids.insert(clp.first);
3578     }
3579     // Subtitles
3580     for (const auto &sub : m_model->m_allSubtitles) {
3581         ids.insert(sub.first);
3582     }
3583     m_model->requestSetSelection(ids);
3584 }
3585 
3586 void TimelineController::selectCurrentTrack()
3587 {
3588     if (m_activeTrack == -1) {
3589         return;
3590     }
3591     std::unordered_set<int> ids;
3592     if (m_model->isSubtitleTrack(m_activeTrack)) {
3593         for (const auto &sub : m_model->m_allSubtitles) {
3594             ids.insert(sub.first);
3595         }
3596     } else {
3597         for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
3598             ids.insert(clp.first);
3599         }
3600         for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
3601             ids.insert(clp.first);
3602         }
3603     }
3604     m_model->requestSetSelection(ids);
3605 }
3606 
3607 void TimelineController::deleteEffects(int targetId)
3608 {
3609     std::unordered_set<int> targetIds;
3610     std::unordered_set<int> sel;
3611     if (targetId == -1) {
3612         sel = m_model->getCurrentSelection();
3613     } else {
3614         if (m_model->m_groups->isInGroup(targetId)) {
3615             sel = {m_model->m_groups->getRootId(targetId)};
3616         } else {
3617             sel = {targetId};
3618         }
3619     }
3620     if (sel.empty()) {
3621         pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
3622     }
3623     for (int s : sel) {
3624         if (m_model->isGroup(s)) {
3625             std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3626             for (int current_id : sub) {
3627                 if (m_model->isClip(current_id)) {
3628                     targetIds.insert(current_id);
3629                 }
3630             }
3631         } else if (m_model->isClip(s)) {
3632             targetIds.insert(s);
3633         }
3634     }
3635     if (targetIds.empty()) {
3636         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3637     }
3638     std::function<bool(void)> undo = []() { return true; };
3639     std::function<bool(void)> redo = []() { return true; };
3640     for (int target : targetIds) {
3641         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3642         destStack->removeAllEffects(undo, redo);
3643     }
3644     pCore->pushUndo(undo, redo, i18n("Delete effects"));
3645 }
3646 
3647 void TimelineController::pasteEffects(int targetId)
3648 {
3649     std::unordered_set<int> targetIds;
3650     std::unordered_set<int> sel;
3651     if (targetId == -1) {
3652         sel = m_model->getCurrentSelection();
3653     } else {
3654         if (m_model->m_groups->isInGroup(targetId)) {
3655             sel = {m_model->m_groups->getRootId(targetId)};
3656         } else {
3657             sel = {targetId};
3658         }
3659     }
3660     if (sel.empty()) {
3661         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3662     }
3663     for (int s : sel) {
3664         if (m_model->isGroup(s)) {
3665             std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3666             for (int current_id : sub) {
3667                 if (m_model->isClip(current_id)) {
3668                     targetIds.insert(current_id);
3669                 }
3670             }
3671         } else if (m_model->isClip(s)) {
3672             targetIds.insert(s);
3673         }
3674     }
3675     if (targetIds.empty()) {
3676         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3677     }
3678 
3679     QClipboard *clipboard = QApplication::clipboard();
3680     QString txt = clipboard->text();
3681     if (txt.isEmpty()) {
3682         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3683         return;
3684     }
3685     QDomDocument copiedItems;
3686     copiedItems.setContent(txt);
3687     if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
3688         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3689         return;
3690     }
3691     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
3692     if (clips.isEmpty()) {
3693         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3694         return;
3695     }
3696     std::function<bool(void)> undo = []() { return true; };
3697     std::function<bool(void)> redo = []() { return true; };
3698     QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
3699     effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in")));
3700     for (int i = 1; i < clips.size(); i++) {
3701         QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects"));
3702         QDomNodeList subs = subeffects.childNodes();
3703         while (!subs.isEmpty()) {
3704             subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
3705             effects.appendChild(subs.at(0));
3706         }
3707     }
3708     int insertedEffects = 0;
3709     for (int target : targetIds) {
3710         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3711         if (destStack->fromXml(effects, undo, redo)) {
3712             insertedEffects++;
3713         }
3714     }
3715     if (insertedEffects > 0) {
3716         pCore->pushUndo(undo, redo, i18n("Paste effects"));
3717     } else {
3718         pCore->displayMessage(i18n("Cannot paste effect on selected clip"), ErrorMessage, 500);
3719         undo();
3720     }
3721 }
3722 
3723 double TimelineController::fps() const
3724 {
3725     return pCore->getCurrentFps();
3726 }
3727 
3728 void TimelineController::editItemDuration(int id)
3729 {
3730     if (id == -1) {
3731         id = m_root->property("mainItemId").toInt();
3732         if (id == -1) {
3733             std::unordered_set<int> sel = m_model->getCurrentSelection();
3734             if (!sel.empty()) {
3735                 id = *sel.begin();
3736             }
3737             if (id == -1) {
3738                 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3739                 return;
3740             }
3741         }
3742     }
3743     if (id == -1 || !m_model->isItem(id)) {
3744         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3745         return;
3746     }
3747     int start = m_model->getItemPosition(id);
3748     int in = 0;
3749     int duration = m_model->getItemPlaytime(id);
3750     int maxLength = -1;
3751     bool isComposition = false;
3752     if (m_model->isClip(id)) {
3753         in = m_model->getClipIn(id);
3754         std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(id));
3755         if (clip && clip->hasLimitedDuration()) {
3756             maxLength = clip->getProducerDuration();
3757         }
3758     } else if (m_model->isComposition(id)) {
3759         // nothing to do
3760         isComposition = true;
3761     } else {
3762         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3763         return;
3764     }
3765     int trackId = m_model->getItemTrackId(id);
3766     int maxFrame = qMax(0, start + duration +
3767                                (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
3768                                               : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
3769     int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
3770                                                : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
3771     int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
3772     QScopedPointer<ClipDurationDialog> dialog(new ClipDurationDialog(id, start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()));
3773     if (dialog->exec() == QDialog::Accepted) {
3774         std::function<bool(void)> undo = []() { return true; };
3775         std::function<bool(void)> redo = []() { return true; };
3776         int newPos = dialog->startPos().frames(pCore->getCurrentFps());
3777         int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
3778         int newDuration = dialog->duration().frames(pCore->getCurrentFps());
3779         bool result = true;
3780         if (newPos < start) {
3781             if (!isComposition) {
3782                 result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3783                 if (result && partner > -1) {
3784                     result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3785                 }
3786             } else {
3787                 result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3788             }
3789             if (result && newIn != in) {
3790                 int updatedDuration = duration + (in - newIn);
3791                 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3792                 if (result && partner > -1) {
3793                     result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3794                 }
3795             }
3796             if (newDuration != duration + (in - newIn)) {
3797                 result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
3798                 if (result && partner > -1) {
3799                     result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
3800                 }
3801             }
3802         } else {
3803             // perform resize first
3804             if (newIn != in) {
3805                 int updatedDuration = duration + (in - newIn);
3806                 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3807                 if (result && partner > -1) {
3808                     result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3809                 }
3810             }
3811             if (newDuration != duration + (in - newIn)) {
3812                 result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
3813                 if (result && partner > -1) {
3814                     result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
3815                 }
3816             }
3817             if (start != newPos || newIn != in) {
3818                 if (!isComposition) {
3819                     result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3820                     if (result && partner > -1) {
3821                         result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3822                     }
3823                 } else {
3824                     result = result &&
3825                              m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3826                 }
3827             }
3828         }
3829         if (result) {
3830             pCore->pushUndo(undo, redo, i18n("Edit item"));
3831         } else {
3832             undo();
3833         }
3834     }
3835     Q_EMIT regainFocus();
3836 }
3837 
3838 void TimelineController::focusTimelineSequence(int id)
3839 {
3840     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id));
3841     if (binClip) {
3842         const QUuid uuid = binClip->getSequenceUuid();
3843         int sequencePos = pCore->getMonitorPosition();
3844         sequencePos -= m_model->getClipPosition(id);
3845         if (sequencePos < 0 || sequencePos > m_model->getClipPlaytime(id)) {
3846             sequencePos = -1;
3847         } else {
3848             sequencePos += m_model->getClipIn(id);
3849         }
3850         Fun local_redo = [uuid, binId = binClip->binId(), sequencePos]() { return pCore->projectManager()->openTimeline(binId, uuid, sequencePos); };
3851         if (local_redo()) {
3852             Fun local_undo = [uuid]() {
3853                 if (pCore->projectManager()->closeTimeline(uuid)) {
3854                     pCore->window()->closeTimelineTab(uuid);
3855                 }
3856                 return true;
3857             };
3858             pCore->pushUndo(local_undo, local_redo, i18n("Open sequence"));
3859         }
3860     }
3861 }
3862 
3863 void TimelineController::editTitleClip(int id)
3864 {
3865     if (id == -1) {
3866         id = m_root->property("mainItemId").toInt();
3867         if (id == -1) {
3868             std::unordered_set<int> sel = m_model->getCurrentSelection();
3869             if (!sel.empty()) {
3870                 id = *sel.begin();
3871             }
3872             if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) {
3873                 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3874                 return;
3875             }
3876         }
3877     }
3878     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id));
3879     if (binClip->clipType() != ClipType::Text && binClip->clipType() != ClipType::TextTemplate) {
3880         pCore->displayMessage(i18n("Item is not a title clip"), ErrorMessage, 500);
3881         return;
3882     }
3883     seekToMouse();
3884     pCore->bin()->showTitleWidget(binClip);
3885 }
3886 
3887 void TimelineController::editAnimationClip(int id)
3888 {
3889     if (id == -1) {
3890         id = m_root->property("mainItemId").toInt();
3891         if (id == -1) {
3892             std::unordered_set<int> sel = m_model->getCurrentSelection();
3893             if (!sel.empty()) {
3894                 id = *sel.begin();
3895             }
3896             if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) {
3897                 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3898                 return;
3899             }
3900         }
3901     }
3902     GlaxnimateLauncher::instance().openClip(id);
3903 }
3904 
3905 QPoint TimelineController::selectionInOut() const
3906 {
3907     std::unordered_set<int> ids = m_model->getCurrentSelection();
3908     std::unordered_set<int> items_list;
3909     for (int i : ids) {
3910         if (m_model->isGroup(i)) {
3911             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
3912             items_list.insert(children.begin(), children.end());
3913         } else {
3914             items_list.insert(i);
3915         }
3916     }
3917     int in = -1;
3918     int out = -1;
3919     for (int id : items_list) {
3920         if (m_model->isClip(id) || m_model->isComposition(id)) {
3921             int itemIn = m_model->getItemPosition(id);
3922             int itemOut = itemIn + m_model->getItemPlaytime(id) - 1;
3923             if (in < 0 || itemIn < in) {
3924                 in = itemIn;
3925             }
3926             if (itemOut > out) {
3927                 out = itemOut;
3928             }
3929         }
3930     }
3931     return QPoint(in, out);
3932 }
3933 
3934 void TimelineController::updateClipActions()
3935 {
3936     if (m_model->getCurrentSelection().empty()) {
3937         for (QAction *act : qAsConst(clipActions)) {
3938             const QChar actionData = act->data().toChar();
3939             if (actionData == QLatin1Char('P')) {
3940                 // Position actions should stay enabled in clip monitor
3941                 act->setEnabled(true);
3942             } else {
3943                 act->setEnabled(false);
3944             }
3945         }
3946         Q_EMIT timelineClipSelected(false);
3947         // nothing selected
3948         Q_EMIT showItemEffectStack(QString(), nullptr, QSize(), false);
3949         pCore->timeRemapWidget()->selectedClip(-1, QUuid());
3950         Q_EMIT showSubtitle(-1);
3951         pCore->displaySelectionMessage(QString());
3952         return;
3953     }
3954     std::shared_ptr<ClipModel> clip(nullptr);
3955     std::unordered_set<int> selectedItems = m_model->getCurrentSelection();
3956     int item = *selectedItems.begin();
3957     int selectionSize = selectedItems.size();
3958     if (selectionSize == 1) {
3959         if (m_model->isClip(item) || m_model->isComposition(item)) {
3960             showAsset(item);
3961             Q_EMIT showSubtitle(-1);
3962         } else if (m_model->isSubTitle(item)) {
3963             Q_EMIT showSubtitle(item);
3964         }
3965         pCore->displaySelectionMessage(QString());
3966     } else {
3967         int min = -1;
3968         int max = -1;
3969         for (const auto &id : selectedItems) {
3970             int itemPos = m_model->getItemPosition(id);
3971             int itemOut = itemPos + m_model->getItemPlaytime(id);
3972             if (min == -1 || itemPos < min) {
3973                 min = itemPos;
3974             }
3975             if (max == -1 || itemOut > max) {
3976                 max = itemOut;
3977             }
3978         }
3979         pCore->displaySelectionMessage(i18n("%1 items selected (%2) |", selectionSize, simplifiedTC(max - min)));
3980     }
3981     if (m_model->isClip(item)) {
3982         clip = m_model->getClipPtr(item);
3983         if (clip->hasTimeRemap()) {
3984             Q_EMIT pCore->remapClip(item);
3985         }
3986     }
3987     bool isInGroup = m_model->m_groups->isInGroup(item);
3988     PlaylistState::ClipState state = PlaylistState::ClipState::Unknown;
3989     ClipType::ProducerType type = ClipType::Unknown;
3990     if (clip) {
3991         state = clip->clipState();
3992         type = clip->clipType();
3993     }
3994     for (QAction *act : qAsConst(clipActions)) {
3995         bool enableAction = true;
3996         const QChar actionData = act->data().toChar();
3997         if (actionData == QLatin1Char('G')) {
3998             enableAction = isInSelection(item) && selectionSize > 1;
3999         } else if (actionData == QLatin1Char('U')) {
4000             enableAction = isInGroup;
4001         } else if (actionData == QLatin1Char('A')) {
4002             if (isInGroup && m_model->m_groups->getType(m_model->m_groups->getRootId(item)) == GroupType::AVSplit) {
4003                 enableAction = true;
4004             } else {
4005                 enableAction = state == PlaylistState::AudioOnly;
4006             }
4007         } else if (actionData == QLatin1Char('V')) {
4008             enableAction = state == PlaylistState::VideoOnly;
4009         } else if (actionData == QLatin1Char('D')) {
4010             enableAction = state == PlaylistState::Disabled;
4011         } else if (actionData == QLatin1Char('E')) {
4012             enableAction = state != PlaylistState::Disabled && state != PlaylistState::Unknown;
4013         } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
4014             enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
4015             if (enableAction && actionData == QLatin1Char('S')) {
4016                 if (isInGroup) {
4017                     // Check if all clips in the group have have same state (audio or video)
4018                     int targetRoot = m_model->m_groups->getRootId(item);
4019                     if (m_model->isGroup(targetRoot)) {
4020                         std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
4021                         for (int current_id : sub) {
4022                             if (current_id == item) {
4023                                 continue;
4024                             }
4025                             if (m_model->isClip(current_id) && m_model->getClipPtr(current_id)->clipState() != state) {
4026                                 // Group with audio and video clips, disable split action
4027                                 enableAction = false;
4028                                 break;
4029                             }
4030                         }
4031                     }
4032                 }
4033                 act->setText(state == PlaylistState::AudioOnly ? i18n("Restore video") : i18n("Restore audio"));
4034             }
4035         } else if (actionData == QLatin1Char('W')) {
4036             enableAction = clip != nullptr;
4037             if (enableAction) {
4038                 act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip"));
4039             }
4040         } else if (actionData == QLatin1Char('C') && clip == nullptr) {
4041             enableAction = false;
4042         } else if (actionData == QLatin1Char('P')) {
4043             // Position actions should stay enabled in clip monitor
4044             enableAction = true;
4045         } else if (actionData == QLatin1Char('R')) {
4046             // Time remap action
4047             enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image && qFuzzyCompare(1., m_model->m_allClips[item]->getSpeed());
4048             if (enableAction) {
4049                 act->setChecked(clip->hasTimeRemap());
4050             }
4051         } else if (actionData == QLatin1Char('Q')) {
4052             // Speed change action
4053             enableAction = clip != nullptr && (clip->getSpeed() != 1. || (type != ClipType::Timeline && type != ClipType::Playlist && type != ClipType::Color &&
4054                                                                           type != ClipType::Image && !clip->hasTimeRemap()));
4055         }
4056         act->setEnabled(enableAction);
4057     }
4058     Q_EMIT timelineClipSelected(clip != nullptr);
4059 }
4060 
4061 const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
4062 {
4063     return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
4064 }
4065 
4066 void TimelineController::grabCurrent()
4067 {
4068     if (trimmingActive()) {
4069         return;
4070     }
4071     std::unordered_set<int> ids = m_model->getCurrentSelection();
4072     std::unordered_set<int> items_list;
4073     int mainId = -1;
4074     for (int i : ids) {
4075         if (m_model->isGroup(i)) {
4076             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
4077             items_list.insert(children.begin(), children.end());
4078         } else {
4079             items_list.insert(i);
4080         }
4081     }
4082     for (int id : items_list) {
4083         if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) {
4084             mainId = id;
4085             continue;
4086         }
4087         if (m_model->isClip(id)) {
4088             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
4089             clip->setGrab(!clip->isGrabbed());
4090         } else if (m_model->isComposition(id)) {
4091             std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(id);
4092             clip->setGrab(!clip->isGrabbed());
4093         } else if (m_model->isSubTitle(id)) {
4094             m_model->getSubtitleModel()->switchGrab(id);
4095         }
4096     }
4097     if (mainId > -1) {
4098         if (m_model->isClip(mainId)) {
4099             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(mainId);
4100             clip->setGrab(!clip->isGrabbed());
4101         } else if (m_model->isComposition(mainId)) {
4102             std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(mainId);
4103             clip->setGrab(!clip->isGrabbed());
4104         } else if (m_model->isSubTitle(mainId)) {
4105             m_model->getSubtitleModel()->switchGrab(mainId);
4106         }
4107     }
4108 }
4109 
4110 int TimelineController::getItemMovingTrack(int itemId) const
4111 {
4112     if (m_model->isClip(itemId)) {
4113         int trackId = -1;
4114         if (m_model->m_editMode != TimelineMode::NormalEdit) {
4115             trackId = m_model->m_allClips[itemId]->getFakeTrackId();
4116         }
4117         return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
4118     }
4119     return m_model->m_allCompositions[itemId]->getCurrentTrackId();
4120 }
4121 
4122 bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
4123 {
4124     Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
4125     int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
4126     if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
4127         qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
4128         // Ensure clip height binds again with parent track height
4129         if (m_model->m_groups->isInGroup(clipId)) {
4130             int groupId = m_model->m_groups->getRootId(clipId);
4131             auto all_items = m_model->m_groups->getLeaves(groupId);
4132             for (int item : all_items) {
4133                 if (m_model->isClip(item)) {
4134                     m_model->m_allClips[item]->setFakeTrackId(-1);
4135                     QModelIndex modelIndex = m_model->makeClipIndexFromID(item);
4136                     if (modelIndex.isValid()) {
4137                         m_model->notifyChange(modelIndex, modelIndex, TimelineModel::FakeTrackIdRole);
4138                     }
4139                 }
4140             }
4141         } else {
4142             m_model->m_allClips[clipId]->setFakeTrackId(-1);
4143             QModelIndex modelIndex = m_model->makeClipIndexFromID(clipId);
4144             if (modelIndex.isValid()) {
4145                 m_model->notifyChange(modelIndex, modelIndex, TimelineModel::FakeTrackIdRole);
4146             }
4147         }
4148         return true;
4149     }
4150     if (m_model->m_groups->isInGroup(clipId)) {
4151         // element is in a group.
4152         int groupId = m_model->m_groups->getRootId(clipId);
4153         int current_trackId = m_model->getClipTrackId(clipId);
4154         int track_pos1 = m_model->getTrackPosition(trackId);
4155         int track_pos2 = m_model->getTrackPosition(current_trackId);
4156         int delta_track = track_pos1 - track_pos2;
4157         int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
4158         return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
4159     }
4160     qDebug() << "//////\n//////\nENDING FAKE MOVE: " << trackId << ", POS: " << position;
4161     std::function<bool(void)> undo = []() { return true; };
4162     std::function<bool(void)> redo = []() { return true; };
4163     int startPos = m_model->getClipPosition(clipId);
4164     int duration = m_model->getClipPlaytime(clipId);
4165     int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
4166     bool res = true;
4167     if (currentTrack > -1) {
4168         std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(currentTrack)->getMixInfo(clipId);
4169         if (mixData.first.firstClipId > -1) {
4170             m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
4171         }
4172         if (mixData.second.firstClipId > -1) {
4173             m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
4174         }
4175         res = m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false);
4176     }
4177     if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
4178         res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
4179     } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
4180         // Remove space from previous location
4181         if (currentTrack > -1) {
4182             res = res && TimelineFunctions::removeSpace(m_model, {startPos, startPos + duration}, undo, redo, {currentTrack}, false);
4183         }
4184         int startClipId = m_model->getClipByPosition(trackId, position);
4185         if (startClipId > -1) {
4186             // There is a clip at insert pos
4187             if (m_model->getClipPosition(startClipId) != position) {
4188                 // If position is in the middle of the clip, cut it
4189                 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
4190             }
4191         }
4192         res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, {trackId});
4193     }
4194     res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo, false, false);
4195     if (res) {
4196         // Terminate fake move
4197         if (m_model->isClip(clipId)) {
4198             m_model->m_allClips[clipId]->setFakeTrackId(-1);
4199         }
4200         if (logUndo) {
4201             pCore->pushUndo(undo, redo, i18n("Move item"));
4202         }
4203     } else {
4204         qDebug() << "//// FAKE FAILED";
4205         undo();
4206     }
4207     return res;
4208 }
4209 
4210 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
4211 {
4212     std::function<bool(void)> undo = []() { return true; };
4213     std::function<bool(void)> redo = []() { return true; };
4214     bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
4215     if (res && logUndo) {
4216         // Terminate fake move
4217         if (m_model->isClip(clipId)) {
4218             m_model->m_allClips[clipId]->setFakeTrackId(-1);
4219         }
4220         pCore->pushUndo(undo, redo, i18n("Move group"));
4221     }
4222     return res;
4223 }
4224 
4225 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
4226 {
4227     Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
4228     bool ok = true;
4229     auto all_items = m_model->m_groups->getLeaves(groupId);
4230     Q_ASSERT(all_items.size() > 1);
4231     Fun local_undo = []() { return true; };
4232     Fun local_redo = []() { return true; };
4233 
4234     // Sort clips. We need to delete from right to left to avoid confusing the view
4235     std::vector<int> sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))};
4236     std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) {
4237         int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
4238         int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition();
4239         return p2 < p1;
4240     });
4241 
4242     // 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.
4243     // This way, we ensure that no conflict will arise with clips inside the group being moved
4244     // We are moving a group on another track, delete and re-add
4245 
4246     // First, remove clips
4247     int audio_delta, video_delta;
4248     audio_delta = video_delta = delta_track;
4249     int master_trackId = m_model->getItemTrackId(clipId);
4250     if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
4251         // Master clip is audio, so reverse delta for video clips
4252         video_delta = -delta_track;
4253     } else {
4254         audio_delta = -delta_track;
4255     }
4256     int min = -1;
4257     int max = -1;
4258     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track, new_track_ids;
4259     std::vector<int> affected_trackIds;
4260     std::unordered_map<int, std::pair<QString, QVector<QPair<QString, QVariant>>>> mixToMove;
4261     std::unordered_map<int, MixInfo> mixInfoToMove;
4262     std::unordered_map<int, std::pair<int, int>> mixTracksToMove;
4263     // Remove mixes not part of the group move
4264     for (int item : sorted_clips) {
4265         if (m_model->isClip(item)) {
4266             int tid = m_model->getItemTrackId(item);
4267             affected_trackIds.emplace_back(tid);
4268             std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(item);
4269             if (mixData.first.firstClipId > -1) {
4270                 if (std::find(sorted_clips.begin(), sorted_clips.end(), mixData.first.firstClipId) == sorted_clips.end()) {
4271                     // Clip has startMix
4272                     m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
4273                 } else {
4274                     // Get mix properties
4275                     std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams =
4276                         m_model->getTrackById_const(tid)->getMixParams(mixData.first.secondClipId);
4277                     mixToMove[item] = mixParams;
4278                     mixTracksToMove[item] = m_model->getTrackById_const(tid)->getMixTracks(mixData.first.secondClipId);
4279                     mixInfoToMove[item] = mixData.first;
4280                 }
4281             }
4282             if (mixData.second.firstClipId > -1 && std::find(sorted_clips.begin(), sorted_clips.end(), mixData.second.secondClipId) == sorted_clips.end()) {
4283                 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
4284             }
4285         }
4286     }
4287     for (int item : sorted_clips) {
4288         int old_trackId = m_model->getItemTrackId(item);
4289         old_track_ids[item] = old_trackId;
4290         if (old_trackId != -1) {
4291             if (m_model->isClip(item)) {
4292                 int current_track_position = m_model->getTrackPosition(old_trackId);
4293                 int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
4294                 int target_track_position = current_track_position + d;
4295                 auto it = m_model->m_allTracks.cbegin();
4296                 std::advance(it, target_track_position);
4297                 int target_track = (*it)->getId();
4298                 new_track_ids[item] = target_track;
4299                 affected_trackIds.emplace_back(target_track);
4300                 old_position[item] = m_model->m_allClips[item]->getPosition();
4301                 int duration = m_model->m_allClips[item]->getPlaytime();
4302                 min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
4303                 max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
4304                 ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, true, finalMove, undo, redo, false, false);
4305                 if (m_model->m_editMode == TimelineMode::InsertEdit) {
4306                     // Lift space left by removed clip
4307                     ok = ok && TimelineFunctions::removeSpace(m_model, {old_position[item], old_position[item] + duration}, undo, redo, {old_trackId}, false);
4308                 }
4309             } else {
4310                 // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
4311                 old_position[item] = m_model->m_allCompositions[item]->getPosition();
4312                 old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
4313             }
4314             if (!ok) {
4315                 bool undone = undo();
4316                 Q_ASSERT(undone);
4317                 return false;
4318             }
4319         }
4320     }
4321     bool res = true;
4322     if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
4323         for (int item : sorted_clips) {
4324             if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
4325                 int target_track = new_track_ids[item];
4326                 int target_position = old_position[item] + delta_pos;
4327                 int duration = m_model->m_allClips[item]->getPlaytime();
4328                 res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
4329             }
4330         }
4331     } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
4332         QVector<int> processedTracks;
4333         for (int item : sorted_clips) {
4334             int target_track = new_track_ids[item];
4335             if (processedTracks.contains(target_track)) {
4336                 // already processed
4337                 continue;
4338             }
4339             processedTracks << target_track;
4340             int target_position = min;
4341             int startClipId = m_model->getClipByPosition(target_track, target_position);
4342             if (startClipId > -1) {
4343                 // There is a clip, cut
4344                 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
4345             }
4346         }
4347         res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, processedTracks);
4348     }
4349     for (int item : sorted_clips) {
4350         if (m_model->isClip(item)) {
4351             int target_track = new_track_ids[item];
4352             int target_position = old_position[item] + delta_pos;
4353             ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo);
4354         } else {
4355             // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
4356         }
4357         if (!ok) {
4358             bool undone = undo();
4359             Q_ASSERT(undone);
4360             return false;
4361         }
4362     }
4363 
4364     if (delta_track == 0) {
4365         Fun sync_mix = [this, affected_trackIds, finalMove]() {
4366             for (int t : affected_trackIds) {
4367                 m_model->getTrackById_const(t)->syncronizeMixes(finalMove);
4368             }
4369             return true;
4370         };
4371         sync_mix();
4372         PUSH_LAMBDA(sync_mix, redo);
4373         return true;
4374     }
4375     for (int item : sorted_clips) {
4376         if (mixToMove.find(item) == mixToMove.end()) {
4377             continue;
4378         }
4379         int trackId = new_track_ids[item];
4380         int previous_track = old_track_ids[item];
4381         MixInfo mixData = mixInfoToMove[item];
4382         std::pair<int, int> mixTracks = mixTracksToMove[item];
4383         std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = mixToMove[item];
4384         Fun simple_move_mix = [this, previous_track, trackId, finalMove, mixData, mixTracks, mixParams]() {
4385             // Insert mix on new track
4386             bool result = m_model->getTrackById_const(trackId)->createMix(mixData, mixParams, mixTracks, finalMove);
4387             // Remove mix on old track
4388             m_model->getTrackById_const(previous_track)->removeMix(mixData);
4389             return result;
4390         };
4391         Fun simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, mixTracks, mixParams]() {
4392             bool result = m_model->getTrackById_const(previous_track)->createMix(mixData, mixParams, mixTracks, finalMove);
4393             // Remove mix on old track
4394             m_model->getTrackById_const(trackId)->removeMix(mixData);
4395             return result;
4396         };
4397         simple_move_mix();
4398         if (finalMove) {
4399             PUSH_LAMBDA(simple_restore_mix, undo);
4400             PUSH_LAMBDA(simple_move_mix, redo);
4401         }
4402     }
4403     return true;
4404 }
4405 
4406 const std::unordered_map<QString, std::vector<int>> TimelineController::getThumbKeys()
4407 {
4408     std::unordered_map<QString, std::vector<int>> framesToStore;
4409     for (const auto &clp : m_model->m_allClips) {
4410         if (clp.second->isAudioOnly()) {
4411             // Don't process audio clips
4412             continue;
4413         }
4414         const QString binId = getClipBinId(clp.first);
4415         framesToStore[binId].push_back(clp.second->getIn());
4416         framesToStore[binId].push_back(clp.second->getOut());
4417     }
4418     return framesToStore;
4419 }
4420 
4421 bool TimelineController::isInSelection(int itemId)
4422 {
4423     return m_model->getCurrentSelection().count(itemId) > 0;
4424 }
4425 
4426 bool TimelineController::exists(int itemId)
4427 {
4428     return m_model->isClip(itemId) || m_model->isComposition(itemId);
4429 }
4430 
4431 void TimelineController::slotMultitrackView(bool enable, bool refresh)
4432 {
4433     QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh);
4434     if (!refresh) {
4435         // This is just a temporary state (disable multitrack view for playlist save, don't change scene
4436         return;
4437     }
4438     pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames));
4439     QObject::disconnect(m_connection);
4440     if (enable) {
4441         connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection);
4442         m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() {
4443             int ix = 0;
4444             auto it = m_model->m_allTracks.cbegin();
4445             while (it != m_model->m_allTracks.cend()) {
4446                 int target_track = (*it)->getId();
4447                 ++it;
4448                 if (target_track == m_activeTrack) {
4449                     break;
4450                 }
4451                 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4452                     continue;
4453                 }
4454                 ++ix;
4455             }
4456             pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4457         });
4458         int ix = 0;
4459         auto it = m_model->m_allTracks.cbegin();
4460         while (it != m_model->m_allTracks.cend()) {
4461             int target_track = (*it)->getId();
4462             ++it;
4463             if (target_track == m_activeTrack) {
4464                 break;
4465             }
4466             if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4467                 continue;
4468             }
4469             ++ix;
4470         }
4471         pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4472     } else {
4473         disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack);
4474     }
4475 }
4476 
4477 void TimelineController::updateMultiTrack()
4478 {
4479     QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true);
4480     pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames));
4481 }
4482 
4483 void TimelineController::activateTrackAndSelect(int trackPosition, bool notesMode)
4484 {
4485     int tid = -1;
4486     int ix = 0;
4487     if (notesMode && trackPosition == -2) {
4488         m_activeTrack = -2;
4489         Q_EMIT activeTrackChanged();
4490         return;
4491     }
4492     auto it = m_model->m_allTracks.cbegin();
4493     while (it != m_model->m_allTracks.cend()) {
4494         tid = (*it)->getId();
4495         ++it;
4496         if (!notesMode && (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden())) {
4497             continue;
4498         }
4499         if (trackPosition == ix) {
4500             break;
4501         }
4502         ++ix;
4503     }
4504     if (tid > -1) {
4505         m_activeTrack = tid;
4506         Q_EMIT activeTrackChanged();
4507         if (!notesMode && pCore->window()->getCurrentTimeline()->activeTool() != ToolType::MulticamTool) {
4508             selectCurrentItem(KdenliveObjectType::TimelineClip, true);
4509         }
4510     }
4511 }
4512 
4513 void TimelineController::saveTimelineSelection(const QDir &targetDir)
4514 {
4515     std::unordered_set<int> ids = m_model->getCurrentSelection();
4516     std::unordered_set<int> items_list;
4517     for (int i : ids) {
4518         if (m_model->isGroup(i)) {
4519             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
4520             items_list.insert(children.begin(), children.end());
4521         } else {
4522             items_list.insert(i);
4523         }
4524     }
4525     int startPos = 0;
4526     int endPos = 0;
4527     for (int id : items_list) {
4528         int start = m_model->getItemPosition(id);
4529         int end = start + m_model->getItemPlaytime(id);
4530         if (startPos == 0 || start < startPos) {
4531             startPos = start;
4532         }
4533         if (end > endPos) {
4534             endPos = end;
4535         }
4536     }
4537     TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir, endPos - startPos - 1);
4538 }
4539 
4540 void TimelineController::addEffectKeyframe(int cid, int frame, double val)
4541 {
4542     if (m_model->isClip(cid)) {
4543         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4544         destStack->addEffectKeyFrame(frame, val);
4545     } else if (m_model->isComposition(cid)) {
4546         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4547         listModel->addKeyframe(frame, val);
4548     }
4549 }
4550 
4551 void TimelineController::removeEffectKeyframe(int cid, int frame)
4552 {
4553     if (m_model->isClip(cid)) {
4554         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4555         destStack->removeKeyFrame(frame);
4556     } else if (m_model->isComposition(cid)) {
4557         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4558         listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
4559     }
4560 }
4561 
4562 void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue)
4563 {
4564     if (m_model->isClip(cid)) {
4565         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4566         destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue);
4567     } else if (m_model->isComposition(cid)) {
4568         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4569         listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue);
4570     }
4571 }
4572 
4573 bool TimelineController::hasKeyframeAt(int cid, int frame)
4574 {
4575     if (m_model->isClip(cid)) {
4576         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4577         return destStack->hasKeyFrame(frame);
4578     } else if (m_model->isComposition(cid)) {
4579         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4580         return listModel->hasKeyframe(frame);
4581     }
4582     return false;
4583 }
4584 
4585 QColor TimelineController::videoColor() const
4586 {
4587     KColorScheme scheme(QApplication::palette().currentColorGroup());
4588     return scheme.foreground(KColorScheme::LinkText).color();
4589 }
4590 
4591 QColor TimelineController::targetColor() const
4592 {
4593     KColorScheme scheme(QApplication::palette().currentColorGroup());
4594     QColor base = scheme.foreground(KColorScheme::PositiveText).color();
4595     QColor high = QApplication::palette().highlightedText().color();
4596     double factor = 0.3;
4597     QColor res = QColor(qBound(0, base.red() + int(factor * (high.red() - 128)), 255), qBound(0, base.green() + int(factor * (high.green() - 128)), 255),
4598                         qBound(0, base.blue() + int(factor * (high.blue() - 128)), 255), 255);
4599     return res;
4600 }
4601 
4602 QColor TimelineController::targetTextColor() const
4603 {
4604     KColorScheme scheme(QApplication::palette().currentColorGroup());
4605     return scheme.background(KColorScheme::PositiveBackground).color();
4606 }
4607 
4608 QColor TimelineController::audioColor() const
4609 {
4610     KColorScheme scheme(QApplication::palette().currentColorGroup());
4611     return scheme.foreground(KColorScheme::PositiveText).color().darker(200);
4612 }
4613 
4614 QColor TimelineController::titleColor() const
4615 {
4616     KColorScheme scheme(QApplication::palette().currentColorGroup());
4617     QColor base = scheme.foreground(KColorScheme::LinkText).color();
4618     QColor high = scheme.foreground(KColorScheme::NegativeText).color();
4619     QColor title = QColor(qBound(0, base.red() + int(high.red() - 128), 255), qBound(0, base.green() + int(high.green() - 128), 255),
4620                           qBound(0, base.blue() + int(high.blue() - 128), 255), 255);
4621     return title;
4622 }
4623 
4624 QColor TimelineController::imageColor() const
4625 {
4626     KColorScheme scheme(QApplication::palette().currentColorGroup());
4627     return scheme.foreground(KColorScheme::NeutralText).color();
4628 }
4629 
4630 QColor TimelineController::thumbColor1() const
4631 {
4632     return KdenliveSettings::thumbColor1();
4633 }
4634 
4635 QColor TimelineController::thumbColor2() const
4636 {
4637     return KdenliveSettings::thumbColor2();
4638 }
4639 
4640 QColor TimelineController::slideshowColor() const
4641 {
4642     KColorScheme scheme(QApplication::palette().currentColorGroup());
4643     QColor base = scheme.foreground(KColorScheme::LinkText).color();
4644     QColor high = scheme.foreground(KColorScheme::NeutralText).color();
4645     QColor slide = QColor(qBound(0, base.red() + int(high.red() - 128), 255), qBound(0, base.green() + int(high.green() - 128), 255),
4646                           qBound(0, base.blue() + int(high.blue() - 128), 255), 255);
4647     return slide;
4648 }
4649 
4650 QColor TimelineController::lockedColor() const
4651 {
4652     KColorScheme scheme(QApplication::palette().currentColorGroup());
4653     return scheme.foreground(KColorScheme::NegativeText).color();
4654 }
4655 
4656 QColor TimelineController::groupColor() const
4657 {
4658     KColorScheme scheme(QApplication::palette().currentColorGroup());
4659     return scheme.foreground(KColorScheme::ActiveText).color();
4660 }
4661 
4662 QColor TimelineController::selectionColor() const
4663 {
4664     KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
4665     if (m_model && m_model->singleSelectionMode()) {
4666         return Qt::red;
4667     }
4668     return scheme.foreground(KColorScheme::NeutralText).color();
4669 }
4670 
4671 void TimelineController::switchRecording(int trackId, bool record)
4672 {
4673     if (trackId == -1) {
4674         trackId = pCore->mixer()->recordTrack();
4675     }
4676     if (record) {
4677         if (pCore->isMediaCapturing()) {
4678             // Already recording, abort
4679             return;
4680         }
4681         qDebug() << "start recording" << trackId;
4682         if (!m_model->isTrack(trackId)) {
4683             qDebug() << "ERROR: Starting to capture on invalid track " << trackId;
4684         }
4685         if (m_model->getTrackById_const(trackId)->isLocked()) {
4686             pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500);
4687             return;
4688         }
4689         m_recordStart.first = pCore->getMonitorPosition();
4690         m_recordTrack = trackId;
4691         int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first);
4692         if (maximumSpace == INT_MAX) {
4693             m_recordStart.second = 0;
4694         } else {
4695             m_recordStart.second = maximumSpace - m_recordStart.first;
4696             if (m_recordStart.second < 8) {
4697                 pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or "
4698                                            "choose a different track"),
4699                                       ErrorMessage, 500);
4700                 return;
4701             }
4702         }
4703         pCore->monitorManager()->slotSwitchMonitors(false);
4704         pCore->startMediaCapture(trackId, true, false);
4705         if (KdenliveSettings::disablereccountdown()) {
4706             pCore->startRecording();
4707         } else {
4708             pCore->getMonitor(Kdenlive::ProjectMonitor)->startCountDown();
4709         }
4710 
4711     } else {
4712         pCore->getMonitor(Kdenlive::ProjectMonitor)->stopCountDown();
4713         pCore->stopMediaCapture(trackId, true, false);
4714         Q_EMIT stopAudioRecord();
4715         pCore->monitorManager()->slotPause();
4716     }
4717 }
4718 
4719 void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid)
4720 {
4721     if (droppedFile.isEmpty()) {
4722         // Empty url passed, abort
4723         return;
4724     }
4725     m_recordTrack = tid;
4726     m_recordStart = {frame, -1};
4727     qDebug() << "=== GOT DROPPED FILED: " << droppedFile << "\n======";
4728     if (droppedFile.first().endsWith(QLatin1String(".ass")) || droppedFile.first().endsWith(QLatin1String(".srt"))) {
4729         // Subtitle dropped, import
4730         pCore->window()->showSubtitleTrack();
4731         importSubtitle(QUrl(droppedFile.first()).toLocalFile());
4732     } else {
4733         finishRecording(QUrl(droppedFile.first()).toLocalFile());
4734     }
4735 }
4736 
4737 void TimelineController::finishRecording(const QString &recordedFile)
4738 {
4739     if (recordedFile.isEmpty()) {
4740         return;
4741     }
4742 
4743     Fun undo = []() { return true; };
4744     Fun redo = []() { return true; };
4745     std::function<void(const QString &)> callBack = [this](const QString &binId) {
4746         int id = -1;
4747         if (m_recordTrack == -1) {
4748             return;
4749         }
4750         std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
4751         if (!clip) {
4752             return;
4753         }
4754         pCore->activeBin()->selectClipById(binId);
4755         qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second;
4756         if (m_recordStart.second > 0) {
4757             // Limited space on track
4758             m_recordStart.second = qMin(int(clip->frameDuration() - 1), m_recordStart.second);
4759             QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(m_recordStart.second);
4760             m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false);
4761             m_recordStart.second++;
4762         } else {
4763             m_recordStart.second = clip->frameDuration();
4764             m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false);
4765         }
4766         setPosition(m_recordStart.first + m_recordStart.second);
4767     };
4768     std::shared_ptr<ProjectItemModel> itemModel = pCore->projectItemModel();
4769     std::shared_ptr<ProjectFolder> targetFolder = itemModel->getRootFolder();
4770     if (itemModel->defaultAudioCaptureFolder() > -1) {
4771         const QString audioCaptureFolder = QString::number(itemModel->defaultAudioCaptureFolder());
4772         std::shared_ptr<ProjectFolder> folderItem = itemModel->getFolderByBinId(audioCaptureFolder);
4773         if (folderItem) {
4774             targetFolder = folderItem;
4775         }
4776     }
4777 
4778     QString binId =
4779         ClipCreator::createClipFromFile(recordedFile, targetFolder->clipId(), pCore->projectItemModel(), undo, redo, callBack);
4780     pCore->window()->raiseBin();
4781     if (binId != QStringLiteral("-1")) {
4782         pCore->pushUndo(undo, redo, i18n("Record audio"));
4783     }
4784 }
4785 
4786 void TimelineController::updateVideoTarget()
4787 {
4788     if (videoTarget() > -1) {
4789         m_lastVideoTarget = videoTarget();
4790         m_videoTargetActive = true;
4791         Q_EMIT lastVideoTargetChanged();
4792     } else {
4793         m_videoTargetActive = false;
4794     }
4795 }
4796 
4797 void TimelineController::updateAudioTarget()
4798 {
4799     if (!audioTarget().isEmpty()) {
4800         m_lastAudioTarget = m_model->m_audioTarget;
4801         m_audioTargetActive = true;
4802         Q_EMIT lastAudioTargetChanged();
4803     } else {
4804         m_audioTargetActive = false;
4805     }
4806 }
4807 
4808 bool TimelineController::hasActiveTracks() const
4809 {
4810     auto it = m_model->m_allTracks.cbegin();
4811     while (it != m_model->m_allTracks.cend()) {
4812         int target_track = (*it)->getId();
4813         if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
4814             return true;
4815         }
4816         ++it;
4817     }
4818     return false;
4819 }
4820 
4821 void TimelineController::showMasterEffects()
4822 {
4823     Q_EMIT showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false);
4824 }
4825 
4826 bool TimelineController::refreshIfVisible(int cid)
4827 {
4828     auto it = m_model->m_allTracks.cbegin();
4829     while (it != m_model->m_allTracks.cend()) {
4830         int target_track = (*it)->getId();
4831         if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4832             ++it;
4833             continue;
4834         }
4835         int child = m_model->getClipByPosition(target_track, pCore->getMonitorPosition());
4836         if (child > 0) {
4837             if (m_model->m_allClips[child]->binId().toInt() == cid) {
4838                 return true;
4839             }
4840         }
4841         ++it;
4842     }
4843     return false;
4844 }
4845 
4846 void TimelineController::collapseActiveTrack()
4847 {
4848     if (m_activeTrack == -1) {
4849         return;
4850     }
4851     if (m_model->isSubtitleTrack(m_activeTrack)) {
4852         // Subtitle track
4853         QMetaObject::invokeMethod(m_root, "switchSubtitleTrack", Qt::QueuedConnection);
4854         return;
4855     }
4856     int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt();
4857     // Default unit for timeline.qml objects size
4858     int baseUnit = qMax(28, qRound(QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pixelSize() * 1.8));
4859     m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QString::number(baseUnit));
4860 }
4861 
4862 void TimelineController::setActiveTrackProperty(const QString &name, const QString &value)
4863 {
4864     if (m_activeTrack > -1) {
4865         m_model->setTrackProperty(m_activeTrack, name, value);
4866     }
4867 }
4868 
4869 bool TimelineController::isActiveTrackAudio() const
4870 {
4871     if (m_activeTrack > -1) {
4872         if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
4873             return true;
4874         }
4875     }
4876     return false;
4877 }
4878 
4879 const QVariant TimelineController::getActiveTrackProperty(const QString &name) const
4880 {
4881     if (m_activeTrack > -1) {
4882         return m_model->getTrackProperty(m_activeTrack, name);
4883     }
4884     return QVariant();
4885 }
4886 
4887 void TimelineController::expandActiveClip()
4888 {
4889     std::unordered_set<int> ids = m_model->getCurrentSelection();
4890     std::unordered_set<int> items_list;
4891     for (int i : ids) {
4892         if (m_model->isGroup(i)) {
4893             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
4894             items_list.insert(children.begin(), children.end());
4895         } else {
4896             items_list.insert(i);
4897         }
4898     }
4899     m_model->requestClearSelection();
4900     bool result = true;
4901     for (int id : items_list) {
4902         if (result && m_model->isClip(id)) {
4903             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
4904             if (clip->clipType() == ClipType::Playlist || clip->clipType() == ClipType::Timeline) {
4905                 std::function<bool(void)> undo = []() { return true; };
4906                 std::function<bool(void)> redo = []() { return true; };
4907                 if (m_model->m_groups->isInGroup(id)) {
4908                     int targetRoot = m_model->m_groups->getRootId(id);
4909                     if (m_model->isGroup(targetRoot)) {
4910                         m_model->requestClipUngroup(targetRoot, undo, redo);
4911                     }
4912                 }
4913                 int pos = clip->getPosition();
4914                 int inPos = m_model->getClipIn(id);
4915                 int duration = m_model->getClipPlaytime(id);
4916                 QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id));
4917                 m_model->requestClipDeletion(id, undo, redo);
4918                 result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo, inPos, duration);
4919                 if (result) {
4920                     pCore->pushUndo(undo, redo, i18n("Expand clip"));
4921                 } else {
4922                     undo();
4923                     pCore->displayMessage(i18n("Could not expand clip"), ErrorMessage, 500);
4924                 }
4925             }
4926         }
4927     }
4928 }
4929 
4930 QMap<int, QString> TimelineController::getCurrentTargets(int trackId, int &activeTargetStream)
4931 {
4932     if (m_model->m_binAudioTargets.size() < 2) {
4933         activeTargetStream = -1;
4934         return QMap<int, QString>();
4935     }
4936     if (m_model->m_audioTarget.contains(trackId)) {
4937         activeTargetStream = m_model->m_audioTarget.value(trackId);
4938     } else {
4939         activeTargetStream = -1;
4940     }
4941     return m_model->m_binAudioTargets;
4942 }
4943 
4944 void TimelineController::addTracks(int videoTracks, int audioTracks)
4945 {
4946     bool result = false;
4947     int total = videoTracks + audioTracks;
4948     Fun undo = []() { return true; };
4949     Fun redo = []() { return true; };
4950     while (videoTracks + audioTracks > 0) {
4951         int newTid;
4952         if (audioTracks > 0) {
4953             result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo);
4954             audioTracks--;
4955         } else {
4956             result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo);
4957             videoTracks--;
4958         }
4959         if (!result) {
4960             break;
4961         }
4962     }
4963     if (result) {
4964         pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total));
4965     } else {
4966         pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
4967         undo();
4968     }
4969 }
4970 
4971 void TimelineController::mixClip(int cid, int delta)
4972 {
4973     if (cid == -1) {
4974         std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
4975         if (selectedIds.empty() && m_model->isTrack(m_activeTrack)) {
4976             // Check if timeline playhead is on a cut
4977             int timelinePos = pCore->getMonitorPosition();
4978             int nextClip = m_model->getTrackById_const(m_activeTrack)->getClipByPosition(timelinePos);
4979             int prevClip = m_model->getTrackById_const(m_activeTrack)->getClipByPosition(timelinePos - 1);
4980             if (m_model->isClip(prevClip) && m_model->isClip(nextClip) && prevClip != nextClip) {
4981                 cid = nextClip;
4982             }
4983         }
4984     }
4985     m_model->mixClip(cid, QStringLiteral("luma"), delta);
4986 }
4987 
4988 void TimelineController::temporaryUnplug(const QList<int> &clipIds, bool hide)
4989 {
4990     for (auto &cid : clipIds) {
4991         int tid = m_model->getItemTrackId(cid);
4992         if (tid == -1) {
4993             continue;
4994         }
4995         if (hide) {
4996             m_model->getTrackById_const(tid)->temporaryUnplugClip(cid);
4997         } else {
4998             m_model->getTrackById_const(tid)->temporaryReplugClip(cid);
4999         }
5000     }
5001 }
5002 
5003 void TimelineController::importSubtitle(const QString &path)
5004 {
5005     QScopedPointer<ImportSubtitle> d(new ImportSubtitle(path, pCore->window()));
5006     if (d->exec() == QDialog::Accepted && !d->subtitle_url->url().isEmpty()) {
5007         auto subtitleModel = m_model->getSubtitleModel();
5008         if (d->create_track->isChecked()) {
5009             // Create a new subtitle entry
5010             int ix = subtitleModel->createNewSubtitle(d->track_name->text());
5011             subtitlesListChanged();
5012             // Activate the newly created subtitle track
5013             subtitlesMenuActivated(ix - 1);
5014         }
5015         int offset = 0, startFramerate = 30.00, targetFramerate = 30.00;
5016         if (d->cursor_pos->isChecked()) {
5017             offset = pCore->getMonitorPosition();
5018         }
5019         if (d->transform_framerate_check_box->isChecked()) {
5020             startFramerate = d->caption_original_framerate->value();
5021             targetFramerate = d->caption_target_framerate->value();
5022         }
5023         subtitleModel->importSubtitle(d->subtitle_url->url().toLocalFile(), offset, true, startFramerate, targetFramerate,
5024                                       d->codecs_list->currentText().toUtf8());
5025     }
5026     Q_EMIT regainFocus();
5027 }
5028 
5029 void TimelineController::exportSubtitle()
5030 {
5031     if (!m_model->hasSubtitleModel()) {
5032         return;
5033     }
5034     QString currentSub = m_model->getSubtitleModel()->getUrl();
5035     if (currentSub.isEmpty()) {
5036         pCore->displayMessage(i18n("No subtitles in current project"), ErrorMessage);
5037         return;
5038     }
5039     QString url = QFileDialog::getSaveFileName(qApp->activeWindow(), i18n("Export subtitle file"), pCore->currentDoc()->url().toLocalFile(),
5040                                                i18n("Subtitle File (*.srt)"));
5041     if (url.isEmpty()) {
5042         return;
5043     }
5044     if (!url.endsWith(QStringLiteral(".srt"))) {
5045         url.append(QStringLiteral(".srt"));
5046     }
5047     QFile srcFile(url);
5048     if (srcFile.exists()) {
5049         srcFile.remove();
5050     }
5051     QFile src(currentSub);
5052     if (!src.copy(srcFile.fileName())) {
5053         KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", srcFile.fileName()));
5054     }
5055 }
5056 
5057 void TimelineController::subtitleSpeechRecognition()
5058 {
5059     SpeechDialog d(m_model, m_zone, m_activeTrack, false, false, qApp->activeWindow());
5060     d.exec();
5061 }
5062 
5063 bool TimelineController::subtitlesWarning() const
5064 {
5065     return !EffectsRepository::get()->hasInternalEffect("avfilter.subtitles");
5066 }
5067 
5068 void TimelineController::subtitlesWarningDetails()
5069 {
5070     KMessageBox::error(nullptr, i18n("The avfilter.subtitles filter is required, but was not found."
5071                                      " The subtitles feature will probably not work as expected."));
5072     Q_EMIT regainFocus();
5073 }
5074 
5075 void TimelineController::switchSubtitleDisable()
5076 {
5077     if (m_model->hasSubtitleModel()) {
5078         auto subtitleModel = m_model->getSubtitleModel();
5079         bool disabled = subtitleModel->isDisabled();
5080         Fun local_switch = [this, subtitleModel]() {
5081             subtitleModel->switchDisabled();
5082             Q_EMIT subtitlesDisabledChanged();
5083             pCore->refreshProjectMonitorOnce();
5084             return true;
5085         };
5086         local_switch();
5087         pCore->pushUndo(local_switch, local_switch, disabled ? i18n("Show subtitle track") : i18n("Hide subtitle track"));
5088     }
5089 }
5090 
5091 bool TimelineController::subtitlesDisabled() const
5092 {
5093     if (m_model->hasSubtitleModel()) {
5094         return m_model->getSubtitleModel()->isDisabled();
5095     }
5096     return false;
5097 }
5098 
5099 void TimelineController::switchSubtitleLock()
5100 {
5101     if (m_model->hasSubtitleModel()) {
5102         auto subtitleModel = m_model->getSubtitleModel();
5103         bool locked = subtitleModel->isLocked();
5104         Fun local_switch = [this, subtitleModel]() {
5105             subtitleModel->switchLocked();
5106             Q_EMIT subtitlesLockedChanged();
5107             return true;
5108         };
5109         local_switch();
5110         pCore->pushUndo(local_switch, local_switch, locked ? i18n("Unlock subtitle track") : i18n("Lock subtitle track"));
5111     }
5112 }
5113 bool TimelineController::subtitlesLocked() const
5114 {
5115     if (m_model->hasSubtitleModel()) {
5116         return m_model->getSubtitleModel()->isLocked();
5117     }
5118     return false;
5119 }
5120 
5121 bool TimelineController::guidesLocked() const
5122 {
5123     return KdenliveSettings::lockedGuides();
5124 }
5125 
5126 void TimelineController::showToolTip(const QString &info) const
5127 {
5128     pCore->displayMessage(info, TooltipMessage);
5129 }
5130 
5131 void TimelineController::showKeyBinding(const QString &info) const
5132 {
5133     pCore->window()->showKeyBinding(info);
5134 }
5135 
5136 void TimelineController::showTimelineToolInfo(bool show) const
5137 {
5138     if (show) {
5139         pCore->window()->showToolMessage();
5140     } else {
5141         pCore->window()->setWidgetKeyBinding();
5142     }
5143 }
5144 
5145 void TimelineController::showRulerEffectZone(QPair<int, int> inOut, bool checked)
5146 {
5147     m_effectZone = checked ? QPoint(inOut.first, inOut.second) : QPoint();
5148     Q_EMIT effectZoneChanged();
5149 }
5150 
5151 void TimelineController::updateMasterZones(const QVariantList &zones)
5152 {
5153     m_masterEffectZones = zones;
5154     Q_EMIT masterZonesChanged();
5155 }
5156 
5157 int TimelineController::clipMaxDuration(int cid)
5158 {
5159     if (!m_model->isClip(cid)) {
5160         return -1;
5161     }
5162     return m_model->m_allClips[cid]->getMaxDuration();
5163 }
5164 
5165 void TimelineController::resizeMix(int cid, int duration, MixAlignment align, int leftFrames)
5166 {
5167     if (cid > -1) {
5168         m_model->requestResizeMix(cid, duration, align, leftFrames);
5169     }
5170 }
5171 
5172 int TimelineController::getMixCutPos(int cid) const
5173 {
5174     return m_model->getMixCutPos(cid);
5175 }
5176 
5177 MixAlignment TimelineController::getMixAlign(int cid) const
5178 {
5179     return m_model->getMixAlign(cid);
5180 }
5181 
5182 void TimelineController::processMultitrackOperation(int tid, int in)
5183 {
5184     int out = pCore->getMonitorPosition();
5185     if (out == in) {
5186         // Simply change the reference track, nothing to do here
5187         return;
5188     }
5189     QVector<int> tracks;
5190     auto it = m_model->m_allTracks.cbegin();
5191     // Lift all tracks except tid
5192     while (it != m_model->m_allTracks.cend()) {
5193         int target_track = (*it)->getId();
5194         if (target_track != tid && !(*it)->isAudioTrack() && m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
5195             tracks << target_track;
5196         }
5197         ++it;
5198     }
5199     if (tracks.isEmpty()) {
5200         pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
5201     }
5202     TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), true);
5203 }
5204 
5205 void TimelineController::setMulticamIn(int pos)
5206 {
5207     if (multicamIn != -1) {
5208         // remove previous snap
5209         m_model->removeSnap(multicamIn);
5210     }
5211     multicamIn = pos;
5212     m_model->addSnap(multicamIn);
5213     Q_EMIT multicamInChanged();
5214 }
5215 
5216 void TimelineController::checkClipPosition(const QModelIndex &topLeft, const QModelIndex &, const QVector<int> &roles)
5217 {
5218     if (roles.contains(TimelineModel::StartRole)) {
5219         int id = int(topLeft.internalId());
5220         if (m_model->isComposition(id) || m_model->isClip(id)) {
5221             Q_EMIT updateAssetPosition(id, m_model->uuid());
5222         }
5223     }
5224     if (roles.contains(TimelineModel::ResourceRole)) {
5225         int id = int(topLeft.internalId());
5226         if (m_model->isComposition(id) || m_model->isClip(id)) {
5227             int in = m_model->getItemPosition(id);
5228             int out = in + m_model->getItemPlaytime(id);
5229             pCore->refreshProjectRange({in, out});
5230         }
5231     }
5232 }
5233 
5234 void TimelineController::autofitTrackHeight(int timelineHeight, int collapsedHeight)
5235 {
5236     int tracksCount = m_model->getTracksCount();
5237     if (tracksCount < 1) {
5238         return;
5239     }
5240     // Check how many collapsed tracks we have
5241     int collapsed = 0;
5242     auto it = m_model->m_allTracks.cbegin();
5243     while (it != m_model->m_allTracks.cend()) {
5244         if ((*it)->getProperty(QStringLiteral("kdenlive:collapsed")).toInt() > 0) {
5245             collapsed++;
5246         }
5247         ++it;
5248     }
5249     if (collapsed == tracksCount) {
5250         // All tracks are collapsed, do nothing
5251         return;
5252     }
5253     int trackHeight = qMax(collapsedHeight, (timelineHeight - (collapsed * collapsedHeight)) / (tracksCount - collapsed));
5254     it = m_model->m_allTracks.cbegin();
5255     while (it != m_model->m_allTracks.cend()) {
5256         if ((*it)->getProperty(QStringLiteral("kdenlive:collapsed")).toInt() == 0) {
5257             (*it)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(trackHeight));
5258         }
5259         ++it;
5260     }
5261     // m_model->setTrackProperty(trackId, "kdenlive:collapsed", QStringLiteral("0"));
5262     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
5263     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
5264     Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
5265 }
5266 
5267 QVariantList TimelineController::subtitlesList() const
5268 {
5269     QVariantList result;
5270     auto subtitleModel = m_model->getSubtitleModel();
5271     if (subtitleModel) {
5272         QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList();
5273         if (currentSubs.isEmpty()) {
5274             result << i18nc("@item:inlistbox name for subtitle track", "Subtitles");
5275         } else {
5276             QMapIterator<std::pair<int, QString>, QString> i(currentSubs);
5277             while (i.hasNext()) {
5278                 i.next();
5279                 result << i.key().second;
5280             }
5281         }
5282     } else {
5283         result << i18nc("@item:inlistbox name for subtitle track", "Subtitles");
5284     }
5285     result << i18nc("@item:inlistbox", "Manage Subtitles");
5286     return result;
5287 }
5288 
5289 void TimelineController::subtitlesMenuActivatedAsync(int ix)
5290 {
5291     // This method needs a timer otherwise the qml combobox crashes because we try to chenge its index while it is processing an activated event
5292     QTimer::singleShot(100, this, [&, ix]() { subtitlesMenuActivated(ix); });
5293 }
5294 
5295 void TimelineController::refreshSubtitlesComboIndex()
5296 {
5297     int ix = m_activeSubPosition;
5298     m_activeSubPosition = 0;
5299     Q_EMIT activeSubtitlePositionChanged();
5300     m_activeSubPosition = ix;
5301     Q_EMIT activeSubtitlePositionChanged();
5302 }
5303 
5304 void TimelineController::subtitlesMenuActivated(int ix)
5305 {
5306     auto subtitleModel = m_model->getSubtitleModel();
5307     QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList();
5308     if (subtitleModel) {
5309         if (ix != -1 && ix < currentSubs.size()) {
5310             // Clear selection if a subtitle item is selected
5311             std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
5312             for (auto &id : selectedIds) {
5313                 int tid = m_model->getItemTrackId(id);
5314                 if (m_model->isSubtitleTrack(tid)) {
5315                     m_model->requestClearSelection();
5316                     break;
5317                 }
5318             }
5319             QMapIterator<std::pair<int, QString>, QString> i(currentSubs);
5320             m_activeSubPosition = 0;
5321             int counter = 0;
5322             while (i.hasNext()) {
5323                 i.next();
5324                 ix--;
5325                 if (ix < 0) {
5326                     // Match, switch to another subtitle
5327                     int index = i.key().first;
5328                     m_activeSubPosition = counter;
5329                     subtitleModel->activateSubtitle(index);
5330                     break;
5331                 }
5332                 counter++;
5333             }
5334             Q_EMIT activeSubtitlePositionChanged();
5335             return;
5336         }
5337     }
5338     int currentIx = pCore->currentDoc()->getSequenceProperty(m_model->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt();
5339     if (ix > -1) {
5340         m_activeSubPosition = currentSubs.size();
5341         Q_EMIT activeSubtitlePositionChanged();
5342         // Reselect last active subtitle in combobox
5343         QMapIterator<std::pair<int, QString>, QString> i(currentSubs);
5344         int counter = 0;
5345         while (i.hasNext()) {
5346             i.next();
5347             if (i.key().first == currentIx) {
5348                 m_activeSubPosition = counter;
5349                 break;
5350             }
5351             counter++;
5352         }
5353         Q_EMIT activeSubtitlePositionChanged();
5354     }
5355 
5356     // Show manage dialog
5357     ManageSubtitles *d = new ManageSubtitles(subtitleModel, this, currentIx, qApp->activeWindow());
5358     d->exec();
5359 }