1 /*
2     SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 #include "timelinecontroller.h"
7 #include "../model/timelinefunctions.hpp"
8 #include "assets/keyframes/model/keyframemodellist.hpp"
9 #include "bin/bin.h"
10 #include "bin/clipcreator.hpp"
11 #include "bin/model/markerlistmodel.hpp"
12 #include "bin/model/subtitlemodel.hpp"
13 #include "bin/projectclip.h"
14 #include "bin/projectfolder.h"
15 #include "bin/projectitemmodel.h"
16 #include "core.h"
17 #include "dialogs/spacerdialog.h"
18 #include "dialogs/speeddialog.h"
19 #include "dialogs/speechdialog.h"
20 #include "doc/kdenlivedoc.h"
21 #include "effects/effectsrepository.hpp"
22 #include "effects/effectstack/model/effectstackmodel.hpp"
23 #include "kdenlivesettings.h"
24 #include "lib/audio/audioEnvelope.h"
25 #include "mainwindow.h"
26 #include "monitor/monitormanager.h"
27 #include "previewmanager.h"
28 #include "project/projectmanager.h"
29 #include "timeline2/model/clipmodel.hpp"
30 #include "timeline2/model/compositionmodel.hpp"
31 #include "timeline2/model/groupsmodel.hpp"
32 #include "timeline2/model/timelineitemmodel.hpp"
33 #include "timeline2/model/trackmodel.hpp"
34 #include "timeline2/view/dialogs/clipdurationdialog.h"
35 #include "timeline2/view/dialogs/trackdialog.h"
36 #include "transitions/transitionsrepository.hpp"
37 #include "audiomixer/mixermanager.hpp"
38 #include "ui_import_subtitle_ui.h"
39 #include "timeline2/view/timelinewidget.h"
40 #include "dialogs/timeremap.h"
41 
42 #include <KColorScheme>
43 #include <KMessageBox>
44 #include <KUrlRequesterDialog>
45 #include <KRecentDirs>
46 #include <QApplication>
47 #include <QClipboard>
48 #include <QQuickItem>
49 #include <memory>
50 #include <unistd.h>
51 
52 int TimelineController::m_duration = 0;
53 
TimelineController(QObject * parent)54 TimelineController::TimelineController(QObject *parent)
55     : QObject(parent)
56     , multicamIn(-1)
57     , m_root(nullptr)
58     , m_usePreview(false)
59     , m_audioRef(-1)
60     , m_zone(-1, -1)
61     , m_activeTrack(-1)
62     , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250)
63     , m_timelinePreview(nullptr)
64     , m_ready(false)
65     , m_snapStackIndex(-1)
66     , m_effectZone({0,0})
67 {
68     m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview"));
69     connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview);
70     m_disablePreview->setEnabled(false);
71     connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording);
72     connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged);
73     connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording);
74 }
75 
~TimelineController()76 TimelineController::~TimelineController()
77 {
78 }
79 
prepareClose()80 void TimelineController::prepareClose()
81 {
82     // Clear root so we don't call its methods anymore
83     QObject::disconnect( m_deleteConnection );
84     disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
85     disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
86     disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
87     disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
88     disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
89     disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
90     m_ready = false;
91     m_root = nullptr;
92     // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate
93     delete m_timelinePreview;
94     m_timelinePreview = nullptr;
95     m_model.reset();
96 }
97 
setModel(std::shared_ptr<TimelineItemModel> model)98 void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model)
99 {
100     delete m_timelinePreview;
101     m_zone = QPoint(-1, -1);
102     m_timelinePreview = nullptr;
103     m_model = std::move(model);
104     m_activeSnaps.clear();
105     connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel);
106     m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this] (int id) {
107         if (m_ready) {
108             QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id));
109         }
110     });
111     connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) {
112         if (tid > -1) {
113             showTrackAsset(tid);
114         } else {
115             showMasterEffects();
116         }
117     });
118     connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions);
119     connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateTrimmingMode);
120     connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget);
121     connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget);
122     connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); });
123     connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection);
124     connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration);
125     connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged);
126     connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel);
127     connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged);
128     connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection);
129 }
130 
restoreTargetTracks()131 void TimelineController::restoreTargetTracks()
132 {
133     setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets);
134 }
135 
setTargetTracks(bool hasVideo,QMap<int,QString> audioTargets)136 void TimelineController::setTargetTracks(bool hasVideo, QMap <int, QString> audioTargets)
137 {
138     int videoTrack = -1;
139     m_model->m_binAudioTargets = audioTargets;
140     QMap<int, int> audioTracks;
141     m_hasVideoTarget = hasVideo;
142     m_hasAudioTarget = audioTargets.size();
143     if (m_hasVideoTarget) {
144         videoTrack = m_model->getFirstVideoTrackIndex();
145     }
146     if (m_hasAudioTarget > 0) {
147         QVector <int> tracks;
148         auto it = m_model->m_allTracks.cbegin();
149         while (it != m_model->m_allTracks.cend()) {
150             if ((*it)->isAudioTrack()) {
151                 tracks << (*it)->getId();
152             }
153             ++it;
154         }
155         if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) {
156             pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count());
157         }
158         QMapIterator <int, QString>st(audioTargets);
159         while (st.hasNext()) {
160             st.next();
161             if (tracks.isEmpty()) {
162                 break;
163             }
164             audioTracks.insert(tracks.takeLast(), st.key());
165         }
166     }
167     emit hasAudioTargetChanged();
168     emit hasVideoTargetChanged();
169     setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack);
170     setAudioTarget(audioTracks);
171 }
172 
getModel() const173 std::shared_ptr<TimelineItemModel> TimelineController::getModel() const
174 {
175     return m_model;
176 }
177 
setRoot(QQuickItem * root)178 void TimelineController::setRoot(QQuickItem *root)
179 {
180     m_root = root;
181     m_ready = true;
182 }
183 
tractor()184 Mlt::Tractor *TimelineController::tractor()
185 {
186     return m_model->tractor();
187 }
188 
trackProducer(int tid)189 Mlt::Producer TimelineController::trackProducer(int tid)
190 {
191     return *(m_model->getTrackById(tid).get());
192 }
193 
scaleFactor() const194 double TimelineController::scaleFactor() const
195 {
196     return m_scale;
197 }
198 
getTrackNameFromMltIndex(int trackPos)199 const QString TimelineController::getTrackNameFromMltIndex(int trackPos)
200 {
201     if (trackPos == -1) {
202         return i18n("unknown");
203     }
204     if (trackPos == 0) {
205         return i18n("Black");
206     }
207     return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1));
208 }
209 
getTrackNameFromIndex(int trackIndex)210 const QString TimelineController::getTrackNameFromIndex(int trackIndex)
211 {
212     QString trackName = m_model->getTrackFullName(trackIndex);
213     return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName;
214 }
215 
getTrackNames(bool videoOnly)216 QMap<int, QString> TimelineController::getTrackNames(bool videoOnly)
217 {
218     QMap<int, QString> names;
219     for (const auto &track : m_model->m_iteratorTable) {
220         if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) {
221             continue;
222         }
223         QString trackName = m_model->getTrackFullName(track.first);
224         names[m_model->getTrackMltIndex(track.first)] = trackName;
225     }
226     return names;
227 }
228 
setScaleFactorOnMouse(double scale,bool zoomOnMouse)229 void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse)
230 {
231     if (m_root) {
232         m_root->setProperty("zoomOnMouse", zoomOnMouse ? qBound(0, getMousePos(), duration()) : -1);
233         m_scale = scale;
234         emit scaleFactorChanged();
235     } else {
236         qWarning("Timeline root not created, impossible to zoom in");
237     }
238 }
239 
setScaleFactor(double scale)240 void TimelineController::setScaleFactor(double scale)
241 {
242     m_scale = scale;
243     // Update mainwindow's zoom slider
244     emit updateZoom(scale);
245     // inform qml
246     emit scaleFactorChanged();
247 }
248 
duration() const249 int TimelineController::duration() const
250 {
251     return m_duration;
252 }
253 
fullDuration() const254 int TimelineController::fullDuration() const
255 {
256     return m_duration + TimelineModel::seekDuration;
257 }
258 
checkDuration()259 void TimelineController::checkDuration()
260 {
261     int currentLength = m_model->duration();
262     if (currentLength != m_duration) {
263         m_duration = currentLength;
264         emit durationChanged();
265     }
266 }
267 
selectedTrack() const268 int TimelineController::selectedTrack() const
269 {
270     std::unordered_set<int> sel = m_model->getCurrentSelection();
271     if (sel.empty()) return -1;
272     std::vector<std::pair<int, int>> selected_tracks; // contains pairs of (track position, track id) for each selected item
273     for (int s : sel) {
274         int tid = m_model->getItemTrackId(s);
275         selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid);
276     }
277     // sort by track position
278     std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; });
279     return selected_tracks.front().second;
280 }
281 
selectCurrentItem(ObjectType type,bool select,bool addToCurrent,bool showErrorMsg)282 bool TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent, bool showErrorMsg)
283 {
284     int currentClip = -1;
285     if (type == ObjectType::TimelineClip) {
286         currentClip = m_activeTrack == -2 ? m_model->getSubtitleByPosition(pCore->getTimelinePosition()) : m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
287     } else if (type == ObjectType::TimelineComposition) {
288         currentClip =  m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition());
289     } else if (type == ObjectType::TimelineMix) {
290         if (m_activeTrack >= 0) {
291             currentClip = m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition());
292         }
293         if (currentClip > -1) {
294             if (m_model->hasClipEndMix(currentClip)) {
295                 int mixPartner = m_model->getTrackById_const(m_activeTrack)->getSecondMixPartner(currentClip);
296                 int clipEnd = m_model->getClipPosition(currentClip) + m_model->getClipPlaytime(currentClip);
297                 int mixStart = clipEnd - m_model->getMixDuration(mixPartner);
298                 if (mixStart < pCore->getTimelinePosition() && pCore->getTimelinePosition() < clipEnd) {
299                     if (select) {
300                         m_model->requestMixSelection(mixPartner);
301                         return true;
302                     } else if (selectedMix() == mixPartner) {
303                         m_model->requestClearSelection();
304                         return true;
305                     }
306                 }
307             }
308             int delta = pCore->getTimelinePosition() - m_model->getClipPosition(currentClip);
309             if (m_model->getMixDuration(currentClip) >= delta) {
310                 if (select) {
311                     m_model->requestMixSelection(currentClip);
312                     return true;
313                 } else if (selectedMix() == currentClip) {
314                     m_model->requestClearSelection();
315                     return true;
316                 }
317                 return true;
318             } else {
319                 currentClip = -1;
320             }
321          }
322     }
323 
324     if (currentClip == -1) {
325         if (showErrorMsg) {
326             pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500);
327         }
328         return false;
329     }
330     if (!select) {
331         m_model->requestRemoveFromSelection(currentClip);
332     } else {
333         bool grouped = m_model->m_groups->isInGroup(currentClip);
334         m_model->requestAddToSelection(currentClip, !addToCurrent);
335         if (grouped) {
336             // If part of a group, ensure the effect/composition stack displays the selected item's properties
337             showAsset(currentClip);
338         }
339     }
340     return true;
341 }
342 
selection() const343 QList<int> TimelineController::selection() const
344 {
345     if (!m_root) return QList<int>();
346     std::unordered_set<int> sel = m_model->getCurrentSelection();
347     QList<int> items;
348     for (int id : sel) {
349         items << id;
350     }
351     return items;
352 }
353 
selectedMix() const354 int TimelineController::selectedMix() const
355 {
356     return m_model->m_selectedMix;
357 }
358 
selectItems(const QList<int> & ids)359 void TimelineController::selectItems(const QList<int> &ids)
360 {
361     std::unordered_set<int> ids_s(ids.begin(), ids.end());
362     m_model->requestSetSelection(ids_s);
363 }
364 
setScrollPos(int pos)365 void TimelineController::setScrollPos(int pos)
366 {
367     if (pos > 0 && m_root) {
368         QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos));
369     }
370 }
371 
resetView()372 void TimelineController::resetView()
373 {
374     m_model->_resetView();
375     if (m_root) {
376         QMetaObject::invokeMethod(m_root, "updatePalette");
377     }
378     emit colorsChanged();
379 }
380 
snap()381 bool TimelineController::snap()
382 {
383     return KdenliveSettings::snaptopoints();
384 }
385 
ripple()386 bool TimelineController::ripple()
387 {
388     return false;
389 }
390 
scrub()391 bool TimelineController::scrub()
392 {
393     return false;
394 }
395 
insertClip(int tid,int position,const QString & data_str,bool logUndo,bool refreshView,bool useTargets)396 int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets)
397 {
398     int id;
399     if (tid == -1) {
400         tid = m_activeTrack;
401     }
402     if (position == -1) {
403         position = pCore->getTimelinePosition();
404     }
405     if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) {
406         id = -1;
407     }
408     return id;
409 }
410 
insertClips(int tid,int position,const QStringList & binIds,bool logUndo,bool refreshView)411 QList<int> TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView)
412 {
413     QList<int> clipIds;
414     if (tid == -1) {
415         tid = m_activeTrack;
416     }
417     if (position == -1) {
418         position = pCore->getTimelinePosition();
419     }
420     TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView);
421     // 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.
422     return clipIds;
423 }
424 
insertNewMix(int tid,int position,const QString transitionId)425 void TimelineController::insertNewMix(int tid, int position, const QString transitionId)
426 {
427     int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
428     if (clipId > 0) {
429         m_model->mixClip(clipId, transitionId);
430     }
431 }
432 
insertNewComposition(int tid,int position,const QString & transitionId,bool logUndo)433 int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo)
434 {
435     int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position);
436     if (clipId > 0) {
437         int minimum = m_model->getClipPosition(clipId);
438         return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo);
439     }
440     return insertComposition(tid, position, transitionId, logUndo);
441 }
442 
insertNewComposition(int tid,int clipId,int offset,const QString & transitionId,bool logUndo)443 int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo)
444 {
445     int id;
446     int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset;
447     int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration());
448     int endPos = minimumPos + clip_duration;
449     int position = minimumPos;
450     int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration()));
451     int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid);
452     bool revert = offset > clip_duration / 2;
453     int bottomId = 0;
454     if (lowerVideoTrackId > 0) {
455         bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset);
456     }
457     if (bottomId <= 0) {
458         // No video track underneath
459         if (offset < duration && duration < 2 * clip_duration) {
460             // Composition dropped close to start, keep default composition duration
461         } else if (clip_duration - offset < duration * 1.2  && duration < 2 * clip_duration) {
462             // Composition dropped close to end, keep default composition duration
463             position = endPos - duration;
464         } else {
465             // Use full clip length for duration
466             duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position);
467         }
468     } else {
469         duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position));
470         QPair<int, int> bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime());
471         if (bottom.first > minimumPos) {
472             // Lower clip is after top clip
473             if (position + offset > bottom.first) {
474                 int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first);
475                 if (test_duration > 0) {
476                     offset -= (bottom.first - position);
477                     position = bottom.first;
478                     duration = test_duration;
479                     revert = position > minimumPos;
480                 }
481             }
482         } else if (position >= bottom.first) {
483             // Lower clip is before or at same pos as top clip
484             int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position);
485             if (test_duration > 0) {
486                 duration = qMin(test_duration, clip_duration);
487             }
488         }
489     }
490     int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration());
491     bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition;
492     if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) {
493         duration = defaultLength;
494     } else if (duration <= 1) {
495         // if suggested composition duration is lower than 4 frames, use default
496         duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
497         if (minimumPos + clip_duration - position < 3) {
498             position = minimumPos + clip_duration - duration;
499         }
500     }
501     QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos);
502     position = finalPos.first;
503     duration = finalPos.second;
504 
505     std::unique_ptr<Mlt::Properties> props(nullptr);
506     if (revert) {
507         props = std::make_unique<Mlt::Properties>();
508         if (transitionId == QLatin1String("dissolve")) {
509             props->set("reverse", 1);
510         } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) {
511             props->set("invert", 1);
512         } else if (transitionId == QLatin1String("wipe")) {
513             props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%");
514         }
515     }
516     if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) {
517         id = -1;
518         pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500);
519     }
520     return id;
521 }
522 
isOnCut(int cid) const523 int TimelineController::isOnCut(int cid) const
524 {
525     Q_ASSERT(m_model->isComposition(cid));
526     int tid = m_model->getItemTrackId(cid);
527     return m_model->getTrackById_const(tid)->isOnCut(cid);
528 }
529 
insertComposition(int tid,int position,const QString & transitionId,bool logUndo)530 int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo)
531 {
532     int id;
533     int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration());
534     if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) {
535         id = -1;
536     }
537     return id;
538 }
539 
deleteSelectedClips()540 void TimelineController::deleteSelectedClips()
541 {
542     if (dragOperationRunning()) {
543         // Don't allow timeline operation while drag in progress
544         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
545         return;
546     }
547     auto sel = m_model->getCurrentSelection();
548     if (sel.empty()) {
549         // Check if a mix is selected
550         if (m_model->m_selectedMix > -1 && m_model->isClip(m_model->m_selectedMix)) {
551             m_model->removeMix(m_model->m_selectedMix);
552             m_model->clearAssetView(m_model->m_selectedMix);
553             m_model->requestClearSelection(true);
554         }
555         return;
556     }
557     // only need to delete the first item, the others will be deleted in cascade
558     if (m_model->m_editMode == TimelineMode::InsertEdit) {
559         // In insert mode, perform an extract operation (don't leave gaps)
560         extract(*sel.begin());
561     }
562     else {
563         m_model->requestItemDeletion(*sel.begin());
564     }
565 }
566 
getMainSelectedItem(bool restrictToCurrentPos,bool allowComposition)567 int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition)
568 {
569     auto sel = m_model->getCurrentSelection();
570     if (sel.empty() || sel.size() > 2) {
571         return -1;
572     }
573     int itemId = *(sel.begin());
574     if (sel.size() == 2) {
575         int parentGroup = m_model->m_groups->getRootId(itemId);
576         if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) {
577             return -1;
578         }
579     }
580     if (!restrictToCurrentPos) {
581         if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) {
582             return itemId;
583         }
584     }
585     if (m_model->isClip(itemId)) {
586         int position = pCore->getTimelinePosition();
587         int start = m_model->getClipPosition(itemId);
588         int end = start + m_model->getClipPlaytime(itemId);
589         if (position >= start && position <= end) {
590             return itemId;
591         }
592     }
593     return -1;
594 }
595 
copyItem()596 void TimelineController::copyItem()
597 {
598     std::unordered_set<int> selectedIds = m_model->getCurrentSelection();
599     if (selectedIds.empty()) {
600         return;
601     }
602     int clipId = *(selectedIds.begin());
603     QString copyString = TimelineFunctions::copyClips(m_model, selectedIds);
604     QClipboard *clipboard = QApplication::clipboard();
605     clipboard->setText(copyString);
606     m_root->setProperty("copiedClip", clipId);
607     m_model->requestSetSelection(selectedIds);
608 }
609 
pasteItem(int position,int tid)610 bool TimelineController::pasteItem(int position, int tid)
611 {
612     QClipboard *clipboard = QApplication::clipboard();
613     QString txt = clipboard->text();
614     if (tid == -1) {
615         tid = m_activeTrack;
616     }
617     if (position == -1) {
618         position = getMenuOrTimelinePos();
619     }
620     return TimelineFunctions::pasteClips(m_model, txt, tid, position);
621 }
622 
triggerAction(const QString & name)623 void TimelineController::triggerAction(const QString &name)
624 {
625     pCore->triggerAction(name);
626 }
627 
actionText(const QString & name)628 const QString TimelineController::actionText(const QString &name)
629 {
630     return pCore->actionText(name);
631 }
632 
timecode(int frames) const633 QString TimelineController::timecode(int frames) const
634 {
635     return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
636 }
637 
framesToClock(int frames) const638 QString TimelineController::framesToClock(int frames) const
639 {
640     return m_model->tractor()->frames_to_time(frames, mlt_time_clock);
641 }
642 
simplifiedTC(int frames) const643 QString TimelineController::simplifiedTC(int frames) const
644 {
645     if (KdenliveSettings::frametimecode()) {
646         return QString::number(frames);
647     }
648     QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df);
649     return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s;
650 }
651 
showThumbnails() const652 bool TimelineController::showThumbnails() const
653 {
654     return KdenliveSettings::videothumbnails();
655 }
656 
showAudioThumbnails() const657 bool TimelineController::showAudioThumbnails() const
658 {
659     return KdenliveSettings::audiothumbnails();
660 }
661 
showMarkers() const662 bool TimelineController::showMarkers() const
663 {
664     return KdenliveSettings::showmarkers();
665 }
666 
audioThumbFormat() const667 bool TimelineController::audioThumbFormat() const
668 {
669     return KdenliveSettings::displayallchannels();
670 }
671 
audioThumbNormalize() const672 bool TimelineController::audioThumbNormalize() const
673 {
674     return KdenliveSettings::normalizechannels();
675 }
676 
showWaveforms() const677 bool TimelineController::showWaveforms() const
678 {
679     return KdenliveSettings::audiothumbnails();
680 }
681 
addTrack(int tid)682 void TimelineController::addTrack(int tid)
683 {
684     if (tid == -1) {
685         tid = m_activeTrack;
686     }
687     QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow());
688     if (d->exec() == QDialog::Accepted) {
689         bool audioRecTrack = d->addRecTrack();
690         bool addAVTrack = d->addAVTrack();
691         int tracksCount = d->tracksCount();
692         Fun undo = []() { return true; };
693         Fun redo = []() { return true; };
694         bool result = true;
695         for (int ix = 0; ix < tracksCount; ++ix) {
696             int newTid;
697             result = m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack(), undo, redo);
698             if (result) {
699                 if (addAVTrack) {
700                     int newTid2;
701                     int mirrorPos = 0;
702                     int mirrorId = m_model->getMirrorAudioTrackId(newTid);
703                     if (mirrorId > -1) {
704                         mirrorPos = m_model->getTrackMltIndex(mirrorId);
705                     }
706                     result = m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true, undo, redo);
707                 }
708                 if (audioRecTrack) {
709                     m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1"));
710                 }
711             } else {
712                 break;
713             }
714         }
715         if (result) {
716             pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18nc("@action", "Insert Tracks") : i18nc("@action", "Insert Track"));
717         } else {
718             pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
719             undo();
720         }
721     }
722 }
723 
deleteMultipleTracks(int tid)724 void TimelineController::deleteMultipleTracks(int tid)
725 {
726     Fun undo = []() { return true; };
727     Fun redo = []() { return true; };
728     bool result = true;
729     QPointer<TrackDialog> d = new TrackDialog(m_model, tid, qApp->activeWindow(), true, m_activeTrack);
730     if (tid == -1) {
731         tid = m_activeTrack;
732     }
733     if (d->exec() == QDialog::Accepted) {
734         QList<int> allIds = d->toDeleteTrackIds();
735         for (int selectedTrackIx : qAsConst(allIds)) {
736             result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo);
737             if (!result) {
738                 break;
739             }
740             if (m_activeTrack == -1) {
741                 setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1));
742             }
743         }
744         if (result) {
745           pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track"));
746         }
747     }
748 }
749 
switchTrackRecord(int tid)750 void TimelineController::switchTrackRecord(int tid)
751 {
752     if (tid == -1) {
753         tid = m_activeTrack;
754     }
755     if (!m_model->getTrackById_const(tid)->isAudioTrack()) {
756         pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500);
757     }
758     int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt();
759     if (recDisplayed == 1) {
760         // Disable rec controls
761         m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0"));
762     } else {
763         // Enable rec controls
764         m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1"));
765     }
766     QModelIndex ix = m_model->makeTrackIndexFromID(tid);
767     if (ix.isValid()) {
768         emit m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole});
769     }
770 }
771 
checkTrackDeletion(int selectedTrackIx)772 void TimelineController::checkTrackDeletion(int selectedTrackIx)
773 {
774     if (m_activeTrack == selectedTrackIx) {
775             // Make sure we don't keep an index on a deleted track
776             m_activeTrack = -1;
777             emit activeTrackChanged();
778         }
779         if (m_model->m_audioTarget.contains(selectedTrackIx)) {
780             QMap<int, int> selection = m_model->m_audioTarget;
781             selection.remove(selectedTrackIx);
782             setAudioTarget(selection);
783         }
784         if (m_model->m_videoTarget == selectedTrackIx) {
785             setVideoTarget(-1);
786         }
787         if (m_lastAudioTarget.contains(selectedTrackIx)) {
788             m_lastAudioTarget.remove(selectedTrackIx);
789             emit lastAudioTargetChanged();
790         }
791         if (m_lastVideoTarget == selectedTrackIx) {
792             m_lastVideoTarget = -1;
793             emit lastVideoTargetChanged();
794         }
795 }
796 
showConfig(int page,int tab)797 void TimelineController::showConfig(int page, int tab)
798 {
799     emit pCore->showConfigDialog(page, tab);
800 }
801 
gotoNextSnap()802 void TimelineController::gotoNextSnap()
803 {
804     if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
805         m_snapStackIndex = pCore->undoIndex();
806         m_activeSnaps.clear();
807         m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
808         m_activeSnaps.push_back(m_zone.x());
809         m_activeSnaps.push_back(m_zone.y() - 1);
810     }
811     int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps);
812     if (nextSnap > pCore->getTimelinePosition()) {
813         setPosition(nextSnap);
814     }
815 }
816 
gotoPreviousSnap()817 void TimelineController::gotoPreviousSnap()
818 {
819     if (pCore->getTimelinePosition() > 0) {
820         if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) {
821             m_snapStackIndex = pCore->undoIndex();
822             m_activeSnaps.clear();
823             m_activeSnaps = pCore->currentDoc()->getGuideModel()->getSnapPoints();
824             m_activeSnaps.push_back(m_zone.x());
825             m_activeSnaps.push_back(m_zone.y() - 1);
826         }
827         setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps));
828     }
829 }
830 
gotoNextGuide()831 void TimelineController::gotoNextGuide()
832 {
833     QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
834     int pos = pCore->getTimelinePosition();
835     double fps = pCore->getCurrentFps();
836     for (auto &guide : guides) {
837         if (guide.time().frames(fps) > pos) {
838             setPosition(guide.time().frames(fps));
839             return;
840         }
841     }
842     setPosition(m_duration - 1);
843 }
844 
gotoPreviousGuide()845 void TimelineController::gotoPreviousGuide()
846 {
847     if (pCore->getTimelinePosition() > 0) {
848         QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getAllMarkers();
849         int pos = pCore->getTimelinePosition();
850         double fps = pCore->getCurrentFps();
851         int lastGuidePos = 0;
852         for (auto &guide : guides) {
853             if (guide.time().frames(fps) >= pos) {
854                 setPosition(lastGuidePos);
855                 return;
856             }
857             lastGuidePos = guide.time().frames(fps);
858         }
859         setPosition(lastGuidePos);
860     }
861 }
862 
groupSelection()863 void TimelineController::groupSelection()
864 {
865     if (dragOperationRunning()) {
866         // Don't allow timeline operation while drag in progress
867         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
868         return;
869     }
870     const auto selection = m_model->getCurrentSelection();
871     if (selection.size() < 2) {
872         pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500);
873         return;
874     }
875     m_model->requestClearSelection();
876     m_model->requestClipsGroup(selection);
877     m_model->requestSetSelection(selection);
878 }
879 
unGroupSelection(int cid)880 void TimelineController::unGroupSelection(int cid)
881 {
882     if (dragOperationRunning()) {
883         // Don't allow timeline operation while drag in progress
884         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
885         return;
886     }
887     auto ids = m_model->getCurrentSelection();
888     // ask to unselect if needed
889     m_model->requestClearSelection();
890     if (cid > -1) {
891         ids.insert(cid);
892     }
893     if (!ids.empty()) {
894         m_model->requestClipsUngroup(ids);
895     }
896 }
897 
dragOperationRunning()898 bool TimelineController::dragOperationRunning()
899 {
900     QVariant returnedValue;
901     QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue));
902     return returnedValue.toBool();
903 }
904 
trimmingActive()905 bool TimelineController::trimmingActive()
906 {
907     ToolType::ProjectTool tool = pCore->window()->getCurrentTimeline()->activeTool();
908     return tool == ToolType::SlideTool || tool == ToolType::SlipTool || tool == ToolType::RippleTool || tool == ToolType::RollTool;
909 }
910 
setInPoint(bool ripple)911 void TimelineController::setInPoint(bool ripple)
912 {
913     if (dragOperationRunning()) {
914         // Don't allow timeline operation while drag in progress
915         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
916         qDebug()<< "Cannot operate while dragging";
917         return;
918     }
919     auto requestResize = [this, ripple](int id, int size){
920         if (ripple) {
921             m_model->requestItemRippleResize(m_model, id, size, false, true, 0, false);
922             setPosition(m_model->getItemPosition(id));
923         } else {
924             m_model->requestItemResize(id, size, false, true, 0, false);
925         }
926     };
927     int cursorPos = pCore->getTimelinePosition();
928     const auto selection = m_model->getCurrentSelection();
929     bool selectionFound = false;
930     if (!selection.empty()) {
931         for (int id : selection) {
932             int start = m_model->getItemPosition(id);
933             if (start == cursorPos) {
934                 continue;
935             }
936             int size = start + m_model->getItemPlaytime(id) - cursorPos;
937             requestResize(id, size);
938             selectionFound = true;
939         }
940     }
941     if (!selectionFound) {
942         if (m_activeTrack >= 0) {
943             int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
944             if (cid < 0) {
945                 // Check first item after timeline position
946                 int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos);
947                 if (maximumSpace < INT_MAX) {
948                     cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1);
949                 }
950             }
951             if (cid >= 0) {
952                 int start = m_model->getItemPosition(cid);
953                 if (start != cursorPos) {
954                     int size = start + m_model->getItemPlaytime(cid) - cursorPos;
955                     requestResize(cid, size);
956                     selectionFound = true;
957                 }
958             }
959         } else if (m_activeTrack == -2) {
960             // Subtitle track
961             auto subtitleModel = pCore->getSubtitleModel();
962             if (subtitleModel) {
963                 int sid = -1;
964                 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
965                 if (sids.empty()) {
966                     sids = subtitleModel->getItemsInRange(cursorPos, -1);
967                     for (int s : sids) {
968                         if (sid == -1 || subtitleModel->getStartPosForId(s) < subtitleModel->getStartPosForId(sid)) {
969                             sid = s;
970                         }
971                     }
972                 } else {
973                     sid = *sids.begin();
974                 }
975                 if (sid > -1) {
976                     int start = m_model->getItemPosition(sid);
977                     if (start != cursorPos) {
978                         int size = start + m_model->getItemPlaytime(sid) - cursorPos;
979                         requestResize(sid, size);
980                         selectionFound = true;
981                     }
982                 }
983             }
984         }
985         if (!selectionFound) {
986             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
987         }
988     }
989 }
990 
setOutPoint(bool ripple)991 void TimelineController::setOutPoint(bool ripple)
992 {
993     if (dragOperationRunning()) {
994         // Don't allow timeline operation while drag in progress
995         pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage);
996         qDebug() << "Cannot operate while dragging";
997         return;
998     }
999     auto requestResize = [this, ripple](int id, int size){
1000         if (ripple) {
1001             m_model->requestItemRippleResize(m_model, id, size, true, true, 0, false);
1002         } else {
1003             m_model->requestItemResize(id, size, true, true, 0, false);
1004         }
1005     };
1006     int cursorPos = pCore->getTimelinePosition();
1007     const auto selection = m_model->getCurrentSelection();
1008     bool selectionFound = false;
1009     if (!selection.empty()) {
1010         for (int id : selection) {
1011             int start = m_model->getItemPosition(id);
1012             if (start + m_model->getItemPlaytime(id) == cursorPos) {
1013                 continue;
1014             }
1015             int size = cursorPos - start;
1016             requestResize(id, size);
1017             selectionFound = true;
1018         }
1019     }
1020     if (!selectionFound) {
1021         if (m_activeTrack >= 0) {
1022             int cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1023             if (cid < 0 || cursorPos == m_model->getItemPosition(cid)) {
1024                 // If no clip found at cursor pos or we are at the first frame of a clip, try to find previous clip
1025                 // Check first item before timeline position
1026                 // If we are at a clip start, check space before this clip
1027                 int offset = cid >= 0 ? 1 : 0;
1028                 int previousPos = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos - offset);
1029                 cid = m_model->getClipByPosition(m_activeTrack, qMax(0, previousPos - 1));
1030             }
1031             if (cid >= 0) {
1032                 int start = m_model->getItemPosition(cid);
1033                 if (start + m_model->getItemPlaytime(cid) != cursorPos) {
1034                     int size = cursorPos - start;
1035                     requestResize(cid, size);
1036                     selectionFound = true;
1037                 }
1038             }
1039         } else if (m_activeTrack == -2) {
1040             // Subtitle track
1041             auto subtitleModel = pCore->getSubtitleModel();
1042             if (subtitleModel) {
1043                 int sid = -1;
1044                 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos);
1045                 if (sids.empty()) {
1046                     sids = subtitleModel->getItemsInRange(0, cursorPos);
1047                     for (int s : sids) {
1048                         if (sid == -1 || subtitleModel->getSubtitleEnd(s) > subtitleModel->getSubtitleEnd(sid)) {
1049                             sid = s;
1050                         }
1051                     }
1052                 } else {
1053                     sid = *sids.begin();
1054                 }
1055                 if (sid > -1) {
1056                     int start = m_model->getItemPosition(sid);
1057                     if (start + m_model->getItemPlaytime(sid) != cursorPos) {
1058                         int size = cursorPos - start;
1059                         requestResize(sid, size);
1060                         selectionFound = true;
1061                     }
1062                 }
1063             }
1064         }
1065         if (!selectionFound) {
1066             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1067         }
1068     }
1069 }
1070 
editMarker(int cid,int position)1071 void TimelineController::editMarker(int cid, int position)
1072 {
1073     if (cid == -1) {
1074         cid = getMainSelectedClip();
1075         if (cid == -1) {
1076             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1077             return;
1078         }
1079     }
1080     Q_ASSERT(m_model->isClip(cid));
1081     double speed = m_model->getClipSpeed(cid);
1082     if (position == -1) {
1083         // Calculate marker position relative to timeline cursor
1084         position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1085         position = int(position * speed);
1086     }
1087     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1088         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1089         return;
1090     }
1091     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1092     if (clip->getMarkerModel()->hasMarker(position)) {
1093         GenTime pos(position, pCore->getCurrentFps());
1094         clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get());
1095     } else {
1096         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1097     }
1098 }
1099 
addMarker(int cid,int position)1100 void TimelineController::addMarker(int cid, int position)
1101 {
1102     if (cid == -1) {
1103         cid = getMainSelectedClip();
1104         if (cid == -1) {
1105             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1106             return;
1107         }
1108     }
1109     Q_ASSERT(m_model->isClip(cid));
1110     double speed = m_model->getClipSpeed(cid);
1111     if (position == -1) {
1112         // Calculate marker position relative to timeline cursor
1113         position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1114         position = int(position * speed);
1115     }
1116     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1117         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1118         return;
1119     }
1120     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1121     GenTime pos(position, pCore->getCurrentFps());
1122     clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get());
1123 }
1124 
getMainSelectedClip()1125 int TimelineController::getMainSelectedClip()
1126 {
1127     int clipId = m_root->property("mainItemId").toInt();
1128     if (clipId == -1 || !isInSelection(clipId)) {
1129         std::unordered_set<int> sel = m_model->getCurrentSelection();
1130         for (int i : sel) {
1131             if (m_model->isClip(i)) {
1132                 clipId = i;
1133                 break;
1134             }
1135         }
1136     }
1137     return m_model->isClip(clipId) ? clipId : -1;
1138 }
1139 
addQuickMarker(int cid,int position)1140 void TimelineController::addQuickMarker(int cid, int position)
1141 {
1142     if (cid == -1) {
1143         cid = getMainSelectedClip();
1144         if (cid == -1) {
1145             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1146             return;
1147         }
1148     }
1149     Q_ASSERT(m_model->isClip(cid));
1150     double speed = m_model->getClipSpeed(cid);
1151     if (position == -1) {
1152         // Calculate marker position relative to timeline cursor
1153         position = pCore->getTimelinePosition() - m_model->getClipPosition(cid);
1154         position = int(position * speed);
1155     }
1156     if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) {
1157         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1158         return;
1159     }
1160     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1161     GenTime pos(position, pCore->getCurrentFps());
1162     CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
1163     clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
1164 }
1165 
deleteMarker(int cid,int position)1166 void TimelineController::deleteMarker(int cid, int position)
1167 {
1168     if (cid == -1) {
1169         cid = getMainSelectedClip();
1170         if (cid == -1) {
1171             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1172             return;
1173         }
1174     }
1175     Q_ASSERT(m_model->isClip(cid));
1176     double speed = m_model->getClipSpeed(cid);
1177     if (position == -1) {
1178         // Calculate marker position relative to timeline cursor
1179         position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid);
1180         position = int(position * speed);
1181     }
1182     if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) {
1183         pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500);
1184         return;
1185     }
1186     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1187     GenTime pos(position, pCore->getCurrentFps());
1188     clip->getMarkerModel()->removeMarker(pos);
1189 }
1190 
deleteAllMarkers(int cid)1191 void TimelineController::deleteAllMarkers(int cid)
1192 {
1193     if (cid == -1) {
1194         cid = getMainSelectedClip();
1195         if (cid == -1) {
1196             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
1197             return;
1198         }
1199     }
1200     Q_ASSERT(m_model->isClip(cid));
1201     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid));
1202     clip->getMarkerModel()->removeAllMarkers();
1203 }
1204 
editGuide(int frame)1205 void TimelineController::editGuide(int frame)
1206 {
1207     if (frame == -1) {
1208         frame = pCore->getTimelinePosition();
1209     }
1210     auto guideModel = pCore->currentDoc()->getGuideModel();
1211     GenTime pos(frame, pCore->getCurrentFps());
1212     guideModel->editMarkerGui(pos, qApp->activeWindow(), false);
1213 }
1214 
moveGuide(int frame,int newFrame)1215 void TimelineController::moveGuide(int frame, int newFrame)
1216 {
1217     auto guideModel = pCore->currentDoc()->getGuideModel();
1218     GenTime pos(frame, pCore->getCurrentFps());
1219     GenTime newPos(newFrame, pCore->getCurrentFps());
1220     guideModel->editMarker(pos, newPos);
1221 }
1222 
moveGuideWithoutUndo(int mid,int newFrame)1223 void TimelineController::moveGuideWithoutUndo(int mid, int newFrame)
1224 {
1225     auto guideModel = pCore->currentDoc()->getGuideModel();
1226     GenTime newPos(newFrame, pCore->getCurrentFps());
1227     guideModel->moveMarker(mid, newPos);
1228 }
1229 
moveGuidesInRange(int start,int end,int offset)1230 bool TimelineController::moveGuidesInRange(int start, int end, int offset)
1231 {
1232     std::function<bool(void)> undo = []() { return true; };
1233     std::function<bool(void)> redo = []() { return true; };
1234     bool final = false;
1235     final = moveGuidesInRange(start, end, offset, undo, redo);
1236     if (final) {
1237         if (offset > 0) {
1238             pCore->pushUndo(undo, redo, i18n("Insert space"));
1239         } else {
1240             pCore->pushUndo(undo, redo, i18n("Remove space"));
1241         }
1242         return true;
1243     } else {
1244         undo();
1245     }
1246     return false;
1247 }
1248 
moveGuidesInRange(int start,int end,int offset,Fun & undo,Fun & redo)1249 bool TimelineController::moveGuidesInRange(int start, int end, int offset, Fun &undo, Fun &redo)
1250 {
1251     GenTime fromPos(start, pCore->getCurrentFps());
1252     GenTime toPos(start + offset, pCore->getCurrentFps());
1253     QList<CommentedTime> guides = pCore->currentDoc()->getGuideModel()->getMarkersInRange(start, end);
1254     return pCore->currentDoc()->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo);
1255 }
1256 
switchGuide(int frame,bool deleteOnly,bool showGui)1257 void TimelineController::switchGuide(int frame, bool deleteOnly, bool showGui)
1258 {
1259     bool markerFound = false;
1260     if (frame == -1) {
1261         frame = pCore->getTimelinePosition();
1262     }
1263     CommentedTime marker = pCore->currentDoc()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound);
1264     if (!markerFound) {
1265         if (deleteOnly) {
1266             pCore->displayMessage(i18n("No guide found at current position"), ErrorMessage, 500);
1267             return;
1268         }
1269         GenTime pos(frame, pCore->getCurrentFps());
1270 
1271         if(showGui) {
1272             pCore->currentDoc()->getGuideModel()->editMarkerGui(pos, qApp->activeWindow(), true);
1273         } else {
1274             pCore->currentDoc()->getGuideModel()->addMarker(pos, i18n("guide"));
1275         }
1276     } else {
1277         pCore->currentDoc()->getGuideModel()->removeMarker(marker.time());
1278     }
1279 }
1280 
addAsset(const QVariantMap & data)1281 void TimelineController::addAsset(const QVariantMap &data)
1282 {
1283     QString effect = data.value(QStringLiteral("kdenlive/effect")).toString();
1284     const auto selection = m_model->getCurrentSelection();
1285     bool audioEffect = EffectsRepository::get()->isAudioEffect(effect);
1286     if (!selection.empty()) {
1287         QList<int> effectSelection;
1288         for (int id : selection) {
1289             if (m_model->isClip(id) && audioEffect == m_model->m_allClips.at(id)->isAudioOnly()) {
1290                 effectSelection << id;
1291             }
1292         }
1293         bool foundMatch = false;
1294         for (int id : qAsConst(effectSelection)) {
1295             if (m_model->addClipEffect(id, effect, false)) {
1296                 foundMatch = true;
1297             }
1298         }
1299         if (!foundMatch) {
1300             QString effectName = EffectsRepository::get()->getName(effect);
1301             pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500);
1302         }
1303     } else {
1304         pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
1305     }
1306 }
1307 
requestRefresh()1308 void TimelineController::requestRefresh()
1309 {
1310     pCore->requestMonitorRefresh();
1311 }
1312 
showAsset(int id)1313 void TimelineController::showAsset(int id)
1314 {
1315     if (m_model->isComposition(id)) {
1316         emit showTransitionModel(id, m_model->getCompositionParameterModel(id));
1317     } else if (m_model->isClip(id)) {
1318         QModelIndex clipIx = m_model->makeClipIndexFromID(id);
1319         QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString();
1320         bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt();
1321         qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes;
1322         emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes);
1323     } else if (m_model->isSubTitle(id)) {
1324         emit showSubtitle(id);
1325     }
1326 }
1327 
showTrackAsset(int trackId)1328 void TimelineController::showTrackAsset(int trackId)
1329 {
1330     emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false);
1331 }
1332 
adjustAllTrackHeight(int trackId,int height)1333 void TimelineController::adjustAllTrackHeight(int trackId, int height)
1334 {
1335     bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1336     auto it = m_model->m_allTracks.cbegin();
1337     while (it != m_model->m_allTracks.cend()) {
1338         int target_track = (*it)->getId();
1339         if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1340             m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height));
1341         }
1342         ++it;
1343     }
1344     int tracksCount = m_model->getTracksCount();
1345     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1346     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1347     emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1348 }
1349 
collapseAllTrackHeight(int trackId,bool collapse,int collapsedHeight)1350 void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight)
1351 {
1352     bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack();
1353     auto it = m_model->m_allTracks.cbegin();
1354     while (it != m_model->m_allTracks.cend()) {
1355         int target_track = (*it)->getId();
1356         if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) {
1357             if (collapse) {
1358                 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight));
1359             } else {
1360                 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0"));
1361             }
1362         }
1363         ++it;
1364     }
1365     int tracksCount = m_model->getTracksCount();
1366     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1367     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1368     emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1369 }
1370 
defaultTrackHeight(int trackId)1371 void TimelineController::defaultTrackHeight(int trackId)
1372 {
1373     if (trackId > -1) {
1374         m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1375         QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId);
1376         emit m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole});
1377         return;
1378     }
1379     auto it = m_model->m_allTracks.cbegin();
1380     while (it != m_model->m_allTracks.cend()) {
1381         int target_track = (*it)->getId();
1382         m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
1383         ++it;
1384     }
1385     int tracksCount = m_model->getTracksCount();
1386     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
1387     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
1388     emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
1389 }
1390 
setPosition(int position)1391 void TimelineController::setPosition(int position)
1392 {
1393     // Process seek request
1394     emit seeked(position);
1395 }
1396 
setAudioTarget(QMap<int,int> tracks)1397 void TimelineController::setAudioTarget(QMap<int, int> tracks)
1398 {
1399     // Clear targets before re-adding to trigger qml refresh
1400     m_model->m_audioTarget.clear();
1401     emit audioTargetChanged();
1402 
1403     if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) {
1404         return;
1405     }
1406 
1407     m_model->m_audioTarget = tracks;
1408     emit audioTargetChanged();
1409 }
1410 
switchAudioTarget(int trackId)1411 void TimelineController::switchAudioTarget(int trackId)
1412 {
1413     if (m_model->m_audioTarget.contains(trackId)) {
1414         m_model->m_audioTarget.remove(trackId);
1415     } else {
1416         //TODO: use track description
1417         if (m_model->m_binAudioTargets.count() == 1) {
1418             // Only one audio stream, remove previous and switch
1419             m_model->m_audioTarget.clear();
1420         }
1421         int ix = getFirstUnassignedStream();
1422         if (ix > -1) {
1423             m_model->m_audioTarget.insert(trackId, ix);
1424         }
1425     }
1426     emit audioTargetChanged();
1427 }
1428 
assignCurrentTarget(int index)1429 void TimelineController::assignCurrentTarget(int index)
1430 {
1431     if (m_activeTrack == -1 || !m_model->isTrack(m_activeTrack)) {
1432         pCore->displayMessage(i18n("No active track"), ErrorMessage, 500);
1433         return;
1434     }
1435     bool isAudio = m_model->isAudioTrack(m_activeTrack);
1436     if (isAudio) {
1437         // Select audio target stream
1438         if (index >= 0 && index < m_model->m_binAudioTargets.size()) {
1439             // activate requested stream
1440             int stream = m_model->m_binAudioTargets.keys().at(index);
1441             assignAudioTarget(m_activeTrack, stream);
1442         } else {
1443             // Remove audio target
1444             m_model->m_audioTarget.remove(m_activeTrack);
1445             emit audioTargetChanged();
1446         }
1447         return;
1448     } else {
1449         // Select video target stream
1450         setVideoTarget(m_activeTrack);
1451         return;
1452     }
1453 }
1454 
assignAudioTarget(int trackId,int stream)1455 void TimelineController::assignAudioTarget(int trackId, int stream)
1456 {
1457     QList <int> assignedStreams = m_model->m_audioTarget.values();
1458     if (assignedStreams.contains(stream)) {
1459         // This stream was assigned to another track, remove
1460         m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream));
1461     }
1462     //Remove and re-add target track to trigger a refresh in qml track headers
1463     m_model->m_audioTarget.remove(trackId);
1464     emit audioTargetChanged();
1465 
1466     m_model->m_audioTarget.insert(trackId, stream);
1467     emit audioTargetChanged();
1468 }
1469 
1470 
getFirstUnassignedStream() const1471 int TimelineController::getFirstUnassignedStream() const
1472 {
1473     QList <int> keys = m_model->m_binAudioTargets.keys();
1474     QList <int> assigned = m_model->m_audioTarget.values();
1475     for (int k : qAsConst(keys)) {
1476         if (!assigned.contains(k)) {
1477             return k;
1478         }
1479     }
1480     return -1;
1481 }
1482 
setVideoTarget(int track)1483 void TimelineController::setVideoTarget(int track)
1484 {
1485     if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) {
1486         m_model->m_videoTarget = -1;
1487         return;
1488     }
1489     m_model->m_videoTarget = track;
1490     emit videoTargetChanged();
1491 }
1492 
setActiveTrack(int track)1493 void TimelineController::setActiveTrack(int track)
1494 {
1495     if (track > -1 && !m_model->isTrack(track)) {
1496         return;
1497     }
1498     m_activeTrack = track;
1499     emit activeTrackChanged();
1500 }
1501 
setZone(const QPoint & zone,bool withUndo)1502 void TimelineController::setZone(const QPoint &zone, bool withUndo)
1503 {
1504     if (m_zone.x() > 0) {
1505         m_model->removeSnap(m_zone.x());
1506     }
1507     if (m_zone.y() > 0) {
1508         m_model->removeSnap(m_zone.y() - 1);
1509     }
1510     if (zone.x() > 0) {
1511         m_model->addSnap(zone.x());
1512     }
1513     if (zone.y() > 0) {
1514         m_model->addSnap(zone.y() - 1);
1515     }
1516     updateZone(m_zone, zone, withUndo);
1517 }
1518 
updateZone(const QPoint oldZone,const QPoint newZone,bool withUndo)1519 void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1520 {
1521     if (!withUndo) {
1522         m_zone = newZone;
1523         emit zoneChanged();
1524         // Update monitor zone
1525         emit zoneMoved(m_zone);
1526         return;
1527     }
1528     Fun undo_zone = [this, oldZone]() {
1529             setZone(oldZone, false);
1530             return true;
1531     };
1532     Fun redo_zone = [this, newZone]() {
1533             setZone(newZone, false);
1534             return true;
1535     };
1536     redo_zone();
1537     pCore->pushUndo(undo_zone, redo_zone, i18n("Set Zone"));
1538 }
1539 
updateEffectZone(const QPoint oldZone,const QPoint newZone,bool withUndo)1540 void TimelineController::updateEffectZone(const QPoint oldZone, const QPoint newZone, bool withUndo)
1541 {
1542     Q_UNUSED(oldZone)
1543     emit pCore->updateEffectZone(newZone, withUndo);
1544 }
1545 
setZoneIn(int inPoint)1546 void TimelineController::setZoneIn(int inPoint)
1547 {
1548     if (m_zone.x() > 0) {
1549         m_model->removeSnap(m_zone.x());
1550     }
1551     if (inPoint > 0) {
1552         m_model->addSnap(inPoint);
1553     }
1554     m_zone.setX(inPoint);
1555     emit zoneChanged();
1556     // Update monitor zone
1557     emit zoneMoved(m_zone);
1558 }
1559 
setZoneOut(int outPoint)1560 void TimelineController::setZoneOut(int outPoint)
1561 {
1562     if (m_zone.y() > 0) {
1563         m_model->removeSnap(m_zone.y() - 1);
1564     }
1565     if (outPoint > 0) {
1566         m_model->addSnap(outPoint - 1);
1567     }
1568     m_zone.setY(outPoint);
1569     emit zoneChanged();
1570     emit zoneMoved(m_zone);
1571 }
1572 
selectItems(const QVariantList & tracks,int startFrame,int endFrame,bool addToSelect,bool selectBottomCompositions,bool selectSubTitles)1573 void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions, bool selectSubTitles)
1574 {
1575     std::unordered_set<int> itemsToSelect;
1576     std::unordered_set<int> subtitlesToSelect;
1577     if (addToSelect) {
1578         itemsToSelect = m_model->getCurrentSelection();
1579     }
1580     for (int i = 0; i < tracks.count(); i++) {
1581         if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) {
1582             continue;
1583         }
1584         auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions);
1585         itemsToSelect.insert(currentClips.begin(), currentClips.end());
1586     }
1587     if (selectSubTitles) {
1588         auto subtitleModel = pCore->getSubtitleModel();
1589         if (subtitleModel) {
1590             auto currentSubs = subtitleModel->getItemsInRange(startFrame, endFrame);
1591             itemsToSelect.insert(currentSubs.begin(), currentSubs.end());
1592         }
1593 
1594     }
1595     m_model->requestSetSelection(itemsToSelect);
1596 }
1597 
requestClipCut(int clipId,int position)1598 void TimelineController::requestClipCut(int clipId, int position)
1599 {
1600     if (position == -1) {
1601         position = pCore->getTimelinePosition();
1602     }
1603     TimelineFunctions::requestClipCut(m_model, clipId, position);
1604 }
1605 
cutClipUnderCursor(int position,int track)1606 void TimelineController::cutClipUnderCursor(int position, int track)
1607 {
1608     if (position == -1) {
1609         position = pCore->getTimelinePosition();
1610     }
1611     QMutexLocker lk(&m_metaMutex);
1612     bool foundClip = false;
1613     const auto selection = m_model->getCurrentSelection();
1614     if (track == -2) {
1615         // Subtitle cut
1616         auto subtitleModel = pCore->getSubtitleModel();
1617         subtitleModel->cutSubtitle(position);
1618         return;
1619     }
1620     if (track == -1) {
1621         for (int cid : selection) {
1622             if ((m_model->isClip(cid) || m_model->isSubTitle(cid)) && positionIsInItem(cid)) {
1623                 if (TimelineFunctions::requestClipCut(m_model, cid, position)) {
1624                     foundClip = true;
1625                     // Cutting clips in the selection group is handled in TimelineFunctions
1626                     break;
1627                 }
1628             }
1629         }
1630     }
1631     if (!foundClip) {
1632         if (track == -1) {
1633             track = m_activeTrack;
1634         }
1635         if (track >= 0) {
1636             int cid = m_model->getClipByPosition(track, position);
1637             if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) {
1638                 foundClip = true;
1639             }
1640         } else if (track == -2) {
1641             // Subtitle cut
1642             auto subtitleModel = pCore->getSubtitleModel();
1643             foundClip = subtitleModel->cutSubtitle(position);
1644         }
1645     }
1646     if (!foundClip) {
1647         pCore->displayMessage(i18n("No clip to cut"), ErrorMessage, 500);
1648     }
1649 }
1650 
cutSubtitle(int id,int cursorPos)1651 void TimelineController::cutSubtitle(int id, int cursorPos)
1652 {
1653     Q_ASSERT(m_model->isSubTitle(id));
1654     if (cursorPos <= 0) {
1655         return requestClipCut(id, -1);
1656     }
1657     // Cut subtitle at edit position
1658     int timelinePos = pCore->getTimelinePosition();
1659     GenTime position(timelinePos, pCore->getCurrentFps());
1660     GenTime start = m_model->m_allSubtitles.at(id);
1661     auto subtitleModel = pCore->getSubtitleModel();
1662     SubtitledTime subData = subtitleModel->getSubtitle(start);
1663     if (position > start && position < subData.end()) {
1664         QString originalText = subData.subtitle();
1665         QString firstText = originalText;
1666         QString secondText = originalText.right(originalText.length() - cursorPos);
1667         firstText.truncate(cursorPos);
1668         Fun undo = []() { return true; };
1669         Fun redo = []() { return true; };
1670         int newId = subtitleModel->cutSubtitle(timelinePos, undo, redo);
1671         if (newId > -1) {
1672             Fun local_redo = [subtitleModel, id, newId, firstText, secondText]() {
1673                 subtitleModel->editSubtitle(id, firstText);
1674                 subtitleModel->editSubtitle(newId, secondText);
1675                 return true;
1676             };
1677             Fun local_undo = [subtitleModel, id, originalText]() {
1678                 subtitleModel->editSubtitle(id, originalText);
1679                 return true;
1680             };
1681             local_redo();
1682             UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
1683             pCore->pushUndo(undo, redo, i18n("Cut clip"));
1684         }
1685     }
1686 }
1687 
cutAllClipsUnderCursor(int position)1688 void TimelineController::cutAllClipsUnderCursor(int position)
1689 {
1690     if (position == -1) {
1691         position = pCore->getTimelinePosition();
1692     }
1693     QMutexLocker lk(&m_metaMutex);
1694     TimelineFunctions::requestClipCutAll(m_model, position);
1695 }
1696 
requestSpacerStartOperation(int trackId,int position)1697 int TimelineController::requestSpacerStartOperation(int trackId, int position)
1698 {
1699     QMutexLocker lk(&m_metaMutex);
1700     int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position);
1701     return itemId;
1702 }
1703 
spacerMinPos() const1704 int TimelineController::spacerMinPos() const
1705 {
1706     return TimelineFunctions::spacerMinPos();
1707 }
1708 
spacerMoveGuides(QVector<int> ids,int offset)1709 void TimelineController::spacerMoveGuides(QVector<int> ids, int offset)
1710 {
1711     pCore->currentDoc()->getGuideModel()->moveMarkersWithoutUndo(ids, offset);
1712 }
1713 
spacerSelection(int startFrame)1714 QVector<int> TimelineController::spacerSelection(int startFrame)
1715 {
1716     return pCore->currentDoc()->getGuideModel()->getMarkersIdInRange(startFrame, -1);
1717 }
1718 
getGuidePosition(int id)1719 int TimelineController::getGuidePosition(int id)
1720 {
1721     return pCore->currentDoc()->getGuideModel()->getMarkerPos(id);
1722 }
1723 
requestSpacerEndOperation(int clipId,int startPosition,int endPosition,int affectedTrack,QVector<int> selectedGuides,int guideStart)1724 bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition, int affectedTrack, QVector <int> selectedGuides, int guideStart)
1725 {
1726     QMutexLocker lk(&m_metaMutex);
1727     // Start undoable command
1728     std::function<bool(void)> undo = []() { return true; };
1729     std::function<bool(void)> redo = []() { return true; };
1730     if(guideStart > -1) {
1731         // Move guides back to original position
1732         pCore->currentDoc()->getGuideModel()->moveMarkersWithoutUndo(selectedGuides, startPosition - endPosition, false);
1733         moveGuidesInRange(guideStart, -1, endPosition - startPosition, undo, redo);
1734     }
1735     bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition, affectedTrack, false, undo, redo);
1736     return result;
1737 }
1738 
seekCurrentClip(bool seekToEnd)1739 void TimelineController::seekCurrentClip(bool seekToEnd)
1740 {
1741     const auto selection = m_model->getCurrentSelection();
1742     int cid = -1;
1743     if (!selection.empty()) {
1744         cid = *selection.begin();
1745     } else {
1746         int cursorPos = pCore->getTimelinePosition();
1747         cid = m_model->getClipByPosition(m_activeTrack, cursorPos);
1748         if (cid < 0) {
1749             /* If the cursor is at the clip end it is one frame after the clip,
1750              * make it possible to jump to the clip start in that situation too
1751              */
1752             cid = m_model->getClipByPosition(m_activeTrack, cursorPos - 1);
1753         }
1754     }
1755     if (cid > -1) {
1756         seekToClip(cid, seekToEnd);
1757     }
1758 }
1759 
seekToClip(int cid,bool seekToEnd)1760 void TimelineController::seekToClip(int cid, bool seekToEnd)
1761 {
1762     int start = m_model->getItemPosition(cid);
1763     if (seekToEnd) {
1764         start += m_model->getItemPlaytime(cid);
1765     }
1766     setPosition(start);
1767 }
1768 
seekToMouse()1769 void TimelineController::seekToMouse()
1770 {
1771     int mousePos = getMousePos();
1772     if (mousePos > -1) {
1773         setPosition(mousePos);
1774     }
1775 }
1776 
getMousePos()1777 int TimelineController::getMousePos()
1778 {
1779     QVariant returnedValue;
1780     QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue));
1781     return returnedValue.toInt();
1782 }
1783 
getMouseTrack()1784 int TimelineController::getMouseTrack()
1785 {
1786     QVariant returnedValue;
1787     QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue));
1788     return returnedValue.toInt();
1789 }
1790 
positionIsInItem(int id)1791 bool TimelineController::positionIsInItem(int id)
1792 {
1793     int in = m_model->getItemPosition(id);
1794     int position = pCore->getTimelinePosition();
1795     if (in > position) {
1796         return false;
1797     }
1798     if (position <= in + m_model->getItemPlaytime(id)) {
1799         return true;
1800     }
1801     return false;
1802 }
1803 
refreshItem(int id)1804 void TimelineController::refreshItem(int id)
1805 {
1806     if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) {
1807         return;
1808     }
1809     if (positionIsInItem(id)) {
1810         pCore->requestMonitorRefresh();
1811     }
1812 }
1813 
getTracksCount() const1814 QPair<int, int> TimelineController::getTracksCount() const
1815 {
1816     return m_model->getAVtracksCount();
1817 }
1818 
extractCompositionLumas() const1819 QStringList TimelineController::extractCompositionLumas() const
1820 {
1821     return m_model->extractCompositionLumas();
1822 }
1823 
extractExternalEffectFiles() const1824 QStringList TimelineController::extractExternalEffectFiles() const
1825 {
1826     return m_model->extractExternalEffectFiles();
1827 }
1828 
addEffectToCurrentClip(const QStringList & effectData)1829 void TimelineController::addEffectToCurrentClip(const QStringList &effectData)
1830 {
1831     QList<int> activeClips;
1832     for (int track = m_model->getTracksCount() - 1; track >= 0; track--) {
1833         int trackIx = m_model->getTrackIndexFromPosition(track);
1834         int cid = m_model->getClipByPosition(trackIx, pCore->getTimelinePosition());
1835         if (cid > -1) {
1836             activeClips << cid;
1837         }
1838     }
1839     if (!activeClips.isEmpty()) {
1840         if (effectData.count() == 4) {
1841             QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3);
1842             m_model->copyClipEffect(activeClips.first(), effectString);
1843         } else {
1844             m_model->addClipEffect(activeClips.first(), effectData.constFirst());
1845         }
1846     }
1847 }
1848 
adjustFade(int cid,const QString & effectId,int duration,int initialDuration)1849 void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration)
1850 {
1851     if (initialDuration == -2) {
1852         // Add default fade
1853         duration = pCore->getDurationFromString(KdenliveSettings::fade_duration());
1854         initialDuration = 0;
1855     }
1856     if (duration <= 0) {
1857         // remove fade
1858         if (initialDuration > 0) {
1859             // Restore original fade duration
1860             m_model->adjustEffectLength(cid, effectId, initialDuration, -1);
1861         }
1862         m_model->removeFade(cid, effectId == QLatin1String("fadein"));
1863     } else {
1864         m_model->adjustEffectLength(cid, effectId, duration, initialDuration);
1865     }
1866 }
1867 
getCompositionATrack(int cid) const1868 QPair<int, int> TimelineController::getCompositionATrack(int cid) const
1869 {
1870     QPair<int, int> result;
1871     std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
1872     if (compo) {
1873         result = QPair<int, int>(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId()));
1874     }
1875     return result;
1876 }
1877 
setCompositionATrack(int cid,int aTrack)1878 void TimelineController::setCompositionATrack(int cid, int aTrack)
1879 {
1880     TimelineFunctions::setCompositionATrack(m_model, cid, aTrack);
1881 }
1882 
compositionAutoTrack(int cid) const1883 bool TimelineController::compositionAutoTrack(int cid) const
1884 {
1885     std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid);
1886     return compo && compo->getForcedTrack() == -1;
1887 }
1888 
getClipBinId(int clipId) const1889 const QString TimelineController::getClipBinId(int clipId) const
1890 {
1891     return m_model->getClipBinId(clipId);
1892 }
1893 
focusItem(int itemId)1894 void TimelineController::focusItem(int itemId)
1895 {
1896     int start = m_model->getItemPosition(itemId);
1897     setPosition(start);
1898     emit centerView();
1899 }
1900 
headerWidth() const1901 int TimelineController::headerWidth() const
1902 {
1903     return qMax(10, KdenliveSettings::headerwidth());
1904 }
1905 
setHeaderWidth(int width)1906 void TimelineController::setHeaderWidth(int width)
1907 {
1908     KdenliveSettings::setHeaderwidth(width);
1909 }
1910 
createSplitOverlay(int clipId,std::shared_ptr<Mlt::Filter> filter)1911 bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr<Mlt::Filter> filter)
1912 {
1913     if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) {
1914         return true;
1915     }
1916     if (clipId == -1) {
1917         pCore->displayMessage(i18n("Select a clip to compare effect"), ErrorMessage, 500);
1918         return false;
1919     }
1920     std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId);
1921     const QString binId = clip->binId();
1922 
1923     // Get clean bin copy of the clip
1924     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
1925     std::shared_ptr<Mlt::Producer> binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut()));
1926 
1927     // Get copy of timeline producer
1928     std::shared_ptr<Mlt::Producer> clipProducer(new Mlt::Producer(*clip));
1929 
1930     // Built tractor and compositing
1931     Mlt::Tractor trac(*m_model->m_tractor->profile());
1932     Mlt::Playlist play(*m_model->m_tractor->profile());
1933     Mlt::Playlist play2(*m_model->m_tractor->profile());
1934     play.append(*clipProducer.get());
1935     play2.append(*binProd);
1936     trac.set_track(play, 0);
1937     trac.set_track(play2, 1);
1938     play2.attach(*filter.get());
1939     QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
1940     Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData());
1941     t.set("always_active", 1);
1942     trac.plant_transition(t, 0, 1);
1943     int startPos = m_model->getClipPosition(clipId);
1944 
1945     // plug in overlay playlist
1946     auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile());
1947     overlay->insert_blank(0, startPos);
1948     Mlt::Producer split(trac.get_producer());
1949     overlay->insert_at(startPos, &split, 1);
1950 
1951     // insert in tractor
1952     if (!m_timelinePreview) {
1953         initializePreview();
1954     }
1955     if(m_timelinePreview){
1956         m_timelinePreview->setOverlayTrack(overlay);
1957         m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
1958     }
1959     return true;
1960 }
1961 
removeSplitOverlay()1962 void TimelineController::removeSplitOverlay()
1963 {
1964     if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) {
1965         return;
1966     }
1967     // disconnect
1968     m_timelinePreview->removeOverlayTrack();
1969     m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
1970 }
1971 
requestItemRippleResize(int itemId,int size,bool right,bool logUndo,int snapDistance,bool allowSingleResize)1972 int TimelineController::requestItemRippleResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) {
1973     return m_model->requestItemRippleResize(m_model, itemId, size, right, logUndo, snapDistance, allowSingleResize);
1974 }
1975 
updateTrimmingMode()1976 void TimelineController::updateTrimmingMode() {
1977     if (trimmingActive()) {
1978         requestStartTrimmingMode();
1979     } else {
1980         requestEndTrimmingMode();
1981     }
1982 }
1983 
trimmingBoundOffset(int offset)1984 int TimelineController::trimmingBoundOffset(int offset) {
1985     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
1986     return qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
1987 }
1988 
slipPosChanged(int offset)1989 void TimelineController::slipPosChanged(int offset) {
1990     if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
1991         return;
1992     }
1993     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
1994     offset = qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn());
1995     int outPoint = mainClip->getOut() - offset;
1996     int inPoint = mainClip->getIn() - offset;
1997 
1998     pCore->monitorManager()->projectMonitor()->slotTrimmingPos(inPoint, offset, inPoint, outPoint);
1999     showToolTip(i18n("In:%1, Out:%2 (%3%4)", simplifiedTC(inPoint), simplifiedTC(outPoint), (offset < 0 ? "-" : "+"), simplifiedTC(qFabs(offset))));
2000 }
2001 
ripplePosChanged(int size,bool right)2002 void TimelineController::ripplePosChanged(int size, bool right) {
2003     if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) {
2004         return;
2005     }
2006     if (size < 0) {
2007         return;
2008     }
2009     qDebug() << "ripplePosChanged" << size << right;
2010     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2011     int delta = size - mainClip->getPlaytime();
2012     if (!right) {
2013         delta *= -1;
2014     }
2015     int pos = right ? mainClip->getOut() : mainClip->getIn();
2016     pos += delta;
2017     if (mainClip->getMaxDuration() > -1) {
2018         pos = qBound(0, pos, mainClip->getMaxDuration());
2019     } else {
2020         pos = qMax(0, pos);
2021     }
2022     pCore->monitorManager()->projectMonitor()->slotTrimmingPos(pos + 1, delta, right ? mainClip->getIn() : pos, right ? pos : mainClip->getOut());
2023 }
2024 
slipProcessSelection(int mainClipId,bool addToSelection)2025 bool TimelineController::slipProcessSelection(int mainClipId, bool addToSelection) {
2026     std::unordered_set<int> sel = m_model->getCurrentSelection();
2027     std::unordered_set<int> newSel;
2028 
2029     for (int i : sel) {
2030         if (m_model->isClip(i) && m_model->getClipPtr(i)->getMaxDuration() != -1) {
2031             newSel.insert(i);
2032         }
2033     }
2034 
2035     if (mainClipId != -1 && !m_model->isClip(mainClipId) && m_model->getClipPtr(mainClipId)->getMaxDuration() == -1) {
2036         mainClipId = -1;
2037     }
2038 
2039     if ((newSel.empty() || !isInSelection(mainClipId)) && mainClipId != -1) {
2040         m_trimmingMainClip = mainClipId;
2041         emit trimmingMainClipChanged();
2042         if (!addToSelection) {
2043             newSel.clear();
2044         }
2045         newSel.insert(mainClipId);
2046     }
2047 
2048     if (newSel != sel) {
2049         m_model->requestSetSelection(newSel);
2050         return false;
2051     }
2052 
2053     if (sel.empty()) {
2054         return false;
2055     }
2056 
2057     Q_ASSERT(!sel.empty());
2058 
2059     if (mainClipId == -1) {
2060         mainClipId = getMainSelectedClip();
2061     }
2062 
2063     if (m_model->getTrackById(m_model->getClipTrackId(mainClipId))->isLocked()) {
2064         int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2065         if (partnerId == -1 || m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2066             mainClipId = -1;
2067             for (int i : sel) {
2068                 if (i != mainClipId && !m_model->getTrackById(m_model->getClipTrackId(i))->isLocked()) {
2069                     mainClipId = i;
2070                     break;
2071                 }
2072             }
2073         } else {
2074             mainClipId = partnerId;
2075         }
2076     }
2077 
2078     if (mainClipId == -1) {
2079         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2080         return false;
2081     }
2082 
2083     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(mainClipId);
2084 
2085     if (mainClip->getMaxDuration() == -1) {
2086         return false;
2087     }
2088 
2089     int partnerId = m_model->m_groups->getSplitPartner(mainClipId);
2090 
2091     if (mainClip->isAudioOnly() && partnerId != -1 && !m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) {
2092         mainClip = m_model->getClipPtr(partnerId);
2093     }
2094 
2095     m_trimmingMainClip = mainClip->getId();
2096     emit trimmingMainClipChanged();
2097     return true;
2098 }
2099 
requestStartTrimmingMode(int mainClipId,bool addToSelection,bool right)2100 bool TimelineController::requestStartTrimmingMode(int mainClipId, bool addToSelection, bool right)
2101 {
2102 
2103     if (pCore->monitorManager()->isTrimming() && m_trimmingMainClip == mainClipId) {
2104         return true;
2105     }
2106 
2107     if (pCore->activeTool() == ToolType::SlipTool && !slipProcessSelection(mainClipId, addToSelection)) {
2108         return false;
2109     }
2110 
2111     if (pCore->activeTool() == ToolType::RippleTool) {
2112         if (m_model.get()->isClip(mainClipId)) {
2113             m_trimmingMainClip = mainClipId;
2114             emit trimmingMainClipChanged();
2115         } else {
2116             return false;
2117         }
2118     }
2119 
2120     std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip);
2121 
2122     const int previousClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() - 1);
2123     std::shared_ptr<Mlt::Producer> previousFrame;
2124     if (pCore->activeTool() == ToolType::SlipTool && previousClipId > -1) {
2125         std::shared_ptr<ClipModel> previousClip = m_model->getClipPtr(previousClipId);
2126         previousFrame = std::shared_ptr<Mlt::Producer>(previousClip->getProducer()->cut(0));
2127         Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2128         filter.set("mlt_service", "freeze");
2129         filter.set("frame", previousClip->getOut());
2130         previousFrame->attach(filter);
2131     } else {
2132         previousFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2133     }
2134 
2135     const int nextClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() + mainClip->getPlaytime());
2136     std::shared_ptr<Mlt::Producer> nextFrame;
2137     if (pCore->activeTool() == ToolType::SlipTool && nextClipId > -1) {
2138         std::shared_ptr<ClipModel> nextClip = m_model->getClipPtr(nextClipId);
2139         nextFrame = std::shared_ptr<Mlt::Producer>(nextClip->getProducer()->cut(0));
2140         Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2141         filter.set("mlt_service", "freeze");
2142         filter.set("frame", nextClip->getIn());
2143         nextFrame->attach(filter);
2144     } else {
2145         nextFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2146     }
2147 
2148     std::shared_ptr<Mlt::Producer> inOutFrame;
2149     if (pCore->activeTool() == ToolType::RippleTool) {
2150         inOutFrame = std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0));
2151         Mlt::Filter filter(*m_model->m_tractor->profile(), "freeze");
2152         filter.set("mlt_service", "freeze");
2153         filter.set("frame", right ? mainClip->getIn() : mainClip->getOut());
2154         inOutFrame->attach(filter);
2155     }
2156 
2157     std::vector<std::shared_ptr<Mlt::Producer>> producers;
2158     int previewLength = 0;
2159     switch (pCore->activeTool()) {
2160     case ToolType::SlipTool:
2161         // Get copy of timeline producer
2162         /* This is an example using a playlist. This does not work with switch -> use if else instead
2163         Mlt::Producer test(mainClip->getProducer()->cut(0));
2164         Mlt::Playlist list(*m_model->m_tractor->profile());
2165         list.append(test);
2166         producers.push_back(Mlt::Producer(list)); */
2167         producers.push_back(std::shared_ptr<Mlt::Producer>(previousFrame));
2168         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2169         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(mainClip->getOut() - mainClip->getIn())));
2170         producers.push_back(nextFrame);
2171         previewLength = producers[1]->get_length();
2172     break;
2173     case ToolType::SlideTool:
2174     break;
2175     case ToolType::RollTool:
2176     break;
2177     case ToolType::RippleTool:
2178         if (right) {
2179             producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2180         }
2181         producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)));
2182         if (!right) {
2183             producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame));
2184             previewLength = producers[0]->get_length();
2185         } else {
2186             previewLength = producers[1]->get_length();
2187         }
2188     break;
2189     default:
2190         return false;
2191     }
2192 
2193     // Built tractor
2194     Mlt::Tractor trac(*m_model->m_tractor->profile());
2195 
2196     // Now that we know the length of the preview create and add black background producer
2197     std::shared_ptr<Mlt::Producer> black(new Mlt::Producer(*m_model->m_tractor->profile(), "color:black"));
2198     black->set("length", previewLength);
2199     black->set_in_and_out(0, previewLength);
2200     trac.set_track(*black.get(), 0);
2201     //trac.set_track( 1);
2202 
2203 
2204     if (!mainClip->isAudioOnly()) {
2205         int count = 1; // 0 is background track so we start at 1
2206         for (auto const &producer : producers) {
2207             trac.set_track(*producer.get(), count);
2208             count++;
2209         }
2210 
2211         // Add "composite" transitions for multi clip view
2212         for (int i = 0; i < int(producers.size()); i++) {
2213             // Construct transition
2214             Mlt::Transition transition(*trac.profile(), "composite");
2215             transition.set("mlt_service", "composite");
2216             transition.set("a_track", 0);
2217             transition.set("b_track", i + 1);
2218             transition.set("distort", 0);
2219             transition.set("aligned", 0);
2220             // 200 is an arbitrary number so we can easily remove these transition later
2221             //transition.set("internal_added", 200);
2222 
2223             QString geometry;
2224             switch (pCore->activeTool()) {
2225             case ToolType::RollTool:
2226             case ToolType::RippleTool:
2227                 switch (i) {
2228                 case 0:
2229                     geometry = QStringLiteral("0 0 50% 100%");
2230                     break;
2231                 case 1:
2232                     geometry = QStringLiteral("50% 0 50% 100%");
2233                     break;
2234                 }
2235                 break;
2236             case ToolType::SlipTool:
2237                 switch (i) {
2238                 case 0:
2239                     geometry = QStringLiteral("0 0 25% 25%");
2240                     break;
2241                 case 1:
2242                     geometry = QStringLiteral("0 25% 50% 50%");
2243                     break;
2244                 case 2:
2245                     geometry = QStringLiteral("50% 25% 50% 50%");
2246                     break;
2247                 case 3:
2248                     geometry = QStringLiteral("75% 75% 25% 25%");
2249                     break;
2250                 }
2251                 break;
2252             case ToolType::SlideTool:
2253                 switch (i) {
2254                 case 0:
2255                     geometry = QStringLiteral("0 0 25% 25%");
2256                     break;
2257                 case 1:
2258                     geometry = QStringLiteral("50% 25% 50% 50%");
2259                     break;
2260                 case 2:
2261                     geometry = QStringLiteral("0 25% 50% 50%");
2262                     break;
2263                 case 3:
2264                     geometry = QStringLiteral("50% 75% 25% 25%");
2265                     break;
2266                 }
2267                 break;
2268             default:
2269                 break;
2270             }
2271 
2272             // Add transition to track:
2273             transition.set("geometry", geometry.toUtf8().constData());
2274             transition.set("always_active", 1);
2275             trac.plant_transition(transition, 0, i + 1);
2276         }
2277 
2278     }
2279 
2280     pCore->monitorManager()->projectMonitor()->setProducer(std::make_shared<Mlt::Producer>(trac), -2);
2281     pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(true);
2282 
2283     switch (pCore->activeTool()) {
2284     case ToolType::RollTool:
2285     case ToolType::RippleTool:
2286         ripplePosChanged(mainClip->getPlaytime(), right);
2287         break;
2288     case ToolType::SlipTool:
2289         slipPosChanged(0);
2290         break;
2291     case ToolType::SlideTool:
2292         break;
2293     default:
2294         break;
2295     }
2296 
2297     return true;
2298 }
2299 
requestEndTrimmingMode()2300 void TimelineController::requestEndTrimmingMode() {
2301     if (pCore->monitorManager()->isTrimming()) {
2302         pCore->monitorManager()->projectMonitor()->setProducer(pCore->window()->getCurrentTimeline()->model()->producer(), 0);
2303         pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(false);
2304     }
2305 }
2306 
addPreviewRange(bool add)2307 void TimelineController::addPreviewRange(bool add)
2308 {
2309     if (m_zone.isNull()) {
2310         return;
2311     }
2312     if (!m_timelinePreview) {
2313         initializePreview();
2314     }
2315     if (m_timelinePreview) {
2316         m_timelinePreview->addPreviewRange(m_zone, add);
2317     }
2318 }
2319 
clearPreviewRange(bool resetZones)2320 void TimelineController::clearPreviewRange(bool resetZones)
2321 {
2322     if (m_timelinePreview) {
2323         m_timelinePreview->clearPreviewRange(resetZones);
2324     }
2325 }
2326 
startPreviewRender()2327 void TimelineController::startPreviewRender()
2328 {
2329     // Timeline preview stuff
2330     if (!m_timelinePreview) {
2331         initializePreview();
2332     } else if (m_disablePreview->isChecked()) {
2333         m_disablePreview->setChecked(false);
2334         disablePreview(false);
2335     }
2336     if (m_timelinePreview) {
2337         if (!m_usePreview) {
2338             m_timelinePreview->buildPreviewTrack();
2339             m_usePreview = true;
2340             m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2341         }
2342         m_timelinePreview->startPreviewRender();
2343     }
2344 }
2345 
stopPreviewRender()2346 void TimelineController::stopPreviewRender()
2347 {
2348     if (m_timelinePreview) {
2349         m_timelinePreview->abortRendering();
2350     }
2351 }
2352 
initializePreview()2353 void TimelineController::initializePreview()
2354 {
2355     if (m_timelinePreview) {
2356         // Update parameters
2357         if (!m_timelinePreview->loadParams()) {
2358             if (m_usePreview) {
2359                 // Disconnect preview track
2360                 m_timelinePreview->disconnectTrack();
2361                 m_usePreview = false;
2362             }
2363             delete m_timelinePreview;
2364             m_timelinePreview = nullptr;
2365         }
2366     } else {
2367         m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get());
2368         if (!m_timelinePreview->initialize()) {
2369             // TODO warn user
2370             delete m_timelinePreview;
2371             m_timelinePreview = nullptr;
2372         } else {
2373         }
2374     }
2375     QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone"));
2376     if (previewRender) {
2377         previewRender->setEnabled(m_timelinePreview != nullptr);
2378     }
2379     m_disablePreview->setEnabled(m_timelinePreview != nullptr);
2380     m_disablePreview->blockSignals(true);
2381     m_disablePreview->setChecked(false);
2382     m_disablePreview->blockSignals(false);
2383 }
2384 
hasPreviewTrack() const2385 bool TimelineController::hasPreviewTrack() const
2386 {
2387     return (m_timelinePreview  && (m_timelinePreview->hasOverlayTrack() || m_timelinePreview->hasPreviewTrack()));
2388 }
2389 
updatePreviewConnection(bool enable)2390 void TimelineController::updatePreviewConnection(bool enable)
2391 {
2392     if (m_timelinePreview) {
2393         if (enable) {
2394             m_timelinePreview->enable();
2395         } else {
2396             m_timelinePreview->disable();
2397         }
2398     }
2399 }
2400 
disablePreview(bool disable)2401 void TimelineController::disablePreview(bool disable)
2402 {
2403     if (disable) {
2404         m_timelinePreview->deletePreviewTrack();
2405         m_usePreview = false;
2406         m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2407     } else {
2408         if (!m_usePreview) {
2409             if (!m_timelinePreview->buildPreviewTrack()) {
2410                 // preview track already exists, reconnect
2411                 m_model->m_tractor->lock();
2412                 m_timelinePreview->reconnectTrack();
2413                 m_model->m_tractor->unlock();
2414             }
2415             Mlt::Playlist playlist;
2416             m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime(), playlist);
2417             m_usePreview = true;
2418         }
2419     }
2420     m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2421 }
2422 
dirtyChunks() const2423 QVariantList TimelineController::dirtyChunks() const
2424 {
2425     return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList();
2426 }
2427 
renderedChunks() const2428 QVariantList TimelineController::renderedChunks() const
2429 {
2430     return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList();
2431 }
2432 
workingPreview() const2433 int TimelineController::workingPreview() const
2434 {
2435     return m_timelinePreview ? m_timelinePreview->workingPreview : -1;
2436 }
2437 
useRuler() const2438 bool TimelineController::useRuler() const
2439 {
2440     return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1;
2441 }
2442 
scrollVertically() const2443 bool TimelineController::scrollVertically() const
2444 {
2445     return KdenliveSettings::scrollvertically() == 1;
2446 }
2447 
resetPreview()2448 void TimelineController::resetPreview()
2449 {
2450     if (m_timelinePreview) {
2451         m_timelinePreview->clearPreviewRange(true);
2452         initializePreview();
2453     }
2454 }
2455 
loadPreview(const QString & chunks,const QString & dirty,const QDateTime & documentDate,int enable,Mlt::Playlist & playlist)2456 void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable, Mlt::Playlist &playlist)
2457 {
2458     if (chunks.isEmpty() && dirty.isEmpty()) {
2459         return;
2460     }
2461     if (!m_timelinePreview) {
2462         initializePreview();
2463     }
2464     QVariantList renderedChunks;
2465     QVariantList dirtyChunks;
2466 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
2467     QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts);
2468 #else
2469     QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts);
2470 #endif
2471 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
2472     QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts);
2473 #else
2474     QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts);
2475 #endif
2476     for (const QString &frame : qAsConst(chunksList)) {
2477         renderedChunks << frame.toInt();
2478     }
2479     for (const QString &frame : qAsConst(dirtyList)) {
2480         dirtyChunks << frame.toInt();
2481     }
2482 
2483     if ( m_disablePreview ) {
2484         m_disablePreview->blockSignals(true);
2485         m_disablePreview->setChecked(enable);
2486         m_disablePreview->blockSignals(false);
2487     }
2488     if ( m_timelinePreview ) {
2489         if (!enable) {
2490             m_timelinePreview->buildPreviewTrack();
2491             m_usePreview = true;
2492             m_model->m_overlayTrackCount = m_timelinePreview->addedTracks();
2493         }
2494         m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate, playlist);
2495     }
2496 }
2497 
documentProperties()2498 QMap<QString, QString> TimelineController::documentProperties()
2499 {
2500     QMap<QString, QString> props = pCore->currentDoc()->documentProperties();
2501     int audioTarget = m_model->m_audioTarget.isEmpty() ? -1 : m_model->getTrackPosition(m_model->m_audioTarget.firstKey());
2502     int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget);
2503     int activeTrack = m_activeTrack < 0 ? m_activeTrack : m_model->getTrackPosition(m_activeTrack);
2504     props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget));
2505     props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget));
2506     props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack));
2507     props.insert(QStringLiteral("position"), QString::number(pCore->getTimelinePosition()));
2508     QVariant returnedValue;
2509     QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue));
2510     int scrollPos = returnedValue.toInt();
2511     props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos));
2512     props.insert(QStringLiteral("zonein"), QString::number(m_zone.x()));
2513     props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y()));
2514     if (m_timelinePreview) {
2515         QPair<QStringList, QStringList> chunks = m_timelinePreview->previewChunks();
2516         props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(',')));
2517         props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(',')));
2518     }
2519     props.insert(QStringLiteral("disablepreview"), QString::number(m_disablePreview->isChecked()));
2520     return props;
2521 }
2522 
getMenuOrTimelinePos() const2523 int TimelineController::getMenuOrTimelinePos() const
2524 {
2525     int frame = m_root->property("clickFrame").toInt();
2526     if (frame == -1) {
2527         frame = pCore->getTimelinePosition();
2528     }
2529     return frame;
2530 }
2531 
insertSpace(int trackId,int frame)2532 void TimelineController::insertSpace(int trackId, int frame)
2533 {
2534     if (frame == -1) {
2535         frame = getMenuOrTimelinePos();
2536     }
2537     if (trackId == -1) {
2538         trackId = m_activeTrack;
2539     }
2540     QPointer<SpacerDialog> d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow());
2541     if (d->exec() != QDialog::Accepted) {
2542         delete d;
2543         return;
2544     }
2545     bool affectAllTracks = d->affectAllTracks();
2546     int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame);
2547     int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps());
2548     delete d;
2549     if (cid == -1) {
2550         pCore->displayMessage(i18n("No clips found to insert space"), ErrorMessage, 500);
2551         return;
2552     }
2553     int start = m_model->getItemPosition(cid);
2554     requestSpacerEndOperation(cid, start, start + spaceDuration, affectAllTracks ? -1 : trackId);
2555 }
2556 
removeSpace(int trackId,int frame,bool affectAllTracks)2557 void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks)
2558 {
2559     if (frame == -1) {
2560         frame = getMenuOrTimelinePos();
2561     }
2562     if (trackId == -1) {
2563         trackId = m_activeTrack;
2564     }
2565     bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks);
2566     if (!res) {
2567         pCore->displayMessage(i18n("Cannot remove space at given position"), ErrorMessage, 500);
2568     }
2569 }
2570 
invalidateItem(int cid)2571 void TimelineController::invalidateItem(int cid)
2572 {
2573     if (!m_timelinePreview || !m_model->isItem(cid)) {
2574         return;
2575     }
2576     const int tid = m_model->getItemTrackId(cid);
2577     if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) {
2578         return;
2579     }
2580     int start = m_model->getItemPosition(cid);
2581     int end = start + m_model->getItemPlaytime(cid);
2582     m_timelinePreview->invalidatePreview(start, end);
2583 }
2584 
invalidateTrack(int tid)2585 void TimelineController::invalidateTrack(int tid)
2586 {
2587     if (!m_timelinePreview || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) {
2588         return;
2589     }
2590     for (const auto &clp : m_model->getTrackById_const(tid)->m_allClips) {
2591         invalidateItem(clp.first);
2592     }
2593 }
2594 
invalidateZone(int in,int out)2595 void TimelineController::invalidateZone(int in, int out)
2596 {
2597     if (!m_timelinePreview) {
2598         return;
2599     }
2600     m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out);
2601 }
2602 
remapItemTime(int clipId)2603 void TimelineController::remapItemTime(int clipId)
2604 {
2605     if (clipId == -1) {
2606         clipId = getMainSelectedClip();
2607     }
2608     // Don't allow remaping a clip with speed effect
2609     if (clipId == -1 || !m_model->isClip(clipId) || !qFuzzyCompare(1., m_model->m_allClips[clipId]->getSpeed())) {
2610         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2611         return;
2612     }
2613     ClipType::ProducerType type = m_model->m_allClips[clipId]->clipType();
2614     if (type == ClipType::Color || type == ClipType::Image) {
2615         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2616         return;
2617     }
2618     if (m_model->m_allClips[clipId]->isChain()) {
2619         // Remove remap effect
2620         m_model->requestClipTimeRemap(clipId, false);
2621         emit pCore->remapClip(-1);
2622     } else {
2623         // Add remap effect
2624         emit pCore->remapClip(clipId);
2625     }
2626     updateClipActions();
2627 }
2628 
changeItemSpeed(int clipId,double speed)2629 void TimelineController::changeItemSpeed(int clipId, double speed)
2630 {
2631     /*if (clipId == -1) {
2632         clipId = getMainSelectedItem(false, true);
2633     }*/
2634     if (clipId == -1) {
2635         clipId = getMainSelectedClip();
2636     }
2637     if (clipId == -1) {
2638         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
2639         return;
2640     }
2641     bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
2642     if (qFuzzyCompare(speed, -1)) {
2643         speed = 100 * m_model->getClipSpeed(clipId);
2644         int duration = m_model->getItemPlaytime(clipId);
2645         // this is the max speed so that the clip is at least one frame long
2646         double maxSpeed = duration * qAbs(speed);
2647         // this is the min speed so that the clip doesn't bump into the next one on track
2648         double minSpeed = duration * qAbs(speed) / (duration + double(m_model->getBlankSizeNearClip(clipId, true)));
2649 
2650         // if there is a split partner, we must also take it into account
2651         int partner = m_model->getClipSplitPartner(clipId);
2652         if (partner != -1) {
2653             double duration2 = m_model->getItemPlaytime(partner);
2654             double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner));
2655             double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true)));
2656             minSpeed = std::max(minSpeed, minSpeed2);
2657             maxSpeed = std::min(maxSpeed, maxSpeed2);
2658         }
2659         QScopedPointer<SpeedDialog> d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), duration, minSpeed, maxSpeed, speed < 0, pitchCompensate));
2660         if (d->exec() != QDialog::Accepted) {
2661             emit regainFocus();
2662             return;
2663         }
2664         emit regainFocus();
2665         speed = d->getValue();
2666         pitchCompensate = d->getPitchCompensate();
2667         qDebug() << "requesting speed " << speed;
2668     }
2669     bool res = m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true);
2670     if (res) {
2671         updateClipActions();
2672     }
2673 }
2674 
switchCompositing(int mode)2675 void TimelineController::switchCompositing(int mode)
2676 {
2677     // m_model->m_tractor->lock();
2678     pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode));
2679     QScopedPointer<Mlt::Service> service(m_model->m_tractor->field());
2680     QScopedPointer<Mlt::Field>field(m_model->m_tractor->field());
2681     field->lock();
2682     while ((service != nullptr) && service->is_valid()) {
2683         if (service->type() == mlt_service_transition_type) {
2684             Mlt::Transition t(mlt_transition(service->get_service()));
2685             service.reset(service->producer());
2686             QString serviceName = t.get("mlt_service");
2687             if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) {
2688                 // remove all compositing transitions
2689                 field->disconnect_service(t);
2690                 t.disconnect_all_producers();
2691             }
2692         } else {
2693             service.reset(service->producer());
2694         }
2695     }
2696     if (mode > 0) {
2697         // Loop through tracks
2698         for (int track = 0; track < m_model->getTracksCount(); track++) {
2699             if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) {
2700                 // This is a video track
2701                 Mlt::Transition t(*m_model->m_tractor->profile(),
2702                                   mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData());
2703                 t.set("always_active", 1);
2704                 t.set_tracks(0, track + 1);
2705                 t.set("internal_added", 237);
2706                 field->plant_transition(t, 0, track + 1);
2707             }
2708         }
2709     }
2710     field->unlock();
2711     pCore->requestMonitorRefresh();
2712 }
2713 
extractZone(QPoint zone,bool liftOnly)2714 void TimelineController::extractZone(QPoint zone, bool liftOnly)
2715 {
2716     QVector<int> tracks;
2717     auto it = m_model->m_allTracks.cbegin();
2718     while (it != m_model->m_allTracks.cend()) {
2719         int target_track = (*it)->getId();
2720         if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
2721             tracks << target_track;
2722         }
2723         ++it;
2724     }
2725     if (tracks.isEmpty()) {
2726         pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
2727     }
2728     if (m_zone.isNull()) {
2729         // Use current timeline position and clip zone length
2730         zone.setY(pCore->getTimelinePosition() + zone.y() - zone.x());
2731         zone.setX(pCore->getTimelinePosition());
2732     }
2733     TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly);
2734     if (!liftOnly && !m_zone.isNull()) {
2735         setPosition(m_zone.x());
2736     }
2737 }
2738 
extract(int clipId)2739 void TimelineController::extract(int clipId)
2740 {
2741     if (clipId == -1) {
2742         std::unordered_set<int> sel = m_model->getCurrentSelection();
2743         for (int i : sel) {
2744             if (m_model->isClip(i)) {
2745                 clipId = i;
2746                 break;
2747             }
2748         }
2749         if (clipId == -1) {
2750             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2751             return;
2752         }
2753     }
2754     int in = m_model->getClipPosition(clipId);
2755     int out = in + m_model->getClipPlaytime(clipId);
2756     int tid = m_model->getClipTrackId(clipId);
2757     std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(clipId);
2758     if (mixData.first.firstClipId > -1) {
2759         // Clip has a start mix, adjust in point
2760         in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
2761     }
2762     if (mixData.second.firstClipId > -1) {
2763         // Clip has end mix, adjust out point
2764         out -= mixData.second.mixOffset;
2765     }
2766     QVector <int> tracks = {tid};
2767     if (m_model->m_groups->isInGroup(clipId)) {
2768         int targetRoot = m_model->m_groups->getRootId(clipId);
2769         if (m_model->isGroup(targetRoot)) {
2770             std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
2771             for (int current_id : sub) {
2772                 if (current_id == clipId) {
2773                     continue;
2774                 }
2775                 if (m_model->isClip(current_id)) {
2776                     int newIn = m_model->getClipPosition(current_id);
2777                     int newOut = newIn + m_model->getClipPlaytime(current_id);
2778                     int tk = m_model->getClipTrackId(current_id);
2779                     std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tk)->getMixInfo(current_id);
2780                     if (mixData.first.firstClipId > -1) {
2781                         // Clip has a start mix, adjust in point
2782                         newIn += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
2783                     }
2784                     if (mixData.second.firstClipId > -1) {
2785                         // Clip has end mix, adjust out point
2786                         newOut -= mixData.second.mixOffset;
2787                     }
2788                     in = qMin(in, newIn);
2789                     out = qMax(out, newOut);
2790                     if (!tracks.contains(tk)) {
2791                         tracks << tk;
2792                     }
2793                 }
2794             }
2795         }
2796     }
2797     TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false);
2798 }
2799 
saveZone(int clipId)2800 void TimelineController::saveZone(int clipId)
2801 {
2802     if (clipId == -1) {
2803         clipId = getMainSelectedClip();
2804         if (clipId == -1) {
2805             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
2806             return;
2807         }
2808     }
2809     int in = m_model->getClipIn(clipId);
2810     int out = in + m_model->getClipPlaytime(clipId);
2811     QString id;
2812     pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId());
2813 }
2814 
insertClipZone(const QString & binId,int tid,int position)2815 bool TimelineController::insertClipZone(const QString &binId, int tid, int position)
2816 {
2817     QStringList binIdData = binId.split(QLatin1Char('/'));
2818     int in = 0;
2819     int out = -1;
2820     if (binIdData.size() >= 3) {
2821         in = binIdData.at(1).toInt();
2822         out = binIdData.at(2).toInt();
2823     }
2824 
2825     QString bid = binIdData.first();
2826     // dropType indicates if we want a normal drop (disabled), audio only or video only drop
2827     PlaylistState::ClipState dropType = PlaylistState::Disabled;
2828     if (bid.startsWith(QLatin1Char('A'))) {
2829         dropType = PlaylistState::AudioOnly;
2830         bid = bid.remove(0, 1);
2831     } else if (bid.startsWith(QLatin1Char('V'))) {
2832         dropType = PlaylistState::VideoOnly;
2833         bid = bid.remove(0, 1);
2834     }
2835     QList <int> audioTracks;
2836     int vTrack = -1;
2837     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(bid);
2838     if (out <= in) {
2839         out = int(clip->frameDuration() - 1);
2840     }
2841     QList <int> audioStreams = m_model->m_binAudioTargets.keys();
2842     if (dropType == PlaylistState::VideoOnly) {
2843         vTrack = tid;
2844     } else if (dropType == PlaylistState::AudioOnly) {
2845         audioTracks << tid;
2846         if (audioStreams.size() > 1) {
2847             // insert the other audio streams
2848             QList <int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
2849             while (audioStreams.size() > 1 && !lower.isEmpty()) {
2850                 audioTracks << lower.takeFirst();
2851                 audioStreams.takeFirst();
2852             }
2853         }
2854     } else {
2855         if (m_model->getTrackById_const(tid)->isAudioTrack()) {
2856             audioTracks << tid;
2857             if (audioStreams.size() > 1) {
2858                 // insert the other audio streams
2859                 QList <int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack);
2860                 while (audioStreams.size() > 1 && !lower.isEmpty()) {
2861                     audioTracks << lower.takeFirst();
2862                     audioStreams.takeFirst();
2863                 }
2864             }
2865             vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(tid) : -1;
2866         } else {
2867             vTrack = tid;
2868             if (clip->hasAudioAndVideo()) {
2869                 int firstAudio = m_model->getMirrorAudioTrackId(vTrack);
2870                 audioTracks << firstAudio;
2871                 if (audioStreams.size() > 1) {
2872                     // insert the other audio streams
2873                     QList <int> lower = m_model->getLowerTracksId(firstAudio, TrackType::AudioTrack);
2874                     while (audioStreams.size() > 1 && !lower.isEmpty()) {
2875                         audioTracks << lower.takeFirst();
2876                         audioStreams.takeFirst();
2877                     }
2878                 }
2879             }
2880         }
2881     }
2882     QList<int> target_tracks;
2883     if (vTrack > -1) {
2884         target_tracks << vTrack;
2885     }
2886     if (!audioTracks.isEmpty()) {
2887         target_tracks << audioTracks;
2888     }
2889     qDebug()<<"=====================\n\nREADY TO INSERT IN TRACKS: "<<audioTracks<<" / VIDEO: "<<vTrack<<"\n\n=========";
2890     std::function<bool(void)> undo = []() { return true; };
2891     std::function<bool(void)> redo = []() { return true; };
2892     bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit;
2893     QPoint zone(in, out + 1);
2894     bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo);
2895     if (res) {
2896         int newPos = position + (zone.y() - zone.x());
2897         int currentPos = pCore->getTimelinePosition();
2898         Fun redoPos = [this, newPos]() {
2899             Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
2900             pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
2901             pCore->monitorManager()->refreshProjectMonitor();
2902             setPosition(newPos);
2903             pCore->monitorManager()->activateMonitor(activeMonitor);
2904             return true;
2905         };
2906         Fun undoPos = [this, currentPos]() {
2907             Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id();
2908             pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
2909             pCore->monitorManager()->refreshProjectMonitor();
2910             setPosition(currentPos);
2911             pCore->monitorManager()->activateMonitor(activeMonitor);
2912             return true;
2913         };
2914         redoPos();
2915         UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
2916         pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
2917     } else {
2918         pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
2919         undo();
2920     }
2921     return res;
2922 }
2923 
insertZone(const QString & binId,QPoint zone,bool overwrite)2924 int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite)
2925 {
2926     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
2927     int aTrack = -1;
2928     int vTrack = -1;
2929     if (clip->hasAudio() && !m_model->m_audioTarget.isEmpty()) {
2930         QList<int> audioTracks = m_model->m_audioTarget.keys();
2931         for (int tid : qAsConst(audioTracks)) {
2932             if (m_model->getTrackById_const(tid)->shouldReceiveTimelineOp()) {
2933                 aTrack = tid;
2934                 break;
2935             }
2936         }
2937     }
2938     if (clip->hasVideo()) {
2939         vTrack = videoTarget();
2940     }
2941 
2942     int insertPoint;
2943     QPoint sourceZone;
2944     if (useRuler() && m_zone != QPoint()) {
2945         // We want to use timeline zone for in/out insert points
2946         insertPoint = m_zone.x();
2947         sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x());
2948     } else {
2949         // Use current timeline pos and clip zone for in/out
2950         insertPoint = pCore->getTimelinePosition();
2951         sourceZone = zone;
2952     }
2953     QList<int> target_tracks;
2954     if (vTrack > -1) {
2955         target_tracks << vTrack;
2956     }
2957     if (aTrack > -1) {
2958         target_tracks << aTrack;
2959     }
2960     if (target_tracks.isEmpty()) {
2961         pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), ErrorMessage);
2962         return -1;
2963     }
2964     std::function<bool(void)> undo = []() { return true; };
2965     std::function<bool(void)> redo = []() { return true; };
2966     bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo);
2967     if (res) {
2968         int newPos = insertPoint + (sourceZone.y() - sourceZone.x());
2969         int currentPos = pCore->getTimelinePosition();
2970         Fun redoPos = [this, newPos]() {
2971             setPosition(newPos);
2972             pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
2973             return true;
2974         };
2975         Fun undoPos = [this, currentPos]() {
2976             setPosition(currentPos);
2977             pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive();
2978             return true;
2979         };
2980         redoPos();
2981         UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo);
2982         pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
2983     } else {
2984         pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
2985         undo();
2986     }
2987     return res;
2988 }
2989 
updateClip(int clipId,const QVector<int> & roles)2990 void TimelineController::updateClip(int clipId, const QVector<int> &roles)
2991 {
2992     QModelIndex ix = m_model->makeClipIndexFromID(clipId);
2993     if (ix.isValid()) {
2994         emit m_model->dataChanged(ix, ix, roles);
2995     }
2996 }
2997 
showClipKeyframes(int clipId,bool value)2998 void TimelineController::showClipKeyframes(int clipId, bool value)
2999 {
3000     TimelineFunctions::showClipKeyframes(m_model, clipId, value);
3001 }
3002 
showCompositionKeyframes(int clipId,bool value)3003 void TimelineController::showCompositionKeyframes(int clipId, bool value)
3004 {
3005     TimelineFunctions::showCompositionKeyframes(m_model, clipId, value);
3006 }
3007 
switchEnableState(std::unordered_set<int> selection)3008 void TimelineController::switchEnableState(std::unordered_set<int> selection)
3009 {
3010     if (selection.empty()) {
3011         selection = m_model->getCurrentSelection();
3012         //clipId = getMainSelectedItem(false, false);
3013     }
3014     if (selection.empty()) {
3015         return;
3016     }
3017     TimelineFunctions::switchEnableState(m_model, selection);
3018 }
3019 
addCompositionToClip(const QString & assetId,int clipId,int offset)3020 void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset)
3021 {
3022     if (clipId == -1) {
3023         clipId = getMainSelectedClip();
3024         if (clipId == -1) {
3025             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3026             return;
3027         }
3028     }
3029     if (offset == -1) {
3030         offset = m_root->property("clickFrame").toInt();
3031     }
3032     int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack;
3033     int compoId = -1;
3034     if (assetId.isEmpty()) {
3035         QStringList compositions = KdenliveSettings::favorite_transitions();
3036         if (compositions.isEmpty()) {
3037             pCore->displayMessage(i18n("Select a favorite composition"), ErrorMessage, 500);
3038             return;
3039         }
3040         compoId = insertNewComposition(track, clipId, offset, compositions.first(), true);
3041     } else {
3042         compoId = insertNewComposition(track, clipId, offset, assetId, true);
3043     }
3044     if (compoId > 0) {
3045         m_model->requestSetSelection({compoId});
3046     }
3047 }
3048 
addEffectToClip(const QString & assetId,int clipId)3049 void TimelineController::addEffectToClip(const QString &assetId, int clipId)
3050 {
3051     if (clipId == -1) {
3052         clipId = getMainSelectedClip();
3053         if (clipId == -1) {
3054             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3055             return;
3056         }
3057     }
3058     qDebug() << "/// ADDING ASSET: " << assetId;
3059     m_model->addClipEffect(clipId, assetId);
3060 }
3061 
splitAV()3062 bool TimelineController::splitAV()
3063 {
3064     int cid = *m_model->getCurrentSelection().begin();
3065     if (m_model->isClip(cid)) {
3066         std::shared_ptr<ClipModel> clip = m_model->getClipPtr(cid);
3067         if (clip->clipState() == PlaylistState::AudioOnly) {
3068             return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget());
3069         } else {
3070             QVariantList aTargets = audioTarget();
3071             int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3072             return TimelineFunctions::requestSplitAudio(m_model, cid, targetTrack);
3073         }
3074     }
3075     pCore->displayMessage(i18n("No clip found to perform AV split operation"), ErrorMessage, 500);
3076     return false;
3077 }
3078 
splitAudio(int clipId)3079 void TimelineController::splitAudio(int clipId)
3080 {
3081     QVariantList aTargets = audioTarget();
3082     int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt();
3083     TimelineFunctions::requestSplitAudio(m_model, clipId, targetTrack);
3084 }
3085 
splitVideo(int clipId)3086 void TimelineController::splitVideo(int clipId)
3087 {
3088     TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget());
3089 }
3090 
setAudioRef(int clipId)3091 void TimelineController::setAudioRef(int clipId)
3092 {
3093     if (clipId == -1) {
3094         clipId = getMainSelectedClip();
3095         if (clipId == -1) {
3096             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3097             return;
3098         }
3099     }
3100     m_audioRef = clipId;
3101     std::unique_ptr<AudioEnvelope> envelope(new AudioEnvelope(getClipBinId(clipId), clipId));
3102     m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope)));
3103     connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, this, [&](int cid, int shift) {
3104         // Ensure the clip was not deleted while processing calculations
3105         if (m_model->isClip(cid)) {
3106             int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef);
3107             bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true);
3108             if (!result) {
3109                 pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), ErrorMessage, 500);
3110             }
3111         } else {
3112             // Clip was deleted, discard audio reference
3113             m_audioRef = -1;
3114         }
3115     });
3116     connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage);
3117 }
3118 
alignAudio(int clipId)3119 void TimelineController::alignAudio(int clipId)
3120 {
3121     // find other clip
3122     if (clipId == -1) {
3123         clipId = getMainSelectedClip();
3124         if (clipId == -1) {
3125             pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3126             return;
3127         }
3128     }
3129     if (m_audioRef == -1 || m_audioRef == clipId || !m_model->isClip(m_audioRef)) {
3130         pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500);
3131         return;
3132     }
3133     const QString masterBinClipId = getClipBinId(m_audioRef);
3134     std::unordered_set<int> clipsToAnalyse;
3135     if (m_model->m_groups->isInGroup(clipId)) {
3136         clipsToAnalyse = m_model->getGroupElements(clipId);
3137         m_model->requestClearSelection();
3138     } else {
3139         clipsToAnalyse.insert(clipId);
3140     }
3141     QList <int> processedGroups;
3142     int processed = 0;
3143     for (int cid : clipsToAnalyse) {
3144         if (!m_model->isClip(cid) || cid == m_audioRef) {
3145             continue;
3146         }
3147         const QString otherBinId = getClipBinId(cid);
3148         if (m_model->m_groups->isInGroup(cid)) {
3149             int parentGroup = m_model->m_groups->getRootId(cid);
3150             if (processedGroups.contains(parentGroup)) {
3151                 continue;
3152             }
3153             // Only process one clip from the group
3154             std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(otherBinId);
3155             if (clip->hasAudio()) {
3156                 processedGroups << parentGroup;
3157             } else {
3158                 continue;
3159             }
3160         }
3161         if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) {
3162             // Cannot process non audi clips
3163             continue;
3164         }
3165         if (otherBinId == masterBinClipId) {
3166             // easy, same clip.
3167             int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid);
3168             if (newPos) {
3169                 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true);
3170                 processed ++;
3171                 if (!result) {
3172                     pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), ErrorMessage, 500);
3173                 }
3174                 continue;
3175             }
3176         }
3177         processed ++;
3178         // Perform audio calculation
3179         auto *envelope = new AudioEnvelope(otherBinId, cid,
3180                                            size_t(m_model->getClipIn(cid)),
3181                                            size_t(m_model->getClipPlaytime(cid)),
3182                                            size_t(m_model->getClipPosition(cid)));
3183         m_audioCorrelator->addChild(envelope);
3184     }
3185     if (processed == 0) {
3186         //TODO: improve feedback message after freeze
3187         pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500);
3188     }
3189 }
3190 
switchTrackActive(int trackId)3191 void TimelineController::switchTrackActive(int trackId)
3192 {
3193     if (trackId == -1) {
3194         trackId = m_activeTrack;
3195     }
3196     if (trackId < 0) {
3197         return;
3198     }
3199     bool active = m_model->getTrackById_const(trackId)->isTimelineActive();
3200     m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3201     m_activeSnaps.clear();
3202 }
3203 
switchAllTrackActive()3204 void TimelineController::switchAllTrackActive()
3205 {
3206     auto it = m_model->m_allTracks.cbegin();
3207     while (it != m_model->m_allTracks.cend()) {
3208         bool active = (*it)->isTimelineActive();
3209         int target_track = (*it)->getId();
3210         m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1"));
3211         ++it;
3212     }
3213     m_activeSnaps.clear();
3214 }
3215 
makeAllTrackActive()3216 void TimelineController::makeAllTrackActive()
3217 {
3218     // Check current status
3219     auto it = m_model->m_allTracks.cbegin();
3220     bool makeActive = false;
3221     while (it != m_model->m_allTracks.cend()) {
3222         if (!(*it)->isTimelineActive()) {
3223             // There is an inactive track, activate all
3224             makeActive = true;
3225             break;
3226         }
3227         ++it;
3228     }
3229     it = m_model->m_allTracks.cbegin();
3230     while (it != m_model->m_allTracks.cend()) {
3231         int target_track = (*it)->getId();
3232         m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0"));
3233         ++it;
3234     }
3235     m_activeSnaps.clear();
3236 }
3237 
switchTrackLock(bool applyToAll)3238 void TimelineController::switchTrackLock(bool applyToAll)
3239 {
3240     if (!applyToAll) {
3241         // apply to active track only
3242         if (m_activeTrack == -2) {
3243             // Subtitle track
3244             switchSubtitleLock();
3245         } else {
3246             bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked();
3247             m_model->setTrackLockedState(m_activeTrack, !locked);
3248         }
3249     } else {
3250         // Invert track lock
3251         const auto ids = m_model->getAllTracksIds();
3252         // count the number of tracks to be locked
3253         int toBeLockedCount =
3254             std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); });
3255         auto subtitleModel = pCore->getSubtitleModel();
3256         bool subLocked = false;
3257         bool hasSubtitleTrack = false;
3258         if (subtitleModel) {
3259             hasSubtitleTrack = true;
3260             subLocked= subtitleModel->isLocked();
3261             if (!subLocked) {
3262                 toBeLockedCount++;
3263             }
3264         }
3265         bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount() + hasSubtitleTrack ? true : false;
3266         for (const int id : ids) {
3267             // leave active track unlocked
3268             if (leaveOneUnlocked && id == m_activeTrack) {
3269                 continue;
3270             }
3271             bool isLocked = m_model->getTrackById_const(id)->isLocked();
3272             m_model->setTrackLockedState(id, !isLocked);
3273         }
3274         if (hasSubtitleTrack) {
3275             if (!leaveOneUnlocked || m_activeTrack != -2) {
3276                 switchSubtitleLock();
3277             }
3278         }
3279     }
3280 }
3281 
switchTargetTrack()3282 void TimelineController::switchTargetTrack()
3283 {
3284     if (m_activeTrack < 0) {
3285         return;
3286     }
3287     bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1;
3288     if (isAudio) {
3289         QMap<int, int> current = m_model->m_audioTarget;
3290         if (current.contains(m_activeTrack)) {
3291             current.remove(m_activeTrack);
3292         } else {
3293             int ix = getFirstUnassignedStream();
3294             if (ix > -1) {
3295                 current.insert(m_activeTrack, ix);
3296             }
3297         }
3298         setAudioTarget(current);
3299     } else {
3300         setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack);
3301     }
3302 }
3303 
audioTarget() const3304 QVariantList TimelineController::audioTarget() const
3305 {
3306     QVariantList audioTracks;
3307     QMapIterator <int, int>i(m_model->m_audioTarget);
3308     while (i.hasNext()) {
3309         i.next();
3310         audioTracks << i.key();
3311     }
3312     return audioTracks;
3313 }
3314 
lastAudioTarget() const3315 QVariantList TimelineController::lastAudioTarget() const
3316 {
3317     QVariantList audioTracks;
3318     QMapIterator <int, int>i(m_lastAudioTarget);
3319     while (i.hasNext()) {
3320         i.next();
3321         audioTracks << i.key();
3322     }
3323     return audioTracks;
3324 }
3325 
audioTargetName(int tid) const3326 const QString TimelineController::audioTargetName(int tid) const
3327 {
3328     if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) {
3329         int streamIndex = m_model->m_audioTarget.value(tid);
3330         if (m_model->m_binAudioTargets.contains(streamIndex)) {
3331             QString targetName = m_model->m_binAudioTargets.value(streamIndex);
3332             return targetName.isEmpty() ? QChar('x') : targetName.section(QLatin1Char('|'), 0, 0);
3333         } else {
3334             qDebug()<<"STREAM INDEX NOT IN TARGET : "<<streamIndex<<" = "<<m_model->m_binAudioTargets;
3335         }
3336     } else {
3337         qDebug()<<"TRACK NOT IN TARGET : "<<tid<<" = "<<m_model->m_audioTarget.keys();
3338     }
3339     return QString();
3340 }
3341 
videoTarget() const3342 int TimelineController::videoTarget() const
3343 {
3344     return m_model->m_videoTarget;
3345 }
3346 
hasAudioTarget() const3347 int TimelineController::hasAudioTarget() const
3348 {
3349     return m_hasAudioTarget;
3350 }
3351 
clipTargets() const3352 int TimelineController::clipTargets() const
3353 {
3354     return m_model->m_binAudioTargets.size();
3355 }
3356 
hasVideoTarget() const3357 bool TimelineController::hasVideoTarget() const
3358 {
3359     return m_hasVideoTarget;
3360 }
3361 
autoScroll() const3362 bool TimelineController::autoScroll() const
3363 {
3364     return !pCore->monitorManager()->projectMonitor()->isPlaying() || KdenliveSettings::autoscroll();
3365 }
3366 
resetTrackHeight()3367 void TimelineController::resetTrackHeight()
3368 {
3369     int tracksCount = m_model->getTracksCount();
3370     for (int track = tracksCount - 1; track >= 0; track--) {
3371         int trackIx = m_model->getTrackIndexFromPosition(track);
3372         m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight()));
3373     }
3374     QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0));
3375     QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1));
3376     emit m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole});
3377 }
3378 
selectAll()3379 void TimelineController::selectAll()
3380 {
3381     std::unordered_set<int> ids;
3382     for (const auto &clp : m_model->m_allClips) {
3383         ids.insert(clp.first);
3384     }
3385     for (const auto &clp : m_model->m_allCompositions) {
3386         ids.insert(clp.first);
3387     }
3388     // Subtitles
3389     for (const auto &sub : m_model->m_allSubtitles) {
3390         ids.insert(sub.first);
3391     }
3392     m_model->requestSetSelection(ids);
3393 }
3394 
selectCurrentTrack()3395 void TimelineController::selectCurrentTrack()
3396 {
3397     if (m_activeTrack == -1) {
3398         return;
3399     }
3400     std::unordered_set<int> ids;
3401     if (m_activeTrack == -2) {
3402         for (const auto &sub : m_model->m_allSubtitles) {
3403             ids.insert(sub.first);
3404         }
3405     } else {
3406         for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) {
3407             ids.insert(clp.first);
3408         }
3409         for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) {
3410             ids.insert(clp.first);
3411         }
3412     }
3413     m_model->requestSetSelection(ids);
3414 }
3415 
deleteEffects(int targetId)3416 void TimelineController::deleteEffects(int targetId)
3417 {
3418     std::unordered_set<int> targetIds;
3419     std::unordered_set<int> sel;
3420     if (targetId == -1) {
3421         sel = m_model->getCurrentSelection();
3422     } else {
3423         if (m_model->m_groups->isInGroup(targetId)) {
3424             sel = {m_model->m_groups->getRootId(targetId)};
3425         } else {
3426             sel = {targetId};
3427         }
3428     }
3429     if (sel.empty()) {
3430         pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500);
3431     }
3432     for (int s : sel) {
3433         if (m_model->isGroup(s)) {
3434             std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3435             for (int current_id : sub) {
3436                 if (m_model->isClip(current_id)) {
3437                     targetIds.insert(current_id);
3438                 }
3439             }
3440         } else if (m_model->isClip(s)) {
3441             targetIds.insert(s);
3442         }
3443     }
3444     if (targetIds.empty()) {
3445         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3446     }
3447     std::function<bool(void)> undo = []() { return true; };
3448     std::function<bool(void)> redo = []() { return true; };
3449     for (int target : targetIds) {
3450         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3451         destStack->removeAllEffects(undo, redo);
3452     }
3453     pCore->pushUndo(undo, redo, i18n("Delete effects"));
3454 }
3455 
pasteEffects(int targetId)3456 void TimelineController::pasteEffects(int targetId)
3457 {
3458     std::unordered_set<int> targetIds;
3459     std::unordered_set<int> sel;
3460     if (targetId == -1) {
3461         sel = m_model->getCurrentSelection();
3462     } else {
3463         if (m_model->m_groups->isInGroup(targetId)) {
3464             sel = {m_model->m_groups->getRootId(targetId)};
3465         } else {
3466             sel = {targetId};
3467         }
3468     }
3469     if (sel.empty()) {
3470         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3471     }
3472     for (int s : sel) {
3473         if (m_model->isGroup(s)) {
3474             std::unordered_set<int> sub = m_model->m_groups->getLeaves(s);
3475             for (int current_id : sub) {
3476                 if (m_model->isClip(current_id)) {
3477                     targetIds.insert(current_id);
3478                 }
3479             }
3480         } else if (m_model->isClip(s)) {
3481             targetIds.insert(s);
3482         }
3483     }
3484     if (targetIds.empty()) {
3485         pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3486     }
3487 
3488     QClipboard *clipboard = QApplication::clipboard();
3489     QString txt = clipboard->text();
3490     if (txt.isEmpty()) {
3491         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3492         return;
3493     }
3494     QDomDocument copiedItems;
3495     copiedItems.setContent(txt);
3496     if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) {
3497         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3498         return;
3499     }
3500     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
3501     if (clips.isEmpty()) {
3502         pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500);
3503         return;
3504     }
3505     std::function<bool(void)> undo = []() { return true; };
3506     std::function<bool(void)> redo = []() { return true; };
3507     QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects"));
3508     effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in")));
3509     for (int i = 1; i < clips.size(); i++) {
3510         QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects"));
3511         QDomNodeList subs = subeffects.childNodes();
3512         while (!subs.isEmpty()) {
3513             subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in")));
3514             effects.appendChild(subs.at(0));
3515         }
3516     }
3517     int insertedEffects = 0;
3518     for (int target : targetIds) {
3519         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target);
3520         if (destStack->fromXml(effects, undo, redo)) {
3521             insertedEffects++;
3522         }
3523     }
3524     if (insertedEffects > 0) {
3525         pCore->pushUndo(undo, redo, i18n("Paste effects"));
3526     } else {
3527         pCore->displayMessage(i18n("Cannot paste effect on selected clip"), ErrorMessage, 500);
3528         undo();
3529     }
3530 }
3531 
fps() const3532 double TimelineController::fps() const
3533 {
3534     return pCore->getCurrentFps();
3535 }
3536 
editItemDuration(int id)3537 void TimelineController::editItemDuration(int id)
3538 {
3539     if (id == -1) {
3540         id = m_root->property("mainItemId").toInt();
3541         if (id == -1) {
3542             std::unordered_set<int> sel = m_model->getCurrentSelection();
3543             if (!sel.empty()) {
3544                 id = *sel.begin();
3545             }
3546             if (id == -1) {
3547                 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3548                 return;
3549             }
3550         }
3551     }
3552     if (id == -1 || !m_model->isItem(id)) {
3553         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3554         return;
3555     }
3556     int start = m_model->getItemPosition(id);
3557     int in = 0;
3558     int duration = m_model->getItemPlaytime(id);
3559     int maxLength = -1;
3560     bool isComposition = false;
3561     if (m_model->isClip(id)) {
3562         in = m_model->getClipIn(id);
3563         std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(id));
3564         if (clip && clip->hasLimitedDuration()) {
3565             maxLength = clip->getProducerDuration();
3566         }
3567     } else if (m_model->isComposition(id)) {
3568         // nothing to do
3569         isComposition = true;
3570     } else {
3571         pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500);
3572         return;
3573     }
3574     int trackId = m_model->getItemTrackId(id);
3575     int maxFrame = qMax(0, start + duration +
3576                                (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true)
3577                                               : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true)));
3578     int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false)
3579                                                : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false)));
3580     int partner = isComposition ? -1 : m_model->getClipSplitPartner(id);
3581     QPointer<ClipDurationDialog> dialog =
3582         new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow());
3583     if (dialog->exec() == QDialog::Accepted) {
3584         std::function<bool(void)> undo = []() { return true; };
3585         std::function<bool(void)> redo = []() { return true; };
3586         int newPos = dialog->startPos().frames(pCore->getCurrentFps());
3587         int newIn = dialog->cropStart().frames(pCore->getCurrentFps());
3588         int newDuration = dialog->duration().frames(pCore->getCurrentFps());
3589         bool result = true;
3590         if (newPos < start) {
3591             if (!isComposition) {
3592                 result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3593                 if (result && partner > -1) {
3594                     result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3595                 }
3596             } else {
3597                 result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3598             }
3599             if (result && newIn != in) {
3600                 int updatedDuration = duration + (in - newIn);
3601                 m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3602                 if (result && partner > -1) {
3603                     result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3604                 }
3605             }
3606             if (newDuration != duration + (in - newIn)) {
3607                 result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo);
3608                 if (result && partner > -1) {
3609                     result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo);
3610                 }
3611             }
3612         } else {
3613             // perform resize first
3614             if (newIn != in) {
3615                 int updatedDuration = duration + (in - newIn);
3616                 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo);
3617                 if (result && partner > -1) {
3618                     result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo);
3619                 }
3620             }
3621             if (newDuration != duration + (in - newIn)) {
3622                 result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo);
3623                 if (result && partner > -1) {
3624                     result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo);
3625                 }
3626             }
3627             if (start != newPos || newIn != in) {
3628                 if (!isComposition) {
3629                     result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo);
3630                     if (result && partner > -1) {
3631                         result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo);
3632                     }
3633                 } else {
3634                     result = result &&
3635                              m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo);
3636                 }
3637             }
3638         }
3639         if (result) {
3640             pCore->pushUndo(undo, redo, i18n("Edit item"));
3641         } else {
3642             undo();
3643         }
3644     }
3645     emit regainFocus();
3646 }
3647 
editTitleClip(int id)3648 void TimelineController::editTitleClip(int id)
3649 {
3650     if (id == -1) {
3651         id = m_root->property("mainItemId").toInt();
3652         if (id == -1) {
3653             std::unordered_set<int> sel = m_model->getCurrentSelection();
3654             if (!sel.empty()) {
3655                 id = *sel.begin();
3656             }
3657             if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) {
3658                 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500);
3659                 return;
3660             }
3661         }
3662     }
3663     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id));
3664     if(binClip->clipType() != ClipType::Text && binClip->clipType() != ClipType::TextTemplate) {
3665         pCore->displayMessage(i18n("Item is not a title clip"), ErrorMessage, 500);
3666         return;
3667     }
3668     seekToMouse();
3669     pCore->bin()->showTitleWidget(binClip);
3670 }
3671 
selectionInOut() const3672 QPoint TimelineController::selectionInOut() const
3673 {
3674     std::unordered_set<int> ids = m_model->getCurrentSelection();
3675     std::unordered_set<int> items_list;
3676     for (int i : ids) {
3677         if (m_model->isGroup(i)) {
3678             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
3679             items_list.insert(children.begin(), children.end());
3680         } else {
3681             items_list.insert(i);
3682         }
3683     }
3684     int in = -1;
3685     int out = -1;
3686     for (int id : items_list) {
3687         if (m_model->isClip(id) || m_model->isComposition(id)) {
3688             int itemIn = m_model->getItemPosition(id);
3689             int itemOut = itemIn + m_model->getItemPlaytime(id);
3690             if (in < 0 || itemIn < in) {
3691                 in = itemIn;
3692             }
3693             if (itemOut > out) {
3694                 out = itemOut;
3695             }
3696         }
3697     }
3698     return QPoint(in, out);
3699 }
3700 
updateClipActions()3701 void TimelineController::updateClipActions()
3702 {
3703     if (m_model->getCurrentSelection().empty()) {
3704         for (QAction *act : qAsConst(clipActions)) {
3705             act->setEnabled(false);
3706         }
3707         emit timelineClipSelected(false);
3708         // nothing selected
3709         emit showItemEffectStack(QString(), nullptr, QSize(), false);
3710         pCore->timeRemapWidget()->selectedClip(-1);
3711         emit showSubtitle(-1);
3712         return;
3713     }
3714     std::shared_ptr<ClipModel> clip(nullptr);
3715     int item = *m_model->getCurrentSelection().begin();
3716     int selectionSize = m_model->getCurrentSelection().size();
3717     if (selectionSize == 1) {
3718         if (m_model->isClip(item) || m_model->isComposition(item)) {
3719             showAsset(item);
3720             emit showSubtitle(-1);
3721         } else if (m_model->isSubTitle(item)) {
3722             emit showSubtitle(item);
3723         }
3724     }
3725     if (m_model->isClip(item)) {
3726         clip = m_model->getClipPtr(item);
3727         if (clip->isChain()) {
3728             emit pCore->remapClip(item);
3729         }
3730     }
3731     bool isInGroup = m_model->m_groups->isInGroup(item);
3732     PlaylistState::ClipState state = PlaylistState::ClipState::Unknown;
3733     ClipType::ProducerType type = ClipType::Unknown;
3734     if (clip) {
3735         state = clip->clipState();
3736         type = clip->clipType();
3737     }
3738     for (QAction *act : qAsConst(clipActions)) {
3739         bool enableAction = true;
3740         const QChar actionData = act->data().toChar();
3741         if (actionData == QLatin1Char('G')) {
3742             enableAction = isInSelection(item) && selectionSize > 1;
3743         } else if (actionData == QLatin1Char('U')) {
3744             enableAction = isInGroup;
3745         } else if (actionData == QLatin1Char('A')) {
3746             if (isInGroup && m_model->m_groups->getType(m_model->m_groups->getRootId(item)) == GroupType::AVSplit) {
3747                 enableAction = true;
3748             } else {
3749                 enableAction = state == PlaylistState::AudioOnly;
3750             }
3751         } else if (actionData == QLatin1Char('V')) {
3752             enableAction = state == PlaylistState::VideoOnly;
3753         } else if (actionData == QLatin1Char('D')) {
3754             enableAction = state == PlaylistState::Disabled;
3755         } else if (actionData == QLatin1Char('E')) {
3756             enableAction = state != PlaylistState::Disabled && state != PlaylistState::Unknown;
3757         } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) {
3758             enableAction = clip && clip->canBeVideo() && clip->canBeAudio();
3759             if (enableAction && actionData == QLatin1Char('S')) {
3760                 if (isInGroup) {
3761                     // Check if all clips in the group have have same state (audio or video)
3762                     int targetRoot = m_model->m_groups->getRootId(item);
3763                     if (m_model->isGroup(targetRoot)) {
3764                         std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot);
3765                         for (int current_id : sub) {
3766                             if (current_id == item) {
3767                                 continue;
3768                             }
3769                             if (m_model->isClip(current_id) && m_model->getClipPtr(current_id)->clipState() != state) {
3770                                 // Group with audio and video clips, disable split action
3771                                 enableAction = false;
3772                                 break;
3773                             }
3774                         }
3775                     }
3776                 }
3777                 act->setText(state == PlaylistState::AudioOnly ? i18n("Restore video") : i18n("Restore audio"));
3778             }
3779         } else if (actionData == QLatin1Char('W')) {
3780             enableAction = clip != nullptr;
3781             if (enableAction) {
3782                 act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip"));
3783             }
3784         } else if (actionData == QLatin1Char('C') && clip == nullptr) {
3785             enableAction = false;
3786         } else if (actionData == QLatin1Char('P')) {
3787             // Position actions should stay enabled in clip monitor
3788             enableAction = true;
3789         } else if (actionData == QLatin1Char('R')) {
3790             // Time remap action
3791             enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image
3792                     && qFuzzyCompare(1., m_model->m_allClips[item]->getSpeed());
3793             if (enableAction) {
3794                 act->setChecked(clip->isChain());
3795             }
3796         } else if (actionData == QLatin1Char('Q')) {
3797             // Speed change action
3798             enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image
3799                     && !clip->isChain();
3800         }
3801         act->setEnabled(enableAction);
3802     }
3803     emit timelineClipSelected(clip != nullptr);
3804 }
3805 
getAssetName(const QString & assetId,bool isTransition)3806 const QString TimelineController::getAssetName(const QString &assetId, bool isTransition)
3807 {
3808     return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId);
3809 }
3810 
grabCurrent()3811 void TimelineController::grabCurrent()
3812 {
3813     if (trimmingActive()) {
3814         return;
3815     }
3816     std::unordered_set<int> ids = m_model->getCurrentSelection();
3817     std::unordered_set<int> items_list;
3818     int mainId = -1;
3819     for (int i : ids) {
3820         if (m_model->isGroup(i)) {
3821             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
3822             items_list.insert(children.begin(), children.end());
3823         } else {
3824             items_list.insert(i);
3825         }
3826     }
3827     for (int id : items_list) {
3828         if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) {
3829             mainId = id;
3830             continue;
3831         }
3832         if (m_model->isClip(id)) {
3833             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
3834             clip->setGrab(!clip->isGrabbed());
3835         } else if (m_model->isComposition(id)) {
3836             std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(id);
3837             clip->setGrab(!clip->isGrabbed());
3838         } else if (m_model->isSubTitle(id)) {
3839             pCore->getSubtitleModel()->switchGrab(id);
3840         }
3841     }
3842     if (mainId > -1) {
3843         if (m_model->isClip(mainId)) {
3844             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(mainId);
3845             clip->setGrab(!clip->isGrabbed());
3846         } else if (m_model->isComposition(mainId)) {
3847             std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(mainId);
3848             clip->setGrab(!clip->isGrabbed());
3849         } else if (m_model->isSubTitle(mainId)) {
3850             pCore->getSubtitleModel()->switchGrab(mainId);
3851         }
3852     }
3853 }
3854 
getItemMovingTrack(int itemId) const3855 int TimelineController::getItemMovingTrack(int itemId) const
3856 {
3857     if (m_model->isClip(itemId)) {
3858         int trackId = -1;
3859         if (m_model->m_editMode != TimelineMode::NormalEdit) {
3860             trackId = m_model->m_allClips[itemId]->getFakeTrackId();
3861         }
3862         return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId;
3863     }
3864     return m_model->m_allCompositions[itemId]->getCurrentTrackId();
3865 }
3866 
endFakeMove(int clipId,int position,bool updateView,bool logUndo,bool invalidateTimeline)3867 bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
3868 {
3869     Q_ASSERT(m_model->m_allClips.count(clipId) > 0);
3870     int trackId = m_model->m_allClips[clipId]->getFakeTrackId();
3871     if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) {
3872         qDebug() << "* * ** END FAKE; NO MOVE RQSTED";
3873         return true;
3874     }
3875     if (m_model->m_groups->isInGroup(clipId)) {
3876         // element is in a group.
3877         int groupId = m_model->m_groups->getRootId(clipId);
3878         int current_trackId = m_model->getClipTrackId(clipId);
3879         int track_pos1 = m_model->getTrackPosition(trackId);
3880         int track_pos2 = m_model->getTrackPosition(current_trackId);
3881         int delta_track = track_pos1 - track_pos2;
3882         int delta_pos = position - m_model->m_allClips[clipId]->getPosition();
3883         return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
3884     }
3885     qDebug() << "//////\n//////\nENDING FAKE MOVE: " << trackId << ", POS: " << position;
3886     std::function<bool(void)> undo = []() { return true; };
3887     std::function<bool(void)> redo = []() { return true; };
3888     int startPos = m_model->getClipPosition(clipId);
3889     int duration = m_model->getClipPlaytime(clipId);
3890     int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId();
3891     bool res = true;
3892     if (currentTrack > -1) {
3893         std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(currentTrack)->getMixInfo(clipId);
3894         if (mixData.first.firstClipId > -1) {
3895             m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3896         }
3897         if (mixData.second.firstClipId > -1) {
3898             m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3899         }
3900         res = m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false);
3901     }
3902     if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
3903         res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo);
3904     } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
3905         // Remove space from previous location
3906         if (currentTrack > -1) {
3907             res = res && TimelineFunctions::removeSpace(m_model, {startPos,startPos + duration}, undo, redo, {currentTrack}, false);
3908         }
3909         int startClipId = m_model->getClipByPosition(trackId, position);
3910         if (startClipId > -1) {
3911             // There is a clip at insert pos
3912             if (m_model->getClipPosition(startClipId) != position) {
3913                 // If position is in the middle of the clip, cut it
3914                 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo);
3915             }
3916         }
3917         res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, {trackId});
3918     }
3919     res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo);
3920     if (res) {
3921         // Terminate fake move
3922         if (m_model->isClip(clipId)) {
3923             m_model->m_allClips[clipId]->setFakeTrackId(-1);
3924         }
3925         if (logUndo) {
3926             pCore->pushUndo(undo, redo, i18n("Move item"));
3927         }
3928     } else {
3929         qDebug() << "//// FAKE FAILED";
3930         undo();
3931     }
3932     return res;
3933 }
3934 
endFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool logUndo)3935 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
3936 {
3937     std::function<bool(void)> undo = []() { return true; };
3938     std::function<bool(void)> redo = []() { return true; };
3939     bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
3940     if (res && logUndo) {
3941         // Terminate fake move
3942         if (m_model->isClip(clipId)) {
3943             m_model->m_allClips[clipId]->setFakeTrackId(-1);
3944         }
3945         pCore->pushUndo(undo, redo, i18n("Move group"));
3946     }
3947     return res;
3948 }
3949 
endFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool finalMove,Fun & undo,Fun & redo)3950 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo)
3951 {
3952     Q_ASSERT(m_model->m_allGroups.count(groupId) > 0);
3953     bool ok = true;
3954     auto all_items = m_model->m_groups->getLeaves(groupId);
3955     Q_ASSERT(all_items.size() > 1);
3956     Fun local_undo = []() { return true; };
3957     Fun local_redo = []() { return true; };
3958 
3959     // Sort clips. We need to delete from right to left to avoid confusing the view
3960     std::vector<int> sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))};
3961     std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) {
3962         int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition();
3963         int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition();
3964         return p2 < p1;
3965     });
3966 
3967     // 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.
3968     // This way, we ensure that no conflict will arise with clips inside the group being moved
3969     // We are moving a group on another track, delete and re-add
3970 
3971     // First, remove clips
3972     int audio_delta, video_delta;
3973     audio_delta = video_delta = delta_track;
3974     int master_trackId = m_model->getItemTrackId(clipId);
3975     if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) {
3976         // Master clip is audio, so reverse delta for video clips
3977         video_delta = -delta_track;
3978     } else {
3979         audio_delta = -delta_track;
3980     }
3981     int min = -1;
3982     int max = -1;
3983     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track, new_track_ids;
3984     std::vector<int> affected_trackIds;
3985     std::unordered_map<int, std::pair<QString,QVector<QPair<QString, QVariant>>>> mixToMove;
3986     std::unordered_map<int, MixInfo> mixInfoToMove;
3987     // Remove mixes not part of the group move
3988     for (int item : sorted_clips) {
3989         if (m_model->isClip(item)) {
3990             int tid = m_model->getItemTrackId(item);
3991             affected_trackIds.emplace_back(tid);
3992             std::pair<MixInfo,MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(item);
3993             if (mixData.first.firstClipId > -1) {
3994                 if (std::find(sorted_clips.begin(), sorted_clips.end(), mixData.first.firstClipId) == sorted_clips.end()) {
3995                     // Clip has startMix
3996                     m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo);
3997                 } else {
3998                     // Get mix properties
3999                     std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = m_model->getTrackById_const(tid)->getMixParams(mixData.first.secondClipId);
4000                     mixToMove[item] = mixParams;
4001                     mixInfoToMove[item] = mixData.first;
4002                 }
4003             }
4004             if (mixData.second.firstClipId > -1 && std::find(sorted_clips.begin(), sorted_clips.end(), mixData.second.secondClipId) == sorted_clips.end()) {
4005                 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo);
4006             }
4007         }
4008     }
4009     for (int item : sorted_clips) {
4010         int old_trackId = m_model->getItemTrackId(item);
4011         old_track_ids[item] = old_trackId;
4012         if (old_trackId != -1) {
4013             bool updateThisView = true;
4014             if (m_model->isClip(item)) {
4015                 int current_track_position = m_model->getTrackPosition(old_trackId);
4016                 int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta;
4017                 int target_track_position = current_track_position + d;
4018                 auto it = m_model->m_allTracks.cbegin();
4019                 std::advance(it, target_track_position);
4020                 int target_track = (*it)->getId();
4021                 new_track_ids[item] = target_track;
4022                 affected_trackIds.emplace_back(target_track);
4023                 old_position[item] = m_model->m_allClips[item]->getPosition();
4024                 int duration = m_model->m_allClips[item]->getPlaytime();
4025                 min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos);
4026                 max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration);
4027                 ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false);
4028                 if (m_model->m_editMode == TimelineMode::InsertEdit) {
4029                     // Lift space left by removed clip
4030                     ok = ok && TimelineFunctions::removeSpace(m_model, {old_position[item],old_position[item] + duration}, undo, redo, {old_trackId}, false);
4031                 }
4032             } else {
4033                 // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo);
4034                 old_position[item] = m_model->m_allCompositions[item]->getPosition();
4035                 old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack();
4036             }
4037             if (!ok) {
4038                 bool undone = undo();
4039                 Q_ASSERT(undone);
4040                 return false;
4041             }
4042         }
4043     }
4044     bool res = true;
4045     if (m_model->m_editMode == TimelineMode::OverwriteEdit) {
4046         for (int item : sorted_clips) {
4047             if (m_model->isClip(item) && new_track_ids.count(item) > 0) {
4048                 int target_track = new_track_ids[item];
4049                 int target_position = old_position[item] + delta_pos;
4050                 int duration = m_model->m_allClips[item]->getPlaytime();
4051                 res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo);
4052             }
4053         }
4054     } else if (m_model->m_editMode == TimelineMode::InsertEdit) {
4055         QVector<int> processedTracks;
4056         for (int item : sorted_clips) {
4057             int target_track = new_track_ids[item];
4058             if (processedTracks.contains(target_track)) {
4059                 // already processed
4060                 continue;
4061             }
4062             processedTracks << target_track;
4063             int target_position = min;
4064             int startClipId = m_model->getClipByPosition(target_track, target_position);
4065             if (startClipId > -1) {
4066                 // There is a clip, cut
4067                 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo);
4068             }
4069         }
4070         res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, processedTracks);
4071     }
4072     for (int item : sorted_clips) {
4073         if (m_model->isClip(item)) {
4074             int target_track = new_track_ids[item];
4075             int target_position = old_position[item] + delta_pos;
4076             ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo);
4077         } else {
4078             // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo);
4079         }
4080         if (!ok) {
4081             bool undone = undo();
4082             Q_ASSERT(undone);
4083             return false;
4084         }
4085     }
4086 
4087     if (delta_track == 0) {
4088         Fun sync_mix = [this, affected_trackIds, finalMove]() {
4089             for (int t : affected_trackIds) {
4090                 m_model->getTrackById_const(t)->syncronizeMixes(finalMove);
4091             }
4092             return true;
4093         };
4094         sync_mix();
4095         PUSH_LAMBDA(sync_mix, redo);
4096         return true;
4097     }
4098     for (int item : sorted_clips) {
4099         if (mixToMove.find(item) == mixToMove.end()) {
4100             continue;
4101         }
4102         int trackId = new_track_ids[item];
4103         int previous_track = old_track_ids[item];
4104         MixInfo mixData = mixInfoToMove[item];
4105         std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = mixToMove[item];
4106         Fun simple_move_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
4107             // Insert mix on new track
4108             bool result = m_model->getTrackById_const(trackId)->createMix(mixData, mixParams, finalMove);
4109             // Remove mix on old track
4110             m_model->getTrackById_const(previous_track)->removeMix(mixData);
4111             return result;
4112         };
4113         Fun simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
4114             bool result = m_model->getTrackById_const(previous_track)->createMix(mixData, mixParams, finalMove);
4115             // Remove mix on old track
4116             m_model->getTrackById_const(trackId)->removeMix(mixData);
4117             return result;
4118         };
4119         simple_move_mix();
4120         if (finalMove) {
4121             PUSH_LAMBDA(simple_restore_mix, undo);
4122             PUSH_LAMBDA(simple_move_mix, redo);
4123         }
4124     }
4125     return true;
4126 }
4127 
getThumbKeys()4128 QStringList TimelineController::getThumbKeys()
4129 {
4130     QStringList result;
4131     for (const auto &clp : m_model->m_allClips) {
4132         const QString binId = getClipBinId(clp.first);
4133         std::shared_ptr<ProjectClip> binClip = pCore->bin()->getBinClip(binId);
4134         result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".jpg");
4135         result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".jpg");
4136     }
4137     result.removeDuplicates();
4138     return result;
4139 }
4140 
isInSelection(int itemId)4141 bool TimelineController::isInSelection(int itemId)
4142 {
4143     return m_model->getCurrentSelection().count(itemId) > 0;
4144 }
4145 
exists(int itemId)4146 bool TimelineController::exists(int itemId)
4147 {
4148     return m_model->isClip(itemId) || m_model->isComposition(itemId);
4149 }
4150 
slotMultitrackView(bool enable,bool refresh)4151 void TimelineController::slotMultitrackView(bool enable, bool refresh)
4152 {
4153     QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh);
4154     if (!refresh) {
4155         // This is just a temporary state (disable multitrack view for playlist save, don't change scene
4156         return;
4157     }
4158     pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames));
4159     QObject::disconnect( m_connection );
4160     if (enable) {
4161         connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection);
4162         m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() {
4163             int ix = 0;
4164             auto it = m_model->m_allTracks.cbegin();
4165             while (it != m_model->m_allTracks.cend()) {
4166                 int target_track = (*it)->getId();
4167                 ++it;
4168                 if (target_track == m_activeTrack) {
4169                     break;
4170                 }
4171                 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4172                     continue;
4173                 }
4174                 ++ix;
4175             }
4176             pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4177         });
4178         int ix = 0;
4179         auto it = m_model->m_allTracks.cbegin();
4180         while (it != m_model->m_allTracks.cend()) {
4181             int target_track = (*it)->getId();
4182             ++it;
4183             if (target_track == m_activeTrack) {
4184                 break;
4185             }
4186             if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4187                 continue;
4188             }
4189             ++ix;
4190         }
4191         pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix);
4192     } else {
4193         disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack);
4194     }
4195 }
4196 
updateMultiTrack()4197 void TimelineController::updateMultiTrack()
4198 {
4199     QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true);
4200     pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames));
4201 }
4202 
activateTrackAndSelect(int trackPosition,bool notesMode)4203 void TimelineController::activateTrackAndSelect(int trackPosition, bool notesMode)
4204 {
4205     int tid = -1;
4206     int ix = 0;
4207     if (notesMode && trackPosition == -2) {
4208         m_activeTrack = -2;
4209         emit activeTrackChanged();
4210         return;
4211     }
4212     auto it = m_model->m_allTracks.cbegin();
4213     while (it != m_model->m_allTracks.cend()) {
4214         tid = (*it)->getId();
4215         ++it;
4216         if (!notesMode && (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden())) {
4217             continue;
4218         }
4219         if (trackPosition == ix) {
4220             break;
4221         }
4222         ++ix;
4223     }
4224     if (tid > -1) {
4225         m_activeTrack = tid;
4226         emit activeTrackChanged();
4227         if (!notesMode && pCore->window()->getCurrentTimeline()->activeTool() != ToolType::MulticamTool) {
4228             selectCurrentItem(ObjectType::TimelineClip, true);
4229         }
4230     }
4231 }
4232 
saveTimelineSelection(const QDir & targetDir)4233 void TimelineController::saveTimelineSelection(const QDir &targetDir)
4234 {
4235     TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir);
4236 }
4237 
addEffectKeyframe(int cid,int frame,double val)4238 void TimelineController::addEffectKeyframe(int cid, int frame, double val)
4239 {
4240     if (m_model->isClip(cid)) {
4241         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4242         destStack->addEffectKeyFrame(frame, val);
4243     } else if (m_model->isComposition(cid)) {
4244         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4245         listModel->addKeyframe(frame, val);
4246     }
4247 }
4248 
removeEffectKeyframe(int cid,int frame)4249 void TimelineController::removeEffectKeyframe(int cid, int frame)
4250 {
4251     if (m_model->isClip(cid)) {
4252         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4253         destStack->removeKeyFrame(frame);
4254     } else if (m_model->isComposition(cid)) {
4255         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4256         listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps()));
4257     }
4258 }
4259 
updateEffectKeyframe(int cid,int oldFrame,int newFrame,const QVariant & normalizedValue)4260 void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue)
4261 {
4262     if (m_model->isClip(cid)) {
4263         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4264         destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue);
4265     } else if (m_model->isComposition(cid)) {
4266         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4267         listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue);
4268     }
4269 }
4270 
hasKeyframeAt(int cid,int frame)4271 bool TimelineController::hasKeyframeAt(int cid, int frame)
4272 {
4273     if (m_model->isClip(cid)) {
4274         std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid);
4275         return destStack->hasKeyFrame(frame);
4276     } else if (m_model->isComposition(cid)) {
4277         std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel();
4278         return listModel->hasKeyframe(frame);
4279     }
4280     return false;
4281 }
4282 
darkBackground() const4283 bool TimelineController::darkBackground() const
4284 {
4285     KColorScheme scheme(QApplication::palette().currentColorGroup());
4286     return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5;
4287 }
4288 
videoColor() const4289 QColor TimelineController::videoColor() const
4290 {
4291     KColorScheme scheme(QApplication::palette().currentColorGroup());
4292     return scheme.foreground(KColorScheme::LinkText).color();
4293 }
4294 
targetColor() const4295 QColor TimelineController::targetColor() const
4296 {
4297     KColorScheme scheme(QApplication::palette().currentColorGroup());
4298     QColor base = scheme.foreground(KColorScheme::PositiveText).color();
4299     QColor high = QApplication::palette().highlightedText().color();
4300     double factor = 0.3;
4301     QColor res = QColor(qBound(0, base.red() + int(factor*(high.red() - 128)), 255),
4302                         qBound(0, base.green() + int(factor*(high.green() - 128)), 255),
4303                         qBound(0, base.blue() + int(factor*(high.blue() - 128)), 255),
4304                         255);
4305     return res;
4306 }
4307 
targetTextColor() const4308 QColor TimelineController::targetTextColor() const
4309 {
4310     KColorScheme scheme(QApplication::palette().currentColorGroup());
4311     return scheme.background(KColorScheme::PositiveBackground).color();
4312 }
4313 
audioColor() const4314 QColor TimelineController::audioColor() const
4315 {
4316     KColorScheme scheme(QApplication::palette().currentColorGroup());
4317     return scheme.foreground(KColorScheme::PositiveText).color().darker(200);
4318 }
4319 
titleColor() const4320 QColor TimelineController::titleColor() const
4321 {
4322     KColorScheme scheme(QApplication::palette().currentColorGroup());
4323     QColor base = scheme.foreground(KColorScheme::LinkText).color();
4324     QColor high = scheme.foreground(KColorScheme::NegativeText).color();
4325     QColor title = QColor(qBound(0, base.red() + int(high.red() - 128), 255),
4326                           qBound(0, base.green() + int(high.green() - 128), 255),
4327                           qBound(0, base.blue() + int(high.blue() - 128), 255),
4328                           255);
4329     return title;
4330 }
4331 
imageColor() const4332 QColor TimelineController::imageColor() const
4333 {
4334     KColorScheme scheme(QApplication::palette().currentColorGroup());
4335     return scheme.foreground(KColorScheme::NeutralText).color();
4336 }
4337 
thumbColor1() const4338 QColor TimelineController::thumbColor1() const
4339 {
4340     return KdenliveSettings::thumbColor1();
4341 }
4342 
thumbColor2() const4343 QColor TimelineController::thumbColor2() const
4344 {
4345     return KdenliveSettings::thumbColor2();
4346 }
4347 
slideshowColor() const4348 QColor TimelineController::slideshowColor() const
4349 {
4350     KColorScheme scheme(QApplication::palette().currentColorGroup());
4351     QColor base = scheme.foreground(KColorScheme::LinkText).color();
4352     QColor high = scheme.foreground(KColorScheme::NeutralText).color();
4353     QColor slide = QColor(qBound(0, base.red() + int(high.red() - 128), 255),
4354                           qBound(0, base.green() + int(high.green() - 128), 255),
4355                           qBound(0, base.blue() + int(high.blue() - 128), 255),
4356                           255);
4357     return slide;
4358 }
4359 
lockedColor() const4360 QColor TimelineController::lockedColor() const
4361 {
4362     KColorScheme scheme(QApplication::palette().currentColorGroup());
4363     return scheme.foreground(KColorScheme::NegativeText).color();
4364 }
4365 
groupColor() const4366 QColor TimelineController::groupColor() const
4367 {
4368     KColorScheme scheme(QApplication::palette().currentColorGroup());
4369     return scheme.foreground(KColorScheme::ActiveText).color();
4370 }
4371 
selectionColor() const4372 QColor TimelineController::selectionColor() const
4373 {
4374     KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary);
4375     return scheme.foreground(KColorScheme::NeutralText).color();
4376 }
4377 
switchRecording(int trackId)4378 void TimelineController::switchRecording(int trackId)
4379 {
4380     if (!pCore->isMediaCapturing()) {
4381         qDebug() << "start recording" << trackId;
4382         if (!m_model->isTrack(trackId)) {
4383             qDebug() << "ERROR: Starting to capture on invalid track " << trackId;
4384         }
4385         if (m_model->getTrackById_const(trackId)->isLocked()) {
4386             pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500);
4387             return;
4388         }
4389         m_recordStart.first = pCore->getTimelinePosition();
4390         m_recordTrack = trackId;
4391         int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first);
4392         if (maximumSpace == INT_MAX) {
4393             m_recordStart.second = 0;
4394         } else {
4395             m_recordStart.second = maximumSpace - m_recordStart.first;
4396             if (m_recordStart.second < 8) {
4397                 pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or "
4398                                            "choose a different track"),
4399                                       ErrorMessage, 500);
4400                 return;
4401             }
4402         }
4403         pCore->monitorManager()->slotSwitchMonitors(false);
4404         pCore->startMediaCapture(trackId, true, false);
4405         pCore->monitorManager()->slotPlay();
4406     } else {
4407         pCore->stopMediaCapture(trackId, true, false);
4408         pCore->monitorManager()->slotPause();
4409     }
4410 }
4411 
urlDropped(QStringList droppedFile,int frame,int tid)4412 void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid)
4413 {
4414     if (droppedFile.isEmpty()) {
4415         // Empty url passed, abort
4416         return;
4417     }
4418     m_recordTrack = tid;
4419     m_recordStart = {frame, -1};
4420     qDebug()<<"=== GOT DROPPED FILED: "<<droppedFile<<"\n======";
4421     if (droppedFile.first().endsWith(QLatin1String(".ass")) || droppedFile.first().endsWith(QLatin1String(".srt"))) {
4422         // Subtitle dropped, import
4423         pCore->window()->showSubtitleTrack();
4424         importSubtitle(droppedFile.first());
4425     } else {
4426         finishRecording(QUrl(droppedFile.first()).toLocalFile());
4427     }
4428 }
4429 
finishRecording(const QString & recordedFile)4430 void TimelineController::finishRecording(const QString &recordedFile)
4431 {
4432     if (recordedFile.isEmpty()) {
4433         return;
4434     }
4435 
4436     Fun undo = []() { return true; };
4437     Fun redo = []() { return true; };
4438     std::function<void(const QString &)> callBack = [this](const QString &binId) {
4439         int id = -1;
4440         if (m_recordTrack == -1) {
4441             return;
4442         }
4443         std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
4444         if (!clip) {
4445             return;
4446         }
4447         pCore->activeBin()->selectClipById(binId);
4448         qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second;
4449         if (m_recordStart.second > 0) {
4450             // Limited space on track
4451             int out = qMin(int(clip->frameDuration() - 1), m_recordStart.second - 1);
4452             QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out);
4453             m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false);
4454         } else {
4455             m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false);
4456         }
4457     };
4458     QString binId =
4459         ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack);
4460     pCore->window()->raiseBin();
4461     if (binId != QStringLiteral("-1")) {
4462         pCore->pushUndo(undo, redo, i18n("Record audio"));
4463     }
4464 }
4465 
4466 
updateVideoTarget()4467 void TimelineController::updateVideoTarget()
4468 {
4469     if (videoTarget() > -1) {
4470         m_lastVideoTarget = videoTarget();
4471         m_videoTargetActive = true;
4472         emit lastVideoTargetChanged();
4473     } else {
4474         m_videoTargetActive = false;
4475     }
4476 }
4477 
updateAudioTarget()4478 void TimelineController::updateAudioTarget()
4479 {
4480     if (!audioTarget().isEmpty()) {
4481         m_lastAudioTarget = m_model->m_audioTarget;
4482         m_audioTargetActive = true;
4483         emit lastAudioTargetChanged();
4484     } else {
4485         m_audioTargetActive = false;
4486     }
4487 }
4488 
hasActiveTracks() const4489 bool TimelineController::hasActiveTracks() const
4490 {
4491     auto it = m_model->m_allTracks.cbegin();
4492     while (it != m_model->m_allTracks.cend()) {
4493         int target_track = (*it)->getId();
4494         if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
4495             return true;
4496         }
4497         ++it;
4498     }
4499     return false;
4500 }
4501 
showMasterEffects()4502 void TimelineController::showMasterEffects()
4503 {
4504     emit showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false);
4505 }
4506 
refreshIfVisible(int cid)4507 bool TimelineController::refreshIfVisible(int cid)
4508 {
4509     auto it = m_model->m_allTracks.cbegin();
4510     while (it != m_model->m_allTracks.cend()) {
4511         int target_track = (*it)->getId();
4512         if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) {
4513             ++it;
4514             continue;
4515         }
4516         int child = m_model->getClipByPosition(target_track, pCore->getTimelinePosition());
4517         if (child > 0) {
4518             if (m_model->m_allClips[child]->binId().toInt() == cid) {
4519                 return true;
4520             }
4521         }
4522         ++it;
4523     }
4524     return false;
4525 }
4526 
collapseActiveTrack()4527 void TimelineController::collapseActiveTrack()
4528 {
4529     if (m_activeTrack == -1) {
4530         return;
4531     }
4532     if (m_activeTrack == -2) {
4533         // Subtitle track
4534         QMetaObject::invokeMethod(m_root, "switchSubtitleTrack", Qt::QueuedConnection);
4535         return;
4536     }
4537     int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt();
4538     // Default unit for timeline.qml objects size
4539     int baseUnit = qMax(28, int(QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pixelSize() * 1.8 + 0.5));
4540     m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QString::number(baseUnit));
4541 }
4542 
setActiveTrackProperty(const QString & name,const QString & value)4543 void TimelineController::setActiveTrackProperty(const QString &name, const QString &value)
4544 {
4545     if (m_activeTrack > -1) {
4546         m_model->setTrackProperty(m_activeTrack, name, value);
4547     }
4548 }
4549 
isActiveTrackAudio() const4550 bool TimelineController::isActiveTrackAudio() const
4551 {
4552     if (m_activeTrack > -1) {
4553         if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) {
4554             return true;
4555         }
4556     }
4557     return false;
4558 }
4559 
getActiveTrackProperty(const QString & name) const4560 const QVariant TimelineController::getActiveTrackProperty(const QString &name) const
4561 {
4562     if (m_activeTrack > -1) {
4563         return m_model->getTrackProperty(m_activeTrack, name);
4564     }
4565     return QVariant();
4566 }
4567 
expandActiveClip()4568 void TimelineController::expandActiveClip()
4569 {
4570     std::unordered_set<int> ids = m_model->getCurrentSelection();
4571     std::unordered_set<int> items_list;
4572     for (int i : ids) {
4573         if (m_model->isGroup(i)) {
4574             std::unordered_set<int> children = m_model->m_groups->getLeaves(i);
4575             items_list.insert(children.begin(), children.end());
4576         } else {
4577             items_list.insert(i);
4578         }
4579     }
4580     m_model->requestClearSelection();
4581     bool result = true;
4582     for (int id : items_list) {
4583         if (result && m_model->isClip(id)) {
4584             std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id);
4585             if (clip->clipType() == ClipType::Playlist) {
4586                 std::function<bool(void)> undo = []() { return true; };
4587                 std::function<bool(void)> redo = []() { return true; };
4588                 if (m_model->m_groups->isInGroup(id)) {
4589                     int targetRoot = m_model->m_groups->getRootId(id);
4590                     if (m_model->isGroup(targetRoot)) {
4591                         m_model->requestClipUngroup(targetRoot, undo, redo);
4592                     }
4593                 }
4594                 int pos = clip->getPosition();
4595                 QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id));
4596                 m_model->requestClipDeletion(id, undo, redo);
4597                 result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo);
4598                 if (result) {
4599                     pCore->pushUndo(undo, redo, i18n("Expand clip"));
4600                 } else {
4601                     undo();
4602                     pCore->displayMessage(i18n("Could not expand clip"), ErrorMessage, 500);
4603                 }
4604             }
4605         }
4606     }
4607 }
4608 
getCurrentTargets(int trackId,int & activeTargetStream)4609 QMap <int, QString> TimelineController::getCurrentTargets(int trackId, int &activeTargetStream)
4610 {
4611     if (m_model->m_binAudioTargets.size() < 2) {
4612         activeTargetStream = -1;
4613         return QMap <int, QString>();
4614     }
4615     if (m_model->m_audioTarget.contains(trackId)) {
4616         activeTargetStream = m_model->m_audioTarget.value(trackId);
4617     } else {
4618         activeTargetStream = -1;
4619     }
4620     return m_model->m_binAudioTargets;
4621 }
4622 
addTracks(int videoTracks,int audioTracks)4623 void TimelineController::addTracks(int videoTracks, int audioTracks)
4624 {
4625     bool result = false;
4626     int total = videoTracks + audioTracks;
4627     Fun undo = []() { return true; };
4628     Fun redo = []() { return true; };
4629     for (int ix = 0; videoTracks + audioTracks > 0; ++ix) {
4630         int newTid;
4631         if (audioTracks > 0) {
4632             result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo);
4633             audioTracks--;
4634         } else {
4635             result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo);
4636             videoTracks--;
4637         }
4638         if (!result) {
4639             break;
4640         }
4641     }
4642     if (result) {
4643         pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total));
4644     } else {
4645         pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500);
4646         undo();
4647     }
4648 }
4649 
mixClip(int cid,int delta)4650 void TimelineController::mixClip(int cid, int delta)
4651 {
4652     m_model->mixClip(cid, QStringLiteral("luma"), delta);
4653 }
4654 
temporaryUnplug(QList<int> clipIds,bool hide)4655 void TimelineController::temporaryUnplug(QList<int> clipIds, bool hide)
4656 {
4657     for (auto &cid : clipIds) {
4658         int tid = m_model->getItemTrackId(cid);
4659         if (tid == -1) {
4660             continue;
4661         }
4662         if (hide) {
4663             m_model->getTrackById_const(tid)->temporaryUnplugClip(cid);
4664         } else {
4665             m_model->getTrackById_const(tid)->temporaryReplugClip(cid);
4666         }
4667     }
4668 }
4669 
editSubtitle(int id,QString newText,QString oldText)4670 void TimelineController::editSubtitle(int id, QString newText, QString oldText)
4671 {
4672     qDebug()<<"Editing existing subtitle :"<<id;
4673     if (oldText == newText) {
4674         return;
4675     }
4676     auto subtitleModel = pCore->getSubtitleModel();
4677     Fun local_redo = [subtitleModel, id, newText]() {
4678         subtitleModel->editSubtitle(id, newText);
4679         QPair<int, int> range = subtitleModel->getInOut(id);
4680         pCore->invalidateRange(range);
4681         pCore->refreshProjectRange(range);
4682         return true;
4683     };
4684     Fun local_undo = [subtitleModel, id, oldText]() {
4685         subtitleModel->editSubtitle(id, oldText);
4686         QPair<int, int> range = subtitleModel->getInOut(id);
4687         pCore->invalidateRange(range);
4688         pCore->refreshProjectRange(range);
4689         return true;
4690     };
4691     local_redo();
4692     pCore->pushUndo(local_undo, local_redo, i18n("Edit subtitle"));
4693 }
4694 
resizeSubtitle(int startFrame,int endFrame,int oldEndFrame,bool refreshModel)4695 void TimelineController::resizeSubtitle(int startFrame, int endFrame, int oldEndFrame, bool refreshModel)
4696 {
4697     qDebug()<<"Editing existing subtitle in controller at:"<<startFrame;
4698     auto subtitleModel = pCore->getSubtitleModel();
4699     int max = qMax(endFrame, oldEndFrame);
4700     Fun local_redo = [subtitleModel, startFrame, endFrame, max, refreshModel]() {
4701         subtitleModel->editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(endFrame, pCore->getCurrentFps()), refreshModel);
4702         pCore->refreshProjectRange({startFrame, max});
4703         return true;
4704     };
4705     Fun local_undo = [subtitleModel, startFrame, oldEndFrame, max, refreshModel]() {
4706         subtitleModel->editEndPos(GenTime(startFrame, pCore->getCurrentFps()), GenTime(oldEndFrame, pCore->getCurrentFps()), refreshModel);
4707         pCore->refreshProjectRange({startFrame, max});
4708         return true;
4709     };
4710     local_redo();
4711     if (refreshModel) {
4712         pCore->pushUndo(local_undo, local_redo, i18n("Resize subtitle"));
4713     }
4714 }
4715 
4716 
addSubtitle(int startframe,QString text)4717 void TimelineController::addSubtitle(int startframe, QString text)
4718 {
4719     if (startframe == -1) {
4720         startframe = pCore->getTimelinePosition();
4721     }
4722     int endframe = startframe + pCore->getDurationFromString(KdenliveSettings::subtitle_duration());
4723     auto subtitleModel = pCore->getSubtitleModel(true);
4724     int id = TimelineModel::getNextId();
4725     if (text.isEmpty()) {
4726         text = i18n("Add text");
4727     }
4728     Fun local_undo = [subtitleModel, id, startframe, endframe]() {
4729         subtitleModel->removeSubtitle(id);
4730         QPair<int, int> range = {startframe, endframe};
4731         pCore->invalidateRange(range);
4732         pCore->refreshProjectRange(range);
4733         return true;
4734     };
4735     Fun local_redo = [subtitleModel, id, startframe, endframe, text]() {
4736         if (subtitleModel->addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text)) {
4737             QPair<int, int> range = {startframe, endframe};
4738             pCore->invalidateRange(range);
4739             pCore->refreshProjectRange(range);
4740             return true;
4741         }
4742         return false;
4743     };
4744     if (local_redo()) {
4745         m_model->requestAddToSelection(id, true);
4746         pCore->pushUndo(local_undo, local_redo, i18n("Add subtitle"));
4747         int index = m_model->positionForIndex(id);
4748         if (index > -1) {
4749             QMetaObject::invokeMethod(m_root, "highlightSub", Qt::QueuedConnection, Q_ARG(QVariant, index));
4750         }
4751     }
4752 }
4753 
importSubtitle(const QString path)4754 void TimelineController::importSubtitle(const QString path)
4755 {
4756     QPointer<QDialog> d = new QDialog;
4757     Ui::ImportSub_UI view;
4758     view.setupUi(d);
4759     if (!path.isEmpty()) {
4760         view.subtitle_url->setText(path);
4761     }
4762     d->setWindowTitle(i18n("Import Subtitle"));
4763     if (d->exec() == QDialog::Accepted && !view.subtitle_url->url().isEmpty()) {
4764         auto subtitleModel = pCore->getSubtitleModel(true);
4765         int offset = 0;
4766         if (view.cursor_pos->isChecked()) {
4767             offset = pCore->getTimelinePosition();
4768         }
4769         subtitleModel->importSubtitle(view.subtitle_url->url().toLocalFile(), offset, true);
4770     }
4771     emit regainFocus();
4772 }
4773 
exportSubtitle()4774 void TimelineController::exportSubtitle()
4775 {
4776     auto subtitleModel = pCore->getSubtitleModel();
4777     if (subtitleModel == nullptr) {
4778         return;
4779     }
4780     QString currentSub = subtitleModel->getUrl();
4781     if (currentSub.isEmpty()) {
4782         pCore->displayMessage(i18n("No subtitles in current project"), ErrorMessage);
4783         return;
4784     }
4785     const QString url =
4786         QFileDialog::getSaveFileName(qApp->activeWindow(), i18n("Export subtitle file"), pCore->currentDoc()->url().toLocalFile(), i18n("Subtitle File (*.srt)"));
4787     if (url.isEmpty()) {
4788         return;
4789     }
4790     QFile srcFile(url);
4791     if (!url.endsWith(QStringLiteral(".srt"))) {
4792         KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", url));
4793         return;
4794     }
4795     if (srcFile.exists()) {
4796         srcFile.remove();
4797     }
4798     QFile src(currentSub);
4799     if (!src.copy(srcFile.fileName())) {
4800         KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", srcFile.fileName()));
4801     }
4802 }
4803 
subtitleSpeechRecognition()4804 void TimelineController::subtitleSpeechRecognition()
4805 {
4806     SpeechDialog d(m_model, m_zone, false, false, qApp->activeWindow());
4807     d.exec();
4808 }
4809 
deleteSubtitle(int startframe,int endframe,QString text)4810 void TimelineController::deleteSubtitle(int startframe, int endframe, QString text)
4811 {
4812     auto subtitleModel = pCore->getSubtitleModel();
4813     int id = subtitleModel->getIdForStartPos(GenTime(startframe, pCore->getCurrentFps()));
4814     Fun local_redo = [subtitleModel, id, startframe, endframe]() {
4815         subtitleModel->removeSubtitle(id);
4816         pCore->refreshProjectRange({startframe, endframe});
4817         return true;
4818     };
4819     Fun local_undo = [subtitleModel, id, startframe, endframe, text]() {
4820         subtitleModel->addSubtitle(id, GenTime(startframe, pCore->getCurrentFps()), GenTime(endframe, pCore->getCurrentFps()), text);
4821         pCore->refreshProjectRange({startframe, endframe});
4822         return true;
4823     };
4824     local_redo();
4825     pCore->pushUndo(local_undo, local_redo, i18n("Delete subtitle"));
4826 }
4827 
switchSubtitleDisable()4828 void TimelineController::switchSubtitleDisable()
4829 {
4830     auto subtitleModel = pCore->getSubtitleModel();
4831     if (subtitleModel) {
4832         bool disabled = subtitleModel->isDisabled();
4833         Fun local_switch = [this, subtitleModel]() {
4834             subtitleModel->switchDisabled();
4835             emit subtitlesDisabledChanged();
4836             pCore->requestMonitorRefresh();
4837             return true;
4838         };
4839         local_switch();
4840         pCore->pushUndo(local_switch, local_switch, disabled ? i18n("Show subtitle track") : i18n("Hide subtitle track"));
4841     }
4842 }
4843 
subtitlesDisabled() const4844 bool TimelineController::subtitlesDisabled() const
4845 {
4846     auto subtitleModel = pCore->getSubtitleModel();
4847     if (subtitleModel) {
4848         return subtitleModel->isDisabled();
4849     }
4850     return false;
4851 }
4852 
switchSubtitleLock()4853 void TimelineController::switchSubtitleLock()
4854 {
4855     auto subtitleModel = pCore->getSubtitleModel();
4856     if (subtitleModel) {
4857         bool locked = subtitleModel->isLocked();
4858         Fun local_switch = [this, subtitleModel]() {
4859             subtitleModel->switchLocked();
4860             emit subtitlesLockedChanged();
4861             return true;
4862         };
4863         local_switch();
4864         pCore->pushUndo(local_switch, local_switch, locked ? i18n("Unlock subtitle track") : i18n("Lock subtitle track"));
4865     }
4866 }
subtitlesLocked() const4867 bool TimelineController::subtitlesLocked() const
4868 {
4869     auto subtitleModel = pCore->getSubtitleModel();
4870     if (subtitleModel) {
4871         return subtitleModel->isLocked();
4872     }
4873     return false;
4874 }
4875 
guidesLocked() const4876 bool TimelineController::guidesLocked() const
4877 {
4878     return KdenliveSettings::lockedGuides();
4879 }
4880 
showToolTip(const QString & info) const4881 void TimelineController::showToolTip(const QString &info) const
4882 {
4883     pCore->displayMessage(info, TooltipMessage);
4884 }
4885 
showKeyBinding(const QString & info) const4886 void TimelineController::showKeyBinding(const QString &info) const
4887 {
4888     pCore->window()->showKeyBinding(info);
4889 }
4890 
4891 
showTimelineToolInfo(bool show) const4892 void TimelineController::showTimelineToolInfo(bool show) const
4893 {
4894     if (show) {
4895         pCore->window()->showToolMessage();
4896     } else {
4897         pCore->window()->setWidgetKeyBinding();
4898     }
4899 }
4900 
showRulerEffectZone(QPair<int,int> inOut,bool checked)4901 void TimelineController::showRulerEffectZone(QPair <int, int>inOut, bool checked)
4902 {
4903     m_effectZone = checked ? QPoint(inOut.first, inOut.second) : QPoint();
4904     emit effectZoneChanged();
4905 }
4906 
updateMasterZones(QVariantList zones)4907 void TimelineController::updateMasterZones(QVariantList zones)
4908 {
4909     m_masterEffectZones = zones;
4910     emit masterZonesChanged();
4911 }
4912 
clipMaxDuration(int cid)4913 int TimelineController::clipMaxDuration(int cid)
4914 {
4915     if (!m_model->isClip(cid)) {
4916         return -1;
4917     }
4918     return m_model->m_allClips[cid]->getMaxDuration();
4919 }
4920 
resizeMix(int cid,int duration,MixAlignment align,int rightFrames)4921 void TimelineController::resizeMix(int cid, int duration, MixAlignment align, int rightFrames)
4922 {
4923     if (cid > -1) {
4924         m_model->requestResizeMix(cid, duration, align, rightFrames);
4925     }
4926 }
4927 
getMixCutPos(int cid) const4928 int TimelineController::getMixCutPos(int cid) const
4929 {
4930     return m_model->getMixCutPos(cid);
4931 }
4932 
getMixAlign(int cid) const4933 MixAlignment TimelineController::getMixAlign(int cid) const
4934 {
4935     return m_model->getMixAlign(cid);
4936 }
4937 
processMultitrackOperation(int tid,int in)4938 void TimelineController::processMultitrackOperation(int tid, int in)
4939 {
4940     int out = pCore->getTimelinePosition();
4941     if (out == in) {
4942         // Simply change the reference track, nothing to do here
4943         return;
4944     }
4945     QVector<int> tracks;
4946     auto it = m_model->m_allTracks.cbegin();
4947     // Lift all tracks except tid
4948     while (it != m_model->m_allTracks.cend()) {
4949         int target_track = (*it)->getId();
4950         if (target_track != tid && !(*it)->isAudioTrack() && m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
4951             tracks << target_track;
4952         }
4953         ++it;
4954     }
4955     if (tracks.isEmpty()) {
4956         pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage);
4957     }
4958     TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), true);
4959 }
4960 
setMulticamIn(int pos)4961 void TimelineController::setMulticamIn(int pos)
4962 {
4963     if (multicamIn != -1) {
4964         // remove previous snap
4965         m_model->removeSnap(multicamIn);
4966     }
4967     multicamIn = pos;
4968     m_model->addSnap(multicamIn);
4969     emit multicamInChanged();
4970 }
4971